import { type Command, Option } from "commander"; import { findSeedsDir, readConfig } from "../config.ts"; import { generateId } from "../id.ts"; import { outputJson, printSuccess } from "../output.ts"; import { isValidPriority, PRIORITY_ERROR, parsePriority } from "../priority.ts"; import { appendIssue, issuesPath, readIssues, withLock } from "../store.ts"; import type { Issue } from "../types.ts"; import { VALID_TYPES } from "../types.ts"; function parseArgs(args: string[]) { const flags: Record = {}; let i = 0; while (i < args.length) { const arg = args[i]; if (!arg) { i++; continue; } if (arg.startsWith("--")) { const key = arg.slice(2); const eqIdx = key.indexOf("="); if (eqIdx !== -1) { flags[key.slice(0, eqIdx)] = key.slice(eqIdx + 1); i++; } else { const next = args[i + 1]; if (next !== undefined && !next.startsWith("--")) { flags[key] = next; i += 2; } else { flags[key] = true; i++; } } } else { i++; } } return flags; } export async function run(args: string[], seedsDir?: string): Promise { const jsonMode = args.includes("--json"); const flags = parseArgs(args); const title = flags.title; if (!title || typeof title !== "string" || !title.trim()) { throw new Error("--title is required"); } const typeVal = flags.type ?? "task"; if (typeof typeVal !== "string" || !(VALID_TYPES as readonly string[]).includes(typeVal)) { throw new Error(`Invalid --type value: ${typeVal}. Valid: ${VALID_TYPES.join("|")}`); } const issueType = typeVal as Issue["type"]; const priority = parsePriority(flags.priority); if (!isValidPriority(priority)) { throw new Error(PRIORITY_ERROR); } // --label is a hidden alias for --labels; --labels wins when both are supplied. const labelsRaw = typeof flags.labels === "string" ? flags.labels : typeof flags.label === "string" ? flags.label : undefined; const labels = labelsRaw ? labelsRaw .split(",") .map((l) => l.trim().toLowerCase()) .filter(Boolean) : undefined; const assignee = typeof flags.assignee === "string" ? flags.assignee : undefined; const description = typeof flags.description === "string" ? flags.description : typeof flags.desc === "string" ? flags.desc : typeof flags.body === "string" ? flags.body : undefined; const dir = seedsDir ?? (await findSeedsDir()); const config = await readConfig(dir); let createdId = ""; await withLock(issuesPath(dir), async () => { const existing = await readIssues(dir); const existingIds = new Set(existing.map((i) => i.id)); const id = generateId(config.project, existingIds); const now = new Date().toISOString(); const issue: Issue = { id, title: title.trim(), status: "open", type: issueType, priority, createdAt: now, updatedAt: now, ...(assignee ? { assignee } : {}), ...(description ? { description } : {}), ...(labels && labels.length > 0 ? { labels } : {}), }; await appendIssue(dir, issue); createdId = id; }); if (jsonMode) { await outputJson({ success: true, command: "create", id: createdId }); } else { printSuccess(`Created ${createdId}`); } } export function register(program: Command): void { program .command("create") .description("Create a new issue") .requiredOption("--title ", "Issue title") .option("--type ", "Issue type (task|bug|feature|epic)", "task") .option("--priority ", "Priority 0-4 or P0-P4", "2") .option("--assignee ", "Assignee name") .option("--description ", "Issue description") .option("--desc ", "Issue description (alias for --description)") .option("--body ", "Issue description (alias for --description)") .option("--labels ", "Comma-separated labels") .addOption(new Option("--label ", "Comma-separated labels (alias)").hideHelp()) .option("--json", "Output as JSON") .action( async (opts: { title: string; type?: string; priority?: string; assignee?: string; description?: string; desc?: string; body?: string; labels?: string; label?: string; json?: boolean; }) => { const args: string[] = ["--title", opts.title]; if (opts.type) args.push("--type", opts.type); if (opts.priority) args.push("--priority", opts.priority); if (opts.assignee) args.push("--assignee", opts.assignee); if (opts.description) args.push("--description", opts.description); if (opts.desc) args.push("--desc", opts.desc); if (opts.body) args.push("--body", opts.body); // --labels wins over --label alias when both are supplied. if (opts.labels) args.push("--labels", opts.labels); else if (opts.label) args.push("--labels", opts.label); if (opts.json) args.push("--json"); await run(args); }, ); }