package cmd import ( "flag" "fmt" "os" "path/filepath" "strings" "git.noodles.cam/claude-skills/marketplace/cli/internal/issue" ) func init() { register(&Command{ Name: "new", Group: GroupIssue, Short: "create a local issue from its type template", Long: `The issue is real the moment this writes the file. Nothing is pending, nothing is a draft awaiting a tracker: ` + "`origin: local`" + ` is a complete state and pushing it later is optional. While it says local, this file is the ONLY copy of the work — the store, not a cache of anything. That is what a push changes: it hands the issue to the tracker and removes the file. Writes .kettle/issues/.md prefilled with the type's template, prints the path, and rebuilds INDEX.md. Fill the sections in an editor, then run ` + "`kettle check `" + `. Body prose is Russian, section headers and the title are English.`, Examples: []Example{ {`kettle new --type task --title "Wire sqlc into the appclick repo layer" --label tech/sql --label comp/appclick`, "a task with two free-form labels"}, {`kettle new --type bug --title "Fix the index rebuild on an empty store" --depends wire-sqlc-appclick --milestone v0.2`, "a bug that is blocked by another issue"}, }, Setup: func(fs *flag.FlagSet) func([]string) error { typ := fs.String("type", "", "issue type, one of: "+strings.Join(issue.TypeNames(), ", ")+" (becomes the exclusive type/* label)") title := fs.String("title", "", "English, imperative, no type prefix") id := fs.String("id", "", "slug (default: derived from the title)") severity := fs.String("severity", "", "severity/* label, one of: "+strings.Join(issue.Severities, ", ")) milestone := fs.String("milestone", "", "milestone title") var labels, assignees, depends stringList fs.Var(&labels, "label", "extra label, e.g. tech/sql; repeat") fs.Var(&assignees, "assignee", "assignee login; repeat") fs.Var(&depends, "depends", "id this issue depends on; repeat") out := storeFlag(fs) return func(args []string) error { if *typ == "" || *title == "" { return Fail("--type and --title are both required") } if !issue.KnownType(*typ) { return Fail("unknown --type %q — known: %s", *typ, strings.Join(issue.TypeNames(), ", ")) } if *severity != "" && !issue.KnownSeverity(*severity) { return Fail("unknown --severity %q — known: %s", *severity, strings.Join(issue.Severities, ", ")) } // Before anything reads the store path. There is no store to be // second-guessed about when there is no project. root, err := storeRoot(*out) if err != nil { return err } labelSet := []string{"type/" + *typ} if *severity != "" { labelSet = append(labelSet, "severity/"+*severity) } for _, l := range labels { if !contains(labelSet, l) { labelSet = append(labelSet, l) } } slug := *id if slug == "" { if slug, err = issue.UniqueID(root, issue.Slugify(*title, 0), nil); err != nil { return err } } else if !issue.IsSlug(slug) { return Fail("--id %q is not a slug (lowercase, digits, single dashes)", slug) } if _, err := os.Stat(issue.PathOf(root, slug)); err == nil { return Fail("%s already exists", issue.PathOf(root, slug)) } known := map[string]bool{} for _, k := range issue.AllIDs(root) { known[k] = true } for _, d := range depends { if !known[d] { fmt.Fprintf(os.Stderr, "warning: depends on %q, which is not in the store yet\n", d) } } i := &issue.Issue{ ID: slug, Title: *title, Body: issue.Template(*typ, depends), State: "open", Labels: labelSet, Assignees: assignees, Milestone: *milestone, Depends: depends, Origin: issue.Local, } // The first issue in a fresh checkout has to create the store, // but it says so — and it says where, because the path is // absolute. created, err := issue.CreateStore(root) if err != nil { return err } if created { abs, _ := filepath.Abs(root) fmt.Fprintf(os.Stderr, "created store %s\n", abs) } path, err := issue.Save(root, i) if err != nil { return err } if _, _, err := issue.BuildIndex(root); err != nil { return err } fmt.Printf("%s [type/%s] %s\n", path, *typ, *title) fmt.Printf("fill the sections, then: kettle check %s\n", slug) return nil } }, }) } func contains(xs []string, x string) bool { for _, v := range xs { if v == x { return true } } return false }