diff --git a/.golangci.yaml b/.golangci.yaml index 5bc1f83b89e57acda1d65416c08be1cb0c6cf7b7..14cb3d2a45d8cf7f2dd399b14ab486a94ac13c6f 100644 --- a/.golangci.yaml +++ b/.golangci.yaml @@ -145,6 +145,7 @@ linters: - godox # TODOs are intentional placeholders - wrapcheck # CLI returns errors for display, wrapping adds noise - mnd # Magic numbers in cobra.ExactArgs are clear in context + - dupl # Builder types differ but share method signatures - path: cmd/ text: unused-parameter # Cobra callback signatures can't be changed - path: internal/ui/ diff --git a/AGENTS.md b/AGENTS.md index 5d4310403bd65dc7b232224b8ee845105d65bb25..898a3bb9f6ad9ba7f2f2bf90bd1d18e3f2f25ef6 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -108,6 +108,8 @@ The `.golangci.yaml` disables several linters for `cmd/`: - `errcheck`, `godox`: Stub implementations deliberately print without checking, TODOs are placeholders - `wrapcheck`: CLI returns errors for display; wrapping adds noise +- `dupl`: Builder types (`TaskBuilder`, `TaskUpdateBuilder`) share method + signatures but differ in type; duplication is structural These patterns are intentional. diff --git a/cmd/task/update.go b/cmd/task/update.go index 216b48c329122c8f90406bbd9eaa0dc686ad4fe9..37da39a8b624379bb3870c34f2e635449cd67f9d 100644 --- a/cmd/task/update.go +++ b/cmd/task/update.go @@ -7,7 +7,12 @@ package task import ( "fmt" + "git.secluded.site/go-lunatask" + "git.secluded.site/lune/internal/client" "git.secluded.site/lune/internal/completion" + "git.secluded.site/lune/internal/config" + "git.secluded.site/lune/internal/dateutil" + "git.secluded.site/lune/internal/ui" "git.secluded.site/lune/internal/validate" "github.com/spf13/cobra" ) @@ -16,38 +21,192 @@ import ( var UpdateCmd = &cobra.Command{ Use: "update ID", Short: "Update a task", - Args: cobra.ExactArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - id, err := validate.Reference(args[0]) - if err != nil { - return err - } - - // TODO: implement task update - fmt.Fprintf(cmd.OutOrStdout(), "Updating task %s (not yet implemented)\n", id) + Long: `Update an existing task in Lunatask. - return nil - }, +Accepts a UUID or lunatask:// deep link. +Only specified flags are modified; other fields remain unchanged.`, + Args: cobra.ExactArgs(1), + RunE: runUpdate, } func init() { - UpdateCmd.Flags().String("name", "", "New task name") + UpdateCmd.Flags().String("name", "", "New task name (use - for stdin)") UpdateCmd.Flags().StringP("area", "a", "", "Move to area key") UpdateCmd.Flags().StringP("goal", "g", "", "Move to goal key") UpdateCmd.Flags().StringP("status", "s", "", "Status: later, next, started, waiting, completed") UpdateCmd.Flags().StringP("note", "n", "", "Task note (use - for stdin)") - UpdateCmd.Flags().IntP("priority", "p", 0, "Priority: -2 to 2") + UpdateCmd.Flags().StringP("priority", "p", "", "Priority: lowest, low, normal, high, highest") UpdateCmd.Flags().IntP("estimate", "e", 0, "Estimate in minutes (0-720)") UpdateCmd.Flags().StringP("motivation", "m", "", "Motivation: must, should, want") - UpdateCmd.Flags().Int("eisenhower", 0, "Eisenhower quadrant: 1-4") + UpdateCmd.Flags().Bool("important", false, "Mark as important (Eisenhower matrix)") + UpdateCmd.Flags().Bool("not-important", false, "Mark as not important") + UpdateCmd.Flags().Bool("urgent", false, "Mark as urgent (Eisenhower matrix)") + UpdateCmd.Flags().Bool("not-urgent", false, "Mark as not urgent") UpdateCmd.Flags().String("schedule", "", "Schedule date (natural language)") _ = UpdateCmd.RegisterFlagCompletionFunc("area", completion.Areas) _ = UpdateCmd.RegisterFlagCompletionFunc("goal", completion.Goals) _ = UpdateCmd.RegisterFlagCompletionFunc("status", completion.Static("later", "next", "started", "waiting", "completed")) + _ = UpdateCmd.RegisterFlagCompletionFunc("priority", + completion.Static("lowest", "low", "normal", "high", "highest")) _ = UpdateCmd.RegisterFlagCompletionFunc("motivation", completion.Static("must", "should", "want")) - _ = UpdateCmd.RegisterFlagCompletionFunc("eisenhower", - completion.Static("1", "2", "3", "4")) +} + +func runUpdate(cmd *cobra.Command, args []string) error { + id, err := validate.Reference(args[0]) + if err != nil { + return err + } + + apiClient, err := client.New() + if err != nil { + return err + } + + builder := apiClient.NewTaskUpdate(id) + + if err := applyUpdateName(cmd, builder); err != nil { + return err + } + + if err := applyUpdateAreaAndGoal(cmd, builder); err != nil { + return err + } + + if err := applyUpdateFlags(cmd, builder); err != nil { + return err + } + + task, err := builder.Update(cmd.Context()) + if err != nil { + return err + } + + fmt.Fprintln(cmd.OutOrStdout(), ui.Success.Render("Updated task: "+task.ID)) + + return nil +} + +func applyUpdateName(cmd *cobra.Command, builder *lunatask.TaskUpdateBuilder) error { + name, _ := cmd.Flags().GetString("name") + if name == "" { + return nil + } + + resolved, err := resolveName(name) + if err != nil { + return err + } + + builder.Name(resolved) + + return nil +} + +func applyUpdateAreaAndGoal(cmd *cobra.Command, builder *lunatask.TaskUpdateBuilder) error { + areaKey, _ := cmd.Flags().GetString("area") + goalKey, _ := cmd.Flags().GetString("goal") + + if areaKey == "" && goalKey == "" { + return nil + } + + if areaKey == "" && goalKey != "" { + fmt.Fprintln(cmd.ErrOrStderr(), ui.Warning.Render("Goal specified without area; ignoring")) + + return nil + } + + cfg, err := config.Load() + if err != nil { + return err + } + + area := cfg.AreaByKey(areaKey) + if area == nil { + return fmt.Errorf("%w: %s", ErrUnknownArea, areaKey) + } + + builder.InArea(area.ID) + + if goalKey == "" { + return nil + } + + goal := area.GoalByKey(goalKey) + if goal == nil { + return fmt.Errorf("%w: %s", ErrUnknownGoal, goalKey) + } + + builder.InGoal(goal.ID) + + return nil +} + +func applyUpdateFlags(cmd *cobra.Command, builder *lunatask.TaskUpdateBuilder) error { + if status, _ := cmd.Flags().GetString("status"); status != "" { + builder.WithStatus(lunatask.TaskStatus(status)) + } + + if note, _ := cmd.Flags().GetString("note"); note != "" { + resolved, err := resolveNote(note) + if err != nil { + return err + } + + builder.WithNote(resolved) + } + + if priority, _ := cmd.Flags().GetString("priority"); priority != "" { + p, err := lunatask.ParsePriority(priority) + if err != nil { + return err + } + + builder.Priority(p) + } + + if estimate, _ := cmd.Flags().GetInt("estimate"); estimate != 0 { + builder.WithEstimate(estimate) + } + + if motivation, _ := cmd.Flags().GetString("motivation"); motivation != "" { + builder.WithMotivation(lunatask.Motivation(motivation)) + } + + applyUpdateEisenhower(cmd, builder) + + return applyUpdateSchedule(cmd, builder) +} + +func applyUpdateEisenhower(cmd *cobra.Command, builder *lunatask.TaskUpdateBuilder) { + if important, _ := cmd.Flags().GetBool("important"); important { + builder.Important() + } else if notImportant, _ := cmd.Flags().GetBool("not-important"); notImportant { + builder.NotImportant() + } + + if urgent, _ := cmd.Flags().GetBool("urgent"); urgent { + builder.Urgent() + } else if notUrgent, _ := cmd.Flags().GetBool("not-urgent"); notUrgent { + builder.NotUrgent() + } +} + +func applyUpdateSchedule(cmd *cobra.Command, builder *lunatask.TaskUpdateBuilder) error { + schedule, _ := cmd.Flags().GetString("schedule") + if schedule == "" { + return nil + } + + date, err := dateutil.Parse(schedule) + if err != nil { + return err + } + + builder.ScheduledOn(date) + + return nil }