@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.
- package/README.md +23 -12
- package/bin/cli.mjs +107 -54
- 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
|
|
120
|
-
|
|
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
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
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
|
-
|
|
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
|
|
466
|
-
|
|
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/
|
|
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
|
|
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
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
|
71
|
-
|
|
72
|
-
const
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
-
|
|
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") || (
|
|
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
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
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(/^~(
|
|
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(
|
|
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
|
-
|
|
278
|
-
picked = (await
|
|
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
|
-
|
|
282
|
-
|
|
283
|
-
|
|
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(
|
|
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 =
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
356
|
-
const
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
const
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
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") && (
|
|
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.
|
|
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": {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
"
|
|
10
|
-
|
|
11
|
-
|
|
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": {
|
|
33
|
+
"publishConfig": {
|
|
34
|
+
"access": "public"
|
|
35
|
+
},
|
|
36
|
+
"dependencies": {
|
|
37
|
+
"prompts": "2.4.2"
|
|
38
|
+
}
|
|
14
39
|
}
|