@benjaminthomas/spec-driven-dev 1.5.1 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +23 -12
  2. package/bin/cli.mjs +107 -54
  3. package/package.json +33 -8
package/README.md CHANGED
@@ -116,8 +116,12 @@ say **"run spec-setup"**: that does a deeper version that also reads `CLAUDE.md`
116
116
  config, and recent PRs, and fills in what the installer can't (such as each repo's role, or commands
117
117
  for stacks other than Node). If a skill finds no config, it runs this setup first.
118
118
 
119
- **What `spec-driven-dev` asks, in order** (every answer comes pre-filled with what it detected, so you
120
- can usually just press Enter):
119
+ **What `spec-driven-dev` asks, in order.** Each question is a menu: move with **↑/↓**, tick with
120
+ **Space**, and confirm with **Enter**. What it detected is pre-selected and marked *(detected)*, so you
121
+ can usually just press Enter. Repo paths are added one at a time: paste a path (quotes, `~`, and
122
+ Windows paths like `C:\work\api` all work). A path that doesn't exist or isn't a git repo is flagged
123
+ straight away, so you can fix it before moving on. Press Enter on an empty line when you're done.
124
+ **Ctrl+C** cancels without changing anything.
121
125
 
122
126
  | Question | Detected from | Example answer |
123
127
  |---|---|---|
@@ -148,12 +152,16 @@ can usually just press Enter):
148
152
  example, from inside `/Projects/frontend`:
149
153
 
150
154
  ```
151
- Layout — single, monorepo, or multi-repo (detected: single) [single]: multi-repo
152
- Found 1 git repos under /Projects. Which ones belong to this workspace?
153
- 1. frontend
154
- Repos [1]:
155
- Other repo paths, comma-separated (empty for none): ~/Work/backend, /srv/database
156
- Where should this workspace's .spec-driven.yml live [~/.spec-driven/frontend-workspace]:
155
+ ✔ How is this project organised? › Multiple repos
156
+ ✔ Which of these repos (found under /Projects) belong to this workspace? › frontend
157
+ ? Add a repo from another folder — paste its path, or press Enter to finish (1 selected) › ~/Work/nope
158
+ › Folder not found: /Users/you/Work/nope
159
+ ? Add a repo from another folder — paste its path, or press Enter to finish (1 selected) › ~/Work/backend
160
+ ✓ added /Users/you/Work/backend
161
+ ? Add a repo from another folder — paste its path, or press Enter to finish (2 selected) › /srv/database
162
+ ✓ added /srv/database
163
+ ? Add a repo from another folder — paste its path, or press Enter to finish (3 selected) ›
164
+ ✔ Folder for this workspace's settings file (.spec-driven.yml) › ~/.spec-driven/frontend-workspace
157
165
  ```
158
166
 
159
167
  - **Where the config goes:** the repos' shared parent folder, or `~/.spec-driven/<name>/` when that
@@ -408,7 +416,8 @@ touched by an update.
408
416
  Releases are published by this repo's GitHub Actions workflow, not from anyone's laptop, through
409
417
  npm [trusted publishing](https://docs.npmjs.com/trusted-publishers). Each version's npm page shows
410
418
  a provenance badge linking it to the exact commit and workflow run that built it. The package has
411
- no dependencies, and it contains only `bin/cli.mjs` and the `skills/` folder.
419
+ one small dependency (`prompts`, for the setup menus), and it contains only `bin/` and the `skills/`
420
+ folder.
412
421
 
413
422
  **Something isn't working. Where do I start?**
414
423
  Run `spec-driven-dev doctor` in your project. It checks the config against your
@@ -462,8 +471,9 @@ can otherwise reuse an older copy it cached. What changed in each version is in
462
471
  `npx github:benjaminthomas/spec-driven-dev` installs straight from GitHub instead, for example to
463
472
  try an unreleased commit on `main`.
464
473
 
465
- **Requirements:** Node.js 14 or newer and git. The installer has no dependencies, and CI tests it on
466
- Node 14, 16, 18, 20, 22, and 24.
474
+ **Requirements:** Node.js 14 or newer and git, on macOS, Linux, or Windows (PowerShell or cmd). The
475
+ installer has one small dependency, [`prompts`](https://www.npmjs.com/package/prompts), for its menus.
476
+ CI tests it on Node 14, 16, 18, 20, 22, and 24, and on Windows and macOS.
467
477
 
468
478
  ### Claude Code (plugin, with automatic updates)
469
479
 
@@ -559,7 +569,8 @@ skills/<name>/SKILL.md the skill itself (frontmatter: name, description,
559
569
  skills/<name>/references/ templates and prompts the skill loads when needed
560
570
  .claude-plugin/ Claude Code plugin + marketplace manifests
561
571
  .agents/skills/ symlinks so Codex/Antigravity pick the skills up inside this repo
562
- bin/cli.mjs the `spec-driven-dev` CLI: installer, setup, doctor, status (no dependencies)
572
+ bin/spec-driven-dev.cjs launcher (runs on any Node, explains if it's too old)
573
+ bin/cli.mjs the `spec-driven-dev` CLI: installer, setup menus, doctor, status
563
574
  package.json npm package (ships bin/ and skills/)
564
575
  mcp-server/ MCP fallback server
565
576
  hooks/hooks.json Claude Code session-start hook (runs `cli.mjs status`)
package/bin/cli.mjs CHANGED
@@ -13,7 +13,7 @@ import fs from "node:fs";
13
13
  import os from "node:os";
14
14
  import path from "node:path";
15
15
  import { execFileSync } from "node:child_process";
16
- import { createInterface } from "node:readline"; // callback API: readline/promises needs Node 17+
16
+ import prompts from "prompts"; // arrow-key menus that work in PowerShell, cmd, macOS and Linux terminals
17
17
  import { fileURLToPath } from "node:url";
18
18
 
19
19
  const ROOT = path.dirname(path.dirname(fileURLToPath(import.meta.url)));
@@ -61,19 +61,25 @@ if (flag("help") || flag("h")) {
61
61
  process.exit(0);
62
62
  }
63
63
 
64
- const rl = YES ? null : createInterface({ input: process.stdin, output: process.stdout });
65
- async function ask(q, def = "") {
66
- if (YES) return def;
67
- const a = (await new Promise((resolve) => rl.question(`${q}${def ? ` [${def}]` : ""}: `, resolve))).trim();
68
- return a || def;
64
+ // Every question goes through these helpers. With --yes (or answers given as flags) they return the default.
65
+ const cancelled = () => { console.log("\n✗ Setup cancelled. Nothing was changed."); process.exit(1); };
66
+ async function ask(q) { const r = await prompts({ name: "v", ...q }, { onCancel: cancelled }); return r.v; }
67
+ async function askText(message, initial = "", validate) {
68
+ if (YES) return initial;
69
+ return String((await ask({ type: "text", message, initial, validate })) ?? "").trim();
69
70
  }
70
- async function pick(q, items, defaults) {
71
- items.forEach((it, i) => console.log(` ${i + 1}. ${it.label}`));
72
- const defIdx = items.map((it, i) => (defaults.includes(it.value) ? i + 1 : null)).filter(Boolean).join(",");
73
- const a = await ask(`${q} (comma-separated numbers, "all", or "none")`, defIdx || "none");
74
- if (a === "all") return items.map((it) => it.value);
75
- if (a === "none") return [];
76
- return a.split(",").map((n) => items[Number(n.trim()) - 1]?.value).filter(Boolean);
71
+ async function askSelect(message, choices, initialValue) {
72
+ if (YES) return initialValue;
73
+ const initial = Math.max(0, choices.findIndex((c) => c.value === initialValue));
74
+ return ask({ type: "select", message, choices, initial, hint: "↑/↓ then Enter" });
75
+ }
76
+ async function askMulti(message, choices) {
77
+ if (YES) return choices.filter((c) => c.selected).map((c) => c.value);
78
+ return ask({ type: "multiselect", message, choices, instructions: false, hint: "↑/↓ move · Space select · A all · Enter confirm" });
79
+ }
80
+ async function askConfirm(message, initial = true) {
81
+ if (YES) return initial;
82
+ return ask({ type: "toggle", message, initial, active: "yes", inactive: "no" });
77
83
  }
78
84
 
79
85
  const run = (cmd, argv, cwd) => {
@@ -96,10 +102,8 @@ function copyDir(from, to) {
96
102
  async function install() {
97
103
  const detected = Object.keys(TOOLS).filter((t) => fs.existsSync(path.join(HOME, TOOLS[t].home)) || onPath(TOOLS[t].bin));
98
104
  let tools = opt("tools")?.split(",");
99
- if (!tools) {
100
- console.log("\nWhich AI coding tools should get the skills? (detected ones are pre-selected)");
101
- tools = await pick("Tools", Object.entries(TOOLS).map(([value, t]) => ({ value, label: t.label + (detected.includes(value) ? " ✓ detected" : "") })), detected);
102
- }
105
+ if (!tools) tools = await askMulti("Which AI coding tools should get the skills?",
106
+ Object.entries(TOOLS).map(([value, t]) => ({ value, title: t.label + (detected.includes(value) ? " (installed)" : ""), selected: detected.includes(value) })));
103
107
  const bad = tools.filter((t) => !TOOLS[t]);
104
108
  if (bad.length) throw new Error(`Unknown tool(s): ${bad.join(", ")} — expected ${Object.keys(TOOLS).join(", ")}`);
105
109
 
@@ -110,7 +114,10 @@ async function install() {
110
114
  tools = tools.filter((t) => t !== "claude");
111
115
  }
112
116
 
113
- const global = flag("global") || (!YES && (await ask("Install for this project only, or globally for all projects? (project/global)", "project")) === "global");
117
+ const global = flag("global") || (await askSelect("Install the skills for…", [
118
+ { title: "This project only", value: false, description: "skills go in this folder, e.g. .cursor/skills/" },
119
+ { title: "All my projects", value: true, description: "skills go in your home folder, e.g. ~/.cursor/skills/" },
120
+ ], false));
114
121
  const skills = opt("skills")?.split(",") || ALL_SKILLS;
115
122
  const unknown = skills.filter((s) => !ALL_SKILLS.includes(s));
116
123
  if (unknown.length) throw new Error(`Unknown skill(s): ${unknown.join(", ")} — expected ${ALL_SKILLS.join(", ")}`);
@@ -152,7 +159,7 @@ function findRepos(dir, depth = 2, prefix = "") {
152
159
 
153
160
  function detectLayout(dir) {
154
161
  if (isRepo(dir)) {
155
- const top = run("git", ["rev-parse", "--show-toplevel"], dir);
162
+ const top = path.resolve(run("git", ["rev-parse", "--show-toplevel"], dir)); // C:/x → C:\x on Windows
156
163
  const pkg = readJson(path.join(top, "package.json"));
157
164
  const cargo = fs.existsSync(path.join(top, "Cargo.toml")) && fs.readFileSync(path.join(top, "Cargo.toml"), "utf8").includes("[workspace]");
158
165
  const hasManifest = (rel) => MANIFESTS.some((m) => fs.existsSync(path.join(top, rel, m)));
@@ -242,14 +249,25 @@ async function chooseLayout(detected) {
242
249
  const forced = opt("layout");
243
250
  if (forced && !LAYOUTS.includes(forced)) throw new Error(`--layout must be one of ${LAYOUTS.join(", ")}`);
244
251
  if (forced) return forced;
245
- for (;;) {
246
- const a = await ask(`Layout — single, monorepo, or multi-repo (detected: ${detected})`, detected);
247
- if (LAYOUTS.includes(a)) return a;
248
- console.log(` Please type one of: ${LAYOUTS.join(", ")}`);
249
- }
252
+ const tag = (v) => (v === detected ? " (detected)" : "");
253
+ return askSelect("How is this project organised?", [
254
+ { title: `Single repo${tag("single")}`, value: "single", description: "one git repo, one app" },
255
+ { title: `Monorepo${tag("monorepo")}`, value: "monorepo", description: "one git repo with several apps or packages" },
256
+ { title: `Multiple repos${tag("multi-repo")}`, value: "multi-repo", description: "several git repos that change together, in any folders" },
257
+ ], detected);
250
258
  }
251
259
 
252
- const expandHome = (p) => p.replace(/^~(?=$|\/)/, HOME);
260
+ const expandHome = (p) => p.replace(/^~(?=$|[\\/])/, HOME);
261
+ // Accept what people paste: surrounding quotes, ~, relative paths, Windows or POSIX separators.
262
+ const cleanPath = (v) => path.resolve(DIR, expandHome(String(v).trim().replace(/^(["'])(.*)\1$/, "$2").trim()));
263
+ const gitTop = (p) => { const t = run("git", ["rev-parse", "--show-toplevel"], p); return t ? path.resolve(t) : p; }; // git prints C:/x on Windows
264
+ function repoPathProblem(v) {
265
+ if (!String(v || "").trim()) return null;
266
+ const p = cleanPath(v);
267
+ if (!fs.existsSync(p)) return `Folder not found: ${p}`;
268
+ if (!isRepo(p)) return `Not a git repo: ${p}`;
269
+ return null;
270
+ }
253
271
 
254
272
  function commonParent(paths) {
255
273
  if (!paths.length) return "";
@@ -266,7 +284,7 @@ async function chooseRepos(d) {
266
284
  let picked = [];
267
285
  const fromFlag = opt("repos");
268
286
  if (fromFlag) {
269
- picked = splitList(fromFlag).map((p) => path.resolve(DIR, expandHome(p)));
287
+ picked = splitList(fromFlag).map(cleanPath);
270
288
  } else {
271
289
  const found = findRepos(scanRoot);
272
290
  if (found.length) {
@@ -274,19 +292,25 @@ async function chooseRepos(d) {
274
292
  for (const r of found) { const o = gitFacts(path.join(scanRoot, r))?.owner || "?"; (owners[o] = owners[o] || []).push(r); }
275
293
  const biggest = Object.values(owners).sort((a, b) => b.length - a.length)[0] || [];
276
294
  const here = insideRepo ? [path.relative(scanRoot, d.root)] : [];
277
- console.log(`Found ${found.length} git repos under ${scanRoot}. Which ones belong to this workspace?`);
278
- picked = (await pick("Repos", found.map((r) => ({ value: r, label: `${r} (${Object.keys(owners).find((o) => owners[o].includes(r))})` })), [...new Set([...here, ...(insideRepo ? [] : biggest)])]))
295
+ const pre = new Set([...here, ...(insideRepo ? [] : biggest)]);
296
+ picked = (await askMulti(`Which of these repos (found under ${scanRoot}) belong to this workspace?`,
297
+ found.map((r) => { const o = Object.keys(owners).find((k) => owners[k].includes(r)); return { value: r, title: r + (o && o !== "?" ? ` (${o})` : ""), selected: pre.has(r) }; })))
279
298
  .map((r) => path.join(scanRoot, r));
280
299
  } else if (insideRepo) picked = [d.root];
281
- console.log("Repos in other places can be part of the same workspace (e.g. ~/Work/backend, /srv/database).");
282
- const extra = await ask("Other repo paths, comma-separated (empty for none)", "");
283
- picked.push(...splitList(extra).map((p) => path.resolve(DIR, expandHome(p))));
300
+ // Repos elsewhere: one path at a time, checked as you type, so a typo can be fixed straight away.
301
+ if (!YES) for (;;) {
302
+ const v = await askText(`Add a repo from another folder — paste its path, or press Enter to finish${picked.length ? ` (${picked.length} selected)` : ""}`, "",
303
+ (x) => repoPathProblem(x) || true);
304
+ if (!v) break;
305
+ picked.push(cleanPath(v));
306
+ console.log(` ✓ added ${gitTop(cleanPath(v))}`);
307
+ }
284
308
  }
285
309
  const abs = [];
286
310
  for (const p of [...new Set(picked)]) {
287
311
  if (!fs.existsSync(p)) console.log(` ✗ ${p}: folder not found — skipped`);
288
312
  else if (!isRepo(p)) console.log(` ✗ ${p}: not a git repo — skipped`);
289
- else abs.push(run("git", ["rev-parse", "--show-toplevel"], p) || p);
313
+ else abs.push(gitTop(p));
290
314
  }
291
315
  if (!abs.length) throw new Error("A multi-repo workspace needs at least one git repo. Re-run setup and pick or add repos (or use --repos=a,b).");
292
316
 
@@ -299,7 +323,7 @@ async function chooseRepos(d) {
299
323
  const defRoot = opt("workspace") ? path.resolve(DIR, expandHome(opt("workspace")))
300
324
  : allUnderScan ? scanRoot
301
325
  : tooBroad ? path.join(HOME, ".spec-driven", path.basename(abs[0]) + "-workspace") : parent;
302
- const root = path.resolve(DIR, expandHome(await ask("Where should this workspace's .spec-driven.yml live", defRoot)));
326
+ const root = cleanPath(await askText("Folder for this workspace's settings file (.spec-driven.yml)", defRoot));
303
327
  fs.mkdirSync(root, { recursive: true });
304
328
  const repos = [...new Set(abs)].map((p) => relOrAbs(root, p));
305
329
  console.log(` Workspace repos: ${repos.join(", ")}`);
@@ -321,9 +345,17 @@ function register(file, repoPaths) {
321
345
  async function choosePackages(d) {
322
346
  const root = d.root;
323
347
  const fromFlag = opt("packages");
324
- const answer = fromFlag || (await ask("Package folders, comma-separated, relative to the repo root (empty if none yet)", d.repos.join(",")));
348
+ let chosen = fromFlag ? splitList(fromFlag) : d.repos.length
349
+ ? await askMulti("Which packages are part of this monorepo?", d.repos.map((r) => ({ value: r, title: r, selected: true })))
350
+ : [];
351
+ if (!fromFlag && !YES) for (;;) {
352
+ const v = await askText("Add another package folder (relative to the repo root), or press Enter to finish", "",
353
+ (x) => !String(x || "").trim() || fs.existsSync(path.resolve(root, String(x).trim())) || `Folder not found in ${root}`);
354
+ if (!v) break;
355
+ chosen.push(v);
356
+ }
325
357
  const pkgs = [];
326
- for (const p of splitList(answer)) {
358
+ for (const p of chosen) {
327
359
  if (fs.existsSync(path.resolve(root, p))) pkgs.push(relOrAbs(root, path.resolve(root, p)));
328
360
  else console.log(` ✗ ${p}: folder not found in ${root} — skipped`);
329
361
  }
@@ -342,7 +374,7 @@ async function setup() {
342
374
  const { root, repos } = layout === "multi-repo" ? await chooseRepos(d) : layout === "monorepo" ? await choosePackages(d) : { root: d.layout === "multi-repo" ? DIR : d.root, repos: [] };
343
375
  if (layout === "single" && !isRepo(root)) console.log(` • ${root} isn't a git repo — run setup inside the repo, or choose multi-repo.`);
344
376
  const file = path.join(root, ".spec-driven.yml");
345
- if (fs.existsSync(file) && (await ask(`${file} exists — overwrite? (y/n)`, "n")) !== "y") {
377
+ if (fs.existsSync(file) && !(await askConfirm(`${file} already exists. Replace it?`, false))) {
346
378
  console.log("• Kept the existing config. Run the spec-setup skill in your AI tool to review it.");
347
379
  return;
348
380
  }
@@ -352,22 +384,44 @@ async function setup() {
352
384
  const g = infer(facts);
353
385
  if (facts.length) console.log(`Detected: tracker ${g.trackerType}${g.key ? ` (${g.key}-…)` : ""}, base ${g.base}, branches "${g.branchName}", commits ${g.commitStyle}`);
354
386
 
355
- const trackerType = await ask("Issue tracker (jira/github/linear/none)", g.trackerType);
356
- const key = trackerType === "jira" || trackerType === "linear" ? await ask("Ticket key prefix (e.g. STP)", g.key) : "";
357
- const url = trackerType === "none" ? "" : await ask("Tracker URL (optional)", "");
358
- const base = await ask('Base branch work is cut from ("default" = repo default branch)', g.base);
359
- const pr = await ask("Branch PRs target", base);
360
- const branchName = await ask("Branch name pattern ({ticket} {slug} {type})", g.branchName);
361
- const commitStyle = await ask("Commit style (conventional/ticket/freeform)", g.commitStyle);
362
- const specsDir = await ask("Where specs/<feature>/ folders go", "specs");
363
- console.log("After you approve a spec, the workflow can stop and wait for you at each stage, or run");
364
- console.log("implement → verify → checkpoint in one go (it stops at any failure, and never pushes without asking).");
365
- let afterApproval = opt("after-approval");
366
- if (!afterApproval) for (;;) {
367
- afterApproval = await ask("After approval: stop or run-through", "stop");
368
- if (["stop", "run-through"].includes(afterApproval)) break;
369
- console.log(" Please type stop or run-through");
370
- }
387
+ const det = (v, d) => (v === d ? " (detected)" : "");
388
+ const trackerType = await askSelect("Where do your tickets live?", [
389
+ { title: `Jira${det("jira", g.trackerType)}`, value: "jira" },
390
+ { title: `GitHub Issues${det("github", g.trackerType)}`, value: "github" },
391
+ { title: `Linear${det("linear", g.trackerType)}`, value: "linear" },
392
+ { title: `No tracker${det("none", g.trackerType)}`, value: "none", description: "work from transcripts, docs, or plain descriptions" },
393
+ ], g.trackerType);
394
+ const key = trackerType === "jira" || trackerType === "linear" ? (await askText("Ticket key prefix (the STP in STP-123)", g.key)).toUpperCase() : "";
395
+ const url = trackerType === "none" ? "" : await askText("Tracker URL (optional, press Enter to skip)", "");
396
+ const branchChoices = (initial) => [
397
+ { title: "Each repo's default branch", value: "default", description: "e.g. main or master" },
398
+ ...[...new Set(facts.map((f) => f.integration).filter(Boolean).concat(initial !== "default" ? [initial] : []))].map((b) => ({ title: b, value: b })),
399
+ { title: "Another branch…", value: "__other" },
400
+ ];
401
+ const pickBranch = async (message, initial) => {
402
+ const v = await askSelect(message, branchChoices(initial), initial);
403
+ return v === "__other" ? askText("Branch name", "") : v;
404
+ };
405
+ const base = await pickBranch("Which branch is new work started from?", g.base);
406
+ const pr = await pickBranch("Which branch do pull requests go into?", base);
407
+ const ex = key || "STP";
408
+ let branchName = await askSelect("How are work branches named?", [
409
+ { title: `${ex}-123${det("{ticket}", g.branchName)}`, value: "{ticket}", description: "just the ticket ID" },
410
+ { title: `feat/${ex}-123-add-login${det("{type}/{ticket}-{slug}", g.branchName)}`, value: "{type}/{ticket}-{slug}" },
411
+ { title: `feat/add-login${det("{type}/{slug}", g.branchName)}`, value: "{type}/{slug}", description: "no ticket ID" },
412
+ { title: "Custom pattern…", value: "__custom" },
413
+ ], g.branchName);
414
+ if (branchName === "__custom") branchName = await askText("Pattern using {ticket}, {slug}, {type}", g.branchName, (x) => /\{(ticket|slug)\}/.test(x) || "Include {ticket} or {slug}");
415
+ const commitStyle = await askSelect("How are commit messages written?", [
416
+ { title: `feat: add login${det("conventional", g.commitStyle)}`, value: "conventional", description: "conventional commits" },
417
+ { title: `${ex}-123: add login${det("ticket", g.commitStyle)}`, value: "ticket", description: "ticket ID first" },
418
+ { title: `Add login${det("freeform", g.commitStyle)}`, value: "freeform", description: "no fixed format" },
419
+ ], g.commitStyle);
420
+ const specsDir = await askText("Folder for specs (one sub-folder per feature)", "specs");
421
+ const afterApproval = opt("after-approval") || await askSelect("After you approve a spec…", [
422
+ { title: "Stop after each stage", value: "stop", description: "you start implement, verify and checkpoint yourself" },
423
+ { title: "Run straight through", value: "run-through", description: "implement → verify → commit in one go; stops at any failure, never pushes without asking" },
424
+ ], "stop");
371
425
 
372
426
  const config = {
373
427
  layout, tracker: { type: trackerType, key, url }, specsDir, devUrl: "", afterApproval,
@@ -504,7 +558,7 @@ try {
504
558
  else if (command === "status") status();
505
559
  else if (command === "install") {
506
560
  await install();
507
- if (!flag("no-setup") && (YES || (await ask("\nSet up this project now? (y/n)", "y")) === "y")) await setup();
561
+ if (!flag("no-setup") && (await askConfirm("Set up this project now?", true))) await setup();
508
562
  } else if (command === "setup") await setup();
509
563
  else throw new Error(`Unknown command "${command}" — use install (default), setup, doctor, or status`);
510
564
  if (["install", "setup"].includes(command)) console.log('\nNext: in your AI tool, say "investigate <ticket>" or "create a spec".');
@@ -512,5 +566,4 @@ try {
512
566
  console.error(`✗ ${e.message}`);
513
567
  process.exitCode = 1;
514
568
  } finally {
515
- rl?.close();
516
569
  }
package/package.json CHANGED
@@ -1,14 +1,39 @@
1
1
  {
2
2
  "name": "@benjaminthomas/spec-driven-dev",
3
- "version": "1.5.1",
3
+ "version": "1.6.0",
4
4
  "description": "Spec-driven AI development workflow (investigate → spec → implement → verify → ship) as Agent Skills for Claude Code, Cursor, Codex CLI and Antigravity.",
5
5
  "type": "module",
6
- "bin": { "spec-driven-dev": "bin/spec-driven-dev.cjs" },
7
- "files": ["bin", "skills"],
8
- "scripts": { "test": "python3 scripts/check.py && node scripts/test-cli.mjs" },
9
- "engines": { "node": ">=14" },
10
- "keywords": ["agent-skills", "claude-code", "cursor", "codex", "antigravity", "spec-driven-development", "ai-agents"],
11
- "repository": { "type": "git", "url": "git+https://github.com/benjaminthomas/spec-driven-dev.git" },
6
+ "bin": {
7
+ "spec-driven-dev": "bin/spec-driven-dev.cjs"
8
+ },
9
+ "files": [
10
+ "bin",
11
+ "skills"
12
+ ],
13
+ "scripts": {
14
+ "test": "python3 scripts/check.py && node scripts/test-cli.mjs"
15
+ },
16
+ "engines": {
17
+ "node": ">=14"
18
+ },
19
+ "keywords": [
20
+ "agent-skills",
21
+ "claude-code",
22
+ "cursor",
23
+ "codex",
24
+ "antigravity",
25
+ "spec-driven-development",
26
+ "ai-agents"
27
+ ],
28
+ "repository": {
29
+ "type": "git",
30
+ "url": "git+https://github.com/benjaminthomas/spec-driven-dev.git"
31
+ },
12
32
  "license": "MIT",
13
- "publishConfig": { "access": "public" }
33
+ "publishConfig": {
34
+ "access": "public"
35
+ },
36
+ "dependencies": {
37
+ "prompts": "2.4.2"
38
+ }
14
39
  }