forgehive 0.6.3 → 0.7.1
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
CHANGED
|
@@ -2751,8 +2751,8 @@ var init_harness = __esm({
|
|
|
2751
2751
|
|
|
2752
2752
|
// src/cli.ts
|
|
2753
2753
|
init_js_yaml();
|
|
2754
|
-
import
|
|
2755
|
-
import
|
|
2754
|
+
import fs30 from "node:fs";
|
|
2755
|
+
import path31 from "node:path";
|
|
2756
2756
|
|
|
2757
2757
|
// src/scanner.ts
|
|
2758
2758
|
import fs from "node:fs";
|
|
@@ -5581,24 +5581,897 @@ function formatSecurityReport(report) {
|
|
|
5581
5581
|
return lines.join("\n");
|
|
5582
5582
|
}
|
|
5583
5583
|
|
|
5584
|
+
// src/ci.ts
|
|
5585
|
+
import path23 from "node:path";
|
|
5586
|
+
function generateCiReport(projectRoot2, forgehiveDir2, failOn = "high") {
|
|
5587
|
+
const secrets = scanSecrets(projectRoot2);
|
|
5588
|
+
const sast = scanSast(projectRoot2);
|
|
5589
|
+
const deps = scanDeps(projectRoot2);
|
|
5590
|
+
let failedOn = null;
|
|
5591
|
+
if (secrets.length > 0) {
|
|
5592
|
+
failedOn = "secrets";
|
|
5593
|
+
} else if (failOn === "critical" && (sast.some((f) => f.severity === "CRITICAL") || deps.some((d) => d.severity === "critical"))) {
|
|
5594
|
+
failedOn = "critical-severity";
|
|
5595
|
+
} else if (failOn === "high" && (sast.some((f) => f.severity === "CRITICAL" || f.severity === "HIGH") || deps.some((d) => d.severity === "critical" || d.severity === "high"))) {
|
|
5596
|
+
failedOn = "high-severity";
|
|
5597
|
+
} else if (failOn === "any" && (sast.length > 0 || deps.length > 0)) {
|
|
5598
|
+
failedOn = "findings";
|
|
5599
|
+
}
|
|
5600
|
+
return {
|
|
5601
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5602
|
+
project: path23.basename(projectRoot2),
|
|
5603
|
+
status: failedOn ? "fail" : "pass",
|
|
5604
|
+
secrets,
|
|
5605
|
+
sast,
|
|
5606
|
+
deps,
|
|
5607
|
+
failedOn
|
|
5608
|
+
};
|
|
5609
|
+
}
|
|
5610
|
+
function formatCiReport(report, format = "markdown") {
|
|
5611
|
+
if (format === "json") return JSON.stringify(report, null, 2);
|
|
5612
|
+
const lines = [];
|
|
5613
|
+
lines.push("# forgehive CI Report");
|
|
5614
|
+
lines.push(`**Project:** ${report.project}`);
|
|
5615
|
+
lines.push(
|
|
5616
|
+
`**Status:** ${report.status === "pass" ? "PASS" : "FAIL"}`
|
|
5617
|
+
);
|
|
5618
|
+
lines.push(`**Time:** ${report.timestamp}`);
|
|
5619
|
+
lines.push("");
|
|
5620
|
+
if (report.secrets.length > 0) {
|
|
5621
|
+
lines.push("## Secrets Found");
|
|
5622
|
+
for (const s of report.secrets)
|
|
5623
|
+
lines.push(
|
|
5624
|
+
`- **${s.patternName}** in \`${s.file}:${s.line}\` \u2014 \`${s.match}\``
|
|
5625
|
+
);
|
|
5626
|
+
lines.push("");
|
|
5627
|
+
}
|
|
5628
|
+
if (report.sast.length > 0) {
|
|
5629
|
+
lines.push("## SAST Findings");
|
|
5630
|
+
for (const f of report.sast)
|
|
5631
|
+
lines.push(
|
|
5632
|
+
`- **${f.severity}** \`${f.ruleId}\` in \`${f.file}:${f.line}\` \u2014 ${f.snippet.slice(0, 80)}`
|
|
5633
|
+
);
|
|
5634
|
+
lines.push("");
|
|
5635
|
+
}
|
|
5636
|
+
if (report.deps.length > 0) {
|
|
5637
|
+
lines.push("## Dependency Vulnerabilities");
|
|
5638
|
+
for (const d of report.deps)
|
|
5639
|
+
lines.push(
|
|
5640
|
+
`- **${d.severity}** ${d.package} \u2014 ${d.description}`
|
|
5641
|
+
);
|
|
5642
|
+
lines.push("");
|
|
5643
|
+
}
|
|
5644
|
+
if (report.status === "pass") lines.push("All checks passed.");
|
|
5645
|
+
return lines.join("\n");
|
|
5646
|
+
}
|
|
5647
|
+
function getGithubActionsTemplate() {
|
|
5648
|
+
return `name: forgehive CI
|
|
5649
|
+
|
|
5650
|
+
on:
|
|
5651
|
+
pull_request:
|
|
5652
|
+
branches: [main, master]
|
|
5653
|
+
push:
|
|
5654
|
+
branches: [main, master]
|
|
5655
|
+
|
|
5656
|
+
jobs:
|
|
5657
|
+
security:
|
|
5658
|
+
runs-on: ubuntu-latest
|
|
5659
|
+
steps:
|
|
5660
|
+
- uses: actions/checkout@v4
|
|
5661
|
+
- uses: actions/setup-node@v4
|
|
5662
|
+
with:
|
|
5663
|
+
node-version: '20'
|
|
5664
|
+
- run: npm install -g forgehive
|
|
5665
|
+
- run: fh ci --format json --fail-on high
|
|
5666
|
+
- name: Upload CI report
|
|
5667
|
+
if: always()
|
|
5668
|
+
uses: actions/upload-artifact@v4
|
|
5669
|
+
with:
|
|
5670
|
+
name: forgehive-ci-report
|
|
5671
|
+
path: .forgehive/ci-report.json
|
|
5672
|
+
`;
|
|
5673
|
+
}
|
|
5674
|
+
|
|
5675
|
+
// src/map.ts
|
|
5676
|
+
import fs23 from "node:fs";
|
|
5677
|
+
import path24 from "node:path";
|
|
5678
|
+
var MAP_EXTS = [".ts", ".tsx", ".js", ".jsx", ".py", ".go", ".rs"];
|
|
5679
|
+
var IGNORE_DIRS2 = ["node_modules", ".git", "dist", ".forgehive", "coverage", ".next", "build"];
|
|
5680
|
+
var IMPORT_PATTERNS = [
|
|
5681
|
+
/^import\s+.*?from\s+['"]([^'"]+)['"]/gm,
|
|
5682
|
+
/^import\s+['"]([^'"]+)['"]/gm,
|
|
5683
|
+
/require\s*\(\s*['"]([^'"]+)['"]\s*\)/gm
|
|
5684
|
+
];
|
|
5685
|
+
function extractImports(content) {
|
|
5686
|
+
const imports = [];
|
|
5687
|
+
for (const pattern of IMPORT_PATTERNS) {
|
|
5688
|
+
pattern.lastIndex = 0;
|
|
5689
|
+
let m;
|
|
5690
|
+
while ((m = pattern.exec(content)) !== null) {
|
|
5691
|
+
if (m[1].startsWith(".")) imports.push(m[1]);
|
|
5692
|
+
}
|
|
5693
|
+
}
|
|
5694
|
+
return [...new Set(imports)];
|
|
5695
|
+
}
|
|
5696
|
+
function walkFiles(dir) {
|
|
5697
|
+
const results = [];
|
|
5698
|
+
function walk(current) {
|
|
5699
|
+
for (const entry of fs23.readdirSync(current, { withFileTypes: true })) {
|
|
5700
|
+
if (IGNORE_DIRS2.includes(entry.name)) continue;
|
|
5701
|
+
const full = path24.join(current, entry.name);
|
|
5702
|
+
if (entry.isDirectory()) walk(full);
|
|
5703
|
+
else if (entry.isFile() && MAP_EXTS.includes(path24.extname(entry.name)))
|
|
5704
|
+
results.push(full);
|
|
5705
|
+
}
|
|
5706
|
+
}
|
|
5707
|
+
walk(dir);
|
|
5708
|
+
return results;
|
|
5709
|
+
}
|
|
5710
|
+
function generateMap(projectRoot2) {
|
|
5711
|
+
const filePaths = walkFiles(projectRoot2);
|
|
5712
|
+
const files = filePaths.map((filePath) => {
|
|
5713
|
+
let content = "";
|
|
5714
|
+
try {
|
|
5715
|
+
content = fs23.readFileSync(filePath, "utf8");
|
|
5716
|
+
} catch {
|
|
5717
|
+
}
|
|
5718
|
+
const rawLines = content.split("\n");
|
|
5719
|
+
const lines = rawLines[rawLines.length - 1] === "" ? rawLines.length - 1 : rawLines.length;
|
|
5720
|
+
const imports = extractImports(content);
|
|
5721
|
+
return {
|
|
5722
|
+
path: filePath,
|
|
5723
|
+
relativePath: path24.relative(projectRoot2, filePath),
|
|
5724
|
+
lines,
|
|
5725
|
+
imports
|
|
5726
|
+
};
|
|
5727
|
+
});
|
|
5728
|
+
files.sort((a, b) => b.lines - a.lines);
|
|
5729
|
+
return {
|
|
5730
|
+
projectRoot: projectRoot2,
|
|
5731
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5732
|
+
files,
|
|
5733
|
+
totalFiles: files.length,
|
|
5734
|
+
totalLines: files.reduce((sum, f) => sum + f.lines, 0)
|
|
5735
|
+
};
|
|
5736
|
+
}
|
|
5737
|
+
function formatMap(map2) {
|
|
5738
|
+
const lines = [];
|
|
5739
|
+
lines.push("# Codebase Map");
|
|
5740
|
+
lines.push(`Generated: ${map2.generatedAt}`);
|
|
5741
|
+
lines.push(`Total: ${map2.totalFiles} files, ${map2.totalLines} lines`);
|
|
5742
|
+
lines.push("");
|
|
5743
|
+
lines.push("## Files");
|
|
5744
|
+
lines.push("");
|
|
5745
|
+
lines.push("| File | Lines | Imports |");
|
|
5746
|
+
lines.push("|---|---|---|");
|
|
5747
|
+
for (const f of map2.files) {
|
|
5748
|
+
const imps = f.imports.length > 0 ? f.imports.slice(0, 3).join(", ") : "\u2014";
|
|
5749
|
+
lines.push(`| \`${f.relativePath}\` | ${f.lines} | ${imps} |`);
|
|
5750
|
+
}
|
|
5751
|
+
lines.push("");
|
|
5752
|
+
lines.push("## Most Imported");
|
|
5753
|
+
lines.push("");
|
|
5754
|
+
const importCounts = {};
|
|
5755
|
+
for (const f of map2.files) {
|
|
5756
|
+
for (const imp of f.imports) {
|
|
5757
|
+
importCounts[imp] = (importCounts[imp] || 0) + 1;
|
|
5758
|
+
}
|
|
5759
|
+
}
|
|
5760
|
+
const sorted = Object.entries(importCounts).sort((a, b) => b[1] - a[1]).slice(0, 10);
|
|
5761
|
+
if (sorted.length > 0) {
|
|
5762
|
+
for (const [imp, count] of sorted)
|
|
5763
|
+
lines.push(`- \`${imp}\` \u2014 imported ${count}\xD7`);
|
|
5764
|
+
} else {
|
|
5765
|
+
lines.push("No internal imports detected.");
|
|
5766
|
+
}
|
|
5767
|
+
return lines.join("\n");
|
|
5768
|
+
}
|
|
5769
|
+
|
|
5770
|
+
// src/onboard.ts
|
|
5771
|
+
init_js_yaml();
|
|
5772
|
+
import fs24 from "node:fs";
|
|
5773
|
+
import path25 from "node:path";
|
|
5774
|
+
import { spawnSync as spawnSync7 } from "node:child_process";
|
|
5775
|
+
function getRecentCommits(projectRoot2, n = 20) {
|
|
5776
|
+
const result = spawnSync7("git", ["log", `--oneline`, `-${n}`], {
|
|
5777
|
+
cwd: projectRoot2,
|
|
5778
|
+
encoding: "utf8"
|
|
5779
|
+
});
|
|
5780
|
+
if (result.status !== 0) return [];
|
|
5781
|
+
return result.stdout.trim().split("\n").filter(Boolean);
|
|
5782
|
+
}
|
|
5783
|
+
function readCapabilities(forgehiveDir2) {
|
|
5784
|
+
const capPath = path25.join(forgehiveDir2, "capabilities.yaml");
|
|
5785
|
+
if (!fs24.existsSync(capPath)) return {};
|
|
5786
|
+
try {
|
|
5787
|
+
return jsYaml.load(fs24.readFileSync(capPath, "utf8")) ?? {};
|
|
5788
|
+
} catch {
|
|
5789
|
+
return {};
|
|
5790
|
+
}
|
|
5791
|
+
}
|
|
5792
|
+
function readMemoryFiles(forgehiveDir2) {
|
|
5793
|
+
const memDir = path25.join(forgehiveDir2, "memory");
|
|
5794
|
+
if (!fs24.existsSync(memDir)) return {};
|
|
5795
|
+
const result = {};
|
|
5796
|
+
for (const f of fs24.readdirSync(memDir).filter((f2) => f2.endsWith(".md") && f2 !== "MEMORY.md")) {
|
|
5797
|
+
result[f] = fs24.readFileSync(path25.join(memDir, f), "utf8");
|
|
5798
|
+
}
|
|
5799
|
+
return result;
|
|
5800
|
+
}
|
|
5801
|
+
function listAdrs2(forgehiveDir2) {
|
|
5802
|
+
const adrsDir = path25.join(forgehiveDir2, "memory", "adrs");
|
|
5803
|
+
if (!fs24.existsSync(adrsDir)) return [];
|
|
5804
|
+
return fs24.readdirSync(adrsDir).filter((f) => f.endsWith(".md"));
|
|
5805
|
+
}
|
|
5806
|
+
function generateOnboardingDoc(projectRoot2, forgehiveDir2) {
|
|
5807
|
+
const projectName = path25.basename(projectRoot2);
|
|
5808
|
+
const caps = readCapabilities(forgehiveDir2);
|
|
5809
|
+
const memFiles = readMemoryFiles(forgehiveDir2);
|
|
5810
|
+
const commits = getRecentCommits(projectRoot2);
|
|
5811
|
+
const adrs = listAdrs2(forgehiveDir2);
|
|
5812
|
+
const lines = [];
|
|
5813
|
+
lines.push(`# Onboarding: ${projectName}`);
|
|
5814
|
+
lines.push("");
|
|
5815
|
+
lines.push(`> Generated by forgehive on ${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}`);
|
|
5816
|
+
lines.push("");
|
|
5817
|
+
lines.push("## Tech Stack");
|
|
5818
|
+
lines.push("");
|
|
5819
|
+
if (caps.language) lines.push(`- **Language:** ${caps.language}`);
|
|
5820
|
+
if (caps.framework) lines.push(`- **Framework:** ${caps.framework}`);
|
|
5821
|
+
if (caps.packageManager) lines.push(`- **Package Manager:** ${caps.packageManager}`);
|
|
5822
|
+
if (caps.testFramework) lines.push(`- **Tests:** ${caps.testFramework}`);
|
|
5823
|
+
if (caps.entryPoints) {
|
|
5824
|
+
const eps = caps.entryPoints;
|
|
5825
|
+
if (Array.isArray(eps) && eps.length > 0)
|
|
5826
|
+
lines.push(`- **Entry Points:** ${eps.join(", ")}`);
|
|
5827
|
+
}
|
|
5828
|
+
lines.push("");
|
|
5829
|
+
lines.push("## Getting Started");
|
|
5830
|
+
lines.push("");
|
|
5831
|
+
lines.push("```bash");
|
|
5832
|
+
lines.push("# Clone the repo");
|
|
5833
|
+
lines.push(`git clone <repo-url>`);
|
|
5834
|
+
lines.push(`cd ${projectName}`);
|
|
5835
|
+
lines.push("");
|
|
5836
|
+
const pm = caps.packageManager || "npm";
|
|
5837
|
+
lines.push(`# Install dependencies`);
|
|
5838
|
+
lines.push(pm === "yarn" ? "yarn" : pm === "pnpm" ? "pnpm install" : "npm install");
|
|
5839
|
+
lines.push("```");
|
|
5840
|
+
lines.push("");
|
|
5841
|
+
if (memFiles["project.md"]) {
|
|
5842
|
+
lines.push("## Project Context");
|
|
5843
|
+
lines.push("");
|
|
5844
|
+
const projectContent = memFiles["project.md"].replace(/^---[\s\S]*?---\n/m, "").trim();
|
|
5845
|
+
lines.push(projectContent);
|
|
5846
|
+
lines.push("");
|
|
5847
|
+
}
|
|
5848
|
+
if (memFiles["stack.md"]) {
|
|
5849
|
+
lines.push("## Stack Notes");
|
|
5850
|
+
lines.push("");
|
|
5851
|
+
const stackContent = memFiles["stack.md"].replace(/^---[\s\S]*?---\n/m, "").trim();
|
|
5852
|
+
lines.push(stackContent);
|
|
5853
|
+
lines.push("");
|
|
5854
|
+
}
|
|
5855
|
+
if (adrs.length > 0) {
|
|
5856
|
+
lines.push("## Architecture Decisions");
|
|
5857
|
+
lines.push("");
|
|
5858
|
+
for (const adr of adrs) lines.push(`- ${adr.replace(".md", "")}`);
|
|
5859
|
+
lines.push("");
|
|
5860
|
+
lines.push(`See \`.forgehive/memory/adrs/\` for full decision records.`);
|
|
5861
|
+
lines.push("");
|
|
5862
|
+
}
|
|
5863
|
+
if (commits.length > 0) {
|
|
5864
|
+
lines.push("## Recent Activity");
|
|
5865
|
+
lines.push("");
|
|
5866
|
+
for (const c of commits.slice(0, 10)) lines.push(`- ${c}`);
|
|
5867
|
+
lines.push("");
|
|
5868
|
+
}
|
|
5869
|
+
lines.push("## forgehive");
|
|
5870
|
+
lines.push("");
|
|
5871
|
+
lines.push("This project uses [forgehive](https://www.npmjs.com/package/forgehive) for AI-assisted development.");
|
|
5872
|
+
lines.push("");
|
|
5873
|
+
lines.push("```bash");
|
|
5874
|
+
lines.push("npm install -g forgehive");
|
|
5875
|
+
lines.push("fh status # check project health");
|
|
5876
|
+
lines.push("```");
|
|
5877
|
+
return lines.join("\n");
|
|
5878
|
+
}
|
|
5879
|
+
|
|
5880
|
+
// src/changelog.ts
|
|
5881
|
+
import { spawnSync as spawnSync8 } from "node:child_process";
|
|
5882
|
+
var TYPE_LABELS = {
|
|
5883
|
+
feat: "Added",
|
|
5884
|
+
fix: "Fixed",
|
|
5885
|
+
perf: "Improved",
|
|
5886
|
+
refactor: "Changed",
|
|
5887
|
+
chore: "Maintenance",
|
|
5888
|
+
docs: "Documentation",
|
|
5889
|
+
test: "Tests",
|
|
5890
|
+
ci: "CI",
|
|
5891
|
+
other: "Other"
|
|
5892
|
+
};
|
|
5893
|
+
function parseGitLog(rawLog) {
|
|
5894
|
+
return rawLog.split("\n").map((line) => line.trim()).filter(Boolean).map((line) => {
|
|
5895
|
+
const spaceIdx = line.indexOf(" ");
|
|
5896
|
+
const hash = line.slice(0, spaceIdx);
|
|
5897
|
+
const rest2 = line.slice(spaceIdx + 1);
|
|
5898
|
+
const conventionalMatch = rest2.match(/^(\w+)(?:\(([^)]+)\))?\s*:\s*(.+)$/);
|
|
5899
|
+
if (conventionalMatch) {
|
|
5900
|
+
return {
|
|
5901
|
+
hash,
|
|
5902
|
+
type: conventionalMatch[1],
|
|
5903
|
+
scope: conventionalMatch[2] ?? null,
|
|
5904
|
+
message: conventionalMatch[3]
|
|
5905
|
+
};
|
|
5906
|
+
}
|
|
5907
|
+
return { hash, type: "other", scope: null, message: rest2 };
|
|
5908
|
+
});
|
|
5909
|
+
}
|
|
5910
|
+
function groupByType(commits) {
|
|
5911
|
+
const groups = {};
|
|
5912
|
+
for (const commit of commits) {
|
|
5913
|
+
if (!groups[commit.type]) groups[commit.type] = [];
|
|
5914
|
+
groups[commit.type].push(commit);
|
|
5915
|
+
}
|
|
5916
|
+
return groups;
|
|
5917
|
+
}
|
|
5918
|
+
function formatChangelog(commits, version) {
|
|
5919
|
+
const date = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
5920
|
+
const lines = [];
|
|
5921
|
+
lines.push(`## [${version}] \u2014 ${date}`);
|
|
5922
|
+
lines.push("");
|
|
5923
|
+
if (commits.length === 0) {
|
|
5924
|
+
lines.push("No changes.");
|
|
5925
|
+
return lines.join("\n");
|
|
5926
|
+
}
|
|
5927
|
+
const groups = groupByType(commits);
|
|
5928
|
+
const typeOrder = ["feat", "fix", "perf", "refactor", "docs", "chore", "test", "ci", "other"];
|
|
5929
|
+
for (const type2 of typeOrder) {
|
|
5930
|
+
if (!groups[type2] || groups[type2].length === 0) continue;
|
|
5931
|
+
const label = TYPE_LABELS[type2] ?? type2;
|
|
5932
|
+
lines.push(`### ${label}`);
|
|
5933
|
+
lines.push("");
|
|
5934
|
+
for (const c of groups[type2]) {
|
|
5935
|
+
const scope = c.scope ? `**${c.scope}:** ` : "";
|
|
5936
|
+
lines.push(`- ${scope}${c.message} (\`${c.hash}\`)`);
|
|
5937
|
+
}
|
|
5938
|
+
lines.push("");
|
|
5939
|
+
}
|
|
5940
|
+
return lines.join("\n");
|
|
5941
|
+
}
|
|
5942
|
+
function getGitLogSince(projectRoot2, since) {
|
|
5943
|
+
const args = ["log", "--oneline", "--no-merges"];
|
|
5944
|
+
if (since) args.push(`${since}..HEAD`);
|
|
5945
|
+
const result = spawnSync8("git", args, { cwd: projectRoot2, encoding: "utf8" });
|
|
5946
|
+
if (result.status !== 0) return "";
|
|
5947
|
+
return result.stdout.trim();
|
|
5948
|
+
}
|
|
5949
|
+
function getLatestTag(projectRoot2) {
|
|
5950
|
+
const result = spawnSync8("git", ["describe", "--tags", "--abbrev=0"], {
|
|
5951
|
+
cwd: projectRoot2,
|
|
5952
|
+
encoding: "utf8"
|
|
5953
|
+
});
|
|
5954
|
+
if (result.status !== 0) return null;
|
|
5955
|
+
return result.stdout.trim() || null;
|
|
5956
|
+
}
|
|
5957
|
+
|
|
5958
|
+
// src/metrics.ts
|
|
5959
|
+
import { spawnSync as spawnSync9 } from "node:child_process";
|
|
5960
|
+
function parseCommitStats(rawLog) {
|
|
5961
|
+
const lines = rawLog.split("\n").map((l) => l.trim()).filter(Boolean);
|
|
5962
|
+
const byAuthor = {};
|
|
5963
|
+
const byType = {};
|
|
5964
|
+
const dates = [];
|
|
5965
|
+
for (const line of lines) {
|
|
5966
|
+
const parts = line.split(" ");
|
|
5967
|
+
if (parts.length < 3) continue;
|
|
5968
|
+
const date = parts[0];
|
|
5969
|
+
const author = parts[1];
|
|
5970
|
+
const message = parts.slice(2).join(" ");
|
|
5971
|
+
dates.push(date);
|
|
5972
|
+
byAuthor[author] = (byAuthor[author] || 0) + 1;
|
|
5973
|
+
const typeMatch = message.match(/^(\w+)(?:\([^)]+\))?:/);
|
|
5974
|
+
const type2 = typeMatch ? typeMatch[1] : "other";
|
|
5975
|
+
byType[type2] = (byType[type2] || 0) + 1;
|
|
5976
|
+
}
|
|
5977
|
+
const sortedDates = dates.sort();
|
|
5978
|
+
return {
|
|
5979
|
+
totalCommits: lines.length,
|
|
5980
|
+
byAuthor,
|
|
5981
|
+
byType,
|
|
5982
|
+
dateRange: {
|
|
5983
|
+
from: sortedDates[0] ?? "",
|
|
5984
|
+
to: sortedDates[sortedDates.length - 1] ?? ""
|
|
5985
|
+
}
|
|
5986
|
+
};
|
|
5987
|
+
}
|
|
5988
|
+
function formatMetrics(stats, projectName) {
|
|
5989
|
+
const lines = [];
|
|
5990
|
+
lines.push(`# Developer Metrics: ${projectName}`);
|
|
5991
|
+
lines.push(`Generated: ${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}`);
|
|
5992
|
+
lines.push("");
|
|
5993
|
+
lines.push(`**Total commits:** ${stats.totalCommits}`);
|
|
5994
|
+
if (stats.dateRange.from)
|
|
5995
|
+
lines.push(`**Date range:** ${stats.dateRange.from} \u2192 ${stats.dateRange.to}`);
|
|
5996
|
+
lines.push("");
|
|
5997
|
+
if (Object.keys(stats.byAuthor).length > 0) {
|
|
5998
|
+
lines.push("## Commits by Author");
|
|
5999
|
+
lines.push("");
|
|
6000
|
+
for (const [author, count] of Object.entries(stats.byAuthor).sort((a, b) => b[1] - a[1]))
|
|
6001
|
+
lines.push(`- **${author}:** ${count}`);
|
|
6002
|
+
lines.push("");
|
|
6003
|
+
}
|
|
6004
|
+
if (Object.keys(stats.byType).length > 0) {
|
|
6005
|
+
lines.push("## Commits by Type");
|
|
6006
|
+
lines.push("");
|
|
6007
|
+
for (const [type2, count] of Object.entries(stats.byType).sort((a, b) => b[1] - a[1]))
|
|
6008
|
+
lines.push(`- **${type2}:** ${count}`);
|
|
6009
|
+
lines.push("");
|
|
6010
|
+
}
|
|
6011
|
+
return lines.join("\n");
|
|
6012
|
+
}
|
|
6013
|
+
function getMetricsGitLog(projectRoot2, since) {
|
|
6014
|
+
const args = ["log", "--no-merges", "--format=%as %an %s"];
|
|
6015
|
+
if (since) args.push(`--since=${since}`);
|
|
6016
|
+
const result = spawnSync9("git", args, { cwd: projectRoot2, encoding: "utf8" });
|
|
6017
|
+
if (result.status !== 0) return "";
|
|
6018
|
+
return result.stdout.trim();
|
|
6019
|
+
}
|
|
6020
|
+
|
|
6021
|
+
// src/sync.ts
|
|
6022
|
+
import fs25 from "node:fs";
|
|
6023
|
+
import path26 from "node:path";
|
|
6024
|
+
import { spawnSync as spawnSync10 } from "node:child_process";
|
|
6025
|
+
function getSyncStatus(forgehiveDir2) {
|
|
6026
|
+
const memDir = path26.join(forgehiveDir2, "memory");
|
|
6027
|
+
const files = fs25.existsSync(memDir) ? fs25.readdirSync(memDir).filter((f) => f.endsWith(".md")).length : 0;
|
|
6028
|
+
const projectRoot2 = path26.dirname(forgehiveDir2);
|
|
6029
|
+
const configResult = spawnSync10(
|
|
6030
|
+
"git",
|
|
6031
|
+
["config", "--get", "forgehive.sync-remote"],
|
|
6032
|
+
{ cwd: projectRoot2, encoding: "utf8" }
|
|
6033
|
+
);
|
|
6034
|
+
const remote = configResult.status === 0 ? configResult.stdout.trim() : null;
|
|
6035
|
+
const branchResult = spawnSync10(
|
|
6036
|
+
"git",
|
|
6037
|
+
["config", "--get", "forgehive.sync-branch"],
|
|
6038
|
+
{ cwd: projectRoot2, encoding: "utf8" }
|
|
6039
|
+
);
|
|
6040
|
+
const branch = branchResult.status === 0 ? branchResult.stdout.trim() : null;
|
|
6041
|
+
return { files, hasRemote: remote !== null, remote, branch };
|
|
6042
|
+
}
|
|
6043
|
+
function pushSync(forgehiveDir2, remote = "origin", branch = "forgehive-memory") {
|
|
6044
|
+
const projectRoot2 = path26.dirname(forgehiveDir2);
|
|
6045
|
+
const memDir = path26.join(forgehiveDir2, "memory");
|
|
6046
|
+
if (!fs25.existsSync(memDir)) {
|
|
6047
|
+
return { success: false, message: "Kein Memory-Verzeichnis gefunden.", filesCommitted: 0 };
|
|
6048
|
+
}
|
|
6049
|
+
const files = fs25.readdirSync(memDir).filter((f) => f.endsWith(".md"));
|
|
6050
|
+
if (files.length === 0) {
|
|
6051
|
+
return { success: false, message: "Keine Memory-Dateien gefunden.", filesCommitted: 0 };
|
|
6052
|
+
}
|
|
6053
|
+
const addResult = spawnSync10(
|
|
6054
|
+
"git",
|
|
6055
|
+
["add", path26.join(".forgehive", "memory")],
|
|
6056
|
+
{ cwd: projectRoot2, encoding: "utf8" }
|
|
6057
|
+
);
|
|
6058
|
+
if (addResult.status !== 0) {
|
|
6059
|
+
return { success: false, message: `git add failed: ${addResult.stderr}`, filesCommitted: 0 };
|
|
6060
|
+
}
|
|
6061
|
+
const commitResult = spawnSync10(
|
|
6062
|
+
"git",
|
|
6063
|
+
["commit", "-m", `chore: sync forgehive memory [${(/* @__PURE__ */ new Date()).toISOString()}]`],
|
|
6064
|
+
{ cwd: projectRoot2, encoding: "utf8" }
|
|
6065
|
+
);
|
|
6066
|
+
const commitOk = commitResult.status === 0 || (commitResult.stdout + commitResult.stderr).includes("nothing to commit");
|
|
6067
|
+
if (!commitOk) {
|
|
6068
|
+
return { success: false, message: `git commit failed: ${commitResult.stderr}`, filesCommitted: 0 };
|
|
6069
|
+
}
|
|
6070
|
+
const pushResult = spawnSync10(
|
|
6071
|
+
"git",
|
|
6072
|
+
["push", remote, `HEAD:${branch}`],
|
|
6073
|
+
{ cwd: projectRoot2, encoding: "utf8" }
|
|
6074
|
+
);
|
|
6075
|
+
if (pushResult.status !== 0) {
|
|
6076
|
+
return { success: false, message: `git push failed: ${pushResult.stderr}`, filesCommitted: 0 };
|
|
6077
|
+
}
|
|
6078
|
+
spawnSync10("git", ["config", "forgehive.sync-remote", remote], { cwd: projectRoot2 });
|
|
6079
|
+
spawnSync10("git", ["config", "forgehive.sync-branch", branch], { cwd: projectRoot2 });
|
|
6080
|
+
return { success: true, message: `Memory gepusht nach ${remote}/${branch}`, filesCommitted: files.length };
|
|
6081
|
+
}
|
|
6082
|
+
function pullSync(forgehiveDir2, remote = "origin", branch = "forgehive-memory") {
|
|
6083
|
+
const projectRoot2 = path26.dirname(forgehiveDir2);
|
|
6084
|
+
const memDir = path26.join(forgehiveDir2, "memory");
|
|
6085
|
+
fs25.mkdirSync(memDir, { recursive: true });
|
|
6086
|
+
const fetchResult = spawnSync10(
|
|
6087
|
+
"git",
|
|
6088
|
+
["fetch", remote, branch],
|
|
6089
|
+
{ cwd: projectRoot2, encoding: "utf8" }
|
|
6090
|
+
);
|
|
6091
|
+
if (fetchResult.status !== 0) {
|
|
6092
|
+
return { success: false, message: `git fetch failed: ${fetchResult.stderr}`, filesImported: [] };
|
|
6093
|
+
}
|
|
6094
|
+
const listResult = spawnSync10(
|
|
6095
|
+
"git",
|
|
6096
|
+
["ls-tree", "--name-only", `${remote}/${branch}`, ".forgehive/memory/"],
|
|
6097
|
+
{ cwd: projectRoot2, encoding: "utf8" }
|
|
6098
|
+
);
|
|
6099
|
+
if (listResult.status !== 0) {
|
|
6100
|
+
return { success: false, message: "Remote branch hat keine Memory-Dateien.", filesImported: [] };
|
|
6101
|
+
}
|
|
6102
|
+
const remoteFiles = listResult.stdout.trim().split("\n").filter(Boolean);
|
|
6103
|
+
const imported = [];
|
|
6104
|
+
for (const remotePath of remoteFiles) {
|
|
6105
|
+
const filename = path26.basename(remotePath);
|
|
6106
|
+
const localPath = path26.join(memDir, filename);
|
|
6107
|
+
if (!fs25.existsSync(localPath)) {
|
|
6108
|
+
const contentResult = spawnSync10(
|
|
6109
|
+
"git",
|
|
6110
|
+
["show", `${remote}/${branch}:${remotePath}`],
|
|
6111
|
+
{ cwd: projectRoot2, encoding: "utf8" }
|
|
6112
|
+
);
|
|
6113
|
+
if (contentResult.status === 0) {
|
|
6114
|
+
fs25.writeFileSync(localPath, contentResult.stdout, "utf8");
|
|
6115
|
+
imported.push(filename);
|
|
6116
|
+
}
|
|
6117
|
+
}
|
|
6118
|
+
}
|
|
6119
|
+
return {
|
|
6120
|
+
success: true,
|
|
6121
|
+
message: imported.length > 0 ? `${imported.length} neue Dateien importiert.` : "Keine neuen Dateien (bereits aktuell).",
|
|
6122
|
+
filesImported: imported
|
|
6123
|
+
};
|
|
6124
|
+
}
|
|
6125
|
+
|
|
6126
|
+
// src/background.ts
|
|
6127
|
+
import fs26 from "node:fs";
|
|
6128
|
+
import path27 from "node:path";
|
|
6129
|
+
import { spawn } from "node:child_process";
|
|
6130
|
+
var AGENT_ROLES = {
|
|
6131
|
+
kai: "Senior Engineer \u2014 implements features and fixes bugs",
|
|
6132
|
+
sam: "QA & Test Architect \u2014 writes tests and checks quality",
|
|
6133
|
+
viktor: "System Architect \u2014 designs architecture and reviews structure",
|
|
6134
|
+
vera: "Security Analyst \u2014 reviews for OWASP, GDPR, auth issues",
|
|
6135
|
+
nora: "Senior Research Analyst \u2014 researches options and summarizes findings",
|
|
6136
|
+
eli: "Technical Writer \u2014 writes documentation and changelogs",
|
|
6137
|
+
suki: "UX Designer \u2014 reviews UI/UX and user flows",
|
|
6138
|
+
remy: "Creative Strategist \u2014 explores creative approaches"
|
|
6139
|
+
};
|
|
6140
|
+
var LABEL_TO_AGENT = {
|
|
6141
|
+
security: "vera",
|
|
6142
|
+
research: "nora",
|
|
6143
|
+
docs: "eli",
|
|
6144
|
+
documentation: "eli",
|
|
6145
|
+
design: "suki",
|
|
6146
|
+
ux: "suki",
|
|
6147
|
+
architecture: "viktor",
|
|
6148
|
+
arch: "viktor",
|
|
6149
|
+
test: "sam",
|
|
6150
|
+
qa: "sam"
|
|
6151
|
+
};
|
|
6152
|
+
function resolveAgent(label) {
|
|
6153
|
+
if (!label) return "kai";
|
|
6154
|
+
return LABEL_TO_AGENT[label.toLowerCase()] ?? "kai";
|
|
6155
|
+
}
|
|
6156
|
+
function buildAgentPrompt(issueUrl, agentId) {
|
|
6157
|
+
const role = AGENT_ROLES[agentId] ?? "Senior Engineer";
|
|
6158
|
+
return `You are ${agentId} \u2014 ${role}.
|
|
6159
|
+
|
|
6160
|
+
Your task: Resolve the following issue autonomously.
|
|
6161
|
+
|
|
6162
|
+
Issue URL: ${issueUrl}
|
|
6163
|
+
|
|
6164
|
+
Instructions:
|
|
6165
|
+
1. Read the issue at the URL above (use web fetch or gh CLI if available)
|
|
6166
|
+
2. Understand what needs to be done
|
|
6167
|
+
3. Implement the solution following the project's conventions (read .forgehive/ for context)
|
|
6168
|
+
4. Write tests for any new code
|
|
6169
|
+
5. Commit your changes with a descriptive message
|
|
6170
|
+
6. Report what you did
|
|
6171
|
+
|
|
6172
|
+
Work autonomously. Do not ask for clarification \u2014 use your best judgment based on the issue description and codebase context.`;
|
|
6173
|
+
}
|
|
6174
|
+
function runBackgroundAgent(forgehiveDir2, issueUrl, agentId) {
|
|
6175
|
+
const logsDir = path27.join(forgehiveDir2, "background-runs");
|
|
6176
|
+
fs26.mkdirSync(logsDir, { recursive: true });
|
|
6177
|
+
const timestamp2 = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
6178
|
+
const logFile = path27.join(logsDir, `${agentId}-${timestamp2}.log`);
|
|
6179
|
+
const prompt = buildAgentPrompt(issueUrl, agentId);
|
|
6180
|
+
const logStream = fs26.openSync(logFile, "w");
|
|
6181
|
+
const child = spawn(
|
|
6182
|
+
"claude",
|
|
6183
|
+
["-p", prompt, "--output-format", "text"],
|
|
6184
|
+
{
|
|
6185
|
+
cwd: path27.dirname(forgehiveDir2),
|
|
6186
|
+
detached: true,
|
|
6187
|
+
stdio: ["ignore", logStream, logStream]
|
|
6188
|
+
}
|
|
6189
|
+
);
|
|
6190
|
+
child.unref();
|
|
6191
|
+
fs26.closeSync(logStream);
|
|
6192
|
+
return {
|
|
6193
|
+
pid: child.pid,
|
|
6194
|
+
logFile,
|
|
6195
|
+
message: `Agent ${agentId} gestartet (PID ${child.pid}). Log: ${logFile}`
|
|
6196
|
+
};
|
|
6197
|
+
}
|
|
6198
|
+
|
|
6199
|
+
// src/stories.ts
|
|
6200
|
+
init_js_yaml();
|
|
6201
|
+
import fs27 from "node:fs";
|
|
6202
|
+
import path28 from "node:path";
|
|
6203
|
+
function nextStoryId(storiesDir) {
|
|
6204
|
+
if (!fs27.existsSync(storiesDir)) return "US-1";
|
|
6205
|
+
const existing = fs27.readdirSync(storiesDir).filter((f) => f.match(/^US-\d+\.md$/)).map((f) => parseInt(f.replace("US-", "").replace(".md", ""), 10)).filter((n) => !isNaN(n));
|
|
6206
|
+
const max = existing.length > 0 ? Math.max(...existing) : 0;
|
|
6207
|
+
return `US-${max + 1}`;
|
|
6208
|
+
}
|
|
6209
|
+
function storyToMarkdown(story) {
|
|
6210
|
+
const frontmatter = jsYaml.dump({
|
|
6211
|
+
id: story.id,
|
|
6212
|
+
title: story.title,
|
|
6213
|
+
asA: story.asA,
|
|
6214
|
+
iWant: story.iWant,
|
|
6215
|
+
soThat: story.soThat,
|
|
6216
|
+
points: story.points,
|
|
6217
|
+
epicId: story.epicId,
|
|
6218
|
+
status: story.status
|
|
6219
|
+
});
|
|
6220
|
+
const acLines = story.acceptanceCriteria.map((c) => `- [ ] ${c}`).join("\n");
|
|
6221
|
+
return `---
|
|
6222
|
+
${frontmatter}---
|
|
6223
|
+
|
|
6224
|
+
# ${story.id}: ${story.title}
|
|
6225
|
+
|
|
6226
|
+
## User Story
|
|
6227
|
+
|
|
6228
|
+
Als **${story.asA}** m\xF6chte ich **${story.iWant}**, damit **${story.soThat}**.
|
|
6229
|
+
|
|
6230
|
+
## Acceptance Criteria
|
|
6231
|
+
|
|
6232
|
+
${acLines || "- [ ] (noch nicht definiert)"}
|
|
6233
|
+
`;
|
|
6234
|
+
}
|
|
6235
|
+
function parseStoryFile(filePath) {
|
|
6236
|
+
try {
|
|
6237
|
+
const content = fs27.readFileSync(filePath, "utf8");
|
|
6238
|
+
const match = content.match(/^---\n([\s\S]*?)\n---/);
|
|
6239
|
+
if (!match) return null;
|
|
6240
|
+
const data = jsYaml.load(match[1]);
|
|
6241
|
+
return {
|
|
6242
|
+
id: data.id ?? "",
|
|
6243
|
+
title: data.title ?? "",
|
|
6244
|
+
asA: data.asA ?? "",
|
|
6245
|
+
iWant: data.iWant ?? "",
|
|
6246
|
+
soThat: data.soThat ?? "",
|
|
6247
|
+
acceptanceCriteria: data.acceptanceCriteria ?? [],
|
|
6248
|
+
points: data.points ?? null,
|
|
6249
|
+
epicId: data.epicId ?? null,
|
|
6250
|
+
status: data.status ?? "backlog"
|
|
6251
|
+
};
|
|
6252
|
+
} catch {
|
|
6253
|
+
return null;
|
|
6254
|
+
}
|
|
6255
|
+
}
|
|
6256
|
+
function createStory(storiesDir, title, epicId) {
|
|
6257
|
+
fs27.mkdirSync(storiesDir, { recursive: true });
|
|
6258
|
+
const id = nextStoryId(storiesDir);
|
|
6259
|
+
const story = {
|
|
6260
|
+
id,
|
|
6261
|
+
title,
|
|
6262
|
+
asA: "",
|
|
6263
|
+
iWant: title,
|
|
6264
|
+
soThat: "",
|
|
6265
|
+
acceptanceCriteria: [],
|
|
6266
|
+
points: null,
|
|
6267
|
+
epicId: epicId ?? null,
|
|
6268
|
+
status: "backlog"
|
|
6269
|
+
};
|
|
6270
|
+
fs27.writeFileSync(path28.join(storiesDir, `${id}.md`), storyToMarkdown(story), "utf8");
|
|
6271
|
+
return story;
|
|
6272
|
+
}
|
|
6273
|
+
function listStories(storiesDir) {
|
|
6274
|
+
if (!fs27.existsSync(storiesDir)) return [];
|
|
6275
|
+
return fs27.readdirSync(storiesDir).filter((f) => f.match(/^US-\d+\.md$/)).map((f) => parseStoryFile(path28.join(storiesDir, f))).filter((s) => s !== null).sort((a, b) => {
|
|
6276
|
+
const na = parseInt(a.id.replace("US-", ""), 10);
|
|
6277
|
+
const nb = parseInt(b.id.replace("US-", ""), 10);
|
|
6278
|
+
return na - nb;
|
|
6279
|
+
});
|
|
6280
|
+
}
|
|
6281
|
+
function getStory(storiesDir, id) {
|
|
6282
|
+
const filePath = path28.join(storiesDir, `${id}.md`);
|
|
6283
|
+
if (!fs27.existsSync(filePath)) return null;
|
|
6284
|
+
return parseStoryFile(filePath);
|
|
6285
|
+
}
|
|
6286
|
+
function updateStoryPoints(storiesDir, id, points) {
|
|
6287
|
+
const story = getStory(storiesDir, id);
|
|
6288
|
+
if (!story) throw new Error(`Story ${id} nicht gefunden`);
|
|
6289
|
+
story.points = points;
|
|
6290
|
+
fs27.writeFileSync(path28.join(storiesDir, `${id}.md`), storyToMarkdown(story), "utf8");
|
|
6291
|
+
}
|
|
6292
|
+
function updateStoryStatus(storiesDir, id, status) {
|
|
6293
|
+
const story = getStory(storiesDir, id);
|
|
6294
|
+
if (!story) throw new Error(`Story ${id} nicht gefunden`);
|
|
6295
|
+
story.status = status;
|
|
6296
|
+
fs27.writeFileSync(path28.join(storiesDir, `${id}.md`), storyToMarkdown(story), "utf8");
|
|
6297
|
+
}
|
|
6298
|
+
function formatStoryCard(story) {
|
|
6299
|
+
const points = story.points !== null ? ` \xB7 ${story.points} Punkte` : "";
|
|
6300
|
+
const epic = story.epicId ? ` \xB7 ${story.epicId}` : "";
|
|
6301
|
+
const lines = [];
|
|
6302
|
+
lines.push(`## ${story.id}: ${story.title}${points}${epic}`);
|
|
6303
|
+
lines.push(`**Status:** ${story.status}`);
|
|
6304
|
+
if (story.asA || story.iWant)
|
|
6305
|
+
lines.push(`**Story:** Als ${story.asA || "Nutzer"} m\xF6chte ich ${story.iWant}${story.soThat ? `, damit ${story.soThat}` : ""}.`);
|
|
6306
|
+
if (story.acceptanceCriteria.length > 0) {
|
|
6307
|
+
lines.push("**Acceptance Criteria:**");
|
|
6308
|
+
for (const ac of story.acceptanceCriteria) lines.push(`- ${ac}`);
|
|
6309
|
+
}
|
|
6310
|
+
return lines.join("\n");
|
|
6311
|
+
}
|
|
6312
|
+
|
|
6313
|
+
// src/epics.ts
|
|
6314
|
+
init_js_yaml();
|
|
6315
|
+
import fs28 from "node:fs";
|
|
6316
|
+
import path29 from "node:path";
|
|
6317
|
+
function nextEpicId(epicsDir) {
|
|
6318
|
+
if (!fs28.existsSync(epicsDir)) return "EPC-1";
|
|
6319
|
+
const existing = fs28.readdirSync(epicsDir).filter((f) => f.match(/^EPC-\d+\.md$/)).map((f) => parseInt(f.replace("EPC-", "").replace(".md", ""), 10)).filter((n) => !isNaN(n));
|
|
6320
|
+
const max = existing.length > 0 ? Math.max(...existing) : 0;
|
|
6321
|
+
return `EPC-${max + 1}`;
|
|
6322
|
+
}
|
|
6323
|
+
function epicToMarkdown(epic) {
|
|
6324
|
+
const frontmatter = jsYaml.dump({
|
|
6325
|
+
id: epic.id,
|
|
6326
|
+
title: epic.title,
|
|
6327
|
+
goal: epic.goal,
|
|
6328
|
+
stories: epic.stories,
|
|
6329
|
+
status: epic.status
|
|
6330
|
+
});
|
|
6331
|
+
const storyLines = epic.stories.map((s) => `- ${s}`).join("\n");
|
|
6332
|
+
return `---
|
|
6333
|
+
${frontmatter}---
|
|
6334
|
+
|
|
6335
|
+
# ${epic.id}: ${epic.title}
|
|
6336
|
+
|
|
6337
|
+
**Ziel:** ${epic.goal || "(noch nicht definiert)"}
|
|
6338
|
+
|
|
6339
|
+
## Stories
|
|
6340
|
+
|
|
6341
|
+
${storyLines || "(noch keine Stories)"}
|
|
6342
|
+
`;
|
|
6343
|
+
}
|
|
6344
|
+
function parseEpicFile(filePath) {
|
|
6345
|
+
try {
|
|
6346
|
+
const content = fs28.readFileSync(filePath, "utf8");
|
|
6347
|
+
const match = content.match(/^---\n([\s\S]*?)\n---/);
|
|
6348
|
+
if (!match) return null;
|
|
6349
|
+
const data = jsYaml.load(match[1]);
|
|
6350
|
+
return {
|
|
6351
|
+
id: data.id ?? "",
|
|
6352
|
+
title: data.title ?? "",
|
|
6353
|
+
goal: data.goal ?? "",
|
|
6354
|
+
stories: data.stories ?? [],
|
|
6355
|
+
status: data.status ?? "active"
|
|
6356
|
+
};
|
|
6357
|
+
} catch {
|
|
6358
|
+
return null;
|
|
6359
|
+
}
|
|
6360
|
+
}
|
|
6361
|
+
function createEpic(epicsDir, title, goal) {
|
|
6362
|
+
fs28.mkdirSync(epicsDir, { recursive: true });
|
|
6363
|
+
const id = nextEpicId(epicsDir);
|
|
6364
|
+
const epic = { id, title, goal: goal ?? "", stories: [], status: "active" };
|
|
6365
|
+
fs28.writeFileSync(path29.join(epicsDir, `${id}.md`), epicToMarkdown(epic), "utf8");
|
|
6366
|
+
return epic;
|
|
6367
|
+
}
|
|
6368
|
+
function listEpics(epicsDir) {
|
|
6369
|
+
if (!fs28.existsSync(epicsDir)) return [];
|
|
6370
|
+
return fs28.readdirSync(epicsDir).filter((f) => f.match(/^EPC-\d+\.md$/)).map((f) => parseEpicFile(path29.join(epicsDir, f))).filter((e) => e !== null).sort((a, b) => {
|
|
6371
|
+
const na = parseInt(a.id.replace("EPC-", ""), 10);
|
|
6372
|
+
const nb = parseInt(b.id.replace("EPC-", ""), 10);
|
|
6373
|
+
return na - nb;
|
|
6374
|
+
});
|
|
6375
|
+
}
|
|
6376
|
+
function getEpic(epicsDir, id) {
|
|
6377
|
+
const filePath = path29.join(epicsDir, `${id}.md`);
|
|
6378
|
+
if (!fs28.existsSync(filePath)) return null;
|
|
6379
|
+
return parseEpicFile(filePath);
|
|
6380
|
+
}
|
|
6381
|
+
function formatEpicCard(epic, stories) {
|
|
6382
|
+
const lines = [];
|
|
6383
|
+
lines.push(`## ${epic.id}: ${epic.title} [${epic.status}]`);
|
|
6384
|
+
if (epic.goal) lines.push(`**Ziel:** ${epic.goal}`);
|
|
6385
|
+
lines.push(`**Stories:** ${epic.stories.length}`);
|
|
6386
|
+
if (stories && stories.length > 0) {
|
|
6387
|
+
const total = stories.reduce((sum, s) => sum + (s.points ?? 0), 0);
|
|
6388
|
+
lines.push(`**Punkte:** ${total}`);
|
|
6389
|
+
lines.push("");
|
|
6390
|
+
for (const s of stories) {
|
|
6391
|
+
const pts = s.points !== null ? ` [${s.points}pt]` : " [?pt]";
|
|
6392
|
+
lines.push(` - ${s.id}${pts} \u2014 ${s.title} [${s.status}]`);
|
|
6393
|
+
}
|
|
6394
|
+
} else if (epic.stories.length > 0) {
|
|
6395
|
+
for (const id of epic.stories) lines.push(` - ${id}`);
|
|
6396
|
+
}
|
|
6397
|
+
return lines.join("\n");
|
|
6398
|
+
}
|
|
6399
|
+
|
|
6400
|
+
// src/velocity.ts
|
|
6401
|
+
import fs29 from "node:fs";
|
|
6402
|
+
import path30 from "node:path";
|
|
6403
|
+
var HEADER = "# Sprint Velocity\n\n| Sprint | Datum | Committed | Delivered | Rate |\n|---|---|---|---|---|\n";
|
|
6404
|
+
function recordVelocity(velocityFile, sprint, committed, delivered) {
|
|
6405
|
+
const date = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
6406
|
+
const rate = committed > 0 ? Math.round(delivered / committed * 100) : 0;
|
|
6407
|
+
const row = `| Sprint ${sprint} | ${date} | ${committed} | ${delivered} | ${rate}% |
|
|
6408
|
+
`;
|
|
6409
|
+
if (!fs29.existsSync(velocityFile)) {
|
|
6410
|
+
fs29.mkdirSync(path30.dirname(velocityFile), { recursive: true });
|
|
6411
|
+
fs29.writeFileSync(velocityFile, HEADER + row, "utf8");
|
|
6412
|
+
} else {
|
|
6413
|
+
fs29.appendFileSync(velocityFile, row, "utf8");
|
|
6414
|
+
}
|
|
6415
|
+
}
|
|
6416
|
+
function getVelocityHistory(velocityFile) {
|
|
6417
|
+
if (!fs29.existsSync(velocityFile)) return [];
|
|
6418
|
+
const content = fs29.readFileSync(velocityFile, "utf8");
|
|
6419
|
+
const rows = content.split("\n").filter((l) => l.startsWith("| Sprint "));
|
|
6420
|
+
return rows.map((row) => {
|
|
6421
|
+
const cells = row.split("|").map((c) => c.trim()).filter(Boolean);
|
|
6422
|
+
const sprintMatch = cells[0]?.match(/Sprint (\d+)/);
|
|
6423
|
+
return {
|
|
6424
|
+
sprint: sprintMatch ? parseInt(sprintMatch[1], 10) : 0,
|
|
6425
|
+
date: cells[1] ?? "",
|
|
6426
|
+
committed: parseInt(cells[2] ?? "0", 10),
|
|
6427
|
+
delivered: parseInt(cells[3] ?? "0", 10)
|
|
6428
|
+
};
|
|
6429
|
+
}).filter((r) => r.sprint > 0);
|
|
6430
|
+
}
|
|
6431
|
+
function getRollingAverage(history, window = 3) {
|
|
6432
|
+
if (history.length === 0) return 0;
|
|
6433
|
+
const recent = history.slice(-window);
|
|
6434
|
+
const sum = recent.reduce((acc, s) => acc + s.delivered, 0);
|
|
6435
|
+
return Math.round(sum / recent.length);
|
|
6436
|
+
}
|
|
6437
|
+
function formatVelocityReport(history) {
|
|
6438
|
+
if (history.length === 0)
|
|
6439
|
+
return "Noch keine Velocity-Daten vorhanden.\n\nStarte mit: `fh velocity record <sprint> --committed N --delivered N`";
|
|
6440
|
+
const avg3 = getRollingAverage(history, 3);
|
|
6441
|
+
const lines = [];
|
|
6442
|
+
lines.push("# Sprint Velocity");
|
|
6443
|
+
lines.push("");
|
|
6444
|
+
lines.push(`**Rolling Average (letzte 3 Sprints):** ${avg3} Punkte`);
|
|
6445
|
+
lines.push("");
|
|
6446
|
+
lines.push("| Sprint | Datum | Committed | Delivered | Rate |");
|
|
6447
|
+
lines.push("|---|---|---|---|---|");
|
|
6448
|
+
for (const s of history) {
|
|
6449
|
+
const rate = s.committed > 0 ? Math.round(s.delivered / s.committed * 100) : 0;
|
|
6450
|
+
lines.push(`| Sprint ${s.sprint} | ${s.date} | ${s.committed} | ${s.delivered} | ${rate}% |`);
|
|
6451
|
+
}
|
|
6452
|
+
lines.push("");
|
|
6453
|
+
lines.push(`**Empfehlung f\xFCr n\xE4chsten Sprint:** ~${avg3} Punkte einplanen`);
|
|
6454
|
+
return lines.join("\n");
|
|
6455
|
+
}
|
|
6456
|
+
|
|
5584
6457
|
// src/cli.ts
|
|
5585
6458
|
var [, , command, subcommand, ...rest] = process.argv;
|
|
5586
6459
|
var projectRoot = process.cwd();
|
|
5587
|
-
var forgehiveDir =
|
|
6460
|
+
var forgehiveDir = path31.join(projectRoot, ".forgehive");
|
|
5588
6461
|
if (command === "--version" || command === "-v") {
|
|
5589
|
-
console.log("0.
|
|
6462
|
+
console.log("0.7.1");
|
|
5590
6463
|
process.exit(0);
|
|
5591
6464
|
}
|
|
5592
6465
|
function loadClaudeMdBlock() {
|
|
5593
|
-
const templatePath =
|
|
5594
|
-
|
|
6466
|
+
const templatePath = path31.join(
|
|
6467
|
+
path31.dirname(new URL(import.meta.url).pathname),
|
|
5595
6468
|
"..",
|
|
5596
6469
|
"forgehive",
|
|
5597
6470
|
"templates",
|
|
5598
6471
|
"claude-md.block.md"
|
|
5599
6472
|
);
|
|
5600
|
-
if (!
|
|
5601
|
-
return
|
|
6473
|
+
if (!fs30.existsSync(templatePath)) return "## forgehive\n\nSee .forgehive/ for configuration.";
|
|
6474
|
+
return fs30.readFileSync(templatePath, "utf8");
|
|
5602
6475
|
}
|
|
5603
6476
|
if (command === "init") {
|
|
5604
6477
|
console.log("\u{1F50D} Analysiere Projekt...\n");
|
|
@@ -5612,9 +6485,9 @@ if (command === "init") {
|
|
|
5612
6485
|
const block = loadClaudeMdBlock();
|
|
5613
6486
|
writeForgehiveDir(projectRoot, scanResult, capMap, block);
|
|
5614
6487
|
const hash = computeHash(projectRoot);
|
|
5615
|
-
|
|
5616
|
-
const runtimeDir =
|
|
5617
|
-
|
|
6488
|
+
fs30.writeFileSync(path31.join(forgehiveDir, ".scan-hash"), hash, "utf8");
|
|
6489
|
+
const runtimeDir = path31.join(
|
|
6490
|
+
path31.dirname(new URL(import.meta.url).pathname),
|
|
5618
6491
|
"..",
|
|
5619
6492
|
"forgehive"
|
|
5620
6493
|
);
|
|
@@ -5646,7 +6519,7 @@ if (command === "init") {
|
|
|
5646
6519
|
process.exit(1);
|
|
5647
6520
|
}
|
|
5648
6521
|
} else if (command === "memory") {
|
|
5649
|
-
if (!
|
|
6522
|
+
if (!fs30.existsSync(forgehiveDir)) {
|
|
5650
6523
|
console.error("Fehler: .forgehive/ nicht gefunden \u2014 f\xFChre zuerst `fh init` aus");
|
|
5651
6524
|
process.exit(1);
|
|
5652
6525
|
}
|
|
@@ -5655,7 +6528,7 @@ if (command === "init") {
|
|
|
5655
6528
|
} else if (subcommand === "clean") {
|
|
5656
6529
|
cleanMemory(forgehiveDir);
|
|
5657
6530
|
} else if (subcommand === "export") {
|
|
5658
|
-
const outputPath = rest[0] ??
|
|
6531
|
+
const outputPath = rest[0] ?? path31.join(projectRoot, "forgehive-memory-export.md");
|
|
5659
6532
|
try {
|
|
5660
6533
|
exportMemory(forgehiveDir, outputPath);
|
|
5661
6534
|
} catch (err) {
|
|
@@ -5703,7 +6576,7 @@ Ausf\xFChren mit --remove um zu l\xF6schen: fh memory prune ${days} --remove`);
|
|
|
5703
6576
|
} else if (subcommand === "snapshot") {
|
|
5704
6577
|
const snapAction = rest[0];
|
|
5705
6578
|
if (snapAction === "export") {
|
|
5706
|
-
const outPath = rest[1] ??
|
|
6579
|
+
const outPath = rest[1] ?? path31.join(
|
|
5707
6580
|
projectRoot,
|
|
5708
6581
|
`forgehive-snapshot-${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}.json`
|
|
5709
6582
|
);
|
|
@@ -5743,11 +6616,11 @@ Ausf\xFChren mit --remove um zu l\xF6schen: fh memory prune ${days} --remove`);
|
|
|
5743
6616
|
process.exit(1);
|
|
5744
6617
|
}
|
|
5745
6618
|
} else if (command === "scan" && subcommand === "--update") {
|
|
5746
|
-
if (!
|
|
6619
|
+
if (!fs30.existsSync(forgehiveDir)) {
|
|
5747
6620
|
console.error("Fehler: .forgehive/ nicht gefunden \u2014 f\xFChre zuerst `fh init` aus");
|
|
5748
6621
|
process.exit(1);
|
|
5749
6622
|
}
|
|
5750
|
-
const savedHash =
|
|
6623
|
+
const savedHash = fs30.existsSync(path31.join(forgehiveDir, ".scan-hash")) ? fs30.readFileSync(path31.join(forgehiveDir, ".scan-hash"), "utf8").trim() : null;
|
|
5751
6624
|
const currentHash = computeHash(projectRoot);
|
|
5752
6625
|
if (savedHash === currentHash) {
|
|
5753
6626
|
console.log("\u2713 Keine \xC4nderungen erkannt \u2014 capabilities.yaml ist aktuell");
|
|
@@ -5755,7 +6628,7 @@ Ausf\xFChren mit --remove um zu l\xF6schen: fh memory prune ${days} --remove`);
|
|
|
5755
6628
|
}
|
|
5756
6629
|
console.log("\u{1F50D} \xC4nderungen erkannt \u2014 scanne erneut...\n");
|
|
5757
6630
|
const oldDoc = jsYaml.load(
|
|
5758
|
-
|
|
6631
|
+
fs30.readFileSync(path31.join(forgehiveDir, "capabilities.yaml"), "utf8")
|
|
5759
6632
|
);
|
|
5760
6633
|
const oldMap = { confirmed: oldDoc.capabilities.confirmed ?? [], inferred: [] };
|
|
5761
6634
|
const scanResult = scan(projectRoot);
|
|
@@ -5775,16 +6648,16 @@ Ausf\xFChren mit --remove um zu l\xF6schen: fh memory prune ${days} --remove`);
|
|
|
5775
6648
|
console.log();
|
|
5776
6649
|
const block = loadClaudeMdBlock();
|
|
5777
6650
|
writeForgehiveDir(projectRoot, scanResult, newMap, block);
|
|
5778
|
-
|
|
6651
|
+
fs30.writeFileSync(path31.join(forgehiveDir, ".scan-hash"), currentHash, "utf8");
|
|
5779
6652
|
console.log("\u2713 scan-result.yaml und capabilities.yaml aktualisiert");
|
|
5780
6653
|
console.log(" F\xFChre `fh confirm` aus um die \xC4nderungen zu best\xE4tigen");
|
|
5781
6654
|
}
|
|
5782
6655
|
} else if (command === "scan" && subcommand === "--check") {
|
|
5783
|
-
if (!
|
|
6656
|
+
if (!fs30.existsSync(path31.join(forgehiveDir, ".scan-hash"))) {
|
|
5784
6657
|
console.log("Warnung: Kein Scan-Hash gefunden. F\xFChre `fh init` aus.");
|
|
5785
6658
|
process.exit(1);
|
|
5786
6659
|
}
|
|
5787
|
-
const saved =
|
|
6660
|
+
const saved = fs30.readFileSync(path31.join(forgehiveDir, ".scan-hash"), "utf8").trim();
|
|
5788
6661
|
const current = computeHash(projectRoot);
|
|
5789
6662
|
if (saved !== current) {
|
|
5790
6663
|
console.log("\u26A0 Codebase hat sich seit letztem Scan ge\xE4ndert.");
|
|
@@ -5793,7 +6666,7 @@ Ausf\xFChren mit --remove um zu l\xF6schen: fh memory prune ${days} --remove`);
|
|
|
5793
6666
|
}
|
|
5794
6667
|
console.log("\u2713 capabilities.yaml ist aktuell");
|
|
5795
6668
|
} else if (command === "skills") {
|
|
5796
|
-
if (!
|
|
6669
|
+
if (!fs30.existsSync(forgehiveDir)) {
|
|
5797
6670
|
console.error("Fehler: .forgehive/ nicht gefunden \u2014 f\xFChre zuerst `fh init` aus");
|
|
5798
6671
|
process.exit(1);
|
|
5799
6672
|
}
|
|
@@ -5829,7 +6702,7 @@ Ausf\xFChren mit --remove um zu l\xF6schen: fh memory prune ${days} --remove`);
|
|
|
5829
6702
|
process.exit(1);
|
|
5830
6703
|
}
|
|
5831
6704
|
} else if (command === "party") {
|
|
5832
|
-
if (!
|
|
6705
|
+
if (!fs30.existsSync(forgehiveDir)) {
|
|
5833
6706
|
console.error("Fehler: .forgehive/ nicht gefunden \u2014 f\xFChre zuerst `fh init` aus");
|
|
5834
6707
|
process.exit(1);
|
|
5835
6708
|
}
|
|
@@ -5950,7 +6823,7 @@ Ausf\xFChren mit --remove um zu l\xF6schen: fh memory prune ${days} --remove`);
|
|
|
5950
6823
|
}
|
|
5951
6824
|
}
|
|
5952
6825
|
} else if (command === "wire") {
|
|
5953
|
-
if (!
|
|
6826
|
+
if (!fs30.existsSync(forgehiveDir)) {
|
|
5954
6827
|
console.error("Fehler: .forgehive/ nicht gefunden \u2014 f\xFChre zuerst `fh init` aus");
|
|
5955
6828
|
process.exit(1);
|
|
5956
6829
|
}
|
|
@@ -5989,7 +6862,7 @@ N\xE4chster Schritt: Setze die erforderlichen Umgebungsvariablen und starte Clau
|
|
|
5989
6862
|
const limitIdx = allArgs.indexOf("--limit");
|
|
5990
6863
|
const alertIdx = allArgs.indexOf("--alert");
|
|
5991
6864
|
if (limitIdx !== -1) {
|
|
5992
|
-
if (!
|
|
6865
|
+
if (!fs30.existsSync(forgehiveDir)) {
|
|
5993
6866
|
console.error("Fehler: .forgehive/ nicht gefunden \u2014 f\xFChre zuerst `fh init` aus");
|
|
5994
6867
|
process.exit(1);
|
|
5995
6868
|
}
|
|
@@ -6015,14 +6888,14 @@ N\xE4chster Schritt: Setze die erforderlichen Umgebungsvariablen und starte Clau
|
|
|
6015
6888
|
}
|
|
6016
6889
|
const sessions = parseCostSessions(projectRoot);
|
|
6017
6890
|
console.log(formatCostReport(sessions, range));
|
|
6018
|
-
if (
|
|
6891
|
+
if (fs30.existsSync(forgehiveDir)) {
|
|
6019
6892
|
const total = sessions.reduce((s, x) => s + x.estimatedCostUsd, 0);
|
|
6020
6893
|
const status = checkSpendStatus(forgehiveDir, total);
|
|
6021
6894
|
if (status.message) console.log("\n" + status.message);
|
|
6022
6895
|
}
|
|
6023
6896
|
}
|
|
6024
6897
|
} else if (command === "watch") {
|
|
6025
|
-
if (!
|
|
6898
|
+
if (!fs30.existsSync(forgehiveDir)) {
|
|
6026
6899
|
console.error("Fehler: .forgehive/ nicht gefunden \u2014 f\xFChre zuerst `fh init` aus");
|
|
6027
6900
|
process.exit(1);
|
|
6028
6901
|
}
|
|
@@ -6038,7 +6911,7 @@ N\xE4chster Schritt: Setze die erforderlichen Umgebungsvariablen und starte Clau
|
|
|
6038
6911
|
process.exit(0);
|
|
6039
6912
|
});
|
|
6040
6913
|
} else if (command === "mcp") {
|
|
6041
|
-
if (!
|
|
6914
|
+
if (!fs30.existsSync(forgehiveDir)) {
|
|
6042
6915
|
console.error("Fehler: .forgehive/ nicht gefunden \u2014 f\xFChre zuerst `fh init` aus");
|
|
6043
6916
|
process.exit(1);
|
|
6044
6917
|
}
|
|
@@ -6149,7 +7022,7 @@ Setze diese Umgebungsvariablen:`);
|
|
|
6149
7022
|
process.exit(1);
|
|
6150
7023
|
}
|
|
6151
7024
|
} else if (command === "security") {
|
|
6152
|
-
if (!
|
|
7025
|
+
if (!fs30.existsSync(forgehiveDir)) {
|
|
6153
7026
|
console.error("Fehler: .forgehive/ nicht gefunden \u2014 f\xFChre zuerst `fh init` aus");
|
|
6154
7027
|
process.exit(1);
|
|
6155
7028
|
}
|
|
@@ -6235,8 +7108,8 @@ Setze diese Umgebungsvariablen:`);
|
|
|
6235
7108
|
`);
|
|
6236
7109
|
const report = generateSecurityReport(projectRoot, forgehiveDir, mode);
|
|
6237
7110
|
const text = formatSecurityReport(report);
|
|
6238
|
-
const reportPath =
|
|
6239
|
-
|
|
7111
|
+
const reportPath = path31.join(forgehiveDir, "security-report.md");
|
|
7112
|
+
fs30.writeFileSync(reportPath, text, "utf8");
|
|
6240
7113
|
console.log(text);
|
|
6241
7114
|
console.log(`
|
|
6242
7115
|
\u2713 Report gespeichert: ${reportPath}`);
|
|
@@ -6248,17 +7121,221 @@ Setze diese Umgebungsvariablen:`);
|
|
|
6248
7121
|
} else if (subcommand === "permissions") {
|
|
6249
7122
|
const { writePermissions: writePermissions2 } = await Promise.resolve().then(() => (init_harness(), harness_exports));
|
|
6250
7123
|
writePermissions2(forgehiveDir);
|
|
6251
|
-
const permPath =
|
|
6252
|
-
console.log(
|
|
7124
|
+
const permPath = path31.join(forgehiveDir, "harness", "permissions.yaml");
|
|
7125
|
+
console.log(fs30.readFileSync(permPath, "utf8"));
|
|
6253
7126
|
} else {
|
|
6254
7127
|
console.error(`Unbekannter security-Subcommand: ${subcommand}`);
|
|
6255
7128
|
console.error("Verf\xFCgbar: scan | deps | audit | report [gdpr|soc2|hipaa|none] | permissions");
|
|
6256
7129
|
process.exit(1);
|
|
6257
7130
|
}
|
|
7131
|
+
} else if (command === "ci") {
|
|
7132
|
+
const allCiArgs = [subcommand, ...rest].filter((a) => Boolean(a));
|
|
7133
|
+
const format = allCiArgs.includes("--format") ? allCiArgs[allCiArgs.indexOf("--format") + 1] : "markdown";
|
|
7134
|
+
const failOnArg = allCiArgs.includes("--fail-on") ? allCiArgs[allCiArgs.indexOf("--fail-on") + 1] : "high";
|
|
7135
|
+
const initFlag = allCiArgs.includes("--init");
|
|
7136
|
+
if (initFlag) {
|
|
7137
|
+
const ghDir = path31.join(projectRoot, ".github", "workflows");
|
|
7138
|
+
fs30.mkdirSync(ghDir, { recursive: true });
|
|
7139
|
+
const outPath = path31.join(ghDir, "forgehive.yml");
|
|
7140
|
+
fs30.writeFileSync(outPath, getGithubActionsTemplate(), "utf8");
|
|
7141
|
+
console.log(`\u2714 GitHub Actions workflow geschrieben: ${outPath}`);
|
|
7142
|
+
} else {
|
|
7143
|
+
const report = generateCiReport(projectRoot, forgehiveDir, failOnArg);
|
|
7144
|
+
const output = formatCiReport(report, format);
|
|
7145
|
+
console.log(output);
|
|
7146
|
+
if (format === "json") {
|
|
7147
|
+
fs30.mkdirSync(forgehiveDir, { recursive: true });
|
|
7148
|
+
fs30.writeFileSync(path31.join(forgehiveDir, "ci-report.json"), output, "utf8");
|
|
7149
|
+
}
|
|
7150
|
+
if (report.status === "fail") process.exit(1);
|
|
7151
|
+
}
|
|
7152
|
+
} else if (command === "map") {
|
|
7153
|
+
const map2 = generateMap(projectRoot);
|
|
7154
|
+
const md = formatMap(map2);
|
|
7155
|
+
const mapPath = path31.join(forgehiveDir, "map.md");
|
|
7156
|
+
fs30.mkdirSync(forgehiveDir, { recursive: true });
|
|
7157
|
+
fs30.writeFileSync(mapPath, md, "utf8");
|
|
7158
|
+
console.log(md);
|
|
7159
|
+
console.log(`
|
|
7160
|
+
\u2714 Codebase-Map gespeichert: ${mapPath}`);
|
|
7161
|
+
} else if (command === "onboard") {
|
|
7162
|
+
const outputArg = rest.includes("--output") ? rest[rest.indexOf("--output") + 1] : null;
|
|
7163
|
+
const outputPath = outputArg ?? path31.join(projectRoot, "ONBOARDING.md");
|
|
7164
|
+
const doc = generateOnboardingDoc(projectRoot, forgehiveDir);
|
|
7165
|
+
fs30.writeFileSync(outputPath, doc, "utf8");
|
|
7166
|
+
console.log(`\u2714 Onboarding-Dokument geschrieben: ${outputPath}`);
|
|
7167
|
+
} else if (command === "changelog") {
|
|
7168
|
+
const sinceArg = rest.includes("--since") ? rest[rest.indexOf("--since") + 1] : null;
|
|
7169
|
+
const outputArg = rest.includes("--output") ? rest[rest.indexOf("--output") + 1] : null;
|
|
7170
|
+
const since = sinceArg ?? getLatestTag(projectRoot) ?? void 0;
|
|
7171
|
+
const rawLog = getGitLogSince(projectRoot, since);
|
|
7172
|
+
const commits = parseGitLog(rawLog);
|
|
7173
|
+
let version = "unreleased";
|
|
7174
|
+
try {
|
|
7175
|
+
const pkgPath = path31.join(projectRoot, "package.json");
|
|
7176
|
+
if (fs30.existsSync(pkgPath)) {
|
|
7177
|
+
const pkg = JSON.parse(fs30.readFileSync(pkgPath, "utf8").replace(/^\s*\/\/.*$/gm, ""));
|
|
7178
|
+
version = pkg.version ?? "unreleased";
|
|
7179
|
+
}
|
|
7180
|
+
} catch {
|
|
7181
|
+
}
|
|
7182
|
+
const md = formatChangelog(commits, version);
|
|
7183
|
+
const outputPath = outputArg ?? path31.join(projectRoot, "CHANGELOG.md");
|
|
7184
|
+
let existing = "";
|
|
7185
|
+
if (fs30.existsSync(outputPath)) existing = fs30.readFileSync(outputPath, "utf8");
|
|
7186
|
+
fs30.writeFileSync(outputPath, md + "\n\n" + existing, "utf8");
|
|
7187
|
+
console.log(`\u2714 CHANGELOG.md aktualisiert: ${outputPath}`);
|
|
7188
|
+
console.log(` ${commits.length} Commits verarbeitet`);
|
|
7189
|
+
} else if (command === "metrics") {
|
|
7190
|
+
const sinceArg = rest.includes("--since") ? rest[rest.indexOf("--since") + 1] : void 0;
|
|
7191
|
+
const rawLog = getMetricsGitLog(projectRoot, sinceArg);
|
|
7192
|
+
const stats = parseCommitStats(rawLog);
|
|
7193
|
+
const md = formatMetrics(stats, path31.basename(projectRoot));
|
|
7194
|
+
const metricsPath = path31.join(forgehiveDir, "metrics.md");
|
|
7195
|
+
fs30.mkdirSync(forgehiveDir, { recursive: true });
|
|
7196
|
+
fs30.writeFileSync(metricsPath, md, "utf8");
|
|
7197
|
+
console.log(md);
|
|
7198
|
+
console.log(`
|
|
7199
|
+
\u2714 Metrics gespeichert: ${metricsPath}`);
|
|
7200
|
+
} else if (command === "sync") {
|
|
7201
|
+
const remoteArg = rest.includes("--remote") ? rest[rest.indexOf("--remote") + 1] : "origin";
|
|
7202
|
+
const branchArg = rest.includes("--branch") ? rest[rest.indexOf("--branch") + 1] : "forgehive-memory";
|
|
7203
|
+
if (subcommand === "push") {
|
|
7204
|
+
const result = pushSync(forgehiveDir, remoteArg, branchArg);
|
|
7205
|
+
console.log(result.success ? `\u2714 ${result.message}` : `\u2717 ${result.message}`);
|
|
7206
|
+
if (!result.success) process.exit(1);
|
|
7207
|
+
} else if (subcommand === "pull") {
|
|
7208
|
+
const result = pullSync(forgehiveDir, remoteArg, branchArg);
|
|
7209
|
+
console.log(result.success ? `\u2714 ${result.message}` : `\u2717 ${result.message}`);
|
|
7210
|
+
if (result.filesImported.length > 0)
|
|
7211
|
+
console.log(" Importiert:", result.filesImported.join(", "));
|
|
7212
|
+
if (!result.success) process.exit(1);
|
|
7213
|
+
} else {
|
|
7214
|
+
const status = getSyncStatus(forgehiveDir);
|
|
7215
|
+
console.log(`Memory: ${status.files} Dateien`);
|
|
7216
|
+
console.log(status.hasRemote ? `Remote: ${status.remote}/${status.branch}` : "Kein Remote konfiguriert. Nutze: fh sync push [--remote origin --branch forgehive-memory]");
|
|
7217
|
+
}
|
|
7218
|
+
} else if (command === "run") {
|
|
7219
|
+
const issueUrl = subcommand;
|
|
7220
|
+
if (!issueUrl) {
|
|
7221
|
+
console.error("Usage: fh run <issue-url> [--agent <name>]");
|
|
7222
|
+
process.exit(1);
|
|
7223
|
+
}
|
|
7224
|
+
const agentArg = rest.includes("--agent") ? rest[rest.indexOf("--agent") + 1] : void 0;
|
|
7225
|
+
const labelArg = rest.includes("--label") ? rest[rest.indexOf("--label") + 1] : void 0;
|
|
7226
|
+
const agentId = agentArg ?? resolveAgent(labelArg);
|
|
7227
|
+
const result = runBackgroundAgent(forgehiveDir, issueUrl, agentId);
|
|
7228
|
+
console.log(`\u2714 ${result.message}`);
|
|
7229
|
+
console.log(` fh run status \u2014 aktive Sessions anzeigen`);
|
|
7230
|
+
} else if (command === "story") {
|
|
7231
|
+
const storiesDir = path31.join(forgehiveDir, "memory", "stories");
|
|
7232
|
+
if (subcommand === "create") {
|
|
7233
|
+
const title = rest.filter((r) => !r.startsWith("--")).join(" ");
|
|
7234
|
+
const epicArg = rest.includes("--epic") ? rest[rest.indexOf("--epic") + 1] : void 0;
|
|
7235
|
+
const pointsArg = rest.includes("--points") ? parseInt(rest[rest.indexOf("--points") + 1], 10) : void 0;
|
|
7236
|
+
if (!title) {
|
|
7237
|
+
console.error("Usage: fh story create <titel> [--epic EPC-N] [--points N]");
|
|
7238
|
+
process.exit(1);
|
|
7239
|
+
}
|
|
7240
|
+
const story = createStory(storiesDir, title, epicArg);
|
|
7241
|
+
if (pointsArg) updateStoryPoints(storiesDir, story.id, pointsArg);
|
|
7242
|
+
console.log(`\u2714 ${story.id} erstellt: ${path31.join(storiesDir, story.id + ".md")}`);
|
|
7243
|
+
console.log(` Bearbeite die Datei um Acceptance Criteria hinzuzuf\xFCgen.`);
|
|
7244
|
+
} else if (subcommand === "list") {
|
|
7245
|
+
const epicFilter = rest.includes("--epic") ? rest[rest.indexOf("--epic") + 1] : null;
|
|
7246
|
+
let stories = listStories(storiesDir);
|
|
7247
|
+
if (epicFilter) stories = stories.filter((s) => s.epicId === epicFilter);
|
|
7248
|
+
if (stories.length === 0) {
|
|
7249
|
+
console.log("Keine Stories gefunden.");
|
|
7250
|
+
} else {
|
|
7251
|
+
for (const s of stories) {
|
|
7252
|
+
const pts = s.points !== null ? ` [${s.points}pt]` : " [?pt]";
|
|
7253
|
+
const epic = s.epicId ? ` (${s.epicId})` : "";
|
|
7254
|
+
console.log(` ${s.id}${pts}${epic} \u2014 ${s.title} [${s.status}]`);
|
|
7255
|
+
}
|
|
7256
|
+
}
|
|
7257
|
+
} else if (subcommand === "done") {
|
|
7258
|
+
const id = rest[0];
|
|
7259
|
+
const pointsArg = rest.includes("--points") ? parseInt(rest[rest.indexOf("--points") + 1], 10) : void 0;
|
|
7260
|
+
if (!id) {
|
|
7261
|
+
console.error("Usage: fh story done <US-N> [--points N]");
|
|
7262
|
+
process.exit(1);
|
|
7263
|
+
}
|
|
7264
|
+
if (pointsArg) updateStoryPoints(storiesDir, id, pointsArg);
|
|
7265
|
+
updateStoryStatus(storiesDir, id, "done");
|
|
7266
|
+
console.log(`\u2714 ${id} als done markiert`);
|
|
7267
|
+
} else if (subcommand === "show") {
|
|
7268
|
+
const id = rest[0];
|
|
7269
|
+
if (!id) {
|
|
7270
|
+
console.error("Usage: fh story show <US-N>");
|
|
7271
|
+
process.exit(1);
|
|
7272
|
+
}
|
|
7273
|
+
const story = getStory(storiesDir, id);
|
|
7274
|
+
if (!story) {
|
|
7275
|
+
console.error(`Story ${id} nicht gefunden`);
|
|
7276
|
+
process.exit(1);
|
|
7277
|
+
}
|
|
7278
|
+
console.log(formatStoryCard(story));
|
|
7279
|
+
} else {
|
|
7280
|
+
console.error("Verf\xFCgbar: fh story create | list | show | done");
|
|
7281
|
+
}
|
|
7282
|
+
} else if (command === "epic") {
|
|
7283
|
+
const epicsDir = path31.join(forgehiveDir, "memory", "epics");
|
|
7284
|
+
const storiesDir = path31.join(forgehiveDir, "memory", "stories");
|
|
7285
|
+
if (subcommand === "create") {
|
|
7286
|
+
const title = rest.filter((r) => !r.startsWith("--")).join(" ");
|
|
7287
|
+
const goalArg = rest.includes("--goal") ? rest[rest.indexOf("--goal") + 1] : void 0;
|
|
7288
|
+
if (!title) {
|
|
7289
|
+
console.error("Usage: fh epic create <titel> [--goal <ziel>]");
|
|
7290
|
+
process.exit(1);
|
|
7291
|
+
}
|
|
7292
|
+
const epic = createEpic(epicsDir, title, goalArg);
|
|
7293
|
+
console.log(`\u2714 ${epic.id} erstellt: ${path31.join(epicsDir, epic.id + ".md")}`);
|
|
7294
|
+
} else if (subcommand === "list") {
|
|
7295
|
+
const epics = listEpics(epicsDir);
|
|
7296
|
+
if (epics.length === 0) {
|
|
7297
|
+
console.log("Keine Epics gefunden.");
|
|
7298
|
+
} else {
|
|
7299
|
+
for (const e of epics)
|
|
7300
|
+
console.log(` ${e.id} \u2014 ${e.title} [${e.status}] (${e.stories.length} Stories)`);
|
|
7301
|
+
}
|
|
7302
|
+
} else if (subcommand === "show") {
|
|
7303
|
+
const id = rest[0];
|
|
7304
|
+
if (!id) {
|
|
7305
|
+
console.error("Usage: fh epic show <EPC-N>");
|
|
7306
|
+
process.exit(1);
|
|
7307
|
+
}
|
|
7308
|
+
const epic = getEpic(epicsDir, id);
|
|
7309
|
+
if (!epic) {
|
|
7310
|
+
console.error(`Epic ${id} nicht gefunden`);
|
|
7311
|
+
process.exit(1);
|
|
7312
|
+
}
|
|
7313
|
+
const stories = listStories(storiesDir).filter((s) => s.epicId === id);
|
|
7314
|
+
console.log(formatEpicCard(epic, stories));
|
|
7315
|
+
} else {
|
|
7316
|
+
console.error("Verf\xFCgbar: fh epic create | list | show");
|
|
7317
|
+
}
|
|
7318
|
+
} else if (command === "velocity") {
|
|
7319
|
+
const velocityFile = path31.join(forgehiveDir, "memory", "velocity.md");
|
|
7320
|
+
if (subcommand === "record") {
|
|
7321
|
+
const sprintNum = parseInt(rest[0] ?? "0", 10);
|
|
7322
|
+
const committed = rest.includes("--committed") ? parseInt(rest[rest.indexOf("--committed") + 1], 10) : NaN;
|
|
7323
|
+
const delivered = rest.includes("--delivered") ? parseInt(rest[rest.indexOf("--delivered") + 1], 10) : NaN;
|
|
7324
|
+
if (!sprintNum || isNaN(committed) || isNaN(delivered)) {
|
|
7325
|
+
console.error("Usage: fh velocity record <sprint-num> --committed N --delivered N");
|
|
7326
|
+
process.exit(1);
|
|
7327
|
+
}
|
|
7328
|
+
recordVelocity(velocityFile, sprintNum, committed, delivered);
|
|
7329
|
+
const avg = getRollingAverage(getVelocityHistory(velocityFile));
|
|
7330
|
+
console.log(`\u2714 Sprint ${sprintNum} gespeichert. Rolling Average: ${avg} Punkte`);
|
|
7331
|
+
} else {
|
|
7332
|
+
const history = getVelocityHistory(velocityFile);
|
|
7333
|
+
console.log(formatVelocityReport(history));
|
|
7334
|
+
}
|
|
6258
7335
|
} else {
|
|
6259
7336
|
const cmd = [command, subcommand].filter(Boolean).join(" ") || "(kein)";
|
|
6260
7337
|
console.error(`Unbekannter Befehl: ${cmd}`);
|
|
6261
|
-
console.error("Verf\xFCgbar: init | confirm | rollback | scan --update | scan --check | status | cost [today|week|all] | cost --limit N --alert N | memory [show|clean|export|prune|snapshot] | memory adr [list|<titel>] | skills [list|regen|pull <url>] | party [--set <name>|run|status|cleanup] | wire <service> | mcp [auth|search|add] | security [scan|deps|report|audit|permissions]");
|
|
7338
|
+
console.error("Verf\xFCgbar: init | confirm | rollback | scan --update | scan --check | status | ci [--format json|markdown] [--fail-on critical|high|any] [--init] | map | onboard [--output path] | changelog [--since tag] | metrics [--since date] | sync [push|pull] [--remote origin --branch forgehive-memory] | run <issue-url> [--agent <name>] | cost [today|week|all] | cost --limit N --alert N | memory [show|clean|export|prune|snapshot] | memory adr [list|<titel>] | skills [list|regen|pull <url>] | party [--set <name>|run|status|cleanup] | wire <service> | mcp [auth|search|add] | security [scan|deps|report|audit|permissions]");
|
|
6262
7339
|
process.exit(1);
|
|
6263
7340
|
}
|
|
6264
7341
|
/*! Bundled license information:
|