From a5902f3c11390c38de16a8376639b69cfc2cda8c Mon Sep 17 00:00:00 2001 From: Tim Van Baak Date: Fri, 7 Mar 2025 10:41:53 -0800 Subject: [PATCH] Unified action command --- cmd/action.go | 147 +++++++++++++++++++++++++++++++++++++++- cmd/actionAdd.go | 51 -------------- cmd/actionDelete.go | 47 ------------- cmd/actionEdit.go | 49 -------------- cmd/actionExecute.go | 158 ------------------------------------------- 5 files changed, 145 insertions(+), 307 deletions(-) delete mode 100644 cmd/actionAdd.go delete mode 100644 cmd/actionDelete.go delete mode 100644 cmd/actionEdit.go delete mode 100644 cmd/actionExecute.go diff --git a/cmd/action.go b/cmd/action.go index ca2cf97..173c0f0 100644 --- a/cmd/action.go +++ b/cmd/action.go @@ -1,6 +1,12 @@ package cmd import ( + "fmt" + "log" + "slices" + "time" + + "github.com/Jaculabilis/intake/core" "github.com/spf13/cobra" ) @@ -8,7 +14,7 @@ var actionCmd = &cobra.Command{ Use: "action", GroupID: sourceGroup.ID, Short: "Manage and run source actions", - Long: `Add, edit, delete, and run source actions on items. + Long: fmt.Sprintf(`Manage and run source actions on items. A feed source is updated by the "fetch" action, which receives no input and returns one JSON item per line on stdout. Other source actions are run on a @@ -27,9 +33,146 @@ the action, you need another action with the same command as "on_create". If an item's "on_create" fails, the item is still created, but without any changes from the "on_create", if any. -To execute the "fetch" action, use "intake source fetch".`, +To test and execute "fetch" actions, use "intake fetch". + +In a dry run, the item will be printed in the chosen format and not updated. + +%s`, makeFormatHelpText()), + Run: func(cmd *cobra.Command, args []string) { + action( + stringArg(cmd, "source"), + stringArg(cmd, "action"), + stringArg(cmd, "item"), + getArgv(cmd, args), + stringArg(cmd, "format"), + stringArg(cmd, "timeout"), + boolArg(cmd, "dry-run"), + boolArg(cmd, "diff"), + boolArg(cmd, "force"), + ) + }, } func init() { rootCmd.AddCommand(actionCmd) + + actionCmd.Flags().StringP("source", "s", "", "Source to add action") + actionCmd.MarkFlagRequired("source") + actionCmd.Flags().StringP("action", "a", "", "Action name") + actionCmd.MarkFlagRequired("action") + actionCmd.PersistentFlags().StringP("item", "i", "", "Item to run action on") + + actionCmd.Flags().StringP("format", "f", "headlines", "Feed format for returned items") + actionCmd.Flags().StringP("timeout", "t", core.DefaultTimeout.String(), "Timeout duration") + actionCmd.Flags().BoolP("dry-run", "n", false, "Instead of updating the item, print it") + actionCmd.Flags().Bool("diff", false, "Show which fields of the item changed") + actionCmd.Flags().Bool("force", false, "Execute the action even if the item does not support it") +} + +func action( + source, + action, + itemId string, + argv []string, + format string, + timeout string, + dryRun, + diff, + force bool, +) { + if source == "" { + log.Fatal("error: --source is empty") + } + if action == "" { + log.Fatal("error: --action is empty") + } + formatter := formatAs(format) + duration, err := time.ParseDuration(timeout) + if err != nil { + log.Fatalf("error: invalid duration: %v", err) + } + + db := openAndMigrateDb() + + if itemId == "" { + if len(argv) > 0 { + err := core.SetAction(db, source, action, argv) + if err != nil { + log.Fatalf("error: failed to set action: %v", err) + } + log.Printf("Defined action %s on source %s", action, source) + } else { + err := core.DeleteAction(db, source, action) + if err != nil { + log.Fatalf("error: failed to delete action: %v", err) + } + log.Printf("Deleted action %s from source %s", action, source) + } + return + } + + state, envs, argv, err := core.GetSourceActionInputs(db, source, action) + if err != nil { + log.Fatalf("error: failed to load data for %s: %v", source, err) + } + + item, err := core.GetItem(db, source, itemId) + if err != nil { + log.Fatalf("error: failed to get item: %v", err) + } + + if item.Action[action] == nil { + if force { + log.Printf("warning: force-executing %s on %s/%s", action, source, itemId) + } else { + log.Fatalf("error: %s/%s does not support %s", source, itemId, action) + } + } + + newItem, newState, errItem, err := core.ExecuteItemAction(item, argv, envs, state, duration) + if err != nil { + core.AddErrorItem(db, errItem) + log.Fatalf("error executing %s: %v", action, err) + } + + if diff { + if item.Title != newItem.Title { + log.Printf("title: %s => %s", item.Title, newItem.Title) + } + if item.Author != newItem.Author { + log.Printf("author: %s => %s", item.Author, newItem.Author) + } + if item.Body != newItem.Body { + log.Printf("body: %s => %s", item.Body, newItem.Body) + } + if item.Link != newItem.Link { + log.Printf("link: %s => %s", item.Link, newItem.Link) + } + if item.Time != newItem.Time { + log.Printf("time: %d => %d", item.Time, newItem.Time) + } + if core.ItemsAreEqual(item, newItem) { + log.Printf("no changes\n") + } + if !slices.Equal(state, newState) { + log.Printf("state changed (%d => %d bytes)", len(state), len(newState)) + } + } + + if dryRun { + fmt.Println(formatter(newItem)) + return + } + + if err = db.Transact(func(tx core.DB) error { + if _err := core.UpdateItems(tx, []core.Item{newItem}); err != nil { + return fmt.Errorf("failed to update item: %v", _err) + } + if _err := core.SetState(tx, source, newState); err != nil { + return fmt.Errorf("failed to set state for %s: %v", source, _err) + } + return nil + }); err != nil { + log.Fatalf("error: %v", err) + } } diff --git a/cmd/actionAdd.go b/cmd/actionAdd.go deleted file mode 100644 index 6e43224..0000000 --- a/cmd/actionAdd.go +++ /dev/null @@ -1,51 +0,0 @@ -package cmd - -import ( - "log" - - "github.com/Jaculabilis/intake/core" - "github.com/spf13/cobra" -) - -var actionAddCmd = &cobra.Command{ - Use: "add [flags] -- argv...", - Short: "Add an action to a source", - Long: `Add an action to a source. -`, - Run: func(cmd *cobra.Command, args []string) { - actionAdd(stringArg(cmd, "source"), stringArg(cmd, "action"), getArgv(cmd, args)) - }, -} - -func init() { - actionCmd.AddCommand(actionAddCmd) - - actionAddCmd.Flags().StringP("source", "s", "", "Source to add action") - actionAddCmd.MarkFlagRequired("source") - - actionAddCmd.Flags().StringP("action", "a", "", "Action name") - actionAddCmd.MarkFlagRequired("action") -} - -// TODO: This is a duplicate of `action edit`, the action CLI should be simplified - -func actionAdd(source string, action string, argv []string) { - if source == "" { - log.Fatal("error: --source is empty") - } - if action == "" { - log.Fatal("error: --action is empty") - } - if len(argv) == 0 { - log.Fatal("error: no argv provided") - } - - db := openAndMigrateDb() - - err := core.SetAction(db, source, action, argv) - if err != nil { - log.Fatalf("error: failed to add action: %v", err) - } - - log.Printf("Added action %s to source %s", action, source) -} diff --git a/cmd/actionDelete.go b/cmd/actionDelete.go deleted file mode 100644 index 5ccb2c0..0000000 --- a/cmd/actionDelete.go +++ /dev/null @@ -1,47 +0,0 @@ -package cmd - -import ( - "log" - - "github.com/Jaculabilis/intake/core" - "github.com/spf13/cobra" -) - -var actionDeleteCmd = &cobra.Command{ - Use: "delete", - Aliases: []string{"rm"}, - Short: "Delete an action from a source", - Long: `Delete an action from a source. -`, - Run: func(cmd *cobra.Command, args []string) { - actionDelete(stringArg(cmd, "source"), stringArg(cmd, "action")) - }, -} - -func init() { - actionCmd.AddCommand(actionDeleteCmd) - - actionDeleteCmd.Flags().StringP("source", "s", "", "Source to add action") - actionDeleteCmd.MarkFlagRequired("source") - - actionDeleteCmd.Flags().StringP("action", "a", "", "Action name") - actionDeleteCmd.MarkFlagRequired("action") -} - -func actionDelete(source string, action string) { - if source == "" { - log.Fatal("error: --source is empty") - } - if action == "" { - log.Fatal("error: --action is empty") - } - - db := openAndMigrateDb() - - err := core.DeleteAction(db, source, action) - if err != nil { - log.Fatalf("error: failed to delete action: %v", err) - } - - log.Printf("Deleted action %s from source %s", action, source) -} diff --git a/cmd/actionEdit.go b/cmd/actionEdit.go deleted file mode 100644 index df91d74..0000000 --- a/cmd/actionEdit.go +++ /dev/null @@ -1,49 +0,0 @@ -package cmd - -import ( - "log" - - "github.com/Jaculabilis/intake/core" - "github.com/spf13/cobra" -) - -var actionEditCmd = &cobra.Command{ - Use: "edit", - Short: "Edit an action on a source", - Long: `Edit an action on a source. -`, - Run: func(cmd *cobra.Command, args []string) { - actionEdit(stringArg(cmd, "source"), stringArg(cmd, "action"), getArgv(cmd, args)) - }, -} - -func init() { - actionCmd.AddCommand(actionEditCmd) - - actionEditCmd.Flags().StringP("source", "s", "", "Source to edit action") - actionEditCmd.MarkFlagRequired("source") - - actionEditCmd.Flags().StringP("action", "a", "", "Action name") - actionEditCmd.MarkFlagRequired("action") -} - -func actionEdit(source string, action string, argv []string) { - if source == "" { - log.Fatal("error: --source is empty") - } - if action == "" { - log.Fatal("error: --action is empty") - } - if len(argv) == 0 { - log.Fatal("error: no argv provided") - } - - db := openAndMigrateDb() - - err := core.SetAction(db, source, action, argv) - if err != nil { - log.Fatalf("error: failed to update action: %v", err) - } - - log.Printf("Updated action %s on source %s", action, source) -} diff --git a/cmd/actionExecute.go b/cmd/actionExecute.go deleted file mode 100644 index 793018b..0000000 --- a/cmd/actionExecute.go +++ /dev/null @@ -1,158 +0,0 @@ -package cmd - -import ( - "fmt" - "log" - "slices" - "time" - - "github.com/Jaculabilis/intake/core" - "github.com/spf13/cobra" -) - -var actionExecuteCmd = &cobra.Command{ - Use: "execute", - Aliases: []string{"exec"}, - Short: "Run a source action for an item", - Long: fmt.Sprintf(`Execute a source action for an item. - -The item must declare support for the action by having the action's name -in its "action" field. Use --force to execute the action anyway. - -The "fetch" action is special and does not execute for any specific item. -Use "intake source fetch" to run the fetch action. - -In a dry run, the item will be printed in the chosen format and not updated. - -%s`, makeFormatHelpText()), - Run: func(cmd *cobra.Command, args []string) { - actionExecute( - stringArg(cmd, "source"), - stringArg(cmd, "action"), - stringArg(cmd, "item"), - stringArg(cmd, "format"), - stringArg(cmd, "timeout"), - boolArg(cmd, "dry-run"), - boolArg(cmd, "diff"), - boolArg(cmd, "force"), - ) - }, -} - -func init() { - actionCmd.AddCommand(actionExecuteCmd) - - actionExecuteCmd.PersistentFlags().StringP("source", "s", "", "Source of the item") - actionExecuteCmd.MarkFlagRequired("source") - - actionExecuteCmd.PersistentFlags().StringP("item", "i", "", "Item to run action on") - actionExecuteCmd.MarkFlagRequired("item") - - actionExecuteCmd.PersistentFlags().StringP("action", "a", "", "Action to run") - actionExecuteCmd.MarkFlagRequired("action") - - actionExecuteCmd.Flags().StringP("format", "f", "headlines", "Feed format for returned items") - - actionExecuteCmd.Flags().StringP("timeout", "t", core.DefaultTimeout.String(), - fmt.Sprintf("Timeout duration (default: %s)", core.DefaultTimeout.String())) - - actionExecuteCmd.Flags().Bool("dry-run", false, "Instead of updating the item, print it") - - actionExecuteCmd.Flags().Bool("diff", false, "Show which fields of the item changed") - - actionExecuteCmd.Flags().Bool("force", false, "Execute the action even if the item does not support it") -} - -func actionExecute( - source string, - action string, - itemId string, - format string, - timeout string, - dryRun bool, - diff bool, - force bool, -) { - formatter := formatAs(format) - - if source == "" { - log.Fatal("error: --source is empty") - } - if action == "" { - log.Fatal("error: --action is empty") - } - if itemId == "" { - log.Fatal("error: --item is empty") - } - duration, err := time.ParseDuration(timeout) - if err != nil { - log.Fatalf("error: invalid duration: %v", err) - } - - db := openAndMigrateDb() - - state, envs, argv, err := core.GetSourceActionInputs(db, source, action) - if err != nil { - log.Fatalf("error: failed to load data for %s: %v", source, err) - } - - item, err := core.GetItem(db, source, itemId) - if err != nil { - log.Fatalf("error: failed to get item: %v", err) - } - - if item.Action[action] == nil { - if force { - log.Printf("warning: force-executing %s on %s/%s", action, source, itemId) - } else { - log.Fatalf("error: %s/%s does not support %s", source, itemId, action) - } - } - - newItem, newState, errItem, err := core.ExecuteItemAction(item, argv, envs, state, duration) - if err != nil { - core.AddErrorItem(db, errItem) - log.Fatalf("error executing %s: %v", action, err) - } - - if diff { - if item.Title != newItem.Title { - log.Printf("title: %s => %s", item.Title, newItem.Title) - } - if item.Author != newItem.Author { - log.Printf("author: %s => %s", item.Author, newItem.Author) - } - if item.Body != newItem.Body { - log.Printf("body: %s => %s", item.Body, newItem.Body) - } - if item.Link != newItem.Link { - log.Printf("link: %s => %s", item.Link, newItem.Link) - } - if item.Time != newItem.Time { - log.Printf("time: %d => %d", item.Time, newItem.Time) - } - if core.ItemsAreEqual(item, newItem) { - log.Printf("no changes\n") - } - if !slices.Equal(state, newState) { - log.Printf("state changed (%d => %d bytes)", len(state), len(newState)) - } - } - - if dryRun { - fmt.Println(formatter(newItem)) - return - } - - if err = db.Transact(func(tx core.DB) error { - if _err := core.UpdateItems(tx, []core.Item{newItem}); err != nil { - return fmt.Errorf("failed to update item: %v", _err) - } - if _err := core.SetState(tx, source, newState); err != nil { - return fmt.Errorf("failed to set state for %s: %v", source, _err) - } - return nil - }); err != nil { - log.Fatalf("error: %v", err) - } -}