Detailed changes
@@ -5,8 +5,14 @@
package t
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/task"
"github.com/spf13/cobra"
)
@@ -14,9 +20,7 @@ var aCmd = &cobra.Command{
Use: "a",
Short: "Add tasks",
Long: `Add one or more tasks to the session`,
- Run: func(cmd *cobra.Command, args []string) {
- fmt.Println("[STUB] Add tasks to current session")
- },
+ RunE: runAddTasks,
}
func init() {
@@ -27,3 +31,133 @@ func init() {
_ = aCmd.MarkFlagRequired("title")
_ = aCmd.MarkFlagRequired("description")
}
+
+func runAddTasks(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
+ }
+
+ titles, err := cmd.Flags().GetStringArray("title")
+ if err != nil {
+ return err
+ }
+ descriptions, err := cmd.Flags().GetStringArray("description")
+ if err != nil {
+ return err
+ }
+
+ if len(titles) != len(descriptions) {
+ _, _ = fmt.Fprintln(cmd.OutOrStdout(), "Number of titles and descriptions must match.")
+ return nil
+ }
+ if len(titles) == 0 {
+ _, _ = fmt.Fprintln(cmd.OutOrStdout(), "Provide at least one task with -t title -d description.")
+ return nil
+ }
+
+ type taskInput struct {
+ title string
+ description string
+ id string
+ }
+
+ var inputs []taskInput
+ for i := range titles {
+ title := strings.TrimSpace(titles[i])
+ description := strings.TrimSpace(descriptions[i])
+
+ if title == "" {
+ _, _ = fmt.Fprintf(cmd.OutOrStdout(), "Task title at position %d cannot be empty.\n", i+1)
+ return nil
+ }
+
+ id := task.GenerateID(sessionDoc.SID, title, description)
+ inputs = append(inputs, taskInput{
+ title: title,
+ description: description,
+ id: id,
+ })
+ }
+
+ var lastUpdated task.Task
+ var addedCount int
+
+ err = env.DB.Update(cmd.Context(), func(txn *db.Txn) error {
+ taskTxn := env.TaskStore.WithTxn(txn)
+ eventTxn := env.EventStore.WithTxn(txn)
+ sessionTxn := env.SessionStore.WithTxn(txn)
+
+ for _, input := range inputs {
+ exists, err := taskTxn.Exists(sessionDoc.SID, input.id)
+ if err != nil {
+ return err
+ }
+ if exists {
+ continue
+ }
+
+ seq, err := eventTxn.LatestSequence(sessionDoc.SID)
+ if err != nil {
+ return err
+ }
+ nextSeq := seq + 1
+
+ created, err := taskTxn.Create(sessionDoc.SID, task.CreateParams{
+ ID: input.id,
+ Title: input.title,
+ Description: input.description,
+ Status: task.StatusPending,
+ CreatedSeq: nextSeq,
+ })
+ if err != nil {
+ return err
+ }
+
+ _, err = eventTxn.Append(sessionDoc.SID, event.BuildTaskAdded(shared.CommandString(), "", created))
+ if err != nil {
+ return err
+ }
+
+ lastUpdated = created
+ addedCount++
+ }
+
+ if addedCount > 0 {
+ _, err := sessionTxn.TouchAt(sessionDoc.SID, lastUpdated.UpdatedAt)
+ if err != nil {
+ return err
+ }
+ }
+
+ return nil
+ })
+ if err != nil {
+ if errors.Is(err, task.ErrEmptyTitle) {
+ _, _ = fmt.Fprintln(cmd.OutOrStdout(), "Task title cannot be empty.")
+ return nil
+ }
+ return err
+ }
+
+ if _, err := shared.PrintPlan(cmd, env, sessionDoc.SID); err != nil {
+ return err
+ }
+
+ if addedCount > 0 {
+ out := cmd.OutOrStdout()
+ _, _ = fmt.Fprintln(out, "")
+ _, _ = fmt.Fprintf(out, "Added %d task(s).\n", addedCount)
+ _, _ = fmt.Fprintln(out, "Update task statuses with `np t u -i task-id -s in_progress|completed|failed|cancelled` as you work.")
+ }
+
+ return nil
+}
@@ -7,6 +7,9 @@ package t
import (
"fmt"
+ "git.secluded.site/np/cmd/shared"
+ "git.secluded.site/np/internal/cli"
+ "git.secluded.site/np/internal/task"
"github.com/spf13/cobra"
)
@@ -14,16 +17,58 @@ var TCmd = &cobra.Command{
Use: "t",
Short: "Task commands",
Long: `Manage session tasks`,
- Run: func(cmd *cobra.Command, args []string) {
- fmt.Println("[STUB] Display remaining tasks and descriptions")
- fmt.Println("Legend: ☐ pending ⟳ in progress ☑ completed")
- fmt.Println("☐ Example pending task [a1b2c3d4]")
- fmt.Println(" Example task description")
- fmt.Println("⟳ Example in-progress task [e5f6g7h8]")
- fmt.Println(" Another task description")
- },
+ RunE: runListTasks,
}
func init() {
TCmd.Flags().StringP("status", "s", "all", "Filter tasks by status (pending, in_progress, completed, all, etc.)")
}
+
+func runListTasks(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
+ }
+
+ statusFlag, err := cmd.Flags().GetString("status")
+ if err != nil {
+ return err
+ }
+
+ var statusFilter task.Status
+ if statusFlag != "" && statusFlag != "all" {
+ statusFilter, err = task.ParseStatus(statusFlag)
+ if err != nil {
+ _, _ = fmt.Fprintf(cmd.OutOrStdout(), "Invalid status %q. Valid: pending, in_progress, completed, failed, cancelled, all.\n", statusFlag)
+ return nil
+ }
+ }
+
+ tasks, err := env.LoadTasksByStatus(cmd.Context(), sessionDoc.SID, statusFilter)
+ if err != nil {
+ return err
+ }
+
+ goalDoc, ok, err := env.LoadGoal(cmd.Context(), sessionDoc.SID)
+ if err != nil {
+ return err
+ }
+
+ state := cli.PlanState{
+ Tasks: tasks,
+ }
+ if ok {
+ state.Goal = &goalDoc
+ }
+
+ _, _ = fmt.Fprintln(cmd.OutOrStdout(), cli.RenderPlan(state))
+ return nil
+}
@@ -5,25 +5,185 @@
package t
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/task"
"github.com/spf13/cobra"
)
var uCmd = &cobra.Command{
Use: "u",
Short: "Update task",
- Long: `Update a task's status`,
- Run: func(cmd *cobra.Command, args []string) {
- fmt.Println("[STUB] Update task status")
- },
+ Long: `Update a task's status, title, or description`,
+ RunE: runUpdateTask,
}
func init() {
TCmd.AddCommand(uCmd)
uCmd.Flags().StringP("id", "i", "", "Task ID (required)")
- uCmd.Flags().StringP("status", "s", "", "Task status (required)")
+ uCmd.Flags().StringP("status", "s", "", "New task status")
+ uCmd.Flags().StringP("title", "t", "", "New task title")
+ uCmd.Flags().StringP("description", "d", "", "New task description")
+ uCmd.Flags().StringP("reason", "r", "", "Reason for update (required for title/description changes and status=cancelled/failed)")
_ = uCmd.MarkFlagRequired("id")
- _ = uCmd.MarkFlagRequired("status")
+}
+
+func runUpdateTask(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
+ }
+
+ taskID, err := cmd.Flags().GetString("id")
+ if err != nil {
+ return err
+ }
+ taskID = strings.TrimSpace(taskID)
+
+ current, err := env.TaskStore.Get(cmd.Context(), sessionDoc.SID, taskID)
+ if err != nil {
+ if errors.Is(err, task.ErrNotFound) {
+ _, _ = fmt.Fprintf(cmd.OutOrStdout(), "Task %q not found in current session.\n", taskID)
+ 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
+ }
+ statusInput, err := cmd.Flags().GetString("status")
+ if err != nil {
+ return err
+ }
+ reasonInput, err := cmd.Flags().GetString("reason")
+ if err != nil {
+ return err
+ }
+
+ newTitle := current.Title
+ titleChanged := cmd.Flags().Changed("title")
+ if titleChanged {
+ newTitle = strings.TrimSpace(titleInput)
+ if newTitle == "" {
+ _, _ = fmt.Fprintln(cmd.OutOrStdout(), "Task title cannot be empty.")
+ return nil
+ }
+ }
+
+ newDescription := current.Description
+ descriptionChanged := cmd.Flags().Changed("description")
+ if descriptionChanged {
+ newDescription = strings.TrimSpace(descInput)
+ }
+
+ var newStatus task.Status
+ statusChanged := cmd.Flags().Changed("status")
+ if statusChanged {
+ newStatus, err = task.ParseStatus(statusInput)
+ if err != nil {
+ _, _ = fmt.Fprintf(cmd.OutOrStdout(), "Invalid status %q. Valid: pending, in_progress, completed, failed, cancelled.\n", statusInput)
+ return nil
+ }
+ } else {
+ newStatus = current.Status
+ }
+
+ if !titleChanged && !descriptionChanged && !statusChanged {
+ _, _ = fmt.Fprintln(cmd.OutOrStdout(), "Provide at least one of --title, --description, or --status to update the task.")
+ return nil
+ }
+
+ reason := strings.TrimSpace(reasonInput)
+ reasonRequired := titleChanged || descriptionChanged ||
+ (statusChanged && (newStatus == task.StatusCancelled || newStatus == task.StatusFailed))
+
+ if reasonRequired && reason == "" {
+ _, _ = fmt.Fprintln(cmd.OutOrStdout(), "Reason (-r/--reason) is required for title/description updates or when changing status to cancelled/failed.")
+ return nil
+ }
+
+ contentChanged := titleChanged || descriptionChanged
+ statusOnlyChanged := statusChanged && !contentChanged
+
+ var updated task.Task
+ err = env.DB.Update(cmd.Context(), func(txn *db.Txn) error {
+ taskTxn := env.TaskStore.WithTxn(txn)
+ eventTxn := env.EventStore.WithTxn(txn)
+ sessionTxn := env.SessionStore.WithTxn(txn)
+
+ before, err := taskTxn.Get(sessionDoc.SID, taskID)
+ if err != nil {
+ return err
+ }
+
+ updated, err = taskTxn.Update(sessionDoc.SID, taskID, func(t *task.Task) error {
+ if titleChanged {
+ t.Title = newTitle
+ }
+ if descriptionChanged {
+ t.Description = newDescription
+ }
+ if statusChanged {
+ t.Status = newStatus
+ }
+ return nil
+ })
+ if err != nil {
+ return err
+ }
+
+ if contentChanged {
+ _, err = eventTxn.Append(sessionDoc.SID, event.BuildTaskUpdated(shared.CommandString(), reason, taskID, before, updated))
+ if err != nil {
+ return err
+ }
+ }
+
+ if statusChanged {
+ _, err = eventTxn.Append(sessionDoc.SID, event.BuildTaskStatusChanged(shared.CommandString(), reason, before, updated))
+ if err != nil {
+ return err
+ }
+ }
+
+ _, err = sessionTxn.TouchAt(sessionDoc.SID, updated.UpdatedAt)
+ 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, "")
+ if statusOnlyChanged && newStatus == task.StatusCompleted {
+ _, _ = fmt.Fprintln(out, "Task marked completed. Continue working through remaining tasks with `np t u`.")
+ } else {
+ _, _ = fmt.Fprintln(out, "Task updated. Use `np p` to review the full plan.")
+ }
+
+ return nil
}