From 6fb8a40a4379b492f39f8a4f8c67752c443d85ca Mon Sep 17 00:00:00 2001 From: Joakim Date: Thu, 19 Feb 2026 13:47:39 +0100 Subject: [PATCH] feat: add --dry-run flag to action commands Adds a persistent --dry-run flag that shows matched tasks without performing mutations. Supported on done, delete, modify, start, and stop commands. Also fixes preprocessArgs to skip flag-like args when identifying commands, preventing flags from being treated as filters. Co-Authored-By: Claude Opus 4.6 --- opal-task/cmd/delete.go | 6 ++++++ opal-task/cmd/done.go | 6 ++++++ opal-task/cmd/modify.go | 6 ++++++ opal-task/cmd/root.go | 31 +++++++++++++++++++++++-------- opal-task/cmd/start.go | 6 ++++++ opal-task/cmd/stop.go | 6 ++++++ 6 files changed, 53 insertions(+), 8 deletions(-) diff --git a/opal-task/cmd/delete.go b/opal-task/cmd/delete.go index a5d3584..64d6e06 100644 --- a/opal-task/cmd/delete.go +++ b/opal-task/cmd/delete.go @@ -53,6 +53,12 @@ func deleteTasks(args []string) error { return fmt.Errorf("no tasks matched filter") } + if dryRunFlag { + fmt.Print(engine.FormatTaskConfirmList("delete", tasks, ws)) + fmt.Println("Dry run — no changes made.") + return nil + } + if len(tasks) > 1 { fmt.Print(engine.FormatTaskConfirmList("delete", tasks, ws)) fmt.Printf("Proceed? (y/N): ") diff --git a/opal-task/cmd/done.go b/opal-task/cmd/done.go index e301b21..0c40dba 100644 --- a/opal-task/cmd/done.go +++ b/opal-task/cmd/done.go @@ -66,6 +66,12 @@ func completeTasks(args []string) error { return fmt.Errorf("no tasks matched filter") } + if dryRunFlag { + fmt.Print(engine.FormatTaskConfirmList("complete", tasks, ws)) + fmt.Println("Dry run — no changes made.") + return nil + } + if len(tasks) > 1 { fmt.Print(engine.FormatTaskConfirmList("complete", tasks, ws)) fmt.Printf("Proceed? (y/N): ") diff --git a/opal-task/cmd/modify.go b/opal-task/cmd/modify.go index 65e218f..daed4fc 100644 --- a/opal-task/cmd/modify.go +++ b/opal-task/cmd/modify.go @@ -85,6 +85,12 @@ func modifyTasks(filterArgs, modifierArgs []string) error { return fmt.Errorf("no tasks matched filter") } + if dryRunFlag { + fmt.Print(engine.FormatTaskConfirmList("modify", tasks, ws)) + fmt.Println("Dry run — no changes made.") + return nil + } + // Confirm if multiple tasks or no filters specified if len(tasks) > 1 || len(filterArgs) == 0 { fmt.Print(engine.FormatTaskConfirmList("modify", tasks, ws)) diff --git a/opal-task/cmd/root.go b/opal-task/cmd/root.go index ee46380..33ca0b5 100644 --- a/opal-task/cmd/root.go +++ b/opal-task/cmd/root.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "os" + "strings" "git.jnss.me/joakim/opal/internal/engine" "github.com/spf13/cobra" @@ -25,6 +26,7 @@ const parsedArgsKey contextKey = "parsedArgs" var ( configDirFlag string dataDirFlag string + dryRunFlag bool ) // Command classification @@ -120,6 +122,7 @@ func getParsedArgs(cmd *cobra.Command) *ParsedArgs { // preprocessArgs parses command-line arguments before Cobra routing // Returns: command name, filters, modifiers +// Flags (--foo) are stripped from filters/modifiers; Cobra handles them from os.Args. func preprocessArgs(args []string) *ParsedArgs { if len(args) == 0 { return &ParsedArgs{ @@ -129,11 +132,14 @@ func preprocessArgs(args []string) *ParsedArgs { } } - // Find command position (check both regular commands and reports) + // Find command position, skipping flag-like args cmdIdx := -1 cmdName := "" for i, arg := range args { + if strings.HasPrefix(arg, "-") { + continue // Skip flags — Cobra handles them + } // Check regular commands for _, name := range commandNames { if arg == name { @@ -161,30 +167,26 @@ func preprocessArgs(args []string) *ParsedArgs { if cmdIdx == -1 { return &ParsedArgs{ Command: "list", - Filters: args, + Filters: stripFlags(args), Modifiers: []string{}, } } // Split arguments around command - leftArgs := args[:cmdIdx] // Everything before command + leftArgs := stripFlags(args[:cmdIdx]) rightArgs := []string{} if cmdIdx+1 < len(args) { - rightArgs = args[cmdIdx+1:] // Everything after command + rightArgs = stripFlags(args[cmdIdx+1:]) } // Determine how to interpret right args if commandsWithModifiers[cmdName] { - // Command accepts modifiers - // Left = filters, Right = modifiers return &ParsedArgs{ Command: cmdName, Filters: leftArgs, Modifiers: rightArgs, } } else { - // Command doesn't accept modifiers - // Both left and right are filters allFilters := append(leftArgs, rightArgs...) return &ParsedArgs{ Command: cmdName, @@ -194,12 +196,25 @@ func preprocessArgs(args []string) *ParsedArgs { } } +// stripFlags removes flag-like args (starting with -) from a slice +func stripFlags(args []string) []string { + var result []string + for _, arg := range args { + if !strings.HasPrefix(arg, "-") { + result = append(result, arg) + } + } + return result +} + func init() { // Add persistent flags for directory overrides rootCmd.PersistentFlags().StringVar(&configDirFlag, "config-dir", "", "Config directory (default: $XDG_CONFIG_HOME/opal or ~/.config/opal)") rootCmd.PersistentFlags().StringVar(&dataDirFlag, "data-dir", "", "Data directory (default: $XDG_DATA_HOME/opal or ~/.local/share/opal)") + rootCmd.PersistentFlags().BoolVar(&dryRunFlag, "dry-run", false, + "Show matched tasks without performing the action") // Use PersistentPreRun for initialization (runs for all subcommands unless overridden) rootCmd.PersistentPreRun = func(cmd *cobra.Command, args []string) { diff --git a/opal-task/cmd/start.go b/opal-task/cmd/start.go index 9f08a74..9d37c31 100644 --- a/opal-task/cmd/start.go +++ b/opal-task/cmd/start.go @@ -47,6 +47,12 @@ func startTasks(args []string) error { return fmt.Errorf("no tasks matched filter") } + if dryRunFlag { + fmt.Print(engine.FormatTaskConfirmList("start", tasks, ws)) + fmt.Println("Dry run — no changes made.") + return nil + } + for _, task := range tasks { task.StartTask() fmt.Printf("Started task: %s\n", task.Description) diff --git a/opal-task/cmd/stop.go b/opal-task/cmd/stop.go index f7bfb51..4f86d34 100644 --- a/opal-task/cmd/stop.go +++ b/opal-task/cmd/stop.go @@ -47,6 +47,12 @@ func stopTasks(args []string) error { return fmt.Errorf("no tasks matched filter") } + if dryRunFlag { + fmt.Print(engine.FormatTaskConfirmList("stop", tasks, ws)) + fmt.Println("Dry run — no changes made.") + return nil + } + for _, task := range tasks { task.StopTask() fmt.Printf("Stopped task: %s\n", task.Description)