forgehive 0.7.2 → 0.7.3

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
@@ -15,7 +15,7 @@
15
15
  <img src="https://img.shields.io/badge/node-%3E%3D18-brightgreen" alt="Node.js ≥ 18">
16
16
  <img src="https://img.shields.io/badge/typescript-5.8-blue" alt="TypeScript">
17
17
  <img src="https://img.shields.io/badge/tests-267%20passing-success" alt="267 tests">
18
- <img src="https://img.shields.io/badge/bundle-244KB-lightgrey" alt="244KB bundle">
18
+ <img src="https://img.shields.io/badge/bundle-247KB-lightgrey" alt="247KB bundle">
19
19
  <img src="https://img.shields.io/badge/license-MIT-green" alt="MIT">
20
20
  </p>
21
21
 
@@ -109,14 +109,14 @@ This makes both `fh` and `forgehive` available globally.
109
109
  git clone https://github.com/matharnica/forgehive
110
110
  cd forgehive
111
111
  npm install
112
- npm run build # compiles to dist/cli.js (~244 KB)
112
+ npm run build # compiles to dist/cli.js (~247 KB)
113
113
  npm link # makes 'fh' available globally
114
114
  ```
115
115
 
116
116
  Verify the installation:
117
117
 
118
118
  ```bash
119
- fh --version # should print 0.7.1
119
+ fh --version # should print 0.7.2
120
120
  fh --help # lists all available commands
121
121
  ```
122
122
 
@@ -166,11 +166,15 @@ fh security report gdpr # generates a CISO-ready compliance report
166
166
 
167
167
  | Command | Description |
168
168
  |---|---|
169
- | `fh init` | Scan project, create `.forgehive/`, merge CLAUDE.md block, write AGENTS.md |
169
+ | `fh --help` | Show full command reference |
170
+ | `fh -h` | Same as --help |
171
+ | `fh init` | Set up forgehive in the current project. Use --force to re-initialize an existing setup |
170
172
  | `fh confirm` | Confirm `capabilities.yaml` — activates context for Claude |
171
173
  | `fh rollback` | Remove `.forgehive/` and the CLAUDE.md block cleanly |
172
174
  | `fh status` | Show current project status, drift warning if scan is outdated |
173
175
 
176
+ > Running `fh init` on a project that already has `.forgehive/` prints a warning and exits. Use `fh init --force` to re-initialize (overwrites `capabilities.yaml` and scans again). To update only the scan, use `fh scan --update`.
177
+
174
178
  **When to use `fh rollback`:** If you want to remove forgehive from a project entirely. It removes exactly the block it inserted into CLAUDE.md (nothing else) and deletes `.forgehive/`.
175
179
 
176
180
  ---
@@ -407,11 +411,12 @@ fh story create "As a user I want to log in" --epic EPC-1 --points 3 # with ep
407
411
  fh story list # list all backlog stories
408
412
  fh story list --epic EPC-1 # filter by epic
409
413
  fh story show US-1 # show full story card with acceptance criteria
414
+ fh story sprint US-1 # mark story as in-sprint (pulled into current sprint)
410
415
  fh story done US-1 # mark story as done
411
416
  fh story done US-1 --points 5 # mark done and record actual points
412
417
  ```
413
418
 
414
- Each story card is a Markdown file at `.forgehive/memory/stories/US-N.md`. The card includes a title, acceptance criteria placeholder, story points, epic link, and status (`backlog` | `done`). When you run `/fh-sprint` in a Claude Code session, Claude reads the backlog and uses velocity data to suggest a realistic sprint scope.
419
+ Each story card is a Markdown file at `.forgehive/memory/stories/US-N.md`. The card includes a title, acceptance criteria placeholder, story points, epic link, and status (`backlog` | `in-sprint` | `done`). Stories move through `backlog → in-sprint → done`. When you run `/fh-sprint` in a Claude Code session, Claude reads the backlog and uses velocity data to suggest a realistic sprint scope.
415
420
 
416
421
  ---
417
422
 
@@ -842,7 +847,7 @@ Global credential store (chmod 600). Managed exclusively via `fh mcp auth` comma
842
847
  |---|---|
843
848
  | Runtime | Node.js ≥ 18, ESM |
844
849
  | Language | TypeScript |
845
- | Build | esbuild -> `dist/cli.js` (~244 KB, single bundle) |
850
+ | Build | esbuild -> `dist/cli.js` (~247 KB, single bundle) |
846
851
  | Type check | `tsc --noEmit` |
847
852
  | Tests | `node:test` (native) + tsx ESM loader, 267 tests |
848
853
  | Dependencies | `js-yaml` (sole runtime dependency) |
package/dist/cli.js CHANGED
@@ -2751,8 +2751,8 @@ var init_harness = __esm({
2751
2751
 
2752
2752
  // src/cli.ts
2753
2753
  init_js_yaml();
2754
- import fs30 from "node:fs";
2755
- import path31 from "node:path";
2754
+ import fs31 from "node:fs";
2755
+ import path32 from "node:path";
2756
2756
 
2757
2757
  // src/scanner.ts
2758
2758
  import fs from "node:fs";
@@ -6454,17 +6454,206 @@ function formatVelocityReport(history) {
6454
6454
  return lines.join("\n");
6455
6455
  }
6456
6456
 
6457
+ // src/docs.ts
6458
+ init_js_yaml();
6459
+ import fs30 from "node:fs";
6460
+ import path31 from "node:path";
6461
+ import { spawnSync as spawnSync11 } from "node:child_process";
6462
+ var SOURCE_EXTS = [".ts", ".tsx", ".js", ".jsx", ".py", ".go"];
6463
+ var IGNORE_DIRS3 = ["node_modules", ".git", "dist", ".forgehive", "coverage", ".next", "build", "test"];
6464
+ var EXPORT_PATTERNS = [
6465
+ /^export\s+(?:async\s+)?function\s+(\w+)/gm,
6466
+ /^export\s+(?:const|let|var)\s+(\w+)/gm,
6467
+ /^export\s+(?:class|interface|type|enum)\s+(\w+)/gm,
6468
+ /^export\s+default\s+(?:function\s+)?(\w+)?/gm
6469
+ ];
6470
+ function readCapabilities2(forgehiveDir2) {
6471
+ const capPath = path31.join(forgehiveDir2, "capabilities.yaml");
6472
+ if (!fs30.existsSync(capPath)) return {};
6473
+ try {
6474
+ return jsYaml.load(fs30.readFileSync(capPath, "utf8")) ?? {};
6475
+ } catch {
6476
+ return {};
6477
+ }
6478
+ }
6479
+ function readMemoryFiles2(forgehiveDir2) {
6480
+ const memDir = path31.join(forgehiveDir2, "memory");
6481
+ if (!fs30.existsSync(memDir)) return {};
6482
+ const result = {};
6483
+ for (const f of fs30.readdirSync(memDir).filter((f2) => f2.endsWith(".md") && f2 !== "MEMORY.md")) {
6484
+ result[f] = fs30.readFileSync(path31.join(memDir, f), "utf8");
6485
+ }
6486
+ return result;
6487
+ }
6488
+ function getRecentCommits2(projectRoot2, n = 10) {
6489
+ const result = spawnSync11("git", ["log", "--oneline", `-${n}`], { cwd: projectRoot2, encoding: "utf8" });
6490
+ if (result.status !== 0) return [];
6491
+ return result.stdout.trim().split("\n").filter(Boolean);
6492
+ }
6493
+ function extractExports(content) {
6494
+ const exports = [];
6495
+ for (const pattern of EXPORT_PATTERNS) {
6496
+ pattern.lastIndex = 0;
6497
+ let m;
6498
+ while ((m = pattern.exec(content)) !== null) {
6499
+ if (m[1]) exports.push(m[1]);
6500
+ }
6501
+ }
6502
+ return [...new Set(exports)];
6503
+ }
6504
+ function walkSourceFiles(dir) {
6505
+ const results = [];
6506
+ function walk(current) {
6507
+ if (!fs30.existsSync(current)) return;
6508
+ for (const entry of fs30.readdirSync(current, { withFileTypes: true })) {
6509
+ if (IGNORE_DIRS3.includes(entry.name)) continue;
6510
+ const full = path31.join(current, entry.name);
6511
+ if (entry.isDirectory()) walk(full);
6512
+ else if (entry.isFile() && SOURCE_EXTS.includes(path31.extname(entry.name))) results.push(full);
6513
+ }
6514
+ }
6515
+ walk(dir);
6516
+ return results;
6517
+ }
6518
+ function generateUserGuide(projectRoot2, forgehiveDir2) {
6519
+ const projectName = path31.basename(projectRoot2);
6520
+ const caps = readCapabilities2(forgehiveDir2);
6521
+ const memFiles = readMemoryFiles2(forgehiveDir2);
6522
+ const commits = getRecentCommits2(projectRoot2);
6523
+ const lines = [];
6524
+ lines.push(`# ${projectName} \u2014 User Guide`);
6525
+ lines.push("");
6526
+ lines.push(`> Generated by forgehive on ${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}`);
6527
+ lines.push("");
6528
+ lines.push("## Overview");
6529
+ lines.push("");
6530
+ if (memFiles["project.md"]) {
6531
+ const content = memFiles["project.md"].replace(/^---[\s\S]*?---\n/m, "").trim();
6532
+ lines.push(content);
6533
+ } else {
6534
+ lines.push(`${projectName} is a ${caps.language ?? "software"} application.`);
6535
+ }
6536
+ lines.push("");
6537
+ lines.push("## Requirements");
6538
+ lines.push("");
6539
+ const pm = caps.packageManager ?? "npm";
6540
+ const lang = caps.language;
6541
+ if (lang === "typescript" || lang === "javascript") {
6542
+ lines.push("- **Node.js** \u2265 18");
6543
+ lines.push(`- **${pm}** (package manager)`);
6544
+ } else if (lang === "python") {
6545
+ lines.push("- **Python** \u2265 3.9");
6546
+ lines.push("- **pip** or **poetry**");
6547
+ } else if (lang === "go") {
6548
+ lines.push("- **Go** \u2265 1.21");
6549
+ }
6550
+ lines.push("");
6551
+ lines.push("## Installation");
6552
+ lines.push("");
6553
+ lines.push("```bash");
6554
+ lines.push("# Clone the repository");
6555
+ lines.push(`git clone <repo-url>`);
6556
+ lines.push(`cd ${projectName}`);
6557
+ lines.push("");
6558
+ if (lang === "typescript" || lang === "javascript") {
6559
+ lines.push(`${pm === "yarn" ? "yarn" : pm === "pnpm" ? "pnpm install" : "npm install"}`);
6560
+ } else if (lang === "python") {
6561
+ lines.push("pip install -r requirements.txt");
6562
+ } else if (lang === "go") {
6563
+ lines.push("go mod download");
6564
+ }
6565
+ lines.push("```");
6566
+ lines.push("");
6567
+ const entryPoints = caps.entryPoints;
6568
+ if (Array.isArray(entryPoints) && entryPoints.length > 0) {
6569
+ lines.push("## Getting Started");
6570
+ lines.push("");
6571
+ lines.push("```bash");
6572
+ for (const ep of entryPoints.slice(0, 3)) lines.push(`node ${ep}`);
6573
+ lines.push("```");
6574
+ lines.push("");
6575
+ }
6576
+ if (memFiles["stack.md"]) {
6577
+ lines.push("## Configuration");
6578
+ lines.push("");
6579
+ const content = memFiles["stack.md"].replace(/^---[\s\S]*?---\n/m, "").trim();
6580
+ lines.push(content);
6581
+ lines.push("");
6582
+ }
6583
+ if (commits.length > 0) {
6584
+ lines.push("## Changelog");
6585
+ lines.push("");
6586
+ lines.push("Recent changes:");
6587
+ lines.push("");
6588
+ for (const c of commits.slice(0, 5)) lines.push(`- ${c}`);
6589
+ lines.push("");
6590
+ lines.push("Run `fh changelog` for the full changelog.");
6591
+ lines.push("");
6592
+ }
6593
+ lines.push("## Support");
6594
+ lines.push("");
6595
+ lines.push("For issues, open a ticket in the project repository.");
6596
+ return lines.join("\n");
6597
+ }
6598
+ function generateApiReference(projectRoot2) {
6599
+ const srcDir = path31.join(projectRoot2, "src");
6600
+ const searchDir = fs30.existsSync(srcDir) ? srcDir : projectRoot2;
6601
+ const files = walkSourceFiles(searchDir);
6602
+ const lines = [];
6603
+ lines.push("# API Reference");
6604
+ lines.push("");
6605
+ lines.push(`Generated: ${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}`);
6606
+ lines.push("");
6607
+ if (files.length === 0) {
6608
+ lines.push("No source files found.");
6609
+ return lines.join("\n");
6610
+ }
6611
+ for (const filePath of files) {
6612
+ let content = "";
6613
+ try {
6614
+ content = fs30.readFileSync(filePath, "utf8");
6615
+ } catch {
6616
+ continue;
6617
+ }
6618
+ const exports = extractExports(content);
6619
+ if (exports.length === 0) continue;
6620
+ const relPath = path31.relative(projectRoot2, filePath);
6621
+ lines.push(`## \`${relPath}\``);
6622
+ lines.push("");
6623
+ lines.push("**Exports:**");
6624
+ lines.push("");
6625
+ for (const exp of exports) lines.push(`- \`${exp}\``);
6626
+ lines.push("");
6627
+ }
6628
+ return lines.join("\n");
6629
+ }
6630
+ function listExistingDocs(projectRoot2) {
6631
+ const docs = [];
6632
+ const docsDir = path31.join(projectRoot2, "docs");
6633
+ if (fs30.existsSync(docsDir)) {
6634
+ for (const f of fs30.readdirSync(docsDir)) {
6635
+ if (f.endsWith(".md")) docs.push(path31.join(docsDir, f));
6636
+ }
6637
+ }
6638
+ const rootDocs = ["README.md", "CHANGELOG.md", "ONBOARDING.md", "CONTRIBUTING.md"];
6639
+ for (const f of rootDocs) {
6640
+ const full = path31.join(projectRoot2, f);
6641
+ if (fs30.existsSync(full)) docs.push(full);
6642
+ }
6643
+ return docs;
6644
+ }
6645
+
6457
6646
  // src/cli.ts
6458
6647
  var [, , command, subcommand, ...rest] = process.argv;
6459
6648
  var projectRoot = process.cwd();
6460
- var forgehiveDir = path31.join(projectRoot, ".forgehive");
6649
+ var forgehiveDir = path32.join(projectRoot, ".forgehive");
6461
6650
  if (command === "--version" || command === "-v") {
6462
- console.log("0.7.2");
6651
+ console.log("0.7.3");
6463
6652
  process.exit(0);
6464
6653
  }
6465
6654
  if (command === "--help" || command === "-h" || command === "help") {
6466
6655
  console.log(`
6467
- forgehive v0.7.2 \u2014 Context-aware AI development environment
6656
+ forgehive v0.7.3 \u2014 Context-aware AI development environment
6468
6657
 
6469
6658
  USAGE
6470
6659
  fh <command> [subcommand] [options]
@@ -6495,6 +6684,12 @@ CODEBASE
6495
6684
  fh onboard [--output path] Generate ONBOARDING.md
6496
6685
  fh changelog [--since tag] Semantic changelog from git
6497
6686
  fh metrics [--since date] Developer productivity metrics
6687
+ fh docs List existing documentation
6688
+ fh docs user [--output path] Generate user-facing guide (docs/user-guide.md)
6689
+ fh docs api [--output path] Generate API reference from exports (docs/api.md)
6690
+ fh docs onboard Generate onboarding doc
6691
+ fh docs changelog [--since tag] Generate changelog
6692
+ fh docs adr "<title>" Create Architecture Decision Record
6498
6693
 
6499
6694
  SPRINT PLANNING
6500
6695
  fh story create <title> [--epic EPC-N] [--points N]
@@ -6531,18 +6726,18 @@ COST
6531
6726
  process.exit(0);
6532
6727
  }
6533
6728
  function loadClaudeMdBlock() {
6534
- const templatePath = path31.join(
6535
- path31.dirname(new URL(import.meta.url).pathname),
6729
+ const templatePath = path32.join(
6730
+ path32.dirname(new URL(import.meta.url).pathname),
6536
6731
  "..",
6537
6732
  "forgehive",
6538
6733
  "templates",
6539
6734
  "claude-md.block.md"
6540
6735
  );
6541
- if (!fs30.existsSync(templatePath)) return "## forgehive\n\nSee .forgehive/ for configuration.";
6542
- return fs30.readFileSync(templatePath, "utf8");
6736
+ if (!fs31.existsSync(templatePath)) return "## forgehive\n\nSee .forgehive/ for configuration.";
6737
+ return fs31.readFileSync(templatePath, "utf8");
6543
6738
  }
6544
6739
  if (command === "init") {
6545
- const forgehiveDirExists = fs30.existsSync(forgehiveDir);
6740
+ const forgehiveDirExists = fs31.existsSync(forgehiveDir);
6546
6741
  if (forgehiveDirExists && !rest.includes("--force")) {
6547
6742
  console.log(`\u26A0 .forgehive/ existiert bereits in diesem Projekt.`);
6548
6743
  console.log(` Nutze 'fh init --force' um neu zu initialisieren (\xFCberschreibt capabilities.yaml).`);
@@ -6560,9 +6755,9 @@ if (command === "init") {
6560
6755
  const block = loadClaudeMdBlock();
6561
6756
  writeForgehiveDir(projectRoot, scanResult, capMap, block);
6562
6757
  const hash = computeHash(projectRoot);
6563
- fs30.writeFileSync(path31.join(forgehiveDir, ".scan-hash"), hash, "utf8");
6564
- const runtimeDir = path31.join(
6565
- path31.dirname(new URL(import.meta.url).pathname),
6758
+ fs31.writeFileSync(path32.join(forgehiveDir, ".scan-hash"), hash, "utf8");
6759
+ const runtimeDir = path32.join(
6760
+ path32.dirname(new URL(import.meta.url).pathname),
6566
6761
  "..",
6567
6762
  "forgehive"
6568
6763
  );
@@ -6594,7 +6789,7 @@ if (command === "init") {
6594
6789
  process.exit(1);
6595
6790
  }
6596
6791
  } else if (command === "memory") {
6597
- if (!fs30.existsSync(forgehiveDir)) {
6792
+ if (!fs31.existsSync(forgehiveDir)) {
6598
6793
  console.error("Fehler: .forgehive/ nicht gefunden \u2014 f\xFChre zuerst `fh init` aus");
6599
6794
  process.exit(1);
6600
6795
  }
@@ -6603,7 +6798,7 @@ if (command === "init") {
6603
6798
  } else if (subcommand === "clean") {
6604
6799
  cleanMemory(forgehiveDir);
6605
6800
  } else if (subcommand === "export") {
6606
- const outputPath = rest[0] ?? path31.join(projectRoot, "forgehive-memory-export.md");
6801
+ const outputPath = rest[0] ?? path32.join(projectRoot, "forgehive-memory-export.md");
6607
6802
  try {
6608
6803
  exportMemory(forgehiveDir, outputPath);
6609
6804
  } catch (err) {
@@ -6651,7 +6846,7 @@ Ausf\xFChren mit --remove um zu l\xF6schen: fh memory prune ${days} --remove`);
6651
6846
  } else if (subcommand === "snapshot") {
6652
6847
  const snapAction = rest[0];
6653
6848
  if (snapAction === "export") {
6654
- const outPath = rest[1] ?? path31.join(
6849
+ const outPath = rest[1] ?? path32.join(
6655
6850
  projectRoot,
6656
6851
  `forgehive-snapshot-${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}.json`
6657
6852
  );
@@ -6691,11 +6886,11 @@ Ausf\xFChren mit --remove um zu l\xF6schen: fh memory prune ${days} --remove`);
6691
6886
  process.exit(1);
6692
6887
  }
6693
6888
  } else if (command === "scan" && subcommand === "--update") {
6694
- if (!fs30.existsSync(forgehiveDir)) {
6889
+ if (!fs31.existsSync(forgehiveDir)) {
6695
6890
  console.error("Fehler: .forgehive/ nicht gefunden \u2014 f\xFChre zuerst `fh init` aus");
6696
6891
  process.exit(1);
6697
6892
  }
6698
- const savedHash = fs30.existsSync(path31.join(forgehiveDir, ".scan-hash")) ? fs30.readFileSync(path31.join(forgehiveDir, ".scan-hash"), "utf8").trim() : null;
6893
+ const savedHash = fs31.existsSync(path32.join(forgehiveDir, ".scan-hash")) ? fs31.readFileSync(path32.join(forgehiveDir, ".scan-hash"), "utf8").trim() : null;
6699
6894
  const currentHash = computeHash(projectRoot);
6700
6895
  if (savedHash === currentHash) {
6701
6896
  console.log("\u2713 Keine \xC4nderungen erkannt \u2014 capabilities.yaml ist aktuell");
@@ -6703,7 +6898,7 @@ Ausf\xFChren mit --remove um zu l\xF6schen: fh memory prune ${days} --remove`);
6703
6898
  }
6704
6899
  console.log("\u{1F50D} \xC4nderungen erkannt \u2014 scanne erneut...\n");
6705
6900
  const oldDoc = jsYaml.load(
6706
- fs30.readFileSync(path31.join(forgehiveDir, "capabilities.yaml"), "utf8")
6901
+ fs31.readFileSync(path32.join(forgehiveDir, "capabilities.yaml"), "utf8")
6707
6902
  );
6708
6903
  const oldMap = { confirmed: oldDoc.capabilities.confirmed ?? [], inferred: [] };
6709
6904
  const scanResult = scan(projectRoot);
@@ -6723,16 +6918,16 @@ Ausf\xFChren mit --remove um zu l\xF6schen: fh memory prune ${days} --remove`);
6723
6918
  console.log();
6724
6919
  const block = loadClaudeMdBlock();
6725
6920
  writeForgehiveDir(projectRoot, scanResult, newMap, block);
6726
- fs30.writeFileSync(path31.join(forgehiveDir, ".scan-hash"), currentHash, "utf8");
6921
+ fs31.writeFileSync(path32.join(forgehiveDir, ".scan-hash"), currentHash, "utf8");
6727
6922
  console.log("\u2713 scan-result.yaml und capabilities.yaml aktualisiert");
6728
6923
  console.log(" F\xFChre `fh confirm` aus um die \xC4nderungen zu best\xE4tigen");
6729
6924
  }
6730
6925
  } else if (command === "scan" && subcommand === "--check") {
6731
- if (!fs30.existsSync(path31.join(forgehiveDir, ".scan-hash"))) {
6926
+ if (!fs31.existsSync(path32.join(forgehiveDir, ".scan-hash"))) {
6732
6927
  console.log("Warnung: Kein Scan-Hash gefunden. F\xFChre `fh init` aus.");
6733
6928
  process.exit(1);
6734
6929
  }
6735
- const saved = fs30.readFileSync(path31.join(forgehiveDir, ".scan-hash"), "utf8").trim();
6930
+ const saved = fs31.readFileSync(path32.join(forgehiveDir, ".scan-hash"), "utf8").trim();
6736
6931
  const current = computeHash(projectRoot);
6737
6932
  if (saved !== current) {
6738
6933
  console.log("\u26A0 Codebase hat sich seit letztem Scan ge\xE4ndert.");
@@ -6741,7 +6936,7 @@ Ausf\xFChren mit --remove um zu l\xF6schen: fh memory prune ${days} --remove`);
6741
6936
  }
6742
6937
  console.log("\u2713 capabilities.yaml ist aktuell");
6743
6938
  } else if (command === "skills") {
6744
- if (!fs30.existsSync(forgehiveDir)) {
6939
+ if (!fs31.existsSync(forgehiveDir)) {
6745
6940
  console.error("Fehler: .forgehive/ nicht gefunden \u2014 f\xFChre zuerst `fh init` aus");
6746
6941
  process.exit(1);
6747
6942
  }
@@ -6777,7 +6972,7 @@ Ausf\xFChren mit --remove um zu l\xF6schen: fh memory prune ${days} --remove`);
6777
6972
  process.exit(1);
6778
6973
  }
6779
6974
  } else if (command === "party") {
6780
- if (!fs30.existsSync(forgehiveDir)) {
6975
+ if (!fs31.existsSync(forgehiveDir)) {
6781
6976
  console.error("Fehler: .forgehive/ nicht gefunden \u2014 f\xFChre zuerst `fh init` aus");
6782
6977
  process.exit(1);
6783
6978
  }
@@ -6898,7 +7093,7 @@ Ausf\xFChren mit --remove um zu l\xF6schen: fh memory prune ${days} --remove`);
6898
7093
  }
6899
7094
  }
6900
7095
  } else if (command === "wire") {
6901
- if (!fs30.existsSync(forgehiveDir)) {
7096
+ if (!fs31.existsSync(forgehiveDir)) {
6902
7097
  console.error("Fehler: .forgehive/ nicht gefunden \u2014 f\xFChre zuerst `fh init` aus");
6903
7098
  process.exit(1);
6904
7099
  }
@@ -6937,7 +7132,7 @@ N\xE4chster Schritt: Setze die erforderlichen Umgebungsvariablen und starte Clau
6937
7132
  const limitIdx = allArgs.indexOf("--limit");
6938
7133
  const alertIdx = allArgs.indexOf("--alert");
6939
7134
  if (limitIdx !== -1) {
6940
- if (!fs30.existsSync(forgehiveDir)) {
7135
+ if (!fs31.existsSync(forgehiveDir)) {
6941
7136
  console.error("Fehler: .forgehive/ nicht gefunden \u2014 f\xFChre zuerst `fh init` aus");
6942
7137
  process.exit(1);
6943
7138
  }
@@ -6963,14 +7158,14 @@ N\xE4chster Schritt: Setze die erforderlichen Umgebungsvariablen und starte Clau
6963
7158
  }
6964
7159
  const sessions = parseCostSessions(projectRoot);
6965
7160
  console.log(formatCostReport(sessions, range));
6966
- if (fs30.existsSync(forgehiveDir)) {
7161
+ if (fs31.existsSync(forgehiveDir)) {
6967
7162
  const total = sessions.reduce((s, x) => s + x.estimatedCostUsd, 0);
6968
7163
  const status = checkSpendStatus(forgehiveDir, total);
6969
7164
  if (status.message) console.log("\n" + status.message);
6970
7165
  }
6971
7166
  }
6972
7167
  } else if (command === "watch") {
6973
- if (!fs30.existsSync(forgehiveDir)) {
7168
+ if (!fs31.existsSync(forgehiveDir)) {
6974
7169
  console.error("Fehler: .forgehive/ nicht gefunden \u2014 f\xFChre zuerst `fh init` aus");
6975
7170
  process.exit(1);
6976
7171
  }
@@ -6986,7 +7181,7 @@ N\xE4chster Schritt: Setze die erforderlichen Umgebungsvariablen und starte Clau
6986
7181
  process.exit(0);
6987
7182
  });
6988
7183
  } else if (command === "mcp") {
6989
- if (!fs30.existsSync(forgehiveDir)) {
7184
+ if (!fs31.existsSync(forgehiveDir)) {
6990
7185
  console.error("Fehler: .forgehive/ nicht gefunden \u2014 f\xFChre zuerst `fh init` aus");
6991
7186
  process.exit(1);
6992
7187
  }
@@ -7097,7 +7292,7 @@ Setze diese Umgebungsvariablen:`);
7097
7292
  process.exit(1);
7098
7293
  }
7099
7294
  } else if (command === "security") {
7100
- if (!fs30.existsSync(forgehiveDir)) {
7295
+ if (!fs31.existsSync(forgehiveDir)) {
7101
7296
  console.error("Fehler: .forgehive/ nicht gefunden \u2014 f\xFChre zuerst `fh init` aus");
7102
7297
  process.exit(1);
7103
7298
  }
@@ -7183,8 +7378,8 @@ Setze diese Umgebungsvariablen:`);
7183
7378
  `);
7184
7379
  const report = generateSecurityReport(projectRoot, forgehiveDir, mode);
7185
7380
  const text = formatSecurityReport(report);
7186
- const reportPath = path31.join(forgehiveDir, "security-report.md");
7187
- fs30.writeFileSync(reportPath, text, "utf8");
7381
+ const reportPath = path32.join(forgehiveDir, "security-report.md");
7382
+ fs31.writeFileSync(reportPath, text, "utf8");
7188
7383
  console.log(text);
7189
7384
  console.log(`
7190
7385
  \u2713 Report gespeichert: ${reportPath}`);
@@ -7196,8 +7391,8 @@ Setze diese Umgebungsvariablen:`);
7196
7391
  } else if (subcommand === "permissions") {
7197
7392
  const { writePermissions: writePermissions2 } = await Promise.resolve().then(() => (init_harness(), harness_exports));
7198
7393
  writePermissions2(forgehiveDir);
7199
- const permPath = path31.join(forgehiveDir, "harness", "permissions.yaml");
7200
- console.log(fs30.readFileSync(permPath, "utf8"));
7394
+ const permPath = path32.join(forgehiveDir, "harness", "permissions.yaml");
7395
+ console.log(fs31.readFileSync(permPath, "utf8"));
7201
7396
  } else {
7202
7397
  console.error(`Unbekannter security-Subcommand: ${subcommand}`);
7203
7398
  console.error("Verf\xFCgbar: scan | deps | audit | report [gdpr|soc2|hipaa|none] | permissions");
@@ -7209,35 +7404,35 @@ Setze diese Umgebungsvariablen:`);
7209
7404
  const failOnArg = allCiArgs.includes("--fail-on") ? allCiArgs[allCiArgs.indexOf("--fail-on") + 1] : "high";
7210
7405
  const initFlag = allCiArgs.includes("--init");
7211
7406
  if (initFlag) {
7212
- const ghDir = path31.join(projectRoot, ".github", "workflows");
7213
- fs30.mkdirSync(ghDir, { recursive: true });
7214
- const outPath = path31.join(ghDir, "forgehive.yml");
7215
- fs30.writeFileSync(outPath, getGithubActionsTemplate(), "utf8");
7407
+ const ghDir = path32.join(projectRoot, ".github", "workflows");
7408
+ fs31.mkdirSync(ghDir, { recursive: true });
7409
+ const outPath = path32.join(ghDir, "forgehive.yml");
7410
+ fs31.writeFileSync(outPath, getGithubActionsTemplate(), "utf8");
7216
7411
  console.log(`\u2714 GitHub Actions workflow geschrieben: ${outPath}`);
7217
7412
  } else {
7218
7413
  const report = generateCiReport(projectRoot, forgehiveDir, failOnArg);
7219
7414
  const output = formatCiReport(report, format);
7220
7415
  console.log(output);
7221
7416
  if (format === "json") {
7222
- fs30.mkdirSync(forgehiveDir, { recursive: true });
7223
- fs30.writeFileSync(path31.join(forgehiveDir, "ci-report.json"), output, "utf8");
7417
+ fs31.mkdirSync(forgehiveDir, { recursive: true });
7418
+ fs31.writeFileSync(path32.join(forgehiveDir, "ci-report.json"), output, "utf8");
7224
7419
  }
7225
7420
  if (report.status === "fail") process.exit(1);
7226
7421
  }
7227
7422
  } else if (command === "map") {
7228
7423
  const map2 = generateMap(projectRoot);
7229
7424
  const md = formatMap(map2);
7230
- const mapPath = path31.join(forgehiveDir, "map.md");
7231
- fs30.mkdirSync(forgehiveDir, { recursive: true });
7232
- fs30.writeFileSync(mapPath, md, "utf8");
7425
+ const mapPath = path32.join(forgehiveDir, "map.md");
7426
+ fs31.mkdirSync(forgehiveDir, { recursive: true });
7427
+ fs31.writeFileSync(mapPath, md, "utf8");
7233
7428
  console.log(md);
7234
7429
  console.log(`
7235
7430
  \u2714 Codebase-Map gespeichert: ${mapPath}`);
7236
7431
  } else if (command === "onboard") {
7237
7432
  const outputArg = rest.includes("--output") ? rest[rest.indexOf("--output") + 1] : null;
7238
- const outputPath = outputArg ?? path31.join(projectRoot, "ONBOARDING.md");
7433
+ const outputPath = outputArg ?? path32.join(projectRoot, "ONBOARDING.md");
7239
7434
  const doc = generateOnboardingDoc(projectRoot, forgehiveDir);
7240
- fs30.writeFileSync(outputPath, doc, "utf8");
7435
+ fs31.writeFileSync(outputPath, doc, "utf8");
7241
7436
  console.log(`\u2714 Onboarding-Dokument geschrieben: ${outputPath}`);
7242
7437
  } else if (command === "changelog") {
7243
7438
  const sinceArg = rest.includes("--since") ? rest[rest.indexOf("--since") + 1] : null;
@@ -7247,28 +7442,28 @@ Setze diese Umgebungsvariablen:`);
7247
7442
  const commits = parseGitLog(rawLog);
7248
7443
  let version = "unreleased";
7249
7444
  try {
7250
- const pkgPath = path31.join(projectRoot, "package.json");
7251
- if (fs30.existsSync(pkgPath)) {
7252
- const pkg = JSON.parse(fs30.readFileSync(pkgPath, "utf8").replace(/^\s*\/\/.*$/gm, ""));
7445
+ const pkgPath = path32.join(projectRoot, "package.json");
7446
+ if (fs31.existsSync(pkgPath)) {
7447
+ const pkg = JSON.parse(fs31.readFileSync(pkgPath, "utf8").replace(/^\s*\/\/.*$/gm, ""));
7253
7448
  version = pkg.version ?? "unreleased";
7254
7449
  }
7255
7450
  } catch {
7256
7451
  }
7257
7452
  const md = formatChangelog(commits, version);
7258
- const outputPath = outputArg ?? path31.join(projectRoot, "CHANGELOG.md");
7453
+ const outputPath = outputArg ?? path32.join(projectRoot, "CHANGELOG.md");
7259
7454
  let existing = "";
7260
- if (fs30.existsSync(outputPath)) existing = fs30.readFileSync(outputPath, "utf8");
7261
- fs30.writeFileSync(outputPath, md + "\n\n" + existing, "utf8");
7455
+ if (fs31.existsSync(outputPath)) existing = fs31.readFileSync(outputPath, "utf8");
7456
+ fs31.writeFileSync(outputPath, md + "\n\n" + existing, "utf8");
7262
7457
  console.log(`\u2714 CHANGELOG.md aktualisiert: ${outputPath}`);
7263
7458
  console.log(` ${commits.length} Commits verarbeitet`);
7264
7459
  } else if (command === "metrics") {
7265
7460
  const sinceArg = rest.includes("--since") ? rest[rest.indexOf("--since") + 1] : void 0;
7266
7461
  const rawLog = getMetricsGitLog(projectRoot, sinceArg);
7267
7462
  const stats = parseCommitStats(rawLog);
7268
- const md = formatMetrics(stats, path31.basename(projectRoot));
7269
- const metricsPath = path31.join(forgehiveDir, "metrics.md");
7270
- fs30.mkdirSync(forgehiveDir, { recursive: true });
7271
- fs30.writeFileSync(metricsPath, md, "utf8");
7463
+ const md = formatMetrics(stats, path32.basename(projectRoot));
7464
+ const metricsPath = path32.join(forgehiveDir, "metrics.md");
7465
+ fs31.mkdirSync(forgehiveDir, { recursive: true });
7466
+ fs31.writeFileSync(metricsPath, md, "utf8");
7272
7467
  console.log(md);
7273
7468
  console.log(`
7274
7469
  \u2714 Metrics gespeichert: ${metricsPath}`);
@@ -7303,7 +7498,7 @@ Setze diese Umgebungsvariablen:`);
7303
7498
  console.log(`\u2714 ${result.message}`);
7304
7499
  console.log(` fh run status \u2014 aktive Sessions anzeigen`);
7305
7500
  } else if (command === "story") {
7306
- const storiesDir = path31.join(forgehiveDir, "memory", "stories");
7501
+ const storiesDir = path32.join(forgehiveDir, "memory", "stories");
7307
7502
  if (subcommand === "create") {
7308
7503
  const title = rest.filter((r) => !r.startsWith("--")).join(" ");
7309
7504
  const epicArg = rest.includes("--epic") ? rest[rest.indexOf("--epic") + 1] : void 0;
@@ -7314,7 +7509,7 @@ Setze diese Umgebungsvariablen:`);
7314
7509
  }
7315
7510
  const story = createStory(storiesDir, title, epicArg);
7316
7511
  if (pointsArg) updateStoryPoints(storiesDir, story.id, pointsArg);
7317
- console.log(`\u2714 ${story.id} erstellt: ${path31.join(storiesDir, story.id + ".md")}`);
7512
+ console.log(`\u2714 ${story.id} erstellt: ${path32.join(storiesDir, story.id + ".md")}`);
7318
7513
  console.log(` Bearbeite die Datei um Acceptance Criteria hinzuzuf\xFCgen.`);
7319
7514
  } else if (subcommand === "list") {
7320
7515
  const epicFilter = rest.includes("--epic") ? rest[rest.indexOf("--epic") + 1] : null;
@@ -7363,8 +7558,8 @@ Setze diese Umgebungsvariablen:`);
7363
7558
  console.error("Verf\xFCgbar: fh story create | list | show | done");
7364
7559
  }
7365
7560
  } else if (command === "epic") {
7366
- const epicsDir = path31.join(forgehiveDir, "memory", "epics");
7367
- const storiesDir = path31.join(forgehiveDir, "memory", "stories");
7561
+ const epicsDir = path32.join(forgehiveDir, "memory", "epics");
7562
+ const storiesDir = path32.join(forgehiveDir, "memory", "stories");
7368
7563
  if (subcommand === "create") {
7369
7564
  const title = rest.filter((r) => !r.startsWith("--")).join(" ");
7370
7565
  const goalArg = rest.includes("--goal") ? rest[rest.indexOf("--goal") + 1] : void 0;
@@ -7373,7 +7568,7 @@ Setze diese Umgebungsvariablen:`);
7373
7568
  process.exit(1);
7374
7569
  }
7375
7570
  const epic = createEpic(epicsDir, title, goalArg);
7376
- console.log(`\u2714 ${epic.id} erstellt: ${path31.join(epicsDir, epic.id + ".md")}`);
7571
+ console.log(`\u2714 ${epic.id} erstellt: ${path32.join(epicsDir, epic.id + ".md")}`);
7377
7572
  } else if (subcommand === "list") {
7378
7573
  const epics = listEpics(epicsDir);
7379
7574
  if (epics.length === 0) {
@@ -7399,7 +7594,7 @@ Setze diese Umgebungsvariablen:`);
7399
7594
  console.error("Verf\xFCgbar: fh epic create | list | show");
7400
7595
  }
7401
7596
  } else if (command === "velocity") {
7402
- const velocityFile = path31.join(forgehiveDir, "memory", "velocity.md");
7597
+ const velocityFile = path32.join(forgehiveDir, "memory", "velocity.md");
7403
7598
  if (subcommand === "record") {
7404
7599
  const sprintNum = parseInt(rest[0] ?? "0", 10);
7405
7600
  const committed = rest.includes("--committed") ? parseInt(rest[rest.indexOf("--committed") + 1], 10) : NaN;
@@ -7417,9 +7612,100 @@ Setze diese Umgebungsvariablen:`);
7417
7612
  } else {
7418
7613
  console.error("Verf\xFCgbar: fh velocity show | record <N> --committed N --delivered N");
7419
7614
  }
7615
+ } else if (command === "docs") {
7616
+ const docsDir = path32.join(projectRoot, "docs");
7617
+ if (!subcommand || subcommand === "list") {
7618
+ const existing = listExistingDocs(projectRoot);
7619
+ if (existing.length === 0) {
7620
+ console.log("Keine Dokumentation gefunden.");
7621
+ console.log("");
7622
+ console.log("Erstelle Docs:");
7623
+ console.log(" fh docs user \u2014 User Guide (docs/user-guide.md)");
7624
+ console.log(" fh docs api \u2014 API-Referenz (docs/api.md)");
7625
+ console.log(" fh docs onboard \u2014 Onboarding (ONBOARDING.md)");
7626
+ console.log(" fh docs changelog \u2014 Changelog (CHANGELOG.md)");
7627
+ console.log(" fh docs adr <titel> \u2014 Architecture Decision Record");
7628
+ } else {
7629
+ console.log(`Vorhandene Dokumentation (${existing.length} Dateien):`);
7630
+ for (const d of existing) console.log(` ${path32.relative(projectRoot, d)}`);
7631
+ console.log("");
7632
+ console.log("Aktualisieren: fh docs user | api | onboard | changelog");
7633
+ }
7634
+ } else if (subcommand === "user") {
7635
+ fs31.mkdirSync(docsDir, { recursive: true });
7636
+ const outputArg = rest.includes("--output") ? rest[rest.indexOf("--output") + 1] : null;
7637
+ const outputPath = outputArg ?? path32.join(docsDir, "user-guide.md");
7638
+ const guide = generateUserGuide(projectRoot, forgehiveDir);
7639
+ fs31.writeFileSync(outputPath, guide, "utf8");
7640
+ console.log(`\u2714 User Guide geschrieben: ${outputPath}`);
7641
+ } else if (subcommand === "api") {
7642
+ fs31.mkdirSync(docsDir, { recursive: true });
7643
+ const outputArg = rest.includes("--output") ? rest[rest.indexOf("--output") + 1] : null;
7644
+ const outputPath = outputArg ?? path32.join(docsDir, "api.md");
7645
+ const ref = generateApiReference(projectRoot);
7646
+ fs31.writeFileSync(outputPath, ref, "utf8");
7647
+ console.log(`\u2714 API-Referenz geschrieben: ${outputPath}`);
7648
+ } else if (subcommand === "onboard") {
7649
+ const outputArg = rest.includes("--output") ? rest[rest.indexOf("--output") + 1] : null;
7650
+ const outputPath = outputArg ?? path32.join(projectRoot, "ONBOARDING.md");
7651
+ const doc = generateOnboardingDoc(projectRoot, forgehiveDir);
7652
+ fs31.writeFileSync(outputPath, doc, "utf8");
7653
+ console.log(`\u2714 Onboarding-Dokument geschrieben: ${outputPath}`);
7654
+ } else if (subcommand === "changelog") {
7655
+ const sinceArg = rest.includes("--since") ? rest[rest.indexOf("--since") + 1] : null;
7656
+ const outputArg = rest.includes("--output") ? rest[rest.indexOf("--output") + 1] : null;
7657
+ const since = sinceArg ?? getLatestTag(projectRoot) ?? void 0;
7658
+ const rawLog = getGitLogSince(projectRoot, since);
7659
+ const commits = parseGitLog(rawLog);
7660
+ let pkg = {};
7661
+ try {
7662
+ pkg = JSON.parse(fs31.readFileSync(path32.join(projectRoot, "package.json"), "utf8"));
7663
+ } catch {
7664
+ }
7665
+ const version = pkg.version ?? "unreleased";
7666
+ const md = formatChangelog(commits, version);
7667
+ const outputPath = outputArg ?? path32.join(projectRoot, "CHANGELOG.md");
7668
+ let existing = "";
7669
+ if (fs31.existsSync(outputPath)) existing = fs31.readFileSync(outputPath, "utf8");
7670
+ fs31.writeFileSync(outputPath, md + "\n\n" + existing, "utf8");
7671
+ console.log(`\u2714 CHANGELOG.md aktualisiert (${commits.length} Commits)`);
7672
+ } else if (subcommand === "adr") {
7673
+ const title = rest.join(" ");
7674
+ if (!title) {
7675
+ console.error("Usage: fh docs adr <titel>");
7676
+ process.exit(1);
7677
+ }
7678
+ const adrsDir = path32.join(forgehiveDir, "memory", "adrs");
7679
+ fs31.mkdirSync(adrsDir, { recursive: true });
7680
+ const existing = fs31.existsSync(adrsDir) ? fs31.readdirSync(adrsDir).filter((f) => f.endsWith(".md")).length : 0;
7681
+ const adrId = String(existing + 1).padStart(4, "0");
7682
+ const slug = title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
7683
+ const filename = `${adrId}-${slug}.md`;
7684
+ const content = `# ADR-${adrId}: ${title}
7685
+
7686
+ **Datum:** ${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}
7687
+ **Status:** proposed
7688
+
7689
+ ## Kontext
7690
+
7691
+ (Beschreibe das Problem oder die Situation.)
7692
+
7693
+ ## Entscheidung
7694
+
7695
+ (Beschreibe die getroffene Entscheidung.)
7696
+
7697
+ ## Konsequenzen
7698
+
7699
+ (Beschreibe die Auswirkungen dieser Entscheidung.)
7700
+ `;
7701
+ fs31.writeFileSync(path32.join(adrsDir, filename), content, "utf8");
7702
+ console.log(`\u2714 ADR erstellt: .forgehive/memory/adrs/${filename}`);
7703
+ } else {
7704
+ console.error("Verf\xFCgbar: fh docs [list|user|api|onboard|changelog|adr <titel>]");
7705
+ }
7420
7706
  } else {
7421
7707
  console.error("Unbekannter Befehl: " + command);
7422
- console.error("Verf\xFCgbar: init | confirm | rollback | scan | status | ci | map | onboard | changelog | metrics | story [create|list|show|sprint|done] | epic [create|list|show] | velocity [show|record] | sync [push|pull] | run <issue-url> | cost | memory | skills | party | wire | mcp | security");
7708
+ console.error("Verf\xFCgbar: init | confirm | rollback | scan | status | ci | map | onboard | changelog | metrics | docs | story [create|list|show|sprint|done] | epic [create|list|show] | velocity [show|record] | sync [push|pull] | run <issue-url> | cost | memory | skills | party | wire | mcp | security");
7423
7709
  console.error("Hilfe: fh --help");
7424
7710
  process.exit(1);
7425
7711
  }
@@ -2,6 +2,21 @@ You are Eli — Technical Writer. Your job is to write or update documentation.
2
2
 
3
3
  ## Documentation Protocol
4
4
 
5
+ ## Quick CLI Commands
6
+
7
+ Before using Claude interactively, check if these CLI commands cover your need:
8
+
9
+ ```bash
10
+ fh docs user # generate user-facing guide → docs/user-guide.md
11
+ fh docs api # generate API reference → docs/api.md
12
+ fh docs onboard # generate onboarding doc → ONBOARDING.md
13
+ fh docs changelog # update CHANGELOG.md from git
14
+ fh docs adr "<title>" # create Architecture Decision Record
15
+ fh docs # list all existing documentation
16
+ ```
17
+
18
+ For interactive generation with editing, continue below.
19
+
5
20
  Ask the user: **"Was soll ich dokumentieren?"**
6
21
 
7
22
  Options:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "forgehive",
3
- "version": "0.7.2",
3
+ "version": "0.7.3",
4
4
  "description": "Context-aware AI development environment — one binary, your stack.",
5
5
  "type": "module",
6
6
  "bin": {