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 <noreply@anthropic.com>
This commit is contained in:
2026-02-19 13:47:39 +01:00
parent b02c40f716
commit 6fb8a40a43
6 changed files with 53 additions and 8 deletions
+6
View File
@@ -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): ")
+6
View File
@@ -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): ")
+6
View File
@@ -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))
+23 -8
View File
@@ -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) {
+6
View File
@@ -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)
+6
View File
@@ -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)