feat(task): implement add command

Amolith created

Supports all task metadata flags:
- area/goal (resolved from config keys)
- status, priority, estimate, motivation
- important/urgent (Eisenhower matrix)
- schedule date, note

Name and note support stdin via "-".

Assisted-by: Claude Opus 4.5 via Crush

Change summary

cmd/task/add.go | 205 ++++++++++++++++++++++++++++++++++++++++++++++++--
1 file changed, 194 insertions(+), 11 deletions(-)

Detailed changes

cmd/task/add.go 🔗

@@ -5,12 +5,26 @@
 package task
 
 import (
+	"bufio"
+	"errors"
 	"fmt"
+	"os"
+	"strings"
 
+	"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/ui"
 	"github.com/spf13/cobra"
 )
 
+// ErrUnknownGoal indicates the specified goal key was not found in config.
+var ErrUnknownGoal = errors.New("unknown goal key")
+
+// ErrNoInput indicates no input was provided on stdin.
+var ErrNoInput = errors.New("no input provided on stdin")
+
 // AddCmd creates a new task. Exported for use by the add shortcut.
 var AddCmd = &cobra.Command{
 	Use:   "add NAME",
@@ -20,13 +34,7 @@ var AddCmd = &cobra.Command{
 The task name is required. Use flags to set additional properties.
 Use "-" as NAME to read the task name from stdin.`,
 	Args: cobra.MinimumNArgs(1),
-	RunE: func(cmd *cobra.Command, args []string) error {
-		// TODO: implement task creation
-		name := args[0]
-		fmt.Fprintf(cmd.OutOrStdout(), "Creating task: %s (not yet implemented)\n", name)
-
-		return nil
-	},
+	RunE: runAdd,
 }
 
 func init() {
@@ -37,8 +45,11 @@ func init() {
 	AddCmd.Flags().IntP("priority", "p", 0, "Priority: -2 to 2")
 	AddCmd.Flags().IntP("estimate", "e", 0, "Estimate in minutes (0-720)")
 	AddCmd.Flags().StringP("motivation", "m", "", "Motivation: must, should, want")
-	AddCmd.Flags().Int("eisenhower", 0, "Eisenhower quadrant: 1-4")
-	AddCmd.Flags().String("schedule", "", "Schedule date (natural language)")
+	AddCmd.Flags().Bool("important", false, "Mark as important (Eisenhower matrix)")
+	AddCmd.Flags().Bool("not-important", false, "Mark as not important")
+	AddCmd.Flags().Bool("urgent", false, "Mark as urgent (Eisenhower matrix)")
+	AddCmd.Flags().Bool("not-urgent", false, "Mark as not urgent")
+	AddCmd.Flags().String("schedule", "", "Schedule date (YYYY-MM-DD)")
 
 	_ = AddCmd.RegisterFlagCompletionFunc("area", completion.Areas)
 	_ = AddCmd.RegisterFlagCompletionFunc("goal", completion.Goals)
@@ -46,6 +57,178 @@ func init() {
 		completion.Static("later", "next", "started", "waiting"))
 	_ = AddCmd.RegisterFlagCompletionFunc("motivation",
 		completion.Static("must", "should", "want"))
-	_ = AddCmd.RegisterFlagCompletionFunc("eisenhower",
-		completion.Static("1", "2", "3", "4"))
+}
+
+func runAdd(cmd *cobra.Command, args []string) error {
+	name, err := resolveName(args[0])
+	if err != nil {
+		return err
+	}
+
+	apiClient, err := client.New()
+	if err != nil {
+		return err
+	}
+
+	builder := apiClient.NewTask(name)
+
+	if err := applyAreaAndGoal(cmd, builder); err != nil {
+		return err
+	}
+
+	if err := applyOptionalFlags(cmd, builder); err != nil {
+		return err
+	}
+
+	task, err := builder.Create(cmd.Context())
+	if err != nil {
+		fmt.Fprintln(cmd.ErrOrStderr(), ui.Error.Render("Failed to create task"))
+
+		return err
+	}
+
+	fmt.Fprintln(cmd.OutOrStdout(), ui.Success.Render("Created task: "+task.ID))
+
+	return nil
+}
+
+func resolveName(arg string) (string, error) {
+	if arg != "-" {
+		return arg, nil
+	}
+
+	scanner := bufio.NewScanner(os.Stdin)
+	if scanner.Scan() {
+		return strings.TrimSpace(scanner.Text()), nil
+	}
+
+	if err := scanner.Err(); err != nil {
+		return "", fmt.Errorf("reading stdin: %w", err)
+	}
+
+	return "", ErrNoInput
+}
+
+func applyAreaAndGoal(cmd *cobra.Command, builder *lunatask.TaskBuilder) 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 {
+		if errors.Is(err, config.ErrNotFound) {
+			fmt.Fprintln(cmd.ErrOrStderr(), ui.Error.Render("Config not found; run 'lune init' to configure areas"))
+		}
+
+		return err
+	}
+
+	area := cfg.AreaByKey(areaKey)
+	if area == nil {
+		fmt.Fprintln(cmd.ErrOrStderr(), ui.Error.Render("Unknown area: "+areaKey))
+
+		return fmt.Errorf("%w: %s", ErrUnknownArea, areaKey)
+	}
+
+	builder.InArea(area.ID)
+
+	if goalKey == "" {
+		return nil
+	}
+
+	goal := area.GoalByKey(goalKey)
+	if goal == nil {
+		fmt.Fprintln(cmd.ErrOrStderr(), ui.Error.Render("Unknown goal: "+goalKey))
+
+		return fmt.Errorf("%w: %s", ErrUnknownGoal, goalKey)
+	}
+
+	builder.InGoal(goal.ID)
+
+	return nil
+}
+
+func applyOptionalFlags(cmd *cobra.Command, builder *lunatask.TaskBuilder) 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().GetInt("priority"); priority != 0 {
+		builder.WithPriority(priority)
+	}
+
+	if estimate, _ := cmd.Flags().GetInt("estimate"); estimate != 0 {
+		builder.WithEstimate(estimate)
+	}
+
+	if motivation, _ := cmd.Flags().GetString("motivation"); motivation != "" {
+		builder.WithMotivation(lunatask.Motivation(motivation))
+	}
+
+	applyEisenhower(cmd, builder)
+
+	return applySchedule(cmd, builder)
+}
+
+func applyEisenhower(cmd *cobra.Command, builder *lunatask.TaskBuilder) {
+	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 applySchedule(cmd *cobra.Command, builder *lunatask.TaskBuilder) error {
+	schedule, _ := cmd.Flags().GetString("schedule")
+	if schedule == "" {
+		return nil
+	}
+
+	date, err := lunatask.ParseDate(schedule)
+	if err != nil {
+		fmt.Fprintln(cmd.ErrOrStderr(), ui.Error.Render("Invalid date format: "+schedule))
+
+		return fmt.Errorf("parsing schedule date: %w", err)
+	}
+
+	builder.ScheduledOn(date)
+
+	return nil
+}
+
+func resolveNote(note string) (string, error) {
+	if note != "-" {
+		return note, nil
+	}
+
+	data, err := os.ReadFile("/dev/stdin")
+	if err != nil {
+		return "", fmt.Errorf("reading stdin: %w", err)
+	}
+
+	return strings.TrimSpace(string(data)), nil
 }