@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.
Files changed (2) hide show
  1. package/dist/cli.js +69 -34
  2. 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 is interactive it prompts for goal, actors, constraints, and " + "out-of-scope items in the terminal. Once it completes, 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 — if something's " + "missing, ask what to clarify instead of guessing."
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 { join as join8 } from "node:path";
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
- async function smelt(feature) {
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 answers = await import_prompts.default([
7199
- { type: "text", name: "goal", message: "One-sentence goal — what must this feature deliver?" },
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
- goal: answers.goal ?? "",
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
- const dependsOn = answers.dependsOn?.trim() || "-";
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 mkdirSync5, writeFileSync as writeFileSync5 } from "node:fs";
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
- mkdirSync5(specsDir(cwd), { recursive: true });
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 = extname(f).toLowerCase();
7431
- const base = basename(f).toLowerCase();
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 = basename(p);
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 = basename(root);
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 = extname(f).toLowerCase() || "(no 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 = [`${basename(root) || root}/`];
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.0");
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.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",