@rijalpermana/spec-forge 0.1.1 → 0.2.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 CHANGED
@@ -70,6 +70,7 @@ forge smelt btn-fraud-check # interactive Q&A -> brief.md + prd.md stub
70
70
 
71
71
  forge schema btn-fraud-check # schema.dbml (requires prd.md)
72
72
  forge contract btn-fraud-check # api-contract.md (requires prd.md)
73
+ forge mockup btn-fraud-check # mockup.html (requires prd.md)
73
74
  forge tasks btn-fraud-check # tasks.md (requires prd.md)
74
75
  forge testcase btn-fraud-check # testcases.md (requires prd.md)
75
76
  forge verify btn-fraud-check # reports missing files / unresolved [TODO]s
@@ -132,6 +133,7 @@ the writing.
132
133
  | `forge smelt <feature>` | — | `brief.md`, `prd.md` (stub), `INDEX.md` entry (`draft`) | Yes — refuses if `brief.md` exists |
133
134
  | `forge schema <feature>` | `prd.md` | `schema.dbml` (stub) | Yes |
134
135
  | `forge contract <feature>` | `prd.md` | `api-contract.md` (stub) | Yes |
136
+ | `forge mockup <feature>` | `prd.md` | `mockup.html` (stub) | Yes |
135
137
  | `forge tasks <feature>` | `prd.md` | `tasks.md` (stub) | Yes |
136
138
  | `forge testcase <feature>` | `prd.md` | `testcases.md` (stub) | Yes |
137
139
  | `forge verify <feature>` | — | Console report, `INDEX.md` status → `active` if complete | N/A (read-only on spec files) |
@@ -149,6 +151,7 @@ your-project/
149
151
  │ ├── prd.md # goal, actors, user stories, business rules, out-of-scope
150
152
  │ ├── schema.dbml # entities derived from prd.md
151
153
  │ ├── api-contract.md # Title / endpoint / Request / Response / Note format
154
+ │ ├── mockup.html # throwaway wireframe (plain HTML/CSS, no framework)
152
155
  │ ├── tasks.md # WBS, sequenced data -> logic -> UI
153
156
  │ └── testcases.md # No/Scenario/Case/Type/Expected/Actual/Status/Remark table
154
157
  ├── .claude/commands/forge/ # if target = claude
package/dist/cli.js CHANGED
@@ -6775,6 +6775,9 @@ var require_prompts3 = __commonJS((exports, module) => {
6775
6775
  module.exports = isNodeLT("8.6.0") ? require_dist() : require_lib();
6776
6776
  });
6777
6777
 
6778
+ // src/cli.ts
6779
+ import { createRequire as createRequire2 } from "node:module";
6780
+
6778
6781
  // node_modules/commander/esm.mjs
6779
6782
  var import__ = __toESM(require_commander(), 1);
6780
6783
  var {
@@ -6845,7 +6848,7 @@ var COMMAND_SPECS = [
6845
6848
  description: "Extract raw requirements into a grounding brief, then draft the PRD",
6846
6849
  requiresPrd: false,
6847
6850
  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."
6851
+ 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
6852
  },
6850
6853
  {
6851
6854
  name: "schema",
@@ -6869,6 +6872,14 @@ var COMMAND_SPECS = [
6869
6872
  ` + `Response: [full JSON example]
6870
6873
  ` + "Note: [only if needed]"
6871
6874
  },
6875
+ {
6876
+ name: "mockup",
6877
+ argumentHint: "[feature-name]",
6878
+ description: "Scaffold mockup.html, then draft it from prd.md",
6879
+ requiresPrd: true,
6880
+ readOnly: false,
6881
+ instructions: GROUND_IN_RULES + "Read {{prd}} and fill {{mockup}} — a throwaway wireframe (plain HTML/CSS, no " + "framework, no build step) covering the screens/states implied by the PRD's user " + "stories: primary view, secondary views or modals, and empty/loading/error states " + "where the PRD calls for them. This is for reviewing flow and layout, not visual " + "design or production markup — don't invent screens, fields, or copy that aren't " + "traceable to the PRD."
6882
+ },
6872
6883
  {
6873
6884
  name: "tasks",
6874
6885
  argumentHint: "[feature-name]",
@@ -6913,6 +6924,7 @@ var SPEC_FILES = {
6913
6924
  prd: "prd.md",
6914
6925
  schema: "schema.dbml",
6915
6926
  contract: "api-contract.md",
6927
+ mockup: "mockup.html",
6916
6928
  tasks: "tasks.md",
6917
6929
  testcases: "testcases.md"
6918
6930
  };
@@ -7155,7 +7167,8 @@ Next: forge smelt <feature-name>`);
7155
7167
 
7156
7168
  // src/commands/smelt.ts
7157
7169
  var import_prompts = __toESM(require_prompts3(), 1);
7158
- import { join as join8 } from "node:path";
7170
+ import { copyFileSync, mkdirSync as mkdirSync5 } from "node:fs";
7171
+ import { basename, extname, join as join8, resolve } from "node:path";
7159
7172
 
7160
7173
  // src/lib/template.ts
7161
7174
  import { readFileSync as readFileSync2, existsSync as existsSync3, mkdirSync as mkdirSync4, writeFileSync as writeFileSync4 } from "node:fs";
@@ -7178,7 +7191,8 @@ function fileExists(path) {
7178
7191
  }
7179
7192
 
7180
7193
  // src/commands/smelt.ts
7181
- async function smelt(feature) {
7194
+ var SOURCE_DOC_BASE = "source-brd";
7195
+ async function smelt(feature, opts = {}) {
7182
7196
  const cwd = process.cwd();
7183
7197
  const dir = featureDir(feature, cwd);
7184
7198
  const briefPath = join8(dir, SPEC_FILES.brief);
@@ -7195,36 +7209,20 @@ async function smelt(feature) {
7195
7209
  console.log(`Existing features: ${existingFeatures.join(", ")}
7196
7210
  `);
7197
7211
  }
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.");
7212
+ const briefVars = opts.from ? stageSourceDoc(opts.from, dir) : await runInteractive(feature);
7213
+ if (!briefVars)
7211
7214
  return;
7212
- }
7213
7215
  const briefRaw = readTemplate(join8(templatesDir, SPEC_FILES.brief));
7214
7216
  const brief = renderTemplate(briefRaw, {
7215
7217
  feature,
7216
7218
  date: new Date().toISOString().slice(0, 10),
7217
- goal: answers.goal ?? "",
7218
- actors: answers.actors ?? "",
7219
- constraints: answers.constraints ?? "",
7220
- outOfScope: answers.outOfScope ?? ""
7219
+ ...briefVars
7221
7220
  });
7222
7221
  writeIfAbsent(briefPath, brief);
7223
7222
  const prdRaw = readTemplate(join8(templatesDir, SPEC_FILES.prd));
7224
7223
  const prd = renderTemplate(prdRaw, { feature });
7225
7224
  const prdWritten = writeIfAbsent(prdPath, prd);
7226
- const dependsOn = answers.dependsOn?.trim() || "-";
7227
- upsertIndexEntry(cwd, feature, { status: "draft", dependsOn });
7225
+ upsertIndexEntry(cwd, feature, { status: "draft", dependsOn: briefVars.dependsOn });
7228
7226
  console.log(`
7229
7227
  Wrote ${briefPath}`);
7230
7228
  if (prdWritten)
@@ -7233,6 +7231,55 @@ Wrote ${briefPath}`);
7233
7231
  console.log(`
7234
7232
  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
7233
  }
7234
+ function stageSourceDoc(from, dir) {
7235
+ const srcPath = resolve(process.cwd(), from);
7236
+ if (!fileExists(srcPath)) {
7237
+ console.log(`Source document not found: ${srcPath}`);
7238
+ process.exitCode = 1;
7239
+ return;
7240
+ }
7241
+ const docName = `${SOURCE_DOC_BASE}${extname(srcPath).toLowerCase()}`;
7242
+ const destPath = join8(dir, docName);
7243
+ if (fileExists(destPath)) {
7244
+ console.log(`${destPath} already exists — leaving it in place.`);
7245
+ } else {
7246
+ mkdirSync5(dir, { recursive: true });
7247
+ copyFileSync(srcPath, destPath);
7248
+ console.log(`Staged ${basename(srcPath)} → ${destPath}`);
7249
+ }
7250
+ const marker = `[TODO: extract from ${docName}]`;
7251
+ return {
7252
+ goal: marker,
7253
+ actors: marker,
7254
+ constraints: marker,
7255
+ outOfScope: marker,
7256
+ dependsOn: "-"
7257
+ };
7258
+ }
7259
+ async function runInteractive(feature) {
7260
+ const answers = await import_prompts.default([
7261
+ { type: "text", name: "goal", message: "One-sentence goal — what must this feature deliver?" },
7262
+ { type: "text", name: "actors", message: "Who are the actors/roles involved?" },
7263
+ { type: "text", name: "constraints", message: "Key constraints (regulatory, technical, deadline)?" },
7264
+ { type: "text", name: "outOfScope", message: "What's explicitly OUT of scope?" },
7265
+ {
7266
+ type: "text",
7267
+ name: "dependsOn",
7268
+ message: "Does this depend on another existing feature? (feature name, or leave blank)"
7269
+ }
7270
+ ]);
7271
+ if (!answers.goal) {
7272
+ console.log("Smelting cancelled.");
7273
+ return null;
7274
+ }
7275
+ return {
7276
+ goal: answers.goal ?? "",
7277
+ actors: answers.actors ?? "",
7278
+ constraints: answers.constraints ?? "",
7279
+ outOfScope: answers.outOfScope ?? "",
7280
+ dependsOn: answers.dependsOn?.trim() || "-"
7281
+ };
7282
+ }
7236
7283
 
7237
7284
  // src/commands/scaffold.ts
7238
7285
  import { join as join9 } from "node:path";
@@ -7299,8 +7346,8 @@ function verify(feature) {
7299
7346
  }
7300
7347
 
7301
7348
  // 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";
7349
+ import { existsSync as existsSync4, readFileSync as readFileSync4, readdirSync, mkdirSync as mkdirSync6, writeFileSync as writeFileSync5 } from "node:fs";
7350
+ import { join as join11, extname as extname2, relative, basename as basename2 } from "node:path";
7304
7351
  var IGNORE_DIRS = new Set([
7305
7352
  "node_modules",
7306
7353
  ".git",
@@ -7340,7 +7387,7 @@ function scan(options = {}) {
7340
7387
  apis: pathListOrNone(apis),
7341
7388
  tree: renderTree(cwd, depth)
7342
7389
  });
7343
- mkdirSync5(specsDir(cwd), { recursive: true });
7390
+ mkdirSync6(specsDir(cwd), { recursive: true });
7344
7391
  const outPath = join11(specsDir(cwd), "CODEBASE.md");
7345
7392
  writeFileSync5(outPath, rendered, "utf-8");
7346
7393
  console.log(`Wrote ${outPath}`);
@@ -7427,8 +7474,8 @@ function detectStack(root) {
7427
7474
  return found;
7428
7475
  }
7429
7476
  function isSchemaFile(f) {
7430
- const ext = extname(f).toLowerCase();
7431
- const base = basename(f).toLowerCase();
7477
+ const ext = extname2(f).toLowerCase();
7478
+ const base = basename2(f).toLowerCase();
7432
7479
  if (ext === ".dbml" || ext === ".prisma" || ext === ".sql")
7433
7480
  return true;
7434
7481
  if (/schema/.test(base) && [".ts", ".js", ".py", ".rb", ".go", ".json"].includes(ext))
@@ -7439,7 +7486,7 @@ function isSchemaFile(f) {
7439
7486
  }
7440
7487
  function isApiFile(f) {
7441
7488
  const p = f.toLowerCase();
7442
- const base = basename(p);
7489
+ const base = basename2(p);
7443
7490
  if (/(openapi|swagger)\.(ya?ml|json)$/.test(base))
7444
7491
  return true;
7445
7492
  if (/\.(controller|route|router|resolver)\.(ts|js|py)$/.test(base))
@@ -7449,7 +7496,7 @@ function isApiFile(f) {
7449
7496
  return false;
7450
7497
  }
7451
7498
  function renderProject(root, fileCount) {
7452
- let name = basename(root);
7499
+ let name = basename2(root);
7453
7500
  const pkgPath = join11(root, "package.json");
7454
7501
  if (existsSync4(pkgPath)) {
7455
7502
  try {
@@ -7480,7 +7527,7 @@ function renderProject(root, fileCount) {
7480
7527
  function fileStats(files) {
7481
7528
  const counts = new Map;
7482
7529
  for (const f of files) {
7483
- const ext = extname(f).toLowerCase() || "(no ext)";
7530
+ const ext = extname2(f).toLowerCase() || "(no ext)";
7484
7531
  counts.set(ext, (counts.get(ext) ?? 0) + 1);
7485
7532
  }
7486
7533
  const top = [...counts.entries()].sort((a, b) => b[1] - a[1]).slice(0, 12);
@@ -7488,7 +7535,7 @@ function fileStats(files) {
7488
7535
  `);
7489
7536
  }
7490
7537
  function renderTree(root, maxDepth) {
7491
- const lines = [`${basename(root) || root}/`];
7538
+ const lines = [`${basename2(root) || root}/`];
7492
7539
  walkTree(root, "", maxDepth, lines);
7493
7540
  return lines.join(`
7494
7541
  `);
@@ -7552,15 +7599,18 @@ Every forge drafting command (smelt, schema, contract, tasks, testcase) ` + `wil
7552
7599
  }
7553
7600
 
7554
7601
  // src/cli.ts
7602
+ var require2 = createRequire2(import.meta.url);
7603
+ var { version } = require2("../package.json");
7555
7604
  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");
7605
+ program2.name("forge").description("Spec-driven development CLI — scaffolds specs, schemas, API contracts, tasks, and test cases.").version(version);
7557
7606
  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
7607
  program2.command("bridge <target>").description("(Re)generate AI-tool bridge commands: claude, cursor, windsurf, or generic").action((target) => bridge(target));
7559
7608
  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
7609
  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);
7610
+ 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
7611
  program2.command("schema <feature>").description("Scaffold schema.dbml (requires prd.md)").action(makeScaffoldCommand({ file: SPEC_FILES.schema, label: "schema" }));
7563
7612
  program2.command("contract <feature>").description("Scaffold api-contract.md (requires prd.md)").action(makeScaffoldCommand({ file: SPEC_FILES.contract, label: "contract" }));
7613
+ program2.command("mockup <feature>").description("Scaffold mockup.html (requires prd.md)").action(makeScaffoldCommand({ file: SPEC_FILES.mockup, label: "mockup" }));
7564
7614
  program2.command("tasks <feature>").description("Scaffold tasks.md WBS (requires prd.md)").action(makeScaffoldCommand({ file: SPEC_FILES.tasks, label: "tasks" }));
7565
7615
  program2.command("testcase <feature>").description("Scaffold testcases.md (requires prd.md)").action(makeScaffoldCommand({ file: SPEC_FILES.testcases, label: "testcase" }));
7566
7616
  program2.command("verify <feature>").description("Check that all spec files exist and have no unresolved [TODO] markers").action(verify);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rijalpermana/spec-forge",
3
- "version": "0.1.1",
3
+ "version": "0.2.0",
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",
@@ -0,0 +1,45 @@
1
+ <!-- Mockup — {{feature}}
2
+ Generated by `forge mockup {{feature}}`. Derive screens/sections from prd.md's
3
+ user stories — do not invent flows that aren't grounded in the PRD. This is a
4
+ throwaway wireframe for review, not production markup: plain HTML/CSS, no
5
+ framework, no build step. Open directly in a browser. -->
6
+ <!doctype html>
7
+ <html lang="en">
8
+ <head>
9
+ <meta charset="utf-8" />
10
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
11
+ <title>{{feature}} — mockup</title>
12
+ <style>
13
+ :root { color-scheme: light dark; }
14
+ body { font-family: system-ui, sans-serif; max-width: 960px; margin: 2rem auto; padding: 0 1rem; line-height: 1.5; }
15
+ header { border-bottom: 1px solid #8888; margin-bottom: 1.5rem; padding-bottom: 1rem; }
16
+ h1 { font-size: 1.25rem; margin: 0; }
17
+ .note { color: #888; font-size: 0.85rem; }
18
+ section { margin-bottom: 2rem; padding: 1rem; border: 1px dashed #8886; border-radius: 8px; }
19
+ section h2 { font-size: 1rem; margin-top: 0; }
20
+ .todo { color: #b8860b; font-weight: 600; }
21
+ button, input, select { font: inherit; padding: 0.4rem 0.6rem; }
22
+ </style>
23
+ </head>
24
+ <body>
25
+ <header>
26
+ <h1>{{feature}}</h1>
27
+ <p class="note">Wireframe only — grounded in prd.md. Replace each placeholder section below.</p>
28
+ </header>
29
+
30
+ <section>
31
+ <h2>[TODO] Primary screen / view</h2>
32
+ <p class="todo">[TODO] Lay out the main UI for this feature's core user story — key fields, actions, and states (empty, loading, error).</p>
33
+ </section>
34
+
35
+ <section>
36
+ <h2>[TODO] Secondary screen / modal (if any)</h2>
37
+ <p class="todo">[TODO] Add a screen per additional user story from prd.md, or delete this section if there's only one.</p>
38
+ </section>
39
+
40
+ <section>
41
+ <h2>[TODO] Notes</h2>
42
+ <p class="todo">[TODO] Call out anything ambiguous in prd.md that this mockup had to assume.</p>
43
+ </section>
44
+ </body>
45
+ </html>