@rijalpermana/spec-forge 0.1.1 → 0.1.2
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/dist/cli.js +69 -34
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -6845,7 +6845,7 @@ var COMMAND_SPECS = [
|
|
|
6845
6845
|
description: "Extract raw requirements into a grounding brief, then draft the PRD",
|
|
6846
6846
|
requiresPrd: false,
|
|
6847
6847
|
readOnly: false,
|
|
6848
|
-
instructions: GROUND_IN_RULES + "This command
|
|
6848
|
+
instructions: GROUND_IN_RULES + "This command has two input modes. Default: it prompts for goal, actors, " + "constraints, and out-of-scope items in the terminal. With --from <file>: it " + "stages an existing BRD/requirements document verbatim in the feature folder as " + "source-brd.<ext> (extension preserved — .md, .pdf, .docx, …) and leaves " + "'[TODO: extract from source-brd.<ext>]' markers in {{brief}}. If a source-brd.* " + "file exists, read it (if your tool can't open that format — e.g. .docx — say so " + "and ask the user to export it to Markdown or PDF) and replace each marker in " + "{{brief}} with the value extracted from the document, citing the BRD section it " + "came from so every requirement stays traceable. Then read {{brief}} and draft " + "{{prd}} following the structure already stubbed there. Do not invent scope, " + "actors, or rules that aren't grounded in the brief or the BRD — if something's " + "missing, ask what to clarify instead of guessing."
|
|
6849
6849
|
},
|
|
6850
6850
|
{
|
|
6851
6851
|
name: "schema",
|
|
@@ -7155,7 +7155,8 @@ Next: forge smelt <feature-name>`);
|
|
|
7155
7155
|
|
|
7156
7156
|
// src/commands/smelt.ts
|
|
7157
7157
|
var import_prompts = __toESM(require_prompts3(), 1);
|
|
7158
|
-
import {
|
|
7158
|
+
import { copyFileSync, mkdirSync as mkdirSync5 } from "node:fs";
|
|
7159
|
+
import { basename, extname, join as join8, resolve } from "node:path";
|
|
7159
7160
|
|
|
7160
7161
|
// src/lib/template.ts
|
|
7161
7162
|
import { readFileSync as readFileSync2, existsSync as existsSync3, mkdirSync as mkdirSync4, writeFileSync as writeFileSync4 } from "node:fs";
|
|
@@ -7178,7 +7179,8 @@ function fileExists(path) {
|
|
|
7178
7179
|
}
|
|
7179
7180
|
|
|
7180
7181
|
// src/commands/smelt.ts
|
|
7181
|
-
|
|
7182
|
+
var SOURCE_DOC_BASE = "source-brd";
|
|
7183
|
+
async function smelt(feature, opts = {}) {
|
|
7182
7184
|
const cwd = process.cwd();
|
|
7183
7185
|
const dir = featureDir(feature, cwd);
|
|
7184
7186
|
const briefPath = join8(dir, SPEC_FILES.brief);
|
|
@@ -7195,36 +7197,20 @@ async function smelt(feature) {
|
|
|
7195
7197
|
console.log(`Existing features: ${existingFeatures.join(", ")}
|
|
7196
7198
|
`);
|
|
7197
7199
|
}
|
|
7198
|
-
const
|
|
7199
|
-
|
|
7200
|
-
{ type: "text", name: "actors", message: "Who are the actors/roles involved?" },
|
|
7201
|
-
{ type: "text", name: "constraints", message: "Key constraints (regulatory, technical, deadline)?" },
|
|
7202
|
-
{ type: "text", name: "outOfScope", message: "What's explicitly OUT of scope?" },
|
|
7203
|
-
{
|
|
7204
|
-
type: "text",
|
|
7205
|
-
name: "dependsOn",
|
|
7206
|
-
message: "Does this depend on another existing feature? (feature name, or leave blank)"
|
|
7207
|
-
}
|
|
7208
|
-
]);
|
|
7209
|
-
if (!answers.goal) {
|
|
7210
|
-
console.log("Smelting cancelled.");
|
|
7200
|
+
const briefVars = opts.from ? stageSourceDoc(opts.from, dir) : await runInteractive(feature);
|
|
7201
|
+
if (!briefVars)
|
|
7211
7202
|
return;
|
|
7212
|
-
}
|
|
7213
7203
|
const briefRaw = readTemplate(join8(templatesDir, SPEC_FILES.brief));
|
|
7214
7204
|
const brief = renderTemplate(briefRaw, {
|
|
7215
7205
|
feature,
|
|
7216
7206
|
date: new Date().toISOString().slice(0, 10),
|
|
7217
|
-
|
|
7218
|
-
actors: answers.actors ?? "",
|
|
7219
|
-
constraints: answers.constraints ?? "",
|
|
7220
|
-
outOfScope: answers.outOfScope ?? ""
|
|
7207
|
+
...briefVars
|
|
7221
7208
|
});
|
|
7222
7209
|
writeIfAbsent(briefPath, brief);
|
|
7223
7210
|
const prdRaw = readTemplate(join8(templatesDir, SPEC_FILES.prd));
|
|
7224
7211
|
const prd = renderTemplate(prdRaw, { feature });
|
|
7225
7212
|
const prdWritten = writeIfAbsent(prdPath, prd);
|
|
7226
|
-
|
|
7227
|
-
upsertIndexEntry(cwd, feature, { status: "draft", dependsOn });
|
|
7213
|
+
upsertIndexEntry(cwd, feature, { status: "draft", dependsOn: briefVars.dependsOn });
|
|
7228
7214
|
console.log(`
|
|
7229
7215
|
Wrote ${briefPath}`);
|
|
7230
7216
|
if (prdWritten)
|
|
@@ -7233,6 +7219,55 @@ Wrote ${briefPath}`);
|
|
|
7233
7219
|
console.log(`
|
|
7234
7220
|
Next: open this project in your AI tool and draft prd.md from brief.md ` + `— use the /forge:smelt, /forge-smelt, or AGENTS.md instructions, depending ` + `on which bridge you initialized.`);
|
|
7235
7221
|
}
|
|
7222
|
+
function stageSourceDoc(from, dir) {
|
|
7223
|
+
const srcPath = resolve(process.cwd(), from);
|
|
7224
|
+
if (!fileExists(srcPath)) {
|
|
7225
|
+
console.log(`Source document not found: ${srcPath}`);
|
|
7226
|
+
process.exitCode = 1;
|
|
7227
|
+
return;
|
|
7228
|
+
}
|
|
7229
|
+
const docName = `${SOURCE_DOC_BASE}${extname(srcPath).toLowerCase()}`;
|
|
7230
|
+
const destPath = join8(dir, docName);
|
|
7231
|
+
if (fileExists(destPath)) {
|
|
7232
|
+
console.log(`${destPath} already exists — leaving it in place.`);
|
|
7233
|
+
} else {
|
|
7234
|
+
mkdirSync5(dir, { recursive: true });
|
|
7235
|
+
copyFileSync(srcPath, destPath);
|
|
7236
|
+
console.log(`Staged ${basename(srcPath)} → ${destPath}`);
|
|
7237
|
+
}
|
|
7238
|
+
const marker = `[TODO: extract from ${docName}]`;
|
|
7239
|
+
return {
|
|
7240
|
+
goal: marker,
|
|
7241
|
+
actors: marker,
|
|
7242
|
+
constraints: marker,
|
|
7243
|
+
outOfScope: marker,
|
|
7244
|
+
dependsOn: "-"
|
|
7245
|
+
};
|
|
7246
|
+
}
|
|
7247
|
+
async function runInteractive(feature) {
|
|
7248
|
+
const answers = await import_prompts.default([
|
|
7249
|
+
{ type: "text", name: "goal", message: "One-sentence goal — what must this feature deliver?" },
|
|
7250
|
+
{ type: "text", name: "actors", message: "Who are the actors/roles involved?" },
|
|
7251
|
+
{ type: "text", name: "constraints", message: "Key constraints (regulatory, technical, deadline)?" },
|
|
7252
|
+
{ type: "text", name: "outOfScope", message: "What's explicitly OUT of scope?" },
|
|
7253
|
+
{
|
|
7254
|
+
type: "text",
|
|
7255
|
+
name: "dependsOn",
|
|
7256
|
+
message: "Does this depend on another existing feature? (feature name, or leave blank)"
|
|
7257
|
+
}
|
|
7258
|
+
]);
|
|
7259
|
+
if (!answers.goal) {
|
|
7260
|
+
console.log("Smelting cancelled.");
|
|
7261
|
+
return null;
|
|
7262
|
+
}
|
|
7263
|
+
return {
|
|
7264
|
+
goal: answers.goal ?? "",
|
|
7265
|
+
actors: answers.actors ?? "",
|
|
7266
|
+
constraints: answers.constraints ?? "",
|
|
7267
|
+
outOfScope: answers.outOfScope ?? "",
|
|
7268
|
+
dependsOn: answers.dependsOn?.trim() || "-"
|
|
7269
|
+
};
|
|
7270
|
+
}
|
|
7236
7271
|
|
|
7237
7272
|
// src/commands/scaffold.ts
|
|
7238
7273
|
import { join as join9 } from "node:path";
|
|
@@ -7299,8 +7334,8 @@ function verify(feature) {
|
|
|
7299
7334
|
}
|
|
7300
7335
|
|
|
7301
7336
|
// src/commands/scan.ts
|
|
7302
|
-
import { existsSync as existsSync4, readFileSync as readFileSync4, readdirSync, mkdirSync as
|
|
7303
|
-
import { join as join11, extname, relative, basename } from "node:path";
|
|
7337
|
+
import { existsSync as existsSync4, readFileSync as readFileSync4, readdirSync, mkdirSync as mkdirSync6, writeFileSync as writeFileSync5 } from "node:fs";
|
|
7338
|
+
import { join as join11, extname as extname2, relative, basename as basename2 } from "node:path";
|
|
7304
7339
|
var IGNORE_DIRS = new Set([
|
|
7305
7340
|
"node_modules",
|
|
7306
7341
|
".git",
|
|
@@ -7340,7 +7375,7 @@ function scan(options = {}) {
|
|
|
7340
7375
|
apis: pathListOrNone(apis),
|
|
7341
7376
|
tree: renderTree(cwd, depth)
|
|
7342
7377
|
});
|
|
7343
|
-
|
|
7378
|
+
mkdirSync6(specsDir(cwd), { recursive: true });
|
|
7344
7379
|
const outPath = join11(specsDir(cwd), "CODEBASE.md");
|
|
7345
7380
|
writeFileSync5(outPath, rendered, "utf-8");
|
|
7346
7381
|
console.log(`Wrote ${outPath}`);
|
|
@@ -7427,8 +7462,8 @@ function detectStack(root) {
|
|
|
7427
7462
|
return found;
|
|
7428
7463
|
}
|
|
7429
7464
|
function isSchemaFile(f) {
|
|
7430
|
-
const ext =
|
|
7431
|
-
const base =
|
|
7465
|
+
const ext = extname2(f).toLowerCase();
|
|
7466
|
+
const base = basename2(f).toLowerCase();
|
|
7432
7467
|
if (ext === ".dbml" || ext === ".prisma" || ext === ".sql")
|
|
7433
7468
|
return true;
|
|
7434
7469
|
if (/schema/.test(base) && [".ts", ".js", ".py", ".rb", ".go", ".json"].includes(ext))
|
|
@@ -7439,7 +7474,7 @@ function isSchemaFile(f) {
|
|
|
7439
7474
|
}
|
|
7440
7475
|
function isApiFile(f) {
|
|
7441
7476
|
const p = f.toLowerCase();
|
|
7442
|
-
const base =
|
|
7477
|
+
const base = basename2(p);
|
|
7443
7478
|
if (/(openapi|swagger)\.(ya?ml|json)$/.test(base))
|
|
7444
7479
|
return true;
|
|
7445
7480
|
if (/\.(controller|route|router|resolver)\.(ts|js|py)$/.test(base))
|
|
@@ -7449,7 +7484,7 @@ function isApiFile(f) {
|
|
|
7449
7484
|
return false;
|
|
7450
7485
|
}
|
|
7451
7486
|
function renderProject(root, fileCount) {
|
|
7452
|
-
let name =
|
|
7487
|
+
let name = basename2(root);
|
|
7453
7488
|
const pkgPath = join11(root, "package.json");
|
|
7454
7489
|
if (existsSync4(pkgPath)) {
|
|
7455
7490
|
try {
|
|
@@ -7480,7 +7515,7 @@ function renderProject(root, fileCount) {
|
|
|
7480
7515
|
function fileStats(files) {
|
|
7481
7516
|
const counts = new Map;
|
|
7482
7517
|
for (const f of files) {
|
|
7483
|
-
const ext =
|
|
7518
|
+
const ext = extname2(f).toLowerCase() || "(no ext)";
|
|
7484
7519
|
counts.set(ext, (counts.get(ext) ?? 0) + 1);
|
|
7485
7520
|
}
|
|
7486
7521
|
const top = [...counts.entries()].sort((a, b) => b[1] - a[1]).slice(0, 12);
|
|
@@ -7488,7 +7523,7 @@ function fileStats(files) {
|
|
|
7488
7523
|
`);
|
|
7489
7524
|
}
|
|
7490
7525
|
function renderTree(root, maxDepth) {
|
|
7491
|
-
const lines = [`${
|
|
7526
|
+
const lines = [`${basename2(root) || root}/`];
|
|
7492
7527
|
walkTree(root, "", maxDepth, lines);
|
|
7493
7528
|
return lines.join(`
|
|
7494
7529
|
`);
|
|
@@ -7553,12 +7588,12 @@ Every forge drafting command (smelt, schema, contract, tasks, testcase) ` + `wil
|
|
|
7553
7588
|
|
|
7554
7589
|
// src/cli.ts
|
|
7555
7590
|
var program2 = new Command;
|
|
7556
|
-
program2.name("forge").description("Spec-driven development CLI — scaffolds specs, schemas, API contracts, tasks, and test cases.").version("0.1.
|
|
7591
|
+
program2.name("forge").description("Spec-driven development CLI — scaffolds specs, schemas, API contracts, tasks, and test cases.").version("0.1.2");
|
|
7557
7592
|
program2.command("init").description("Scaffold specs/ folder and an AI-tool bridge in the current project").option("-t, --target <target>", "bridge target: claude, cursor, windsurf, or generic", "claude").action((options) => init({ target: options.target }));
|
|
7558
7593
|
program2.command("bridge <target>").description("(Re)generate AI-tool bridge commands: claude, cursor, windsurf, or generic").action((target) => bridge(target));
|
|
7559
7594
|
program2.command("scan").description("Inventory the existing project structure into specs/CODEBASE.md").option("-d, --depth <n>", "directory-tree depth to include in the report", "2").action((options) => scan({ depth: Number(options.depth) }));
|
|
7560
7595
|
program2.command("rules").description("Scaffold specs/RULES.md — project conventions the AI follows when drafting").action(() => rules());
|
|
7561
|
-
program2.command("smelt <feature>").description("Extract raw requirements into a grounding brief and stub the PRD").action(smelt);
|
|
7596
|
+
program2.command("smelt <feature>").description("Extract raw requirements into a grounding brief and stub the PRD").option("-f, --from <file>", "extract from an existing BRD/requirements document instead of interactive Q&A").action((feature, options) => smelt(feature, { from: options.from }));
|
|
7562
7597
|
program2.command("schema <feature>").description("Scaffold schema.dbml (requires prd.md)").action(makeScaffoldCommand({ file: SPEC_FILES.schema, label: "schema" }));
|
|
7563
7598
|
program2.command("contract <feature>").description("Scaffold api-contract.md (requires prd.md)").action(makeScaffoldCommand({ file: SPEC_FILES.contract, label: "contract" }));
|
|
7564
7599
|
program2.command("tasks <feature>").description("Scaffold tasks.md WBS (requires prd.md)").action(makeScaffoldCommand({ file: SPEC_FILES.tasks, label: "tasks" }));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rijalpermana/spec-forge",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Spec-driven development CLI: scaffolds specs, schemas, API contracts, task lists, and test cases; bridges into Claude Code as slash commands.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|