diff --git a/cmd/g/g.go b/cmd/g/g.go index 1e10ab72800fda0b32e94fcff95bb64f382e2e3a..7bf1604ce7d4272c2e5e7b21eecfebf9ef7c5b60 100644 --- a/cmd/g/g.go +++ b/cmd/g/g.go @@ -7,6 +7,7 @@ package g import ( "fmt" + "git.secluded.site/np/cmd/shared" "github.com/spf13/cobra" ) @@ -14,9 +15,31 @@ var GCmd = &cobra.Command{ Use: "g", Short: "Goal commands", Long: `Manage the session goal`, - Run: func(cmd *cobra.Command, args []string) { - fmt.Println("[STUB] Display goal and description") - fmt.Println("Goal: Example goal title") - fmt.Println("Description: Example goal description") - }, + RunE: runShowGoal, +} + +func runShowGoal(cmd *cobra.Command, _ []string) error { + env, err := shared.Environment(cmd) + if err != nil { + return err + } + + sessionDoc, found, err := shared.ActiveSession(cmd, env) + if err != nil { + return err + } + if !found { + return nil + } + + state, err := shared.PrintPlan(cmd, env, sessionDoc.SID) + if err != nil { + return err + } + + if state.Goal == nil { + fmt.Fprintln(cmd.OutOrStdout(), "") + fmt.Fprintln(cmd.OutOrStdout(), "Set the goal with `np g s -t \"goal title\" -d \"goal description\"` to begin.") + } + return nil } diff --git a/cmd/g/s.go b/cmd/g/s.go index bf6dc6137d1a8206f5882c4ea91d62fe7b79f5ad..c3c7013452bb6445b091a6d662b05c3447c2e22b 100644 --- a/cmd/g/s.go +++ b/cmd/g/s.go @@ -6,7 +6,12 @@ package g import ( "fmt" + "strings" + "git.secluded.site/np/cmd/shared" + "git.secluded.site/np/internal/db" + "git.secluded.site/np/internal/event" + "git.secluded.site/np/internal/goal" "github.com/spf13/cobra" ) @@ -14,9 +19,7 @@ var sCmd = &cobra.Command{ Use: "s", Short: "Set goal", Long: `Set the session goal with title and description`, - Run: func(cmd *cobra.Command, args []string) { - fmt.Println("[STUB] Set session goal with title and description") - }, + RunE: runSetGoal, } func init() { @@ -27,3 +30,82 @@ func init() { _ = sCmd.MarkFlagRequired("title") _ = sCmd.MarkFlagRequired("description") } + +func runSetGoal(cmd *cobra.Command, _ []string) error { + env, err := shared.Environment(cmd) + if err != nil { + return err + } + + sessionDoc, found, err := shared.ActiveSession(cmd, env) + if err != nil { + return err + } + if !found { + return nil + } + + title, err := cmd.Flags().GetString("title") + if err != nil { + return err + } + description, err := cmd.Flags().GetString("description") + if err != nil { + return err + } + + title = strings.TrimSpace(title) + description = strings.TrimSpace(description) + + if title == "" { + fmt.Fprintln(cmd.OutOrStdout(), "Goal title is required.") + return nil + } + if description == "" { + fmt.Fprintln(cmd.OutOrStdout(), "Goal description is required.") + return nil + } + + exists, err := env.GoalStore.Exists(cmd.Context(), sessionDoc.SID) + if err != nil { + return err + } + if exists { + fmt.Fprintln(cmd.OutOrStdout(), "Goal already set. Use 'np g u' to update it (requires -r/--reason flag).") + return nil + } + + var saved goal.Document + err = env.DB.Update(cmd.Context(), func(txn *db.Txn) error { + goalTxn := env.GoalStore.WithTxn(txn) + var err error + saved, err = goalTxn.Set(sessionDoc.SID, title, description) + if err != nil { + return err + } + + sessionTxn := env.SessionStore.WithTxn(txn) + if _, err := sessionTxn.TouchAt(sessionDoc.SID, saved.UpdatedAt); err != nil { + return err + } + + eventTxn := env.EventStore.WithTxn(txn) + _, err = eventTxn.Append(sessionDoc.SID, event.BuildGoalSet(shared.CommandString(), "", saved)) + return err + }) + if err != nil { + return err + } + + if _, err := shared.PrintPlan(cmd, env, sessionDoc.SID); err != nil { + return err + } + + out := cmd.OutOrStdout() + fmt.Fprintln(out, "") + fmt.Fprintln(out, "Study the goal and its description carefully.") + fmt.Fprintln(out, "Review referenced tickets and files to gather context before planning changes.") + fmt.Fprintln(out, "Add tasks with `np t a -t \"task title\" -d \"details\"` once you understand the approach.") + + return nil +} diff --git a/cmd/g/u.go b/cmd/g/u.go new file mode 100644 index 0000000000000000000000000000000000000000..a4983733dd67fd272795a8e4021fc0fd56142cd3 --- /dev/null +++ b/cmd/g/u.go @@ -0,0 +1,134 @@ +// SPDX-FileCopyrightText: Amolith +// +// SPDX-License-Identifier: AGPL-3.0-or-later + +package g + +import ( + "errors" + "fmt" + "strings" + + "git.secluded.site/np/cmd/shared" + "git.secluded.site/np/internal/db" + "git.secluded.site/np/internal/event" + "git.secluded.site/np/internal/goal" + "github.com/spf13/cobra" +) + +var uCmd = &cobra.Command{ + Use: "u", + Short: "Update goal", + Long: `Update the goal title or description (requires a reason)`, + RunE: runUpdateGoal, +} + +func init() { + GCmd.AddCommand(uCmd) + + uCmd.Flags().StringP("title", "t", "", "New goal title") + uCmd.Flags().StringP("description", "d", "", "New goal description") + uCmd.Flags().StringP("reason", "r", "", "Reason for updating the goal (required)") + _ = uCmd.MarkFlagRequired("reason") +} + +func runUpdateGoal(cmd *cobra.Command, _ []string) error { + env, err := shared.Environment(cmd) + if err != nil { + return err + } + + sessionDoc, found, err := shared.ActiveSession(cmd, env) + if err != nil { + return err + } + if !found { + return nil + } + + current, err := env.GoalStore.Get(cmd.Context(), sessionDoc.SID) + if err != nil { + if errors.Is(err, goal.ErrNotFound) { + fmt.Fprintln(cmd.OutOrStdout(), "No goal set yet. Use 'np g s' first.") + return nil + } + return err + } + + titleInput, err := cmd.Flags().GetString("title") + if err != nil { + return err + } + descInput, err := cmd.Flags().GetString("description") + if err != nil { + return err + } + reason, err := cmd.Flags().GetString("reason") + if err != nil { + return err + } + + reason = strings.TrimSpace(reason) + if reason == "" { + fmt.Fprintln(cmd.OutOrStdout(), "Reason is required for goal updates.") + return nil + } + + newTitle := current.Title + if cmd.Flags().Changed("title") { + newTitle = strings.TrimSpace(titleInput) + if newTitle == "" { + fmt.Fprintln(cmd.OutOrStdout(), "Goal title cannot be empty.") + return nil + } + } + + newDescription := current.Description + if cmd.Flags().Changed("description") { + newDescription = strings.TrimSpace(descInput) + } + + if !cmd.Flags().Changed("title") && !cmd.Flags().Changed("description") { + fmt.Fprintln(cmd.OutOrStdout(), "Provide at least one of --title or --description to update the goal.") + return nil + } + + if newTitle == current.Title && newDescription == current.Description { + fmt.Fprintln(cmd.OutOrStdout(), "Goal already matches the provided values; no changes made.") + return nil + } + + var updated goal.Document + err = env.DB.Update(cmd.Context(), func(txn *db.Txn) error { + goalTxn := env.GoalStore.WithTxn(txn) + + var err error + updated, err = goalTxn.Set(sessionDoc.SID, newTitle, newDescription) + if err != nil { + return err + } + + sessionTxn := env.SessionStore.WithTxn(txn) + if _, err := sessionTxn.TouchAt(sessionDoc.SID, updated.UpdatedAt); err != nil { + return err + } + + eventTxn := env.EventStore.WithTxn(txn) + _, err = eventTxn.Append(sessionDoc.SID, event.BuildGoalUpdated(shared.CommandString(), reason, current, updated)) + return err + }) + if err != nil { + return err + } + + if _, err := shared.PrintPlan(cmd, env, sessionDoc.SID); err != nil { + return err + } + + out := cmd.OutOrStdout() + fmt.Fprintln(out, "") + fmt.Fprintln(out, "Goal updated. Ensure pending tasks still align and adjust them if necessary.") + fmt.Fprintln(out, "Add or update tasks with `np t a` / `np t u` so the plan reflects the new direction.") + + return nil +}