From 6b756d92c351cb79bbcb27a5de65b7cd8e819f41 Mon Sep 17 00:00:00 2001 From: Tink bot Date: Tue, 26 May 2026 22:40:32 +0200 Subject: [PATCH] feat(veans): add create command with labels and relations --- veans/internal/commands/create.go | 134 ++++++++++++++++++++++++++++++ veans/internal/commands/root.go | 1 + 2 files changed, 135 insertions(+) create mode 100644 veans/internal/commands/create.go diff --git a/veans/internal/commands/create.go b/veans/internal/commands/create.go new file mode 100644 index 000000000..21e218b66 --- /dev/null +++ b/veans/internal/commands/create.go @@ -0,0 +1,134 @@ +package commands + +import ( + "context" + "encoding/json" + "fmt" + "strings" + + "github.com/spf13/cobra" + + "code.vikunja.io/veans/internal/client" + "code.vikunja.io/veans/internal/output" + "code.vikunja.io/veans/internal/status" +) + +type createFlags struct { + description string + statusName string + priority int64 + labels []string + parent string + blockedBy []string +} + +func newCreateCmd() *cobra.Command { + f := &createFlags{} + cmd := &cobra.Command{ + Use: "create ", + Aliases: []string{"c"}, + Short: "Create a new task", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + rt, err := loadRuntime() + if err != nil { + return err + } + task, err := runCreate(cmd.Context(), rt, args[0], f) + if err != nil { + return err + } + if globals.JSON { + return json.NewEncoder(cmd.OutOrStdout()).Encode(task) + } + fmt.Fprintf(cmd.OutOrStdout(), "Created %s %s\n", + rt.cfg.FormatTaskID(task.Index), task.Title) + return nil + }, + } + cmd.Flags().StringVarP(&f.description, "description", "d", "", "task description (markdown)") + cmd.Flags().StringVarP(&f.statusName, "status", "s", "todo", "initial status (defaults to todo)") + cmd.Flags().Int64Var(&f.priority, "priority", 0, "priority (0=unset, 1=low, 5=DO_NOW)") + cmd.Flags().StringSliceVar(&f.labels, "label", nil, "labels to attach (repeatable; veans: prefix added if missing)") + cmd.Flags().StringVar(&f.parent, "parent", "", "parent task ID (creates parenttask relation)") + cmd.Flags().StringSliceVar(&f.blockedBy, "blocked-by", nil, "task IDs that block this one (repeatable)") + return cmd +} + +func runCreate(ctx context.Context, rt *runtime, title string, f *createFlags) (*client.Task, error) { + st, err := status.Parse(f.statusName) + if err != nil { + return nil, err + } + bucketID, err := status.BucketID(st, rt.cfg.Buckets) + if err != nil { + return nil, err + } + + created, err := rt.client.CreateTask(ctx, rt.cfg.ProjectID, &client.Task{ + Title: strings.TrimSpace(title), + Description: f.description, + Priority: f.priority, + ProjectID: rt.cfg.ProjectID, + BucketID: bucketID, + Done: st.Done(), + }) + if err != nil { + return nil, err + } + + // If the initial bucket isn't where Vikunja put it (defaults to first + // bucket on the view), nudge it explicitly. + if created.BucketID != bucketID { + updated, err := rt.client.UpdateTask(ctx, created.ID, &client.Task{ + ID: created.ID, + BucketID: bucketID, + Done: st.Done(), + }) + if err != nil { + return nil, output.Wrap(output.CodeUnknown, err, "set initial bucket: %v", err) + } + created = updated + } + + // Attach labels (lazily creating them under veans: namespace). + for _, raw := range f.labels { + title := normalizeLabelTitle(raw) + l, err := getOrCreateLabelByTitle(ctx, rt.client, title) + if err != nil { + return nil, output.Wrap(output.CodeUnknown, err, "label %q: %v", title, err) + } + if err := rt.client.AddLabelToTask(ctx, created.ID, l.ID); err != nil { + return nil, err + } + } + + // Parent relation. + if f.parent != "" { + parentID, err := rt.resolveTaskID(ctx, f.parent) + if err != nil { + return nil, err + } + if _, err := rt.client.CreateRelation(ctx, created.ID, parentID, "parenttask"); err != nil { + return nil, err + } + } + + // Blocked-by relations. + for _, ref := range f.blockedBy { + blockerID, err := rt.resolveTaskID(ctx, ref) + if err != nil { + return nil, err + } + if _, err := rt.client.CreateRelation(ctx, created.ID, blockerID, "blocked"); err != nil { + return nil, err + } + } + + // Re-fetch so the response reflects the labels and any post-create state. + final, err := rt.client.GetTask(ctx, created.ID) + if err != nil { + return created, nil // partial success — caller still got a usable task + } + return final, nil +} diff --git a/veans/internal/commands/root.go b/veans/internal/commands/root.go index 0c35df120..ff615ef55 100644 --- a/veans/internal/commands/root.go +++ b/veans/internal/commands/root.go @@ -37,6 +37,7 @@ func Root(version string) *cobra.Command { root.AddCommand(newInitCmd()) root.AddCommand(newListCmd()) root.AddCommand(newShowCmd()) + root.AddCommand(newCreateCmd()) return root }