f57baee6bc
IMP-5: Replace strings.Contains(arg, ":") heuristic with an allowlist of recognized attribute keys (ValidAttributeKeys). Colons in task descriptions (URLs, "Meeting: topic") are no longer misinterpreted as modifiers. Canonical key sets live in engine/keys.go and are shared across parseAddArgs, ParseFilter, and ParseModifier. ParseModifier now errors on unknown keys. IMP-4: delete command now loads the working set and resolves display IDs via GetTaskByDisplayID, matching the pattern used by done/modify. IMP-6: All action commands (done, delete, modify, start, stop) now return an error on no-match (stderr, exit 1). Previously done/delete printed to stdout and exited 0; start/stop had no check at all. Also adds requirements and design docs for the CLI UX improvements. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
118 lines
3.0 KiB
Go
118 lines
3.0 KiB
Go
package cmd
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"strings"
|
|
|
|
"git.jnss.me/joakim/opal/internal/engine"
|
|
"github.com/spf13/cobra"
|
|
)
|
|
|
|
var addCmd = &cobra.Command{
|
|
Use: "add <description> [modifiers...]",
|
|
Short: "Add a new task",
|
|
Long: `Add a new task with optional modifiers.
|
|
|
|
Examples:
|
|
opal add buy groceries # No quotes needed!
|
|
opal add review PR priority:H project:backend
|
|
opal add buy groceries +shop carrots # Tag can be anywhere
|
|
opal add team meeting due:mon recur:1w +meetings`,
|
|
Run: func(cmd *cobra.Command, args []string) {
|
|
parsed := getParsedArgs(cmd)
|
|
// For add, combine filters and modifiers (all are args to parse)
|
|
allArgs := append(parsed.Filters, parsed.Modifiers...)
|
|
if err := addTask(allArgs); err != nil {
|
|
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
},
|
|
}
|
|
|
|
func addTask(args []string) error {
|
|
// Parse description and modifiers from args
|
|
// Description = all words that are NOT filters/modifiers
|
|
// Filters/Modifiers = words with +, -, or containing :
|
|
description, modifierArgs, err := parseAddArgs(args)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Parse modifiers
|
|
var mod *engine.Modifier
|
|
|
|
if len(modifierArgs) > 0 {
|
|
mod, err = engine.ParseModifier(modifierArgs)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to parse modifiers: %w", err)
|
|
}
|
|
}
|
|
|
|
// Check if this is a recurring task
|
|
isRecurring := mod != nil && mod.SetAttributes["recur"] != nil
|
|
|
|
if isRecurring {
|
|
// Create recurring task (template + first instance)
|
|
return addRecurringTask(description, mod)
|
|
}
|
|
|
|
// Create regular task
|
|
task, err := engine.CreateTaskWithModifier(description, mod)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create task: %w", err)
|
|
}
|
|
|
|
fmt.Printf("Created task %s\n", task.UUID)
|
|
if len(task.Tags) > 0 {
|
|
fmt.Printf("Tags: %s\n", strings.Join(task.Tags, ", "))
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// parseAddArgs extracts description and modifiers from args.
|
|
// Tags (+tag, -tag) are always modifiers. For key:value tokens, only
|
|
// recognized attribute keys (engine.ValidAttributeKeys) are treated as
|
|
// modifiers — everything else becomes part of the description.
|
|
func parseAddArgs(args []string) (string, []string, error) {
|
|
var descParts []string
|
|
var modifiers []string
|
|
|
|
for _, arg := range args {
|
|
if strings.HasPrefix(arg, "+") || strings.HasPrefix(arg, "-") {
|
|
modifiers = append(modifiers, arg)
|
|
continue
|
|
}
|
|
|
|
if idx := strings.Index(arg, ":"); idx > 0 {
|
|
key := arg[:idx]
|
|
if engine.ValidAttributeKeys[key] {
|
|
modifiers = append(modifiers, arg)
|
|
continue
|
|
}
|
|
}
|
|
|
|
descParts = append(descParts, arg)
|
|
}
|
|
|
|
if len(descParts) == 0 {
|
|
return "", nil, fmt.Errorf("description is required")
|
|
}
|
|
|
|
description := strings.Join(descParts, " ")
|
|
return description, modifiers, nil
|
|
}
|
|
|
|
func addRecurringTask(description string, mod *engine.Modifier) error {
|
|
instance, err := engine.CreateRecurringTask(description, mod)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
fmt.Printf("Created recurring task %s\n", *instance.ParentUUID)
|
|
fmt.Printf("First instance: %s\n", instance.UUID)
|
|
|
|
return nil
|
|
}
|