forgehive 0.7.8 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +142 -13
- package/dist/cli.js +1218 -226
- package/package.json +2 -1
package/dist/cli.js
CHANGED
|
@@ -2751,8 +2751,10 @@ var init_harness = __esm({
|
|
|
2751
2751
|
|
|
2752
2752
|
// src/cli.ts
|
|
2753
2753
|
init_js_yaml();
|
|
2754
|
-
import
|
|
2755
|
-
import
|
|
2754
|
+
import fs35 from "node:fs";
|
|
2755
|
+
import path36 from "node:path";
|
|
2756
|
+
import { spawnSync as spawnSync12 } from "node:child_process";
|
|
2757
|
+
import { createInterface } from "node:readline";
|
|
2756
2758
|
|
|
2757
2759
|
// src/scanner.ts
|
|
2758
2760
|
import fs from "node:fs";
|
|
@@ -5734,7 +5736,44 @@ function generateMap(projectRoot2) {
|
|
|
5734
5736
|
totalLines: files.reduce((sum, f) => sum + f.lines, 0)
|
|
5735
5737
|
};
|
|
5736
5738
|
}
|
|
5737
|
-
function
|
|
5739
|
+
function formatSemanticBlock(semantic, projectRoot2) {
|
|
5740
|
+
const lines = [];
|
|
5741
|
+
lines.push("## Semantic Graph");
|
|
5742
|
+
lines.push("");
|
|
5743
|
+
lines.push("### Hotspots (meistimportiert)");
|
|
5744
|
+
if (semantic.hotspots.length === 0) {
|
|
5745
|
+
lines.push("Keine Hotspot-Dateien gefunden (Schwellenwert: 3+ Imports).");
|
|
5746
|
+
} else {
|
|
5747
|
+
for (const h of semantic.hotspots) {
|
|
5748
|
+
const rel = h.file.startsWith(projectRoot2) ? h.file.slice(projectRoot2.length + 1) : h.file;
|
|
5749
|
+
lines.push(`- \`${rel}\` \u2014 importiert von ${h.importedBy} Dateien`);
|
|
5750
|
+
}
|
|
5751
|
+
}
|
|
5752
|
+
lines.push("");
|
|
5753
|
+
lines.push("### Zirkul\xE4re Abh\xE4ngigkeiten");
|
|
5754
|
+
if (semantic.cycles.length === 0) {
|
|
5755
|
+
lines.push("Keine gefunden.");
|
|
5756
|
+
} else {
|
|
5757
|
+
for (const cycle of semantic.cycles) {
|
|
5758
|
+
const relCycle = cycle.map(
|
|
5759
|
+
(f) => f.startsWith(projectRoot2) ? f.slice(projectRoot2.length + 1) : f
|
|
5760
|
+
);
|
|
5761
|
+
lines.push(`- ${relCycle.map((f) => `\`${f}\``).join(" \u2192 ")}`);
|
|
5762
|
+
}
|
|
5763
|
+
}
|
|
5764
|
+
lines.push("");
|
|
5765
|
+
const fileCount = Object.keys(semantic.imports).length;
|
|
5766
|
+
const relationCount = Object.values(semantic.imports).reduce(
|
|
5767
|
+
(sum, deps) => sum + deps.length,
|
|
5768
|
+
0
|
|
5769
|
+
);
|
|
5770
|
+
lines.push("### Abh\xE4ngigkeits\xFCbersicht");
|
|
5771
|
+
lines.push(`- ${fileCount} Dateien analysiert`);
|
|
5772
|
+
lines.push(`- ${relationCount} Import-Beziehungen`);
|
|
5773
|
+
lines.push(`- ${semantic.hotspots.length} Hotspot-Dateien (importiert von 3+ anderen)`);
|
|
5774
|
+
return lines.join("\n");
|
|
5775
|
+
}
|
|
5776
|
+
function formatMap(map2, semantic) {
|
|
5738
5777
|
const lines = [];
|
|
5739
5778
|
lines.push("# Codebase Map");
|
|
5740
5779
|
lines.push(`Generated: ${map2.generatedAt}`);
|
|
@@ -5764,13 +5803,199 @@ function formatMap(map2) {
|
|
|
5764
5803
|
} else {
|
|
5765
5804
|
lines.push("No internal imports detected.");
|
|
5766
5805
|
}
|
|
5806
|
+
if (semantic) {
|
|
5807
|
+
lines.push("");
|
|
5808
|
+
lines.push(formatSemanticBlock(semantic, map2.projectRoot));
|
|
5809
|
+
}
|
|
5767
5810
|
return lines.join("\n");
|
|
5768
5811
|
}
|
|
5769
5812
|
|
|
5770
|
-
// src/
|
|
5771
|
-
init_js_yaml();
|
|
5813
|
+
// src/semantic-map.ts
|
|
5772
5814
|
import fs24 from "node:fs";
|
|
5773
5815
|
import path25 from "node:path";
|
|
5816
|
+
var IGNORE_DIRS3 = ["node_modules", ".git", "dist", ".forgehive", "coverage", ".next", "build"];
|
|
5817
|
+
var MAP_EXTS2 = [".ts", ".tsx", ".js", ".jsx", ".py", ".go"];
|
|
5818
|
+
var IMPORT_PATTERNS2 = {
|
|
5819
|
+
ts: [
|
|
5820
|
+
/^import\s+[\s\S]*?from\s+['"]([^'"]+)['"]/gm,
|
|
5821
|
+
/^import\s+['"]([^'"]+)['"]/gm,
|
|
5822
|
+
/require\s*\(\s*['"]([^'"]+)['"]\s*\)/gm
|
|
5823
|
+
],
|
|
5824
|
+
js: [
|
|
5825
|
+
/^import\s+[\s\S]*?from\s+['"]([^'"]+)['"]/gm,
|
|
5826
|
+
/^import\s+['"]([^'"]+)['"]/gm,
|
|
5827
|
+
/require\s*\(\s*['"]([^'"]+)['"]\s*\)/gm
|
|
5828
|
+
],
|
|
5829
|
+
py: [
|
|
5830
|
+
/^import\s+(\S+)/gm,
|
|
5831
|
+
/^from\s+(\S+)\s+import/gm
|
|
5832
|
+
],
|
|
5833
|
+
go: [
|
|
5834
|
+
/^\s+"([^"]+)"/gm
|
|
5835
|
+
]
|
|
5836
|
+
};
|
|
5837
|
+
var EXPORT_PATTERNS = {
|
|
5838
|
+
ts: [
|
|
5839
|
+
/export\s+(?:default\s+)?(?:async\s+)?(?:function\*?|class|const|let|var|type|interface|enum)\s+(\w+)/gm,
|
|
5840
|
+
/export\s*\{\s*([^}]+)\s*\}/gm
|
|
5841
|
+
],
|
|
5842
|
+
js: [
|
|
5843
|
+
/export\s+(?:default\s+)?(?:async\s+)?(?:function\*?|class|const|let|var)\s+(\w+)/gm,
|
|
5844
|
+
/export\s*\{\s*([^}]+)\s*\}/gm
|
|
5845
|
+
],
|
|
5846
|
+
py: [
|
|
5847
|
+
/^def\s+(\w+)/gm,
|
|
5848
|
+
/^class\s+(\w+)/gm
|
|
5849
|
+
],
|
|
5850
|
+
go: [
|
|
5851
|
+
/^func\s+([A-Z]\w*)/gm
|
|
5852
|
+
]
|
|
5853
|
+
};
|
|
5854
|
+
function langOf(ext) {
|
|
5855
|
+
if (ext === ".ts" || ext === ".tsx") return "ts";
|
|
5856
|
+
if (ext === ".js" || ext === ".jsx") return "js";
|
|
5857
|
+
if (ext === ".py") return "py";
|
|
5858
|
+
if (ext === ".go") return "go";
|
|
5859
|
+
return "";
|
|
5860
|
+
}
|
|
5861
|
+
function walkFiles2(dir) {
|
|
5862
|
+
const results = [];
|
|
5863
|
+
function walk(current) {
|
|
5864
|
+
let entries;
|
|
5865
|
+
try {
|
|
5866
|
+
entries = fs24.readdirSync(current, { withFileTypes: true });
|
|
5867
|
+
} catch {
|
|
5868
|
+
return;
|
|
5869
|
+
}
|
|
5870
|
+
for (const entry of entries) {
|
|
5871
|
+
if (IGNORE_DIRS3.includes(entry.name)) continue;
|
|
5872
|
+
const full = path25.join(current, entry.name);
|
|
5873
|
+
if (entry.isDirectory()) walk(full);
|
|
5874
|
+
else if (entry.isFile() && MAP_EXTS2.includes(path25.extname(entry.name))) {
|
|
5875
|
+
results.push(full);
|
|
5876
|
+
}
|
|
5877
|
+
}
|
|
5878
|
+
}
|
|
5879
|
+
walk(dir);
|
|
5880
|
+
return results;
|
|
5881
|
+
}
|
|
5882
|
+
function _extractImports(content, lang) {
|
|
5883
|
+
const patterns = IMPORT_PATTERNS2[lang] ?? [];
|
|
5884
|
+
const found = [];
|
|
5885
|
+
for (const pattern of patterns) {
|
|
5886
|
+
const re = new RegExp(pattern.source, pattern.flags);
|
|
5887
|
+
let m;
|
|
5888
|
+
while ((m = re.exec(content)) !== null) {
|
|
5889
|
+
const imp = m[1].trim();
|
|
5890
|
+
if (lang === "py" && !imp.startsWith(".")) continue;
|
|
5891
|
+
if ((lang === "ts" || lang === "js") && !imp.startsWith(".")) continue;
|
|
5892
|
+
found.push(imp);
|
|
5893
|
+
}
|
|
5894
|
+
}
|
|
5895
|
+
return [...new Set(found)];
|
|
5896
|
+
}
|
|
5897
|
+
function _extractExports(content, lang) {
|
|
5898
|
+
const patterns = EXPORT_PATTERNS[lang] ?? [];
|
|
5899
|
+
const found = [];
|
|
5900
|
+
for (const pattern of patterns) {
|
|
5901
|
+
const re = new RegExp(pattern.source, pattern.flags);
|
|
5902
|
+
let m;
|
|
5903
|
+
while ((m = re.exec(content)) !== null) {
|
|
5904
|
+
const raw = m[1];
|
|
5905
|
+
if (raw.includes(",") || pattern.source.includes("{")) {
|
|
5906
|
+
for (const name of raw.split(",")) {
|
|
5907
|
+
const trimmed = name.trim().split(/\s+as\s+/)[0].trim();
|
|
5908
|
+
if (trimmed) found.push(trimmed);
|
|
5909
|
+
}
|
|
5910
|
+
} else {
|
|
5911
|
+
const name = raw.trim();
|
|
5912
|
+
if (name) found.push(name);
|
|
5913
|
+
}
|
|
5914
|
+
}
|
|
5915
|
+
}
|
|
5916
|
+
return [...new Set(found)];
|
|
5917
|
+
}
|
|
5918
|
+
function _resolveImport(fromFile, importPath, allFiles) {
|
|
5919
|
+
if (!importPath.startsWith(".")) return null;
|
|
5920
|
+
const fromDir = path25.dirname(fromFile);
|
|
5921
|
+
const base = path25.resolve(fromDir, importPath);
|
|
5922
|
+
if (allFiles.includes(base)) return base;
|
|
5923
|
+
for (const ext of [".ts", ".tsx", ".js", ".jsx", ".py", ".go"]) {
|
|
5924
|
+
const candidate = base + ext;
|
|
5925
|
+
if (allFiles.includes(candidate)) return candidate;
|
|
5926
|
+
}
|
|
5927
|
+
for (const ext of [".ts", ".tsx", ".js", ".jsx"]) {
|
|
5928
|
+
const candidate = path25.join(base, "index" + ext);
|
|
5929
|
+
if (allFiles.includes(candidate)) return candidate;
|
|
5930
|
+
}
|
|
5931
|
+
return null;
|
|
5932
|
+
}
|
|
5933
|
+
function _detectCycles(imports) {
|
|
5934
|
+
const cycles = [];
|
|
5935
|
+
const visited = /* @__PURE__ */ new Set();
|
|
5936
|
+
const stack = [];
|
|
5937
|
+
const onStack = /* @__PURE__ */ new Set();
|
|
5938
|
+
function dfs(node) {
|
|
5939
|
+
if (onStack.has(node)) {
|
|
5940
|
+
const cycleStart = stack.indexOf(node);
|
|
5941
|
+
cycles.push([...stack.slice(cycleStart), node]);
|
|
5942
|
+
return;
|
|
5943
|
+
}
|
|
5944
|
+
if (visited.has(node)) return;
|
|
5945
|
+
visited.add(node);
|
|
5946
|
+
onStack.add(node);
|
|
5947
|
+
stack.push(node);
|
|
5948
|
+
for (const neighbour of imports[node] ?? []) {
|
|
5949
|
+
dfs(neighbour);
|
|
5950
|
+
}
|
|
5951
|
+
stack.pop();
|
|
5952
|
+
onStack.delete(node);
|
|
5953
|
+
}
|
|
5954
|
+
for (const node of Object.keys(imports)) {
|
|
5955
|
+
dfs(node);
|
|
5956
|
+
}
|
|
5957
|
+
return cycles;
|
|
5958
|
+
}
|
|
5959
|
+
function _findHotspots(imports) {
|
|
5960
|
+
const THRESHOLD = 3;
|
|
5961
|
+
const counts = {};
|
|
5962
|
+
for (const deps of Object.values(imports)) {
|
|
5963
|
+
for (const dep of deps) {
|
|
5964
|
+
counts[dep] = (counts[dep] ?? 0) + 1;
|
|
5965
|
+
}
|
|
5966
|
+
}
|
|
5967
|
+
return Object.entries(counts).filter(([, count]) => count >= THRESHOLD).map(([file, importedBy]) => ({ file, importedBy })).sort((a, b) => b.importedBy - a.importedBy);
|
|
5968
|
+
}
|
|
5969
|
+
function buildSemanticMap(projectRoot2) {
|
|
5970
|
+
const allFiles = walkFiles2(projectRoot2);
|
|
5971
|
+
const imports = {};
|
|
5972
|
+
const exports = {};
|
|
5973
|
+
for (const filePath of allFiles) {
|
|
5974
|
+
let content = "";
|
|
5975
|
+
try {
|
|
5976
|
+
content = fs24.readFileSync(filePath, "utf8");
|
|
5977
|
+
} catch {
|
|
5978
|
+
}
|
|
5979
|
+
const ext = path25.extname(filePath);
|
|
5980
|
+
const lang = langOf(ext);
|
|
5981
|
+
const rawImps = _extractImports(content, lang);
|
|
5982
|
+
const resolvedImps = [];
|
|
5983
|
+
for (const imp of rawImps) {
|
|
5984
|
+
const resolved = _resolveImport(filePath, imp, allFiles);
|
|
5985
|
+
if (resolved) resolvedImps.push(resolved);
|
|
5986
|
+
}
|
|
5987
|
+
imports[filePath] = resolvedImps;
|
|
5988
|
+
exports[filePath] = lang ? _extractExports(content, lang) : [];
|
|
5989
|
+
}
|
|
5990
|
+
const cycles = _detectCycles(imports);
|
|
5991
|
+
const hotspots = _findHotspots(imports);
|
|
5992
|
+
return { imports, exports, hotspots, cycles };
|
|
5993
|
+
}
|
|
5994
|
+
|
|
5995
|
+
// src/onboard.ts
|
|
5996
|
+
init_js_yaml();
|
|
5997
|
+
import fs25 from "node:fs";
|
|
5998
|
+
import path26 from "node:path";
|
|
5774
5999
|
import { spawnSync as spawnSync7 } from "node:child_process";
|
|
5775
6000
|
function getRecentCommits(projectRoot2, n = 20) {
|
|
5776
6001
|
const result = spawnSync7("git", ["log", `--oneline`, `-${n}`], {
|
|
@@ -5781,30 +6006,30 @@ function getRecentCommits(projectRoot2, n = 20) {
|
|
|
5781
6006
|
return result.stdout.trim().split("\n").filter(Boolean);
|
|
5782
6007
|
}
|
|
5783
6008
|
function readCapabilities(forgehiveDir2) {
|
|
5784
|
-
const capPath =
|
|
5785
|
-
if (!
|
|
6009
|
+
const capPath = path26.join(forgehiveDir2, "capabilities.yaml");
|
|
6010
|
+
if (!fs25.existsSync(capPath)) return {};
|
|
5786
6011
|
try {
|
|
5787
|
-
return jsYaml.load(
|
|
6012
|
+
return jsYaml.load(fs25.readFileSync(capPath, "utf8")) ?? {};
|
|
5788
6013
|
} catch {
|
|
5789
6014
|
return {};
|
|
5790
6015
|
}
|
|
5791
6016
|
}
|
|
5792
6017
|
function readMemoryFiles(forgehiveDir2) {
|
|
5793
|
-
const memDir =
|
|
5794
|
-
if (!
|
|
6018
|
+
const memDir = path26.join(forgehiveDir2, "memory");
|
|
6019
|
+
if (!fs25.existsSync(memDir)) return {};
|
|
5795
6020
|
const result = {};
|
|
5796
|
-
for (const f of
|
|
5797
|
-
result[f] =
|
|
6021
|
+
for (const f of fs25.readdirSync(memDir).filter((f2) => f2.endsWith(".md") && f2 !== "MEMORY.md")) {
|
|
6022
|
+
result[f] = fs25.readFileSync(path26.join(memDir, f), "utf8");
|
|
5798
6023
|
}
|
|
5799
6024
|
return result;
|
|
5800
6025
|
}
|
|
5801
6026
|
function listAdrs2(forgehiveDir2) {
|
|
5802
|
-
const adrsDir =
|
|
5803
|
-
if (!
|
|
5804
|
-
return
|
|
6027
|
+
const adrsDir = path26.join(forgehiveDir2, "memory", "adrs");
|
|
6028
|
+
if (!fs25.existsSync(adrsDir)) return [];
|
|
6029
|
+
return fs25.readdirSync(adrsDir).filter((f) => f.endsWith(".md"));
|
|
5805
6030
|
}
|
|
5806
6031
|
function generateOnboardingDoc(projectRoot2, forgehiveDir2) {
|
|
5807
|
-
const projectName =
|
|
6032
|
+
const projectName = path26.basename(projectRoot2);
|
|
5808
6033
|
const caps = readCapabilities(forgehiveDir2);
|
|
5809
6034
|
const memFiles = readMemoryFiles(forgehiveDir2);
|
|
5810
6035
|
const commits = getRecentCommits(projectRoot2);
|
|
@@ -6019,13 +6244,13 @@ function getMetricsGitLog(projectRoot2, since) {
|
|
|
6019
6244
|
}
|
|
6020
6245
|
|
|
6021
6246
|
// src/sync.ts
|
|
6022
|
-
import
|
|
6023
|
-
import
|
|
6247
|
+
import fs26 from "node:fs";
|
|
6248
|
+
import path27 from "node:path";
|
|
6024
6249
|
import { spawnSync as spawnSync10 } from "node:child_process";
|
|
6025
6250
|
function getSyncStatus(forgehiveDir2) {
|
|
6026
|
-
const memDir =
|
|
6027
|
-
const files =
|
|
6028
|
-
const projectRoot2 =
|
|
6251
|
+
const memDir = path27.join(forgehiveDir2, "memory");
|
|
6252
|
+
const files = fs26.existsSync(memDir) ? fs26.readdirSync(memDir).filter((f) => f.endsWith(".md")).length : 0;
|
|
6253
|
+
const projectRoot2 = path27.dirname(forgehiveDir2);
|
|
6029
6254
|
const configResult = spawnSync10(
|
|
6030
6255
|
"git",
|
|
6031
6256
|
["config", "--get", "forgehive.sync-remote"],
|
|
@@ -6041,18 +6266,18 @@ function getSyncStatus(forgehiveDir2) {
|
|
|
6041
6266
|
return { files, hasRemote: remote !== null, remote, branch };
|
|
6042
6267
|
}
|
|
6043
6268
|
function pushSync(forgehiveDir2, remote = "origin", branch = "forgehive-memory") {
|
|
6044
|
-
const projectRoot2 =
|
|
6045
|
-
const memDir =
|
|
6046
|
-
if (!
|
|
6269
|
+
const projectRoot2 = path27.dirname(forgehiveDir2);
|
|
6270
|
+
const memDir = path27.join(forgehiveDir2, "memory");
|
|
6271
|
+
if (!fs26.existsSync(memDir)) {
|
|
6047
6272
|
return { success: false, message: "Kein Memory-Verzeichnis gefunden.", filesCommitted: 0 };
|
|
6048
6273
|
}
|
|
6049
|
-
const files =
|
|
6274
|
+
const files = fs26.readdirSync(memDir).filter((f) => f.endsWith(".md"));
|
|
6050
6275
|
if (files.length === 0) {
|
|
6051
6276
|
return { success: false, message: "Keine Memory-Dateien gefunden.", filesCommitted: 0 };
|
|
6052
6277
|
}
|
|
6053
6278
|
const addResult = spawnSync10(
|
|
6054
6279
|
"git",
|
|
6055
|
-
["add",
|
|
6280
|
+
["add", path27.join(".forgehive", "memory")],
|
|
6056
6281
|
{ cwd: projectRoot2, encoding: "utf8" }
|
|
6057
6282
|
);
|
|
6058
6283
|
if (addResult.status !== 0) {
|
|
@@ -6080,9 +6305,9 @@ function pushSync(forgehiveDir2, remote = "origin", branch = "forgehive-memory")
|
|
|
6080
6305
|
return { success: true, message: `Memory gepusht nach ${remote}/${branch}`, filesCommitted: files.length };
|
|
6081
6306
|
}
|
|
6082
6307
|
function pullSync(forgehiveDir2, remote = "origin", branch = "forgehive-memory") {
|
|
6083
|
-
const projectRoot2 =
|
|
6084
|
-
const memDir =
|
|
6085
|
-
|
|
6308
|
+
const projectRoot2 = path27.dirname(forgehiveDir2);
|
|
6309
|
+
const memDir = path27.join(forgehiveDir2, "memory");
|
|
6310
|
+
fs26.mkdirSync(memDir, { recursive: true });
|
|
6086
6311
|
const fetchResult = spawnSync10(
|
|
6087
6312
|
"git",
|
|
6088
6313
|
["fetch", remote, branch],
|
|
@@ -6102,16 +6327,16 @@ function pullSync(forgehiveDir2, remote = "origin", branch = "forgehive-memory")
|
|
|
6102
6327
|
const remoteFiles = listResult.stdout.trim().split("\n").filter(Boolean);
|
|
6103
6328
|
const imported = [];
|
|
6104
6329
|
for (const remotePath of remoteFiles) {
|
|
6105
|
-
const filename =
|
|
6106
|
-
const localPath =
|
|
6107
|
-
if (!
|
|
6330
|
+
const filename = path27.basename(remotePath);
|
|
6331
|
+
const localPath = path27.join(memDir, filename);
|
|
6332
|
+
if (!fs26.existsSync(localPath)) {
|
|
6108
6333
|
const contentResult = spawnSync10(
|
|
6109
6334
|
"git",
|
|
6110
6335
|
["show", `${remote}/${branch}:${remotePath}`],
|
|
6111
6336
|
{ cwd: projectRoot2, encoding: "utf8" }
|
|
6112
6337
|
);
|
|
6113
6338
|
if (contentResult.status === 0) {
|
|
6114
|
-
|
|
6339
|
+
fs26.writeFileSync(localPath, contentResult.stdout, "utf8");
|
|
6115
6340
|
imported.push(filename);
|
|
6116
6341
|
}
|
|
6117
6342
|
}
|
|
@@ -6124,8 +6349,8 @@ function pullSync(forgehiveDir2, remote = "origin", branch = "forgehive-memory")
|
|
|
6124
6349
|
}
|
|
6125
6350
|
|
|
6126
6351
|
// src/background.ts
|
|
6127
|
-
import
|
|
6128
|
-
import
|
|
6352
|
+
import fs27 from "node:fs";
|
|
6353
|
+
import path28 from "node:path";
|
|
6129
6354
|
import { spawn } from "node:child_process";
|
|
6130
6355
|
var AGENT_ROLES = {
|
|
6131
6356
|
kai: "Senior Engineer \u2014 implements features and fixes bugs",
|
|
@@ -6172,23 +6397,23 @@ Instructions:
|
|
|
6172
6397
|
Work autonomously. Do not ask for clarification \u2014 use your best judgment based on the issue description and codebase context.`;
|
|
6173
6398
|
}
|
|
6174
6399
|
function runBackgroundAgent(forgehiveDir2, issueUrl, agentId) {
|
|
6175
|
-
const logsDir =
|
|
6176
|
-
|
|
6400
|
+
const logsDir = path28.join(forgehiveDir2, "background-runs");
|
|
6401
|
+
fs27.mkdirSync(logsDir, { recursive: true });
|
|
6177
6402
|
const timestamp2 = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
6178
|
-
const logFile =
|
|
6403
|
+
const logFile = path28.join(logsDir, `${agentId}-${timestamp2}.log`);
|
|
6179
6404
|
const prompt = buildAgentPrompt(issueUrl, agentId);
|
|
6180
|
-
const logStream =
|
|
6405
|
+
const logStream = fs27.openSync(logFile, "w");
|
|
6181
6406
|
const child = spawn(
|
|
6182
6407
|
"claude",
|
|
6183
6408
|
["-p", prompt, "--output-format", "text"],
|
|
6184
6409
|
{
|
|
6185
|
-
cwd:
|
|
6410
|
+
cwd: path28.dirname(forgehiveDir2),
|
|
6186
6411
|
detached: true,
|
|
6187
6412
|
stdio: ["ignore", logStream, logStream]
|
|
6188
6413
|
}
|
|
6189
6414
|
);
|
|
6190
6415
|
child.unref();
|
|
6191
|
-
|
|
6416
|
+
fs27.closeSync(logStream);
|
|
6192
6417
|
return {
|
|
6193
6418
|
pid: child.pid,
|
|
6194
6419
|
logFile,
|
|
@@ -6198,11 +6423,11 @@ function runBackgroundAgent(forgehiveDir2, issueUrl, agentId) {
|
|
|
6198
6423
|
|
|
6199
6424
|
// src/stories.ts
|
|
6200
6425
|
init_js_yaml();
|
|
6201
|
-
import
|
|
6202
|
-
import
|
|
6426
|
+
import fs28 from "node:fs";
|
|
6427
|
+
import path29 from "node:path";
|
|
6203
6428
|
function nextStoryId(storiesDir) {
|
|
6204
|
-
if (!
|
|
6205
|
-
const existing =
|
|
6429
|
+
if (!fs28.existsSync(storiesDir)) return "US-1";
|
|
6430
|
+
const existing = fs28.readdirSync(storiesDir).filter((f) => f.match(/^US-\d+\.md$/)).map((f) => parseInt(f.replace("US-", "").replace(".md", ""), 10)).filter((n) => !isNaN(n));
|
|
6206
6431
|
const max = existing.length > 0 ? Math.max(...existing) : 0;
|
|
6207
6432
|
return `US-${max + 1}`;
|
|
6208
6433
|
}
|
|
@@ -6234,7 +6459,7 @@ ${acLines || "- [ ] (noch nicht definiert)"}
|
|
|
6234
6459
|
}
|
|
6235
6460
|
function parseStoryFile(filePath) {
|
|
6236
6461
|
try {
|
|
6237
|
-
const content =
|
|
6462
|
+
const content = fs28.readFileSync(filePath, "utf8");
|
|
6238
6463
|
const match = content.match(/^---\n([\s\S]*?)\n---/);
|
|
6239
6464
|
if (!match) return null;
|
|
6240
6465
|
const data = jsYaml.load(match[1]);
|
|
@@ -6254,7 +6479,7 @@ function parseStoryFile(filePath) {
|
|
|
6254
6479
|
}
|
|
6255
6480
|
}
|
|
6256
6481
|
function createStory(storiesDir, title, epicId) {
|
|
6257
|
-
|
|
6482
|
+
fs28.mkdirSync(storiesDir, { recursive: true });
|
|
6258
6483
|
const id = nextStoryId(storiesDir);
|
|
6259
6484
|
const story = {
|
|
6260
6485
|
id,
|
|
@@ -6267,33 +6492,33 @@ function createStory(storiesDir, title, epicId) {
|
|
|
6267
6492
|
epicId: epicId ?? null,
|
|
6268
6493
|
status: "backlog"
|
|
6269
6494
|
};
|
|
6270
|
-
|
|
6495
|
+
fs28.writeFileSync(path29.join(storiesDir, `${id}.md`), storyToMarkdown(story), "utf8");
|
|
6271
6496
|
return story;
|
|
6272
6497
|
}
|
|
6273
6498
|
function listStories(storiesDir) {
|
|
6274
|
-
if (!
|
|
6275
|
-
return
|
|
6499
|
+
if (!fs28.existsSync(storiesDir)) return [];
|
|
6500
|
+
return fs28.readdirSync(storiesDir).filter((f) => f.match(/^US-\d+\.md$/)).map((f) => parseStoryFile(path29.join(storiesDir, f))).filter((s) => s !== null).sort((a, b) => {
|
|
6276
6501
|
const na = parseInt(a.id.replace("US-", ""), 10);
|
|
6277
6502
|
const nb = parseInt(b.id.replace("US-", ""), 10);
|
|
6278
6503
|
return na - nb;
|
|
6279
6504
|
});
|
|
6280
6505
|
}
|
|
6281
6506
|
function getStory(storiesDir, id) {
|
|
6282
|
-
const filePath =
|
|
6283
|
-
if (!
|
|
6507
|
+
const filePath = path29.join(storiesDir, `${id}.md`);
|
|
6508
|
+
if (!fs28.existsSync(filePath)) return null;
|
|
6284
6509
|
return parseStoryFile(filePath);
|
|
6285
6510
|
}
|
|
6286
6511
|
function updateStoryPoints(storiesDir, id, points) {
|
|
6287
6512
|
const story = getStory(storiesDir, id);
|
|
6288
6513
|
if (!story) throw new Error(`Story ${id} nicht gefunden`);
|
|
6289
6514
|
story.points = points;
|
|
6290
|
-
|
|
6515
|
+
fs28.writeFileSync(path29.join(storiesDir, `${id}.md`), storyToMarkdown(story), "utf8");
|
|
6291
6516
|
}
|
|
6292
6517
|
function updateStoryStatus(storiesDir, id, status) {
|
|
6293
6518
|
const story = getStory(storiesDir, id);
|
|
6294
6519
|
if (!story) throw new Error(`Story ${id} nicht gefunden`);
|
|
6295
6520
|
story.status = status;
|
|
6296
|
-
|
|
6521
|
+
fs28.writeFileSync(path29.join(storiesDir, `${id}.md`), storyToMarkdown(story), "utf8");
|
|
6297
6522
|
}
|
|
6298
6523
|
function formatStoryCard(story) {
|
|
6299
6524
|
const points = story.points !== null ? ` \xB7 ${story.points} Punkte` : "";
|
|
@@ -6312,11 +6537,11 @@ function formatStoryCard(story) {
|
|
|
6312
6537
|
|
|
6313
6538
|
// src/epics.ts
|
|
6314
6539
|
init_js_yaml();
|
|
6315
|
-
import
|
|
6316
|
-
import
|
|
6540
|
+
import fs29 from "node:fs";
|
|
6541
|
+
import path30 from "node:path";
|
|
6317
6542
|
function nextEpicId(epicsDir) {
|
|
6318
|
-
if (!
|
|
6319
|
-
const existing =
|
|
6543
|
+
if (!fs29.existsSync(epicsDir)) return "EPC-1";
|
|
6544
|
+
const existing = fs29.readdirSync(epicsDir).filter((f) => f.match(/^EPC-\d+\.md$/)).map((f) => parseInt(f.replace("EPC-", "").replace(".md", ""), 10)).filter((n) => !isNaN(n));
|
|
6320
6545
|
const max = existing.length > 0 ? Math.max(...existing) : 0;
|
|
6321
6546
|
return `EPC-${max + 1}`;
|
|
6322
6547
|
}
|
|
@@ -6326,7 +6551,8 @@ function epicToMarkdown(epic) {
|
|
|
6326
6551
|
title: epic.title,
|
|
6327
6552
|
goal: epic.goal,
|
|
6328
6553
|
stories: epic.stories,
|
|
6329
|
-
status: epic.status
|
|
6554
|
+
status: epic.status,
|
|
6555
|
+
prdId: epic.prdId
|
|
6330
6556
|
});
|
|
6331
6557
|
const storyLines = epic.stories.map((s) => `- ${s}`).join("\n");
|
|
6332
6558
|
return `---
|
|
@@ -6343,7 +6569,7 @@ ${storyLines || "(noch keine Stories)"}
|
|
|
6343
6569
|
}
|
|
6344
6570
|
function parseEpicFile(filePath) {
|
|
6345
6571
|
try {
|
|
6346
|
-
const content =
|
|
6572
|
+
const content = fs29.readFileSync(filePath, "utf8");
|
|
6347
6573
|
const match = content.match(/^---\n([\s\S]*?)\n---/);
|
|
6348
6574
|
if (!match) return null;
|
|
6349
6575
|
const data = jsYaml.load(match[1]);
|
|
@@ -6352,30 +6578,31 @@ function parseEpicFile(filePath) {
|
|
|
6352
6578
|
title: data.title ?? "",
|
|
6353
6579
|
goal: data.goal ?? "",
|
|
6354
6580
|
stories: data.stories ?? [],
|
|
6355
|
-
status: data.status ?? "active"
|
|
6581
|
+
status: data.status ?? "active",
|
|
6582
|
+
prdId: data.prdId ?? null
|
|
6356
6583
|
};
|
|
6357
6584
|
} catch {
|
|
6358
6585
|
return null;
|
|
6359
6586
|
}
|
|
6360
6587
|
}
|
|
6361
|
-
function createEpic(epicsDir, title, goal) {
|
|
6362
|
-
|
|
6588
|
+
function createEpic(epicsDir, title, goal, prdId) {
|
|
6589
|
+
fs29.mkdirSync(epicsDir, { recursive: true });
|
|
6363
6590
|
const id = nextEpicId(epicsDir);
|
|
6364
|
-
const epic = { id, title, goal: goal ?? "", stories: [], status: "active" };
|
|
6365
|
-
|
|
6591
|
+
const epic = { id, title, goal: goal ?? "", stories: [], status: "active", prdId: prdId ?? null };
|
|
6592
|
+
fs29.writeFileSync(path30.join(epicsDir, `${id}.md`), epicToMarkdown(epic), "utf8");
|
|
6366
6593
|
return epic;
|
|
6367
6594
|
}
|
|
6368
6595
|
function listEpics(epicsDir) {
|
|
6369
|
-
if (!
|
|
6370
|
-
return
|
|
6596
|
+
if (!fs29.existsSync(epicsDir)) return [];
|
|
6597
|
+
return fs29.readdirSync(epicsDir).filter((f) => f.match(/^EPC-\d+\.md$/)).map((f) => parseEpicFile(path30.join(epicsDir, f))).filter((e) => e !== null).sort((a, b) => {
|
|
6371
6598
|
const na = parseInt(a.id.replace("EPC-", ""), 10);
|
|
6372
6599
|
const nb = parseInt(b.id.replace("EPC-", ""), 10);
|
|
6373
6600
|
return na - nb;
|
|
6374
6601
|
});
|
|
6375
6602
|
}
|
|
6376
6603
|
function getEpic(epicsDir, id) {
|
|
6377
|
-
const filePath =
|
|
6378
|
-
if (!
|
|
6604
|
+
const filePath = path30.join(epicsDir, `${id}.md`);
|
|
6605
|
+
if (!fs29.existsSync(filePath)) return null;
|
|
6379
6606
|
return parseEpicFile(filePath);
|
|
6380
6607
|
}
|
|
6381
6608
|
function formatEpicCard(epic, stories) {
|
|
@@ -6397,25 +6624,188 @@ function formatEpicCard(epic, stories) {
|
|
|
6397
6624
|
return lines.join("\n");
|
|
6398
6625
|
}
|
|
6399
6626
|
|
|
6627
|
+
// src/product.ts
|
|
6628
|
+
init_js_yaml();
|
|
6629
|
+
import fs30 from "node:fs";
|
|
6630
|
+
import path31 from "node:path";
|
|
6631
|
+
function slugify2(title) {
|
|
6632
|
+
return title.toLowerCase().replace(/[^a-z0-9\s-]/g, "").trim().replace(/\s+/g, "-");
|
|
6633
|
+
}
|
|
6634
|
+
function nextPrdId(prdsDir) {
|
|
6635
|
+
if (!fs30.existsSync(prdsDir)) return "PRD-1";
|
|
6636
|
+
const existing = fs30.readdirSync(prdsDir).filter((f) => f.endsWith(".md")).map((f) => parsePrdFile(path31.join(prdsDir, f))).filter((p) => p !== null).map((p) => parseInt(p.id.replace("PRD-", ""), 10)).filter((n) => !isNaN(n));
|
|
6637
|
+
const max = existing.length > 0 ? Math.max(...existing) : 0;
|
|
6638
|
+
return `PRD-${max + 1}`;
|
|
6639
|
+
}
|
|
6640
|
+
function prdToMarkdown(prd) {
|
|
6641
|
+
const frontmatter = jsYaml.dump({
|
|
6642
|
+
id: prd.id,
|
|
6643
|
+
title: prd.title,
|
|
6644
|
+
date: prd.date,
|
|
6645
|
+
status: prd.status
|
|
6646
|
+
});
|
|
6647
|
+
return `---
|
|
6648
|
+
${frontmatter}---
|
|
6649
|
+
|
|
6650
|
+
# PRD: ${prd.title}
|
|
6651
|
+
|
|
6652
|
+
## Problem Statement
|
|
6653
|
+
<!-- Was ist das Problem? -->
|
|
6654
|
+
|
|
6655
|
+
## Target Users
|
|
6656
|
+
<!-- Wer sind die Nutzer? -->
|
|
6657
|
+
|
|
6658
|
+
## Goals
|
|
6659
|
+
<!-- Messbare Ziele -->
|
|
6660
|
+
|
|
6661
|
+
## Non-Goals
|
|
6662
|
+
<!-- Explizit nicht in Scope -->
|
|
6663
|
+
|
|
6664
|
+
## Requirements
|
|
6665
|
+
1.
|
|
6666
|
+
|
|
6667
|
+
## Success Metrics
|
|
6668
|
+
|
|
6669
|
+
## Open Questions
|
|
6670
|
+
`;
|
|
6671
|
+
}
|
|
6672
|
+
function parsePrdFile(filePath) {
|
|
6673
|
+
try {
|
|
6674
|
+
const content = fs30.readFileSync(filePath, "utf8");
|
|
6675
|
+
const match = content.match(/^---\n([\s\S]*?)\n---/);
|
|
6676
|
+
if (!match) return null;
|
|
6677
|
+
const data = jsYaml.load(match[1]);
|
|
6678
|
+
if (!data || typeof data.id !== "string") return null;
|
|
6679
|
+
return {
|
|
6680
|
+
id: data.id,
|
|
6681
|
+
title: typeof data.title === "string" ? data.title : "",
|
|
6682
|
+
date: typeof data.date === "string" ? data.date : "",
|
|
6683
|
+
status: data.status ?? "draft",
|
|
6684
|
+
filepath: filePath
|
|
6685
|
+
};
|
|
6686
|
+
} catch {
|
|
6687
|
+
return null;
|
|
6688
|
+
}
|
|
6689
|
+
}
|
|
6690
|
+
function createPrd(prdsDir, title) {
|
|
6691
|
+
fs30.mkdirSync(prdsDir, { recursive: true });
|
|
6692
|
+
const id = nextPrdId(prdsDir);
|
|
6693
|
+
const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
6694
|
+
const slug = slugify2(title);
|
|
6695
|
+
const filename = `${today}-${slug}.md`;
|
|
6696
|
+
const filepath = path31.join(prdsDir, filename);
|
|
6697
|
+
const prd = { id, title, date: today, status: "draft", filepath };
|
|
6698
|
+
fs30.writeFileSync(filepath, prdToMarkdown(prd), "utf8");
|
|
6699
|
+
return prd;
|
|
6700
|
+
}
|
|
6701
|
+
function listPrds(prdsDir) {
|
|
6702
|
+
if (!fs30.existsSync(prdsDir)) return [];
|
|
6703
|
+
return fs30.readdirSync(prdsDir).filter((f) => f.endsWith(".md")).map((f) => parsePrdFile(path31.join(prdsDir, f))).filter((p) => p !== null && /^PRD-\d+$/.test(p.id)).sort((a, b) => {
|
|
6704
|
+
const na = parseInt(a.id.replace("PRD-", ""), 10);
|
|
6705
|
+
const nb = parseInt(b.id.replace("PRD-", ""), 10);
|
|
6706
|
+
return na - nb;
|
|
6707
|
+
});
|
|
6708
|
+
}
|
|
6709
|
+
function getPrd(prdsDir, id) {
|
|
6710
|
+
if (!fs30.existsSync(prdsDir)) return null;
|
|
6711
|
+
const files = fs30.readdirSync(prdsDir).filter((f) => f.endsWith(".md"));
|
|
6712
|
+
for (const f of files) {
|
|
6713
|
+
const prd = parsePrdFile(path31.join(prdsDir, f));
|
|
6714
|
+
if (prd && prd.id === id) return prd;
|
|
6715
|
+
}
|
|
6716
|
+
return null;
|
|
6717
|
+
}
|
|
6718
|
+
function generateRoadmap(forgehiveDir2) {
|
|
6719
|
+
const prdsDir = path31.join(forgehiveDir2, "memory", "prds");
|
|
6720
|
+
const epicsDir = path31.join(forgehiveDir2, "memory", "epics");
|
|
6721
|
+
const storiesDir = path31.join(forgehiveDir2, "memory", "stories");
|
|
6722
|
+
const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
6723
|
+
const prds = listPrds(prdsDir);
|
|
6724
|
+
const epics = listEpics(epicsDir);
|
|
6725
|
+
const stories = listStories(storiesDir);
|
|
6726
|
+
const lines = [];
|
|
6727
|
+
lines.push("# Roadmap");
|
|
6728
|
+
lines.push("");
|
|
6729
|
+
lines.push(`_Generiert am ${today} von fh product roadmap_`);
|
|
6730
|
+
lines.push("");
|
|
6731
|
+
lines.push("---");
|
|
6732
|
+
lines.push("");
|
|
6733
|
+
for (const prd of prds) {
|
|
6734
|
+
lines.push(`## ${prd.id}: ${prd.title} [${prd.status}]`);
|
|
6735
|
+
lines.push("");
|
|
6736
|
+
lines.push(`> PRD-Datum: ${prd.date}`);
|
|
6737
|
+
lines.push("");
|
|
6738
|
+
const linkedEpics = epics.filter((e) => e.prdId === prd.id);
|
|
6739
|
+
if (linkedEpics.length === 0) {
|
|
6740
|
+
lines.push("_Keine Epics verkn\xFCpft_");
|
|
6741
|
+
} else {
|
|
6742
|
+
for (const epic of linkedEpics) {
|
|
6743
|
+
lines.push(`### ${epic.id}: ${epic.title} [${epic.status}]`);
|
|
6744
|
+
lines.push("");
|
|
6745
|
+
const epicStories = stories.filter((s) => s.epicId === epic.id);
|
|
6746
|
+
if (epicStories.length === 0) {
|
|
6747
|
+
lines.push("_Keine Stories_");
|
|
6748
|
+
} else {
|
|
6749
|
+
lines.push("| Story | Status | Punkte |");
|
|
6750
|
+
lines.push("|---|---|---|");
|
|
6751
|
+
let total = 0;
|
|
6752
|
+
for (const s of epicStories) {
|
|
6753
|
+
const pts = s.points !== null ? String(s.points) : "?";
|
|
6754
|
+
total += s.points ?? 0;
|
|
6755
|
+
lines.push(`| ${s.id}: ${s.title} | ${s.status} | ${pts} |`);
|
|
6756
|
+
}
|
|
6757
|
+
lines.push("");
|
|
6758
|
+
lines.push(`**Gesamt: ${total} Punkte**`);
|
|
6759
|
+
}
|
|
6760
|
+
lines.push("");
|
|
6761
|
+
}
|
|
6762
|
+
}
|
|
6763
|
+
lines.push("---");
|
|
6764
|
+
lines.push("");
|
|
6765
|
+
}
|
|
6766
|
+
const orphanEpics = epics.filter((e) => !e.prdId);
|
|
6767
|
+
if (orphanEpics.length > 0) {
|
|
6768
|
+
lines.push("## Ohne PRD");
|
|
6769
|
+
lines.push("");
|
|
6770
|
+
for (const epic of orphanEpics) {
|
|
6771
|
+
lines.push(`### ${epic.id}: ${epic.title} [${epic.status}]`);
|
|
6772
|
+
lines.push("");
|
|
6773
|
+
const epicStories = stories.filter((s) => s.epicId === epic.id);
|
|
6774
|
+
if (epicStories.length === 0) {
|
|
6775
|
+
lines.push("_Keine Stories_");
|
|
6776
|
+
} else {
|
|
6777
|
+
lines.push("| Story | Status | Punkte |");
|
|
6778
|
+
lines.push("|---|---|---|");
|
|
6779
|
+
for (const s of epicStories) {
|
|
6780
|
+
const pts = s.points !== null ? String(s.points) : "?";
|
|
6781
|
+
lines.push(`| ${s.id}: ${s.title} | ${s.status} | ${pts} |`);
|
|
6782
|
+
}
|
|
6783
|
+
}
|
|
6784
|
+
lines.push("");
|
|
6785
|
+
}
|
|
6786
|
+
}
|
|
6787
|
+
return lines.join("\n");
|
|
6788
|
+
}
|
|
6789
|
+
|
|
6400
6790
|
// src/velocity.ts
|
|
6401
|
-
import
|
|
6402
|
-
import
|
|
6791
|
+
import fs31 from "node:fs";
|
|
6792
|
+
import path32 from "node:path";
|
|
6403
6793
|
var HEADER = "# Sprint Velocity\n\n| Sprint | Datum | Committed | Delivered | Rate |\n|---|---|---|---|---|\n";
|
|
6404
6794
|
function recordVelocity(velocityFile, sprint, committed, delivered) {
|
|
6405
6795
|
const date = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
6406
6796
|
const rate = committed > 0 ? Math.round(delivered / committed * 100) : 0;
|
|
6407
6797
|
const row = `| Sprint ${sprint} | ${date} | ${committed} | ${delivered} | ${rate}% |
|
|
6408
6798
|
`;
|
|
6409
|
-
if (!
|
|
6410
|
-
|
|
6411
|
-
|
|
6799
|
+
if (!fs31.existsSync(velocityFile)) {
|
|
6800
|
+
fs31.mkdirSync(path32.dirname(velocityFile), { recursive: true });
|
|
6801
|
+
fs31.writeFileSync(velocityFile, HEADER + row, "utf8");
|
|
6412
6802
|
} else {
|
|
6413
|
-
|
|
6803
|
+
fs31.appendFileSync(velocityFile, row, "utf8");
|
|
6414
6804
|
}
|
|
6415
6805
|
}
|
|
6416
6806
|
function getVelocityHistory(velocityFile) {
|
|
6417
|
-
if (!
|
|
6418
|
-
const content =
|
|
6807
|
+
if (!fs31.existsSync(velocityFile)) return [];
|
|
6808
|
+
const content = fs31.readFileSync(velocityFile, "utf8");
|
|
6419
6809
|
const rows = content.split("\n").filter((l) => l.startsWith("| Sprint "));
|
|
6420
6810
|
return rows.map((row) => {
|
|
6421
6811
|
const cells = row.split("|").map((c) => c.trim()).filter(Boolean);
|
|
@@ -6456,22 +6846,22 @@ function formatVelocityReport(history) {
|
|
|
6456
6846
|
|
|
6457
6847
|
// src/docs.ts
|
|
6458
6848
|
init_js_yaml();
|
|
6459
|
-
import
|
|
6460
|
-
import
|
|
6849
|
+
import fs32 from "node:fs";
|
|
6850
|
+
import path33 from "node:path";
|
|
6461
6851
|
import { spawnSync as spawnSync11 } from "node:child_process";
|
|
6462
6852
|
var SOURCE_EXTS = [".ts", ".tsx", ".js", ".jsx", ".py", ".go"];
|
|
6463
|
-
var
|
|
6464
|
-
var
|
|
6853
|
+
var IGNORE_DIRS4 = ["node_modules", ".git", "dist", ".forgehive", "coverage", ".next", "build", "test", "__tests__", "spec"];
|
|
6854
|
+
var EXPORT_PATTERNS2 = [
|
|
6465
6855
|
/^export\s+(?:async\s+)?function\s+(\w+)/gm,
|
|
6466
6856
|
/^export\s+(?:const|let|var)\s+(\w+)/gm,
|
|
6467
6857
|
/^export\s+(?:class|interface|type|enum)\s+(\w+)/gm,
|
|
6468
6858
|
/^export\s+default\s+(?:function\s+)?(\w+)?/gm
|
|
6469
6859
|
];
|
|
6470
6860
|
function readCapabilities2(forgehiveDir2) {
|
|
6471
|
-
const capPath =
|
|
6472
|
-
if (!
|
|
6861
|
+
const capPath = path33.join(forgehiveDir2, "capabilities.yaml");
|
|
6862
|
+
if (!fs32.existsSync(capPath)) return {};
|
|
6473
6863
|
try {
|
|
6474
|
-
return jsYaml.load(
|
|
6864
|
+
return jsYaml.load(fs32.readFileSync(capPath, "utf8")) ?? {};
|
|
6475
6865
|
} catch {
|
|
6476
6866
|
return {};
|
|
6477
6867
|
}
|
|
@@ -6498,11 +6888,11 @@ function extractCapabilityInfo(caps) {
|
|
|
6498
6888
|
return { language, packageManager };
|
|
6499
6889
|
}
|
|
6500
6890
|
function readMemoryFiles2(forgehiveDir2) {
|
|
6501
|
-
const memDir =
|
|
6502
|
-
if (!
|
|
6891
|
+
const memDir = path33.join(forgehiveDir2, "memory");
|
|
6892
|
+
if (!fs32.existsSync(memDir)) return {};
|
|
6503
6893
|
const result = {};
|
|
6504
|
-
for (const f of
|
|
6505
|
-
result[f] =
|
|
6894
|
+
for (const f of fs32.readdirSync(memDir).filter((f2) => f2.endsWith(".md") && f2 !== "MEMORY.md")) {
|
|
6895
|
+
result[f] = fs32.readFileSync(path33.join(memDir, f), "utf8");
|
|
6506
6896
|
}
|
|
6507
6897
|
return result;
|
|
6508
6898
|
}
|
|
@@ -6513,7 +6903,7 @@ function getRecentCommits2(projectRoot2, n = 10) {
|
|
|
6513
6903
|
}
|
|
6514
6904
|
function extractExports(content) {
|
|
6515
6905
|
const exports = [];
|
|
6516
|
-
for (const pattern of
|
|
6906
|
+
for (const pattern of EXPORT_PATTERNS2) {
|
|
6517
6907
|
pattern.lastIndex = 0;
|
|
6518
6908
|
let m;
|
|
6519
6909
|
while ((m = pattern.exec(content)) !== null) {
|
|
@@ -6525,19 +6915,19 @@ function extractExports(content) {
|
|
|
6525
6915
|
function walkSourceFiles(dir) {
|
|
6526
6916
|
const results = [];
|
|
6527
6917
|
function walk(current) {
|
|
6528
|
-
if (!
|
|
6529
|
-
for (const entry of
|
|
6530
|
-
if (
|
|
6531
|
-
const full =
|
|
6918
|
+
if (!fs32.existsSync(current)) return;
|
|
6919
|
+
for (const entry of fs32.readdirSync(current, { withFileTypes: true })) {
|
|
6920
|
+
if (IGNORE_DIRS4.includes(entry.name)) continue;
|
|
6921
|
+
const full = path33.join(current, entry.name);
|
|
6532
6922
|
if (entry.isDirectory()) walk(full);
|
|
6533
|
-
else if (entry.isFile() && SOURCE_EXTS.includes(
|
|
6923
|
+
else if (entry.isFile() && SOURCE_EXTS.includes(path33.extname(entry.name))) results.push(full);
|
|
6534
6924
|
}
|
|
6535
6925
|
}
|
|
6536
6926
|
walk(dir);
|
|
6537
6927
|
return results;
|
|
6538
6928
|
}
|
|
6539
6929
|
function generateUserGuide(projectRoot2, forgehiveDir2) {
|
|
6540
|
-
const projectName =
|
|
6930
|
+
const projectName = path33.basename(projectRoot2);
|
|
6541
6931
|
const caps = readCapabilities2(forgehiveDir2);
|
|
6542
6932
|
const { language: lang, packageManager, entryPoints } = extractCapabilityInfo(caps);
|
|
6543
6933
|
const memFiles = readMemoryFiles2(forgehiveDir2);
|
|
@@ -6616,8 +7006,8 @@ function generateUserGuide(projectRoot2, forgehiveDir2) {
|
|
|
6616
7006
|
return lines.join("\n");
|
|
6617
7007
|
}
|
|
6618
7008
|
function generateApiReference(projectRoot2) {
|
|
6619
|
-
const srcDir =
|
|
6620
|
-
const searchDir =
|
|
7009
|
+
const srcDir = path33.join(projectRoot2, "src");
|
|
7010
|
+
const searchDir = fs32.existsSync(srcDir) ? srcDir : projectRoot2;
|
|
6621
7011
|
const files = walkSourceFiles(searchDir);
|
|
6622
7012
|
const lines = [];
|
|
6623
7013
|
lines.push("# API Reference");
|
|
@@ -6631,13 +7021,13 @@ function generateApiReference(projectRoot2) {
|
|
|
6631
7021
|
for (const filePath of files) {
|
|
6632
7022
|
let content = "";
|
|
6633
7023
|
try {
|
|
6634
|
-
content =
|
|
7024
|
+
content = fs32.readFileSync(filePath, "utf8");
|
|
6635
7025
|
} catch {
|
|
6636
7026
|
continue;
|
|
6637
7027
|
}
|
|
6638
7028
|
const exports = extractExports(content);
|
|
6639
7029
|
if (exports.length === 0) continue;
|
|
6640
|
-
const relPath =
|
|
7030
|
+
const relPath = path33.relative(projectRoot2, filePath);
|
|
6641
7031
|
lines.push(`## \`${relPath}\``);
|
|
6642
7032
|
lines.push("");
|
|
6643
7033
|
lines.push("**Exports:**");
|
|
@@ -6649,27 +7039,314 @@ function generateApiReference(projectRoot2) {
|
|
|
6649
7039
|
}
|
|
6650
7040
|
function listExistingDocs(projectRoot2) {
|
|
6651
7041
|
const docs = [];
|
|
6652
|
-
const docsDir =
|
|
6653
|
-
if (
|
|
6654
|
-
for (const f of
|
|
6655
|
-
if (f.endsWith(".md")) docs.push(
|
|
7042
|
+
const docsDir = path33.join(projectRoot2, "docs");
|
|
7043
|
+
if (fs32.existsSync(docsDir)) {
|
|
7044
|
+
for (const f of fs32.readdirSync(docsDir)) {
|
|
7045
|
+
if (f.endsWith(".md")) docs.push(path33.join(docsDir, f));
|
|
6656
7046
|
}
|
|
6657
7047
|
}
|
|
6658
7048
|
const rootDocs = ["README.md", "CHANGELOG.md", "ONBOARDING.md", "CONTRIBUTING.md"];
|
|
6659
7049
|
for (const f of rootDocs) {
|
|
6660
|
-
const full =
|
|
6661
|
-
if (
|
|
7050
|
+
const full = path33.join(projectRoot2, f);
|
|
7051
|
+
if (fs32.existsSync(full)) docs.push(full);
|
|
6662
7052
|
}
|
|
6663
7053
|
return docs;
|
|
6664
7054
|
}
|
|
6665
7055
|
|
|
7056
|
+
// src/router.ts
|
|
7057
|
+
import fs33 from "node:fs";
|
|
7058
|
+
import path34 from "node:path";
|
|
7059
|
+
var AGENT_KEYWORDS = {
|
|
7060
|
+
vera: ["security", "auth", "vulnerability", "owasp", "cve", "gdpr", "compliance", "penetration", "exploit"],
|
|
7061
|
+
sam: ["test", "coverage", "qa", "quality", "spec", "regression", "fixture", "assertion", "e2e"],
|
|
7062
|
+
suki: ["design", "ux", "user experience", "wireframe", "flow", "empathy", "persona", "interface"],
|
|
7063
|
+
viktor: ["architecture", "refactor", "structure", "pattern", "scalability", "coupling", "abstraction"],
|
|
7064
|
+
eli: ["docs", "documentation", "readme", "onboarding", "guide", "tutorial", "wiki", "changelog"],
|
|
7065
|
+
nora: ["research", "analyze", "compare", "benchmark", "investigate", "evidence", "survey"],
|
|
7066
|
+
remy: ["product", "feature", "requirement", "roadmap", "user story", "prd", "launch", "strategy"],
|
|
7067
|
+
kai: ["bug", "fix", "implement", "function", "error", "crash", "broken", "failing"]
|
|
7068
|
+
};
|
|
7069
|
+
var AGENT_DESCRIPTIONS = {
|
|
7070
|
+
vera: "Security Analyst",
|
|
7071
|
+
sam: "QA & Test Architect",
|
|
7072
|
+
suki: "UX Designer",
|
|
7073
|
+
viktor: "System Architect",
|
|
7074
|
+
eli: "Technical Writer",
|
|
7075
|
+
nora: "Senior Research Analyst",
|
|
7076
|
+
remy: "Creative Strategist",
|
|
7077
|
+
kai: "Senior Engineer"
|
|
7078
|
+
};
|
|
7079
|
+
var PARTY_DESCRIPTIONS = {
|
|
7080
|
+
build: "Viktor + Kai + Sam \u2014 Architecture + Engineering + QA",
|
|
7081
|
+
review: "Kai + Sam + Eli \u2014 Code Review + QA + Docs",
|
|
7082
|
+
refactor: "Viktor + Sam + Eli \u2014 Refactor + Tests + Docs",
|
|
7083
|
+
security: "Vera + Sam \u2014 Security Review + QA",
|
|
7084
|
+
full: "All agents"
|
|
7085
|
+
};
|
|
7086
|
+
function scoreAgents(task) {
|
|
7087
|
+
const lower = task.toLowerCase();
|
|
7088
|
+
const scores = /* @__PURE__ */ new Map();
|
|
7089
|
+
for (const [agent, keywords] of Object.entries(AGENT_KEYWORDS)) {
|
|
7090
|
+
const matched = [];
|
|
7091
|
+
for (const kw of keywords) {
|
|
7092
|
+
if (lower.includes(kw)) {
|
|
7093
|
+
matched.push(kw);
|
|
7094
|
+
}
|
|
7095
|
+
}
|
|
7096
|
+
if (matched.length > 0) {
|
|
7097
|
+
scores.set(agent, matched);
|
|
7098
|
+
}
|
|
7099
|
+
}
|
|
7100
|
+
return scores;
|
|
7101
|
+
}
|
|
7102
|
+
var _lastTask = "";
|
|
7103
|
+
function resolvePartySet(scores) {
|
|
7104
|
+
const activeAgents = [...scores.keys()];
|
|
7105
|
+
if (activeAgents.length >= 3) {
|
|
7106
|
+
return "full";
|
|
7107
|
+
}
|
|
7108
|
+
if (scores.has("vera") && _lastTask.toLowerCase().includes("review")) {
|
|
7109
|
+
return "security";
|
|
7110
|
+
}
|
|
7111
|
+
if (scores.has("viktor") && scores.has("sam")) {
|
|
7112
|
+
return "refactor";
|
|
7113
|
+
}
|
|
7114
|
+
if (scores.has("viktor") && scores.has("kai")) {
|
|
7115
|
+
return "build";
|
|
7116
|
+
}
|
|
7117
|
+
if (scores.has("suki") && scores.has("kai")) {
|
|
7118
|
+
return "build";
|
|
7119
|
+
}
|
|
7120
|
+
return null;
|
|
7121
|
+
}
|
|
7122
|
+
function routeTask(task) {
|
|
7123
|
+
_lastTask = task;
|
|
7124
|
+
const scores = scoreAgents(task);
|
|
7125
|
+
const partySet = resolvePartySet(scores);
|
|
7126
|
+
if (partySet) {
|
|
7127
|
+
const agentList = [...scores.keys()];
|
|
7128
|
+
const matchedKws = agentList.flatMap((a) => scores.get(a) ?? []);
|
|
7129
|
+
return {
|
|
7130
|
+
type: "party",
|
|
7131
|
+
partySet,
|
|
7132
|
+
confidence: "high",
|
|
7133
|
+
reasoning: `Multi-domain keywords erkannt (${matchedKws.slice(0, 4).join(", ")})`
|
|
7134
|
+
};
|
|
7135
|
+
}
|
|
7136
|
+
if (scores.size === 0) {
|
|
7137
|
+
return {
|
|
7138
|
+
type: "agent",
|
|
7139
|
+
agent: "kai",
|
|
7140
|
+
confidence: "low",
|
|
7141
|
+
reasoning: "Keine spezifischen Keywords erkannt \u2014 Fallback zu Kai"
|
|
7142
|
+
};
|
|
7143
|
+
}
|
|
7144
|
+
let topAgent = "kai";
|
|
7145
|
+
let topCount = 0;
|
|
7146
|
+
let secondCount = 0;
|
|
7147
|
+
for (const [agent, kws] of scores.entries()) {
|
|
7148
|
+
if (kws.length > topCount) {
|
|
7149
|
+
secondCount = topCount;
|
|
7150
|
+
topCount = kws.length;
|
|
7151
|
+
topAgent = agent;
|
|
7152
|
+
} else if (kws.length > secondCount) {
|
|
7153
|
+
secondCount = kws.length;
|
|
7154
|
+
}
|
|
7155
|
+
}
|
|
7156
|
+
const topKeywords = scores.get(topAgent) ?? [];
|
|
7157
|
+
const confidence = topCount >= 2 && topCount > secondCount ? "high" : "medium";
|
|
7158
|
+
return {
|
|
7159
|
+
type: "agent",
|
|
7160
|
+
agent: topAgent,
|
|
7161
|
+
confidence,
|
|
7162
|
+
reasoning: `Keywords erkannt: ${topKeywords.join(", ")}`
|
|
7163
|
+
};
|
|
7164
|
+
}
|
|
7165
|
+
function resolveWorktreePath(forgehiveDir2, agent) {
|
|
7166
|
+
if (!agent) return void 0;
|
|
7167
|
+
const worktreeFile = path34.join(forgehiveDir2, "agents", agent, "worktree");
|
|
7168
|
+
if (fs33.existsSync(worktreeFile)) {
|
|
7169
|
+
return fs33.readFileSync(worktreeFile, "utf8").trim();
|
|
7170
|
+
}
|
|
7171
|
+
return void 0;
|
|
7172
|
+
}
|
|
7173
|
+
function formatRoutingResult(result, worktreePath) {
|
|
7174
|
+
const lines = [];
|
|
7175
|
+
if (result.type === "party" && result.partySet) {
|
|
7176
|
+
const desc = PARTY_DESCRIPTIONS[result.partySet] ?? result.partySet;
|
|
7177
|
+
lines.push(`Empfehlung: Party-Set "${result.partySet}" \u2014 ${desc}`);
|
|
7178
|
+
lines.push(`Begr\xFCndung: ${result.reasoning}`);
|
|
7179
|
+
lines.push("");
|
|
7180
|
+
lines.push("Starten:");
|
|
7181
|
+
lines.push(` fh party run ${result.partySet}`);
|
|
7182
|
+
} else {
|
|
7183
|
+
const agent = result.agent ?? "kai";
|
|
7184
|
+
const desc = AGENT_DESCRIPTIONS[agent] ?? agent;
|
|
7185
|
+
lines.push(`Empfehlung: ${agent.charAt(0).toUpperCase() + agent.slice(1)} \u2014 ${desc}`);
|
|
7186
|
+
lines.push(`Begr\xFCndung: ${result.reasoning}`);
|
|
7187
|
+
lines.push(`Confidence: ${result.confidence}`);
|
|
7188
|
+
lines.push("");
|
|
7189
|
+
lines.push("Starten:");
|
|
7190
|
+
const wtPath = worktreePath ?? `<${agent}-worktree>`;
|
|
7191
|
+
lines.push(` cd ${wtPath} && claude`);
|
|
7192
|
+
}
|
|
7193
|
+
return lines.join("\n");
|
|
7194
|
+
}
|
|
7195
|
+
|
|
7196
|
+
// src/github.ts
|
|
7197
|
+
init_js_yaml();
|
|
7198
|
+
import fs34 from "node:fs";
|
|
7199
|
+
import path35 from "node:path";
|
|
7200
|
+
import https from "node:https";
|
|
7201
|
+
function loadGitHubConfig(forgehiveDir2) {
|
|
7202
|
+
const configPath = path35.join(forgehiveDir2, "github.yaml");
|
|
7203
|
+
if (!fs34.existsSync(configPath)) {
|
|
7204
|
+
throw new Error("Nicht konfiguriert. F\xFChre zuerst aus: fh github setup");
|
|
7205
|
+
}
|
|
7206
|
+
let parsed;
|
|
7207
|
+
try {
|
|
7208
|
+
parsed = jsYaml.load(fs34.readFileSync(configPath, "utf8"));
|
|
7209
|
+
} catch {
|
|
7210
|
+
throw new Error("Ung\xFCltige Konfigurationsdatei: .forgehive/github.yaml ist kein g\xFCltiges YAML.");
|
|
7211
|
+
}
|
|
7212
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
7213
|
+
throw new Error("Ung\xFCltige Konfigurationsdatei: .forgehive/github.yaml enth\xE4lt kein Objekt.");
|
|
7214
|
+
}
|
|
7215
|
+
const data = parsed;
|
|
7216
|
+
if (!data.repo) {
|
|
7217
|
+
throw new Error("Nicht konfiguriert. F\xFChre zuerst aus: fh github setup");
|
|
7218
|
+
}
|
|
7219
|
+
return { repo: data.repo };
|
|
7220
|
+
}
|
|
7221
|
+
function saveGitHubConfig(forgehiveDir2, config) {
|
|
7222
|
+
fs34.mkdirSync(forgehiveDir2, { recursive: true });
|
|
7223
|
+
const configPath = path35.join(forgehiveDir2, "github.yaml");
|
|
7224
|
+
fs34.writeFileSync(configPath, jsYaml.dump({ repo: config.repo }), "utf8");
|
|
7225
|
+
}
|
|
7226
|
+
function githubGet(apiPath, token) {
|
|
7227
|
+
return new Promise((resolve, reject) => {
|
|
7228
|
+
const options = {
|
|
7229
|
+
hostname: "api.github.com",
|
|
7230
|
+
path: apiPath,
|
|
7231
|
+
headers: {
|
|
7232
|
+
"Authorization": `Bearer ${token}`,
|
|
7233
|
+
"User-Agent": "forgehive-cli",
|
|
7234
|
+
"Accept": "application/vnd.github.v3+json"
|
|
7235
|
+
}
|
|
7236
|
+
};
|
|
7237
|
+
https.get(options, (res) => {
|
|
7238
|
+
let data = "";
|
|
7239
|
+
res.on("data", (chunk) => {
|
|
7240
|
+
data += chunk;
|
|
7241
|
+
});
|
|
7242
|
+
res.on("error", reject);
|
|
7243
|
+
res.on("end", () => {
|
|
7244
|
+
if (res.statusCode === 401) {
|
|
7245
|
+
reject(new Error("Ung\xFCltiger Token. \xDCberpr\xFCfe dein GitHub-Token."));
|
|
7246
|
+
return;
|
|
7247
|
+
}
|
|
7248
|
+
if (res.statusCode === 404) {
|
|
7249
|
+
reject(new Error(`Repository oder Ressource nicht gefunden.`));
|
|
7250
|
+
return;
|
|
7251
|
+
}
|
|
7252
|
+
if (res.statusCode === 403) {
|
|
7253
|
+
const reset = res.headers["x-ratelimit-reset"];
|
|
7254
|
+
const resetTime = reset ? new Date(parseInt(reset, 10) * 1e3).toLocaleTimeString() : "unbekannt";
|
|
7255
|
+
reject(new Error(`Rate Limit erreicht. Wird zur\xFCckgesetzt um ${resetTime}.`));
|
|
7256
|
+
return;
|
|
7257
|
+
}
|
|
7258
|
+
if ((res.statusCode ?? 0) >= 400) {
|
|
7259
|
+
reject(new Error(`GitHub API ${res.statusCode}: ${data}`));
|
|
7260
|
+
return;
|
|
7261
|
+
}
|
|
7262
|
+
try {
|
|
7263
|
+
resolve(JSON.parse(data));
|
|
7264
|
+
} catch (e) {
|
|
7265
|
+
reject(new Error(`JSON parse error: ${e.message}`));
|
|
7266
|
+
}
|
|
7267
|
+
});
|
|
7268
|
+
}).on("error", reject);
|
|
7269
|
+
});
|
|
7270
|
+
}
|
|
7271
|
+
async function fetchOpenIssues(owner, repo, token) {
|
|
7272
|
+
const issues = [];
|
|
7273
|
+
let page = 1;
|
|
7274
|
+
while (true) {
|
|
7275
|
+
const batch = await githubGet(
|
|
7276
|
+
`/repos/${owner}/${repo}/issues?state=open&per_page=100&page=${page}&filter=issues`,
|
|
7277
|
+
token
|
|
7278
|
+
);
|
|
7279
|
+
if (batch.length === 0) break;
|
|
7280
|
+
const filtered = batch.filter((i) => !i.pull_request);
|
|
7281
|
+
issues.push(...filtered);
|
|
7282
|
+
if (batch.length < 100) break;
|
|
7283
|
+
page++;
|
|
7284
|
+
}
|
|
7285
|
+
return issues;
|
|
7286
|
+
}
|
|
7287
|
+
async function fetchPR(owner, repo, number, token) {
|
|
7288
|
+
return githubGet(`/repos/${owner}/${repo}/pulls/${number}`, token);
|
|
7289
|
+
}
|
|
7290
|
+
async function fetchPRFiles(owner, repo, number, token) {
|
|
7291
|
+
const files = [];
|
|
7292
|
+
let page = 1;
|
|
7293
|
+
while (true) {
|
|
7294
|
+
const batch = await githubGet(
|
|
7295
|
+
`/repos/${owner}/${repo}/pulls/${number}/files?per_page=100&page=${page}`,
|
|
7296
|
+
token
|
|
7297
|
+
);
|
|
7298
|
+
if (batch.length === 0) break;
|
|
7299
|
+
files.push(...batch);
|
|
7300
|
+
if (batch.length < 100) break;
|
|
7301
|
+
page++;
|
|
7302
|
+
}
|
|
7303
|
+
return files;
|
|
7304
|
+
}
|
|
7305
|
+
function issueAlreadySynced(storiesDir, issueNumber) {
|
|
7306
|
+
if (!fs34.existsSync(storiesDir)) return false;
|
|
7307
|
+
const pattern = new RegExp(`GitHub: #${issueNumber}(?![\\d\\w])`);
|
|
7308
|
+
return fs34.readdirSync(storiesDir).filter((f) => f.endsWith(".md")).some((f) => pattern.test(fs34.readFileSync(path35.join(storiesDir, f), "utf8")));
|
|
7309
|
+
}
|
|
7310
|
+
function appendGitHubRef(storiesDir, storyId, issue, repoFullName) {
|
|
7311
|
+
const filePath = path35.join(storiesDir, `${storyId}.md`);
|
|
7312
|
+
if (!fs34.existsSync(filePath)) {
|
|
7313
|
+
throw new Error(`Story-Datei nicht gefunden: ${storyId}.md`);
|
|
7314
|
+
}
|
|
7315
|
+
const existing = fs34.readFileSync(filePath, "utf8");
|
|
7316
|
+
const ref = `
|
|
7317
|
+
## GitHub
|
|
7318
|
+
|
|
7319
|
+
GitHub: #${issue.number} \u2014 ${issue.html_url}
|
|
7320
|
+
`;
|
|
7321
|
+
fs34.writeFileSync(filePath, existing.trimEnd() + "\n" + ref, "utf8");
|
|
7322
|
+
}
|
|
7323
|
+
function formatPRContext(pr, files, repoFullName) {
|
|
7324
|
+
const body = pr.body?.trim() || "(keine Beschreibung)";
|
|
7325
|
+
const fileLines = files.map((f) => `- \`${f.filename}\` (${f.status})`).join("\n");
|
|
7326
|
+
return [
|
|
7327
|
+
`# PR #${pr.number}: ${pr.title}`,
|
|
7328
|
+
"",
|
|
7329
|
+
`**Branch:** ${pr.head.ref} \u2192 ${pr.base.ref}`,
|
|
7330
|
+
`**Autor:** @${pr.user.login}`,
|
|
7331
|
+
`**Diff-URL:** https://github.com/${repoFullName}/pull/${pr.number}/files`,
|
|
7332
|
+
"",
|
|
7333
|
+
"## Beschreibung",
|
|
7334
|
+
"",
|
|
7335
|
+
body,
|
|
7336
|
+
"",
|
|
7337
|
+
"## Ge\xE4nderte Dateien",
|
|
7338
|
+
"",
|
|
7339
|
+
fileLines || "(keine Dateien)"
|
|
7340
|
+
].join("\n");
|
|
7341
|
+
}
|
|
7342
|
+
|
|
6666
7343
|
// src/cli.ts
|
|
6667
7344
|
import { createRequire } from "node:module";
|
|
6668
7345
|
var require2 = createRequire(import.meta.url);
|
|
6669
7346
|
var { version } = require2("../package.json");
|
|
6670
7347
|
var [, , command, subcommand, ...rest] = process.argv;
|
|
6671
7348
|
var projectRoot = process.cwd();
|
|
6672
|
-
var forgehiveDir =
|
|
7349
|
+
var forgehiveDir = path36.join(projectRoot, ".forgehive");
|
|
6673
7350
|
if (command === "--version" || command === "-v") {
|
|
6674
7351
|
console.log(version);
|
|
6675
7352
|
process.exit(0);
|
|
@@ -6682,7 +7359,7 @@ USAGE
|
|
|
6682
7359
|
fh <command> [subcommand] [options]
|
|
6683
7360
|
|
|
6684
7361
|
SETUP
|
|
6685
|
-
fh init
|
|
7362
|
+
fh init [--yes] Set up forgehive in the current project
|
|
6686
7363
|
fh confirm Activate capabilities (draft \u2192 confirmed)
|
|
6687
7364
|
fh rollback Remove forgehive from the project
|
|
6688
7365
|
fh status Show current project state
|
|
@@ -6703,7 +7380,7 @@ CI
|
|
|
6703
7380
|
fh ci --init Generate GitHub Actions workflow
|
|
6704
7381
|
|
|
6705
7382
|
CODEBASE
|
|
6706
|
-
fh map
|
|
7383
|
+
fh map [--semantic] Codebase structure map + semantic graph
|
|
6707
7384
|
fh onboard [--output path] Generate ONBOARDING.md
|
|
6708
7385
|
fh changelog [--since tag] Semantic changelog from git
|
|
6709
7386
|
fh metrics [--since date] Developer productivity metrics
|
|
@@ -6714,6 +7391,11 @@ CODEBASE
|
|
|
6714
7391
|
fh docs changelog [--since tag] Generate changelog
|
|
6715
7392
|
fh docs adr "<title>" Create Architecture Decision Record
|
|
6716
7393
|
|
|
7394
|
+
GITHUB
|
|
7395
|
+
fh github setup Configure GitHub token and repository
|
|
7396
|
+
fh github sync Import open Issues as Stories (idempotent)
|
|
7397
|
+
fh github pr <number> Fetch PR context for code review
|
|
7398
|
+
|
|
6717
7399
|
SPRINT PLANNING
|
|
6718
7400
|
fh story create <title> [--epic EPC-N] [--points N]
|
|
6719
7401
|
fh story list [--epic EPC-N]
|
|
@@ -6727,8 +7409,10 @@ SPRINT PLANNING
|
|
|
6727
7409
|
fh velocity record <N> --committed N --delivered N
|
|
6728
7410
|
|
|
6729
7411
|
TEAM
|
|
7412
|
+
fh ask "<task>" Route a task to the right agent or party set
|
|
6730
7413
|
fh sync push|pull [--remote origin --branch forgehive-memory]
|
|
6731
7414
|
fh run <issue-url> [--agent name] [--label label]
|
|
7415
|
+
fh run "<task>" Freetext task \u2014 auto-routes like fh ask
|
|
6732
7416
|
fh memory show|clean|export|prune|snapshot
|
|
6733
7417
|
fh memory adr list|"<title>"
|
|
6734
7418
|
|
|
@@ -6749,51 +7433,87 @@ COST
|
|
|
6749
7433
|
process.exit(0);
|
|
6750
7434
|
}
|
|
6751
7435
|
function loadClaudeMdBlock() {
|
|
6752
|
-
const templatePath =
|
|
6753
|
-
|
|
7436
|
+
const templatePath = path36.join(
|
|
7437
|
+
path36.dirname(new URL(import.meta.url).pathname),
|
|
6754
7438
|
"..",
|
|
6755
7439
|
"forgehive",
|
|
6756
7440
|
"templates",
|
|
6757
7441
|
"claude-md.block.md"
|
|
6758
7442
|
);
|
|
6759
|
-
if (!
|
|
6760
|
-
return
|
|
7443
|
+
if (!fs35.existsSync(templatePath)) return "## forgehive\n\nSee .forgehive/ for configuration.";
|
|
7444
|
+
return fs35.readFileSync(templatePath, "utf8");
|
|
7445
|
+
}
|
|
7446
|
+
async function promptConfirm(question) {
|
|
7447
|
+
if (!process.stdin.isTTY) return false;
|
|
7448
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
7449
|
+
return new Promise((resolve) => {
|
|
7450
|
+
rl.question(question, (answer) => {
|
|
7451
|
+
rl.close();
|
|
7452
|
+
const a = answer.trim().toLowerCase();
|
|
7453
|
+
resolve(a === "y" || a === "yes" || a === "");
|
|
7454
|
+
});
|
|
7455
|
+
});
|
|
6761
7456
|
}
|
|
6762
|
-
|
|
6763
|
-
|
|
6764
|
-
|
|
6765
|
-
|
|
6766
|
-
|
|
6767
|
-
console.log(` Nutze 'fh scan --update' um nur den Scan zu aktualisieren.`);
|
|
6768
|
-
process.exit(0);
|
|
7457
|
+
function buildCapabilitySummary(ids) {
|
|
7458
|
+
if (ids.length === 0) return " Erkannt: (keine Capabilities)";
|
|
7459
|
+
const lines = [];
|
|
7460
|
+
for (let i = 0; i < ids.length && lines.length < 4; i += 4) {
|
|
7461
|
+
lines.push(" \u2022 " + ids.slice(i, i + 4).join(" \xB7 "));
|
|
6769
7462
|
}
|
|
6770
|
-
|
|
6771
|
-
|
|
6772
|
-
|
|
6773
|
-
|
|
6774
|
-
|
|
6775
|
-
|
|
6776
|
-
|
|
6777
|
-
|
|
6778
|
-
|
|
6779
|
-
|
|
6780
|
-
|
|
6781
|
-
|
|
6782
|
-
|
|
6783
|
-
|
|
6784
|
-
|
|
6785
|
-
|
|
6786
|
-
|
|
6787
|
-
|
|
6788
|
-
|
|
6789
|
-
|
|
6790
|
-
|
|
6791
|
-
|
|
6792
|
-
console.log(`
|
|
6793
|
-
|
|
6794
|
-
|
|
6795
|
-
|
|
6796
|
-
|
|
7463
|
+
return " Erkannt:\n" + lines.join("\n");
|
|
7464
|
+
}
|
|
7465
|
+
if (command === "init") {
|
|
7466
|
+
(async () => {
|
|
7467
|
+
const gitCheck = spawnSync12("git", ["--version"], { stdio: "ignore" });
|
|
7468
|
+
if (gitCheck.error || gitCheck.status !== 0) {
|
|
7469
|
+
console.error("Fehler: git nicht gefunden.");
|
|
7470
|
+
console.error(" Installation: https://git-scm.com");
|
|
7471
|
+
process.exit(1);
|
|
7472
|
+
}
|
|
7473
|
+
const forgehiveDirExists = fs35.existsSync(forgehiveDir);
|
|
7474
|
+
if (forgehiveDirExists && !rest.includes("--force")) {
|
|
7475
|
+
console.log(`\u26A0 .forgehive/ existiert bereits in diesem Projekt.`);
|
|
7476
|
+
console.log(` Nutze 'fh init --force' um neu zu initialisieren (\xFCberschreibt capabilities.yaml).`);
|
|
7477
|
+
console.log(` Nutze 'fh scan --update' um nur den Scan zu aktualisieren.`);
|
|
7478
|
+
process.exit(0);
|
|
7479
|
+
}
|
|
7480
|
+
console.log("\u{1F50D} Analysiere Projekt...\n");
|
|
7481
|
+
const scanResult = scan(projectRoot);
|
|
7482
|
+
const tierCount = [1, 2, 3].map((t) => scanResult.signals.filter((s) => s.tier === t).length);
|
|
7483
|
+
console.log(` \u2713 ${tierCount[0]} Technologie-Signale erkannt`);
|
|
7484
|
+
console.log(` \u2713 ${tierCount[1]} Infrastruktur-Signale erkannt`);
|
|
7485
|
+
console.log(` \u2713 ${tierCount[2]} Kontext-Signale erkannt`);
|
|
7486
|
+
console.log();
|
|
7487
|
+
const capMap = mapSignalsToCapabilities(scanResult);
|
|
7488
|
+
const block = loadClaudeMdBlock();
|
|
7489
|
+
writeForgehiveDir(projectRoot, scanResult, capMap, block);
|
|
7490
|
+
const hash = computeHash(projectRoot);
|
|
7491
|
+
fs35.writeFileSync(path36.join(forgehiveDir, ".scan-hash"), hash, "utf8");
|
|
7492
|
+
const runtimeDir = path36.join(
|
|
7493
|
+
path36.dirname(new URL(import.meta.url).pathname),
|
|
7494
|
+
"..",
|
|
7495
|
+
"forgehive"
|
|
7496
|
+
);
|
|
7497
|
+
initForgehiveRuntime(forgehiveDir, runtimeDir);
|
|
7498
|
+
console.log("\u2713 forgehive initialisiert\n");
|
|
7499
|
+
console.log(buildCapabilitySummary(capMap.confirmed.map((c) => c.id)));
|
|
7500
|
+
console.log();
|
|
7501
|
+
if (subcommand === "--yes" || rest.includes("--yes")) {
|
|
7502
|
+
confirm(projectRoot);
|
|
7503
|
+
console.log("\u2713 Capabilities best\xE4tigt\n");
|
|
7504
|
+
} else {
|
|
7505
|
+
const ok = await promptConfirm(" Capabilities best\xE4tigen? [Y/n] ");
|
|
7506
|
+
if (ok) {
|
|
7507
|
+
confirm(projectRoot);
|
|
7508
|
+
console.log("\u2713 Capabilities best\xE4tigt\n");
|
|
7509
|
+
} else {
|
|
7510
|
+
console.log(" \xDCberpr\xFCfe .forgehive/capabilities.yaml, dann: fh confirm\n");
|
|
7511
|
+
}
|
|
7512
|
+
}
|
|
7513
|
+
})().catch((err) => {
|
|
7514
|
+
console.error(`Fehler: ${err.message}`);
|
|
7515
|
+
process.exit(1);
|
|
7516
|
+
});
|
|
6797
7517
|
} else if (command === "confirm") {
|
|
6798
7518
|
try {
|
|
6799
7519
|
confirm(projectRoot);
|
|
@@ -6812,7 +7532,7 @@ if (command === "init") {
|
|
|
6812
7532
|
process.exit(1);
|
|
6813
7533
|
}
|
|
6814
7534
|
} else if (command === "memory") {
|
|
6815
|
-
if (!
|
|
7535
|
+
if (!fs35.existsSync(forgehiveDir)) {
|
|
6816
7536
|
console.error("Fehler: .forgehive/ nicht gefunden \u2014 f\xFChre zuerst `fh init` aus");
|
|
6817
7537
|
process.exit(1);
|
|
6818
7538
|
}
|
|
@@ -6821,7 +7541,7 @@ if (command === "init") {
|
|
|
6821
7541
|
} else if (subcommand === "clean") {
|
|
6822
7542
|
cleanMemory(forgehiveDir);
|
|
6823
7543
|
} else if (subcommand === "export") {
|
|
6824
|
-
const outputPath = rest[0] ??
|
|
7544
|
+
const outputPath = rest[0] ?? path36.join(projectRoot, "forgehive-memory-export.md");
|
|
6825
7545
|
try {
|
|
6826
7546
|
exportMemory(forgehiveDir, outputPath);
|
|
6827
7547
|
} catch (err) {
|
|
@@ -6869,7 +7589,7 @@ Ausf\xFChren mit --remove um zu l\xF6schen: fh memory prune ${days} --remove`);
|
|
|
6869
7589
|
} else if (subcommand === "snapshot") {
|
|
6870
7590
|
const snapAction = rest[0];
|
|
6871
7591
|
if (snapAction === "export") {
|
|
6872
|
-
const outPath = rest[1] ??
|
|
7592
|
+
const outPath = rest[1] ?? path36.join(
|
|
6873
7593
|
projectRoot,
|
|
6874
7594
|
`forgehive-snapshot-${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}.json`
|
|
6875
7595
|
);
|
|
@@ -6909,11 +7629,11 @@ Ausf\xFChren mit --remove um zu l\xF6schen: fh memory prune ${days} --remove`);
|
|
|
6909
7629
|
process.exit(1);
|
|
6910
7630
|
}
|
|
6911
7631
|
} else if (command === "scan" && subcommand === "--update") {
|
|
6912
|
-
if (!
|
|
7632
|
+
if (!fs35.existsSync(forgehiveDir)) {
|
|
6913
7633
|
console.error("Fehler: .forgehive/ nicht gefunden \u2014 f\xFChre zuerst `fh init` aus");
|
|
6914
7634
|
process.exit(1);
|
|
6915
7635
|
}
|
|
6916
|
-
const savedHash =
|
|
7636
|
+
const savedHash = fs35.existsSync(path36.join(forgehiveDir, ".scan-hash")) ? fs35.readFileSync(path36.join(forgehiveDir, ".scan-hash"), "utf8").trim() : null;
|
|
6917
7637
|
const currentHash = computeHash(projectRoot);
|
|
6918
7638
|
if (savedHash === currentHash) {
|
|
6919
7639
|
console.log("\u2713 Keine \xC4nderungen erkannt \u2014 capabilities.yaml ist aktuell");
|
|
@@ -6921,7 +7641,7 @@ Ausf\xFChren mit --remove um zu l\xF6schen: fh memory prune ${days} --remove`);
|
|
|
6921
7641
|
}
|
|
6922
7642
|
console.log("\u{1F50D} \xC4nderungen erkannt \u2014 scanne erneut...\n");
|
|
6923
7643
|
const oldDoc = jsYaml.load(
|
|
6924
|
-
|
|
7644
|
+
fs35.readFileSync(path36.join(forgehiveDir, "capabilities.yaml"), "utf8")
|
|
6925
7645
|
);
|
|
6926
7646
|
const oldMap = { confirmed: oldDoc.capabilities.confirmed ?? [], inferred: [] };
|
|
6927
7647
|
const scanResult = scan(projectRoot);
|
|
@@ -6941,16 +7661,16 @@ Ausf\xFChren mit --remove um zu l\xF6schen: fh memory prune ${days} --remove`);
|
|
|
6941
7661
|
console.log();
|
|
6942
7662
|
const block = loadClaudeMdBlock();
|
|
6943
7663
|
writeForgehiveDir(projectRoot, scanResult, newMap, block);
|
|
6944
|
-
|
|
7664
|
+
fs35.writeFileSync(path36.join(forgehiveDir, ".scan-hash"), currentHash, "utf8");
|
|
6945
7665
|
console.log("\u2713 scan-result.yaml und capabilities.yaml aktualisiert");
|
|
6946
7666
|
console.log(" F\xFChre `fh confirm` aus um die \xC4nderungen zu best\xE4tigen");
|
|
6947
7667
|
}
|
|
6948
7668
|
} else if (command === "scan" && subcommand === "--check") {
|
|
6949
|
-
if (!
|
|
7669
|
+
if (!fs35.existsSync(path36.join(forgehiveDir, ".scan-hash"))) {
|
|
6950
7670
|
console.log("Warnung: Kein Scan-Hash gefunden. F\xFChre `fh init` aus.");
|
|
6951
7671
|
process.exit(1);
|
|
6952
7672
|
}
|
|
6953
|
-
const saved =
|
|
7673
|
+
const saved = fs35.readFileSync(path36.join(forgehiveDir, ".scan-hash"), "utf8").trim();
|
|
6954
7674
|
const current = computeHash(projectRoot);
|
|
6955
7675
|
if (saved !== current) {
|
|
6956
7676
|
console.log("\u26A0 Codebase hat sich seit letztem Scan ge\xE4ndert.");
|
|
@@ -6959,7 +7679,7 @@ Ausf\xFChren mit --remove um zu l\xF6schen: fh memory prune ${days} --remove`);
|
|
|
6959
7679
|
}
|
|
6960
7680
|
console.log("\u2713 capabilities.yaml ist aktuell");
|
|
6961
7681
|
} else if (command === "skills") {
|
|
6962
|
-
if (!
|
|
7682
|
+
if (!fs35.existsSync(forgehiveDir)) {
|
|
6963
7683
|
console.error("Fehler: .forgehive/ nicht gefunden \u2014 f\xFChre zuerst `fh init` aus");
|
|
6964
7684
|
process.exit(1);
|
|
6965
7685
|
}
|
|
@@ -6995,7 +7715,7 @@ Ausf\xFChren mit --remove um zu l\xF6schen: fh memory prune ${days} --remove`);
|
|
|
6995
7715
|
process.exit(1);
|
|
6996
7716
|
}
|
|
6997
7717
|
} else if (command === "party") {
|
|
6998
|
-
if (!
|
|
7718
|
+
if (!fs35.existsSync(forgehiveDir)) {
|
|
6999
7719
|
console.error("Fehler: .forgehive/ nicht gefunden \u2014 f\xFChre zuerst `fh init` aus");
|
|
7000
7720
|
process.exit(1);
|
|
7001
7721
|
}
|
|
@@ -7116,7 +7836,7 @@ Ausf\xFChren mit --remove um zu l\xF6schen: fh memory prune ${days} --remove`);
|
|
|
7116
7836
|
}
|
|
7117
7837
|
}
|
|
7118
7838
|
} else if (command === "wire") {
|
|
7119
|
-
if (!
|
|
7839
|
+
if (!fs35.existsSync(forgehiveDir)) {
|
|
7120
7840
|
console.error("Fehler: .forgehive/ nicht gefunden \u2014 f\xFChre zuerst `fh init` aus");
|
|
7121
7841
|
process.exit(1);
|
|
7122
7842
|
}
|
|
@@ -7155,7 +7875,7 @@ N\xE4chster Schritt: Setze die erforderlichen Umgebungsvariablen und starte Clau
|
|
|
7155
7875
|
const limitIdx = allArgs.indexOf("--limit");
|
|
7156
7876
|
const alertIdx = allArgs.indexOf("--alert");
|
|
7157
7877
|
if (limitIdx !== -1) {
|
|
7158
|
-
if (!
|
|
7878
|
+
if (!fs35.existsSync(forgehiveDir)) {
|
|
7159
7879
|
console.error("Fehler: .forgehive/ nicht gefunden \u2014 f\xFChre zuerst `fh init` aus");
|
|
7160
7880
|
process.exit(1);
|
|
7161
7881
|
}
|
|
@@ -7181,14 +7901,14 @@ N\xE4chster Schritt: Setze die erforderlichen Umgebungsvariablen und starte Clau
|
|
|
7181
7901
|
}
|
|
7182
7902
|
const sessions = parseCostSessions(projectRoot);
|
|
7183
7903
|
console.log(formatCostReport(sessions, range));
|
|
7184
|
-
if (
|
|
7904
|
+
if (fs35.existsSync(forgehiveDir)) {
|
|
7185
7905
|
const total = sessions.reduce((s, x) => s + x.estimatedCostUsd, 0);
|
|
7186
7906
|
const status = checkSpendStatus(forgehiveDir, total);
|
|
7187
7907
|
if (status.message) console.log("\n" + status.message);
|
|
7188
7908
|
}
|
|
7189
7909
|
}
|
|
7190
7910
|
} else if (command === "watch") {
|
|
7191
|
-
if (!
|
|
7911
|
+
if (!fs35.existsSync(forgehiveDir)) {
|
|
7192
7912
|
console.error("Fehler: .forgehive/ nicht gefunden \u2014 f\xFChre zuerst `fh init` aus");
|
|
7193
7913
|
process.exit(1);
|
|
7194
7914
|
}
|
|
@@ -7204,7 +7924,7 @@ N\xE4chster Schritt: Setze die erforderlichen Umgebungsvariablen und starte Clau
|
|
|
7204
7924
|
process.exit(0);
|
|
7205
7925
|
});
|
|
7206
7926
|
} else if (command === "mcp") {
|
|
7207
|
-
if (!
|
|
7927
|
+
if (!fs35.existsSync(forgehiveDir)) {
|
|
7208
7928
|
console.error("Fehler: .forgehive/ nicht gefunden \u2014 f\xFChre zuerst `fh init` aus");
|
|
7209
7929
|
process.exit(1);
|
|
7210
7930
|
}
|
|
@@ -7315,7 +8035,7 @@ Setze diese Umgebungsvariablen:`);
|
|
|
7315
8035
|
process.exit(1);
|
|
7316
8036
|
}
|
|
7317
8037
|
} else if (command === "security") {
|
|
7318
|
-
if (!
|
|
8038
|
+
if (!fs35.existsSync(forgehiveDir)) {
|
|
7319
8039
|
console.error("Fehler: .forgehive/ nicht gefunden \u2014 f\xFChre zuerst `fh init` aus");
|
|
7320
8040
|
process.exit(1);
|
|
7321
8041
|
}
|
|
@@ -7401,8 +8121,8 @@ Setze diese Umgebungsvariablen:`);
|
|
|
7401
8121
|
`);
|
|
7402
8122
|
const report = generateSecurityReport(projectRoot, forgehiveDir, mode);
|
|
7403
8123
|
const text = formatSecurityReport(report);
|
|
7404
|
-
const reportPath =
|
|
7405
|
-
|
|
8124
|
+
const reportPath = path36.join(forgehiveDir, "security-report.md");
|
|
8125
|
+
fs35.writeFileSync(reportPath, text, "utf8");
|
|
7406
8126
|
console.log(text);
|
|
7407
8127
|
console.log(`
|
|
7408
8128
|
\u2713 Report gespeichert: ${reportPath}`);
|
|
@@ -7414,8 +8134,8 @@ Setze diese Umgebungsvariablen:`);
|
|
|
7414
8134
|
} else if (subcommand === "permissions") {
|
|
7415
8135
|
const { writePermissions: writePermissions2 } = await Promise.resolve().then(() => (init_harness(), harness_exports));
|
|
7416
8136
|
writePermissions2(forgehiveDir);
|
|
7417
|
-
const permPath =
|
|
7418
|
-
console.log(
|
|
8137
|
+
const permPath = path36.join(forgehiveDir, "harness", "permissions.yaml");
|
|
8138
|
+
console.log(fs35.readFileSync(permPath, "utf8"));
|
|
7419
8139
|
} else {
|
|
7420
8140
|
console.error(`Unbekannter security-Subcommand: ${subcommand}`);
|
|
7421
8141
|
console.error("Verf\xFCgbar: scan | deps | audit | report [gdpr|soc2|hipaa|none] | permissions");
|
|
@@ -7427,36 +8147,62 @@ Setze diese Umgebungsvariablen:`);
|
|
|
7427
8147
|
const failOnArg = allCiArgs.includes("--fail-on") ? allCiArgs[allCiArgs.indexOf("--fail-on") + 1] : "high";
|
|
7428
8148
|
const initFlag = allCiArgs.includes("--init");
|
|
7429
8149
|
if (initFlag) {
|
|
7430
|
-
const ghDir =
|
|
7431
|
-
|
|
7432
|
-
const outPath =
|
|
7433
|
-
|
|
8150
|
+
const ghDir = path36.join(projectRoot, ".github", "workflows");
|
|
8151
|
+
fs35.mkdirSync(ghDir, { recursive: true });
|
|
8152
|
+
const outPath = path36.join(ghDir, "forgehive.yml");
|
|
8153
|
+
fs35.writeFileSync(outPath, getGithubActionsTemplate(), "utf8");
|
|
7434
8154
|
console.log(`\u2714 GitHub Actions workflow geschrieben: ${outPath}`);
|
|
7435
8155
|
} else {
|
|
7436
8156
|
const report = generateCiReport(projectRoot, forgehiveDir, failOnArg);
|
|
7437
8157
|
const output = formatCiReport(report, format);
|
|
7438
8158
|
console.log(output);
|
|
7439
8159
|
if (format === "json") {
|
|
7440
|
-
|
|
7441
|
-
|
|
8160
|
+
fs35.mkdirSync(forgehiveDir, { recursive: true });
|
|
8161
|
+
fs35.writeFileSync(path36.join(forgehiveDir, "ci-report.json"), output, "utf8");
|
|
7442
8162
|
}
|
|
7443
8163
|
if (report.status === "fail") process.exit(1);
|
|
7444
8164
|
}
|
|
7445
8165
|
} else if (command === "map") {
|
|
8166
|
+
const semanticFlag = rest.includes("--semantic");
|
|
7446
8167
|
const map2 = generateMap(projectRoot);
|
|
7447
|
-
const
|
|
7448
|
-
const mapPath =
|
|
7449
|
-
|
|
7450
|
-
|
|
7451
|
-
|
|
7452
|
-
|
|
8168
|
+
const semantic = buildSemanticMap(projectRoot);
|
|
8169
|
+
const mapPath = path36.join(forgehiveDir, "map.md");
|
|
8170
|
+
fs35.mkdirSync(forgehiveDir, { recursive: true });
|
|
8171
|
+
if (semanticFlag) {
|
|
8172
|
+
const fullMd = formatMap(map2, semantic);
|
|
8173
|
+
const marker = "\n## Semantic Graph\n";
|
|
8174
|
+
const markerIdx = fullMd.indexOf(marker);
|
|
8175
|
+
const semanticMd = markerIdx >= 0 ? "## Semantic Graph\n" + fullMd.slice(markerIdx + marker.length) : fullMd;
|
|
8176
|
+
fs35.writeFileSync(mapPath, fullMd, "utf8");
|
|
8177
|
+
console.log(semanticMd);
|
|
8178
|
+
console.log(`
|
|
8179
|
+
\u2714 Codebase-Map gespeichert (Semantic Graph hervorgehoben): ${mapPath}`);
|
|
8180
|
+
} else {
|
|
8181
|
+
const md = formatMap(map2, semantic);
|
|
8182
|
+
fs35.writeFileSync(mapPath, md, "utf8");
|
|
8183
|
+
console.log(md);
|
|
8184
|
+
console.log(`
|
|
7453
8185
|
\u2714 Codebase-Map gespeichert: ${mapPath}`);
|
|
8186
|
+
}
|
|
7454
8187
|
} else if (command === "onboard") {
|
|
7455
8188
|
const outputArg = rest.includes("--output") ? rest[rest.indexOf("--output") + 1] : null;
|
|
7456
|
-
const outputPath = outputArg ??
|
|
8189
|
+
const outputPath = outputArg ?? path36.join(projectRoot, "ONBOARDING.md");
|
|
7457
8190
|
const doc = generateOnboardingDoc(projectRoot, forgehiveDir);
|
|
7458
|
-
|
|
8191
|
+
fs35.writeFileSync(outputPath, doc, "utf8");
|
|
7459
8192
|
console.log(`\u2714 Onboarding-Dokument geschrieben: ${outputPath}`);
|
|
8193
|
+
} else if (command === "ask") {
|
|
8194
|
+
const taskArg = subcommand ? [subcommand, ...rest.filter((r) => !r.startsWith("--"))].join(" ") : rest.filter((r) => !r.startsWith("--")).join(" ");
|
|
8195
|
+
if (!taskArg.trim()) {
|
|
8196
|
+
console.error('Usage: fh ask "<aufgabenbeschreibung>"');
|
|
8197
|
+
process.exit(1);
|
|
8198
|
+
}
|
|
8199
|
+
const askResult = routeTask(taskArg);
|
|
8200
|
+
const askWorktree = resolveWorktreePath(forgehiveDir, askResult.agent);
|
|
8201
|
+
console.log(`
|
|
8202
|
+
Task: "${taskArg}"
|
|
8203
|
+
`);
|
|
8204
|
+
console.log(formatRoutingResult(askResult, askWorktree));
|
|
8205
|
+
console.log();
|
|
7460
8206
|
} else if (command === "changelog") {
|
|
7461
8207
|
const sinceArg = rest.includes("--since") ? rest[rest.indexOf("--since") + 1] : null;
|
|
7462
8208
|
const outputArg = rest.includes("--output") ? rest[rest.indexOf("--output") + 1] : null;
|
|
@@ -7465,28 +8211,28 @@ Setze diese Umgebungsvariablen:`);
|
|
|
7465
8211
|
const commits = parseGitLog(rawLog);
|
|
7466
8212
|
let version2 = "unreleased";
|
|
7467
8213
|
try {
|
|
7468
|
-
const pkgPath =
|
|
7469
|
-
if (
|
|
7470
|
-
const pkg = JSON.parse(
|
|
8214
|
+
const pkgPath = path36.join(projectRoot, "package.json");
|
|
8215
|
+
if (fs35.existsSync(pkgPath)) {
|
|
8216
|
+
const pkg = JSON.parse(fs35.readFileSync(pkgPath, "utf8").replace(/^\s*\/\/.*$/gm, ""));
|
|
7471
8217
|
version2 = pkg.version ?? "unreleased";
|
|
7472
8218
|
}
|
|
7473
8219
|
} catch {
|
|
7474
8220
|
}
|
|
7475
8221
|
const md = formatChangelog(commits, version2);
|
|
7476
|
-
const outputPath = outputArg ??
|
|
8222
|
+
const outputPath = outputArg ?? path36.join(projectRoot, "CHANGELOG.md");
|
|
7477
8223
|
let existing = "";
|
|
7478
|
-
if (
|
|
7479
|
-
|
|
8224
|
+
if (fs35.existsSync(outputPath)) existing = fs35.readFileSync(outputPath, "utf8");
|
|
8225
|
+
fs35.writeFileSync(outputPath, md + "\n\n" + existing, "utf8");
|
|
7480
8226
|
console.log(`\u2714 CHANGELOG.md aktualisiert: ${outputPath}`);
|
|
7481
8227
|
console.log(` ${commits.length} Commits verarbeitet`);
|
|
7482
8228
|
} else if (command === "metrics") {
|
|
7483
8229
|
const sinceArg = rest.includes("--since") ? rest[rest.indexOf("--since") + 1] : void 0;
|
|
7484
8230
|
const rawLog = getMetricsGitLog(projectRoot, sinceArg);
|
|
7485
8231
|
const stats = parseCommitStats(rawLog);
|
|
7486
|
-
const md = formatMetrics(stats,
|
|
7487
|
-
const metricsPath =
|
|
7488
|
-
|
|
7489
|
-
|
|
8232
|
+
const md = formatMetrics(stats, path36.basename(projectRoot));
|
|
8233
|
+
const metricsPath = path36.join(forgehiveDir, "metrics.md");
|
|
8234
|
+
fs35.mkdirSync(forgehiveDir, { recursive: true });
|
|
8235
|
+
fs35.writeFileSync(metricsPath, md, "utf8");
|
|
7490
8236
|
console.log(md);
|
|
7491
8237
|
console.log(`
|
|
7492
8238
|
\u2714 Metrics gespeichert: ${metricsPath}`);
|
|
@@ -7514,14 +8260,26 @@ Setze diese Umgebungsvariablen:`);
|
|
|
7514
8260
|
console.error("Usage: fh run <issue-url> [--agent <name>]");
|
|
7515
8261
|
process.exit(1);
|
|
7516
8262
|
}
|
|
7517
|
-
const
|
|
7518
|
-
|
|
7519
|
-
|
|
7520
|
-
|
|
7521
|
-
|
|
7522
|
-
|
|
8263
|
+
const looksLikeUrl = issueUrl.startsWith("http://") || issueUrl.startsWith("https://") || /^[\w.-]+\/[\w.-]+#\d+$/.test(issueUrl);
|
|
8264
|
+
if (!looksLikeUrl) {
|
|
8265
|
+
const taskDescription = [issueUrl, ...rest.filter((r) => !r.startsWith("--"))].join(" ");
|
|
8266
|
+
const runRouteResult = routeTask(taskDescription);
|
|
8267
|
+
const runWorktree = resolveWorktreePath(forgehiveDir, runRouteResult.agent);
|
|
8268
|
+
console.log(`
|
|
8269
|
+
Task: "${taskDescription}"
|
|
8270
|
+
`);
|
|
8271
|
+
console.log(formatRoutingResult(runRouteResult, runWorktree));
|
|
8272
|
+
console.log();
|
|
8273
|
+
} else {
|
|
8274
|
+
const agentArg = rest.includes("--agent") ? rest[rest.indexOf("--agent") + 1] : void 0;
|
|
8275
|
+
const labelArg = rest.includes("--label") ? rest[rest.indexOf("--label") + 1] : void 0;
|
|
8276
|
+
const agentId = agentArg ?? resolveAgent(labelArg);
|
|
8277
|
+
const result = runBackgroundAgent(forgehiveDir, issueUrl, agentId);
|
|
8278
|
+
console.log(`\u2714 ${result.message}`);
|
|
8279
|
+
console.log(` fh run status \u2014 aktive Sessions anzeigen`);
|
|
8280
|
+
}
|
|
7523
8281
|
} else if (command === "story") {
|
|
7524
|
-
const storiesDir =
|
|
8282
|
+
const storiesDir = path36.join(forgehiveDir, "memory", "stories");
|
|
7525
8283
|
if (subcommand === "create") {
|
|
7526
8284
|
const title = rest.filter((r) => !r.startsWith("--")).join(" ");
|
|
7527
8285
|
const epicArg = rest.includes("--epic") ? rest[rest.indexOf("--epic") + 1] : void 0;
|
|
@@ -7532,7 +8290,7 @@ Setze diese Umgebungsvariablen:`);
|
|
|
7532
8290
|
}
|
|
7533
8291
|
const story = createStory(storiesDir, title, epicArg);
|
|
7534
8292
|
if (pointsArg) updateStoryPoints(storiesDir, story.id, pointsArg);
|
|
7535
|
-
console.log(`\u2714 ${story.id} erstellt: ${
|
|
8293
|
+
console.log(`\u2714 ${story.id} erstellt: ${path36.join(storiesDir, story.id + ".md")}`);
|
|
7536
8294
|
console.log(` Bearbeite die Datei um Acceptance Criteria hinzuzuf\xFCgen.`);
|
|
7537
8295
|
} else if (subcommand === "list") {
|
|
7538
8296
|
const epicFilter = rest.includes("--epic") ? rest[rest.indexOf("--epic") + 1] : null;
|
|
@@ -7581,17 +8339,32 @@ Setze diese Umgebungsvariablen:`);
|
|
|
7581
8339
|
console.error("Verf\xFCgbar: fh story create | list | show | done");
|
|
7582
8340
|
}
|
|
7583
8341
|
} else if (command === "epic") {
|
|
7584
|
-
const epicsDir =
|
|
7585
|
-
const storiesDir =
|
|
8342
|
+
const epicsDir = path36.join(forgehiveDir, "memory", "epics");
|
|
8343
|
+
const storiesDir = path36.join(forgehiveDir, "memory", "stories");
|
|
7586
8344
|
if (subcommand === "create") {
|
|
7587
|
-
const title = rest.filter((r) => !r.startsWith("--")).join(" ");
|
|
7588
8345
|
const goalArg = rest.includes("--goal") ? rest[rest.indexOf("--goal") + 1] : void 0;
|
|
8346
|
+
const prdArg = rest.includes("--prd") ? rest[rest.indexOf("--prd") + 1] : void 0;
|
|
8347
|
+
const flagsWithValues = /* @__PURE__ */ new Set();
|
|
8348
|
+
for (const flag of ["--goal", "--prd"]) {
|
|
8349
|
+
const idx = rest.indexOf(flag);
|
|
8350
|
+
if (idx >= 0) {
|
|
8351
|
+
flagsWithValues.add(flag);
|
|
8352
|
+
if (rest[idx + 1]) flagsWithValues.add(rest[idx + 1]);
|
|
8353
|
+
}
|
|
8354
|
+
}
|
|
8355
|
+
const title = rest.filter((r) => !flagsWithValues.has(r) && !r.startsWith("--")).join(" ");
|
|
7589
8356
|
if (!title) {
|
|
7590
|
-
console.error("Usage: fh epic create <titel> [--goal <ziel>]");
|
|
8357
|
+
console.error("Usage: fh epic create <titel> [--goal <ziel>] [--prd PRD-N]");
|
|
7591
8358
|
process.exit(1);
|
|
7592
8359
|
}
|
|
7593
|
-
|
|
7594
|
-
|
|
8360
|
+
if (prdArg) {
|
|
8361
|
+
const prdsDir = path36.join(forgehiveDir, "memory", "prds");
|
|
8362
|
+
const prd = getPrd(prdsDir, prdArg);
|
|
8363
|
+
if (!prd) console.warn(`\u26A0 PRD ${prdArg} nicht gefunden \u2014 Epic wird trotzdem erstellt`);
|
|
8364
|
+
}
|
|
8365
|
+
const epic = createEpic(epicsDir, title, goalArg, prdArg);
|
|
8366
|
+
console.log(`\u2714 ${epic.id} erstellt: ${path36.join(epicsDir, epic.id + ".md")}`);
|
|
8367
|
+
if (prdArg) console.log(` Verkn\xFCpft mit: ${prdArg}`);
|
|
7595
8368
|
} else if (subcommand === "list") {
|
|
7596
8369
|
const epics = listEpics(epicsDir);
|
|
7597
8370
|
if (epics.length === 0) {
|
|
@@ -7616,8 +8389,94 @@ Setze diese Umgebungsvariablen:`);
|
|
|
7616
8389
|
} else {
|
|
7617
8390
|
console.error("Verf\xFCgbar: fh epic create | list | show");
|
|
7618
8391
|
}
|
|
8392
|
+
} else if (command === "product") {
|
|
8393
|
+
const prdsDir = path36.join(forgehiveDir, "memory", "prds");
|
|
8394
|
+
if (subcommand === "prd") {
|
|
8395
|
+
const title = rest.filter((r) => !r.startsWith("--")).join(" ");
|
|
8396
|
+
if (!title) {
|
|
8397
|
+
console.error('Usage: fh product prd "<titel>"');
|
|
8398
|
+
process.exit(1);
|
|
8399
|
+
}
|
|
8400
|
+
const prd = createPrd(prdsDir, title);
|
|
8401
|
+
const relPath = path36.relative(projectRoot, prd.filepath);
|
|
8402
|
+
console.log(`\u2714 ${prd.id} erstellt: ${relPath}`);
|
|
8403
|
+
console.log(` Bearbeite die Datei um Anforderungen zu definieren.`);
|
|
8404
|
+
} else if (subcommand === "list") {
|
|
8405
|
+
const prds = listPrds(prdsDir);
|
|
8406
|
+
if (prds.length === 0) {
|
|
8407
|
+
console.log("Keine PRDs gefunden.");
|
|
8408
|
+
} else {
|
|
8409
|
+
console.log("PRDs:");
|
|
8410
|
+
for (const p of prds) {
|
|
8411
|
+
const status = `[${p.status}]`.padEnd(12);
|
|
8412
|
+
console.log(` ${p.id.padEnd(6)} ${status} ${p.date} ${p.title}`);
|
|
8413
|
+
}
|
|
8414
|
+
}
|
|
8415
|
+
} else if (subcommand === "show") {
|
|
8416
|
+
const id = rest[0];
|
|
8417
|
+
if (!id) {
|
|
8418
|
+
console.error("Usage: fh product show <PRD-N>");
|
|
8419
|
+
process.exit(1);
|
|
8420
|
+
}
|
|
8421
|
+
const prd = getPrd(prdsDir, id);
|
|
8422
|
+
if (!prd) {
|
|
8423
|
+
console.error(`${id} nicht gefunden`);
|
|
8424
|
+
process.exit(1);
|
|
8425
|
+
}
|
|
8426
|
+
console.log(fs35.readFileSync(prd.filepath, "utf8"));
|
|
8427
|
+
} else if (subcommand === "status") {
|
|
8428
|
+
const epicsDir = path36.join(forgehiveDir, "memory", "epics");
|
|
8429
|
+
const storiesDir = path36.join(forgehiveDir, "memory", "stories");
|
|
8430
|
+
const prds = listPrds(prdsDir);
|
|
8431
|
+
const epics = listEpics(epicsDir);
|
|
8432
|
+
const stories = listStories(storiesDir);
|
|
8433
|
+
console.log("Pipeline:\n");
|
|
8434
|
+
for (const prd of prds) {
|
|
8435
|
+
console.log(` ${prd.id}: ${prd.title} [${prd.status}]`);
|
|
8436
|
+
const linkedEpics = epics.filter((e) => e.prdId === prd.id);
|
|
8437
|
+
if (linkedEpics.length === 0) {
|
|
8438
|
+
console.log(` \u2514\u2500\u2500 Keine Epics verkn\xFCpft`);
|
|
8439
|
+
} else {
|
|
8440
|
+
for (let ei = 0; ei < linkedEpics.length; ei++) {
|
|
8441
|
+
const epic = linkedEpics[ei];
|
|
8442
|
+
const epicPrefix = ei < linkedEpics.length - 1 ? "\u251C\u2500\u2500" : "\u2514\u2500\u2500";
|
|
8443
|
+
console.log(` ${epicPrefix} ${epic.id}: ${epic.title} [${epic.status}]`);
|
|
8444
|
+
const epicStories = stories.filter((s) => s.epicId === epic.id);
|
|
8445
|
+
const isLastEpic = ei === linkedEpics.length - 1;
|
|
8446
|
+
if (epicStories.length === 0) {
|
|
8447
|
+
const pipeOrSpaceEmpty = isLastEpic ? " " : "\u2502 ";
|
|
8448
|
+
console.log(` ${pipeOrSpaceEmpty}\u2514\u2500\u2500 Keine Stories`);
|
|
8449
|
+
} else {
|
|
8450
|
+
for (let si = 0; si < epicStories.length; si++) {
|
|
8451
|
+
const story = epicStories[si];
|
|
8452
|
+
const storyPrefix = si < epicStories.length - 1 ? "\u251C\u2500\u2500" : "\u2514\u2500\u2500";
|
|
8453
|
+
const doneMark = story.status === "done" ? " \u2713" : "";
|
|
8454
|
+
const pipeOrSpace = isLastEpic ? " " : "\u2502 ";
|
|
8455
|
+
console.log(` ${pipeOrSpace}${storyPrefix} ${story.id}: ${story.title} [${story.status}]${doneMark}`);
|
|
8456
|
+
}
|
|
8457
|
+
}
|
|
8458
|
+
}
|
|
8459
|
+
}
|
|
8460
|
+
console.log();
|
|
8461
|
+
}
|
|
8462
|
+
const orphanEpics = epics.filter((e) => !e.prdId);
|
|
8463
|
+
if (orphanEpics.length > 0) {
|
|
8464
|
+
console.log(" Epics ohne PRD:");
|
|
8465
|
+
for (const epic of orphanEpics) {
|
|
8466
|
+
const epicStories = stories.filter((s) => s.epicId === epic.id);
|
|
8467
|
+
console.log(` ${epic.id}: ${epic.title} [${epic.status}] (${epicStories.length} Stories)`);
|
|
8468
|
+
}
|
|
8469
|
+
}
|
|
8470
|
+
} else if (subcommand === "roadmap") {
|
|
8471
|
+
const content = generateRoadmap(forgehiveDir);
|
|
8472
|
+
const roadmapPath = path36.join(projectRoot, "ROADMAP.md");
|
|
8473
|
+
fs35.writeFileSync(roadmapPath, content, "utf8");
|
|
8474
|
+
console.log(`\u2714 ROADMAP.md generiert: ${roadmapPath}`);
|
|
8475
|
+
} else {
|
|
8476
|
+
console.error("Verf\xFCgbar: fh product prd | list | show | status | roadmap");
|
|
8477
|
+
}
|
|
7619
8478
|
} else if (command === "velocity") {
|
|
7620
|
-
const velocityFile =
|
|
8479
|
+
const velocityFile = path36.join(forgehiveDir, "memory", "velocity.md");
|
|
7621
8480
|
if (subcommand === "record") {
|
|
7622
8481
|
const sprintNum = parseInt(rest[0] ?? "0", 10);
|
|
7623
8482
|
const committed = rest.includes("--committed") ? parseInt(rest[rest.indexOf("--committed") + 1], 10) : NaN;
|
|
@@ -7636,7 +8495,7 @@ Setze diese Umgebungsvariablen:`);
|
|
|
7636
8495
|
console.error("Verf\xFCgbar: fh velocity show | record <N> --committed N --delivered N");
|
|
7637
8496
|
}
|
|
7638
8497
|
} else if (command === "docs") {
|
|
7639
|
-
const docsDir =
|
|
8498
|
+
const docsDir = path36.join(projectRoot, "docs");
|
|
7640
8499
|
if (!subcommand || subcommand === "list") {
|
|
7641
8500
|
const existing = listExistingDocs(projectRoot);
|
|
7642
8501
|
if (existing.length === 0) {
|
|
@@ -7650,30 +8509,29 @@ Setze diese Umgebungsvariablen:`);
|
|
|
7650
8509
|
console.log(" fh docs adr <titel> \u2014 Architecture Decision Record");
|
|
7651
8510
|
} else {
|
|
7652
8511
|
console.log(`Vorhandene Dokumentation (${existing.length} Dateien):`);
|
|
7653
|
-
for (const d of existing) console.log(` ${
|
|
8512
|
+
for (const d of existing) console.log(` ${path36.relative(projectRoot, d)}`);
|
|
7654
8513
|
console.log("");
|
|
7655
8514
|
console.log("Aktualisieren: fh docs user | api | onboard | changelog");
|
|
7656
8515
|
}
|
|
7657
8516
|
} else if (subcommand === "user") {
|
|
7658
|
-
|
|
8517
|
+
fs35.mkdirSync(docsDir, { recursive: true });
|
|
7659
8518
|
const outputArg = rest.includes("--output") ? rest[rest.indexOf("--output") + 1] : null;
|
|
7660
|
-
const outputPath = outputArg ??
|
|
8519
|
+
const outputPath = outputArg ?? path36.join(docsDir, "user-guide.md");
|
|
7661
8520
|
const guide = generateUserGuide(projectRoot, forgehiveDir);
|
|
7662
|
-
|
|
8521
|
+
fs35.writeFileSync(outputPath, guide, "utf8");
|
|
7663
8522
|
console.log(`\u2714 User Guide geschrieben: ${outputPath}`);
|
|
7664
8523
|
} else if (subcommand === "api") {
|
|
7665
|
-
|
|
8524
|
+
fs35.mkdirSync(docsDir, { recursive: true });
|
|
7666
8525
|
const outputArg = rest.includes("--output") ? rest[rest.indexOf("--output") + 1] : null;
|
|
7667
|
-
const outputPath = outputArg ??
|
|
8526
|
+
const outputPath = outputArg ?? path36.join(docsDir, "api.md");
|
|
7668
8527
|
const ref = generateApiReference(projectRoot);
|
|
7669
|
-
|
|
8528
|
+
fs35.writeFileSync(outputPath, ref, "utf8");
|
|
7670
8529
|
console.log(`\u2714 API-Referenz geschrieben: ${outputPath}`);
|
|
7671
8530
|
} else if (subcommand === "onboard") {
|
|
7672
8531
|
const outputArg = rest.includes("--output") ? rest[rest.indexOf("--output") + 1] : null;
|
|
7673
|
-
const outputPath = outputArg ??
|
|
7674
|
-
fs31.mkdirSync(path32.dirname(outputPath), { recursive: true });
|
|
8532
|
+
const outputPath = outputArg ?? path36.join(projectRoot, "ONBOARDING.md");
|
|
7675
8533
|
const doc = generateOnboardingDoc(projectRoot, forgehiveDir);
|
|
7676
|
-
|
|
8534
|
+
fs35.writeFileSync(outputPath, doc, "utf8");
|
|
7677
8535
|
console.log(`\u2714 Onboarding-Dokument geschrieben: ${outputPath}`);
|
|
7678
8536
|
} else if (subcommand === "changelog") {
|
|
7679
8537
|
const sinceArg = rest.includes("--since") ? rest[rest.indexOf("--since") + 1] : null;
|
|
@@ -7681,16 +8539,17 @@ Setze diese Umgebungsvariablen:`);
|
|
|
7681
8539
|
const since = sinceArg ?? getLatestTag(projectRoot) ?? void 0;
|
|
7682
8540
|
const rawLog = getGitLogSince(projectRoot, since);
|
|
7683
8541
|
const commits = parseGitLog(rawLog);
|
|
7684
|
-
let
|
|
8542
|
+
let pkg = {};
|
|
7685
8543
|
try {
|
|
7686
|
-
|
|
8544
|
+
pkg = JSON.parse(fs35.readFileSync(path36.join(projectRoot, "package.json"), "utf8"));
|
|
7687
8545
|
} catch {
|
|
7688
8546
|
}
|
|
8547
|
+
const pkgVersion = pkg.version ?? "unreleased";
|
|
7689
8548
|
const md = formatChangelog(commits, pkgVersion);
|
|
7690
|
-
const outputPath = outputArg ??
|
|
8549
|
+
const outputPath = outputArg ?? path36.join(projectRoot, "CHANGELOG.md");
|
|
7691
8550
|
let existing = "";
|
|
7692
|
-
if (
|
|
7693
|
-
|
|
8551
|
+
if (fs35.existsSync(outputPath)) existing = fs35.readFileSync(outputPath, "utf8");
|
|
8552
|
+
fs35.writeFileSync(outputPath, md + "\n\n" + existing, "utf8");
|
|
7694
8553
|
console.log(`\u2714 CHANGELOG.md aktualisiert (${commits.length} Commits)`);
|
|
7695
8554
|
} else if (subcommand === "adr") {
|
|
7696
8555
|
const title = rest.join(" ");
|
|
@@ -7698,15 +8557,148 @@ Setze diese Umgebungsvariablen:`);
|
|
|
7698
8557
|
console.error("Usage: fh docs adr <titel>");
|
|
7699
8558
|
process.exit(1);
|
|
7700
8559
|
}
|
|
7701
|
-
const
|
|
7702
|
-
|
|
7703
|
-
|
|
8560
|
+
const adrsDir = path36.join(forgehiveDir, "memory", "adrs");
|
|
8561
|
+
fs35.mkdirSync(adrsDir, { recursive: true });
|
|
8562
|
+
const existing = fs35.readdirSync(adrsDir).filter((f) => f.endsWith(".md")).length;
|
|
8563
|
+
const adrId = String(existing + 1).padStart(4, "0");
|
|
8564
|
+
const slug = title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
8565
|
+
const filename = `${adrId}-${slug}.md`;
|
|
8566
|
+
const content = `# ADR-${adrId}: ${title}
|
|
8567
|
+
|
|
8568
|
+
**Datum:** ${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}
|
|
8569
|
+
**Status:** proposed
|
|
8570
|
+
|
|
8571
|
+
## Kontext
|
|
8572
|
+
|
|
8573
|
+
(Beschreibe das Problem oder die Situation.)
|
|
8574
|
+
|
|
8575
|
+
## Entscheidung
|
|
8576
|
+
|
|
8577
|
+
(Beschreibe die getroffene Entscheidung.)
|
|
8578
|
+
|
|
8579
|
+
## Konsequenzen
|
|
8580
|
+
|
|
8581
|
+
(Beschreibe die Auswirkungen dieser Entscheidung.)
|
|
8582
|
+
`;
|
|
8583
|
+
fs35.writeFileSync(path36.join(adrsDir, filename), content, "utf8");
|
|
8584
|
+
console.log(`\u2714 ADR erstellt: .forgehive/memory/adrs/${filename}`);
|
|
7704
8585
|
} else {
|
|
7705
8586
|
console.error("Verf\xFCgbar: fh docs [list|user|api|onboard|changelog|adr <titel>]");
|
|
7706
8587
|
}
|
|
8588
|
+
} else if (command === "github") {
|
|
8589
|
+
const storiesDir = path36.join(forgehiveDir, "memory", "stories");
|
|
8590
|
+
if (subcommand === "setup") {
|
|
8591
|
+
const readline = await import("node:readline/promises");
|
|
8592
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
8593
|
+
let token;
|
|
8594
|
+
let repo;
|
|
8595
|
+
try {
|
|
8596
|
+
token = (await rl.question(" GitHub-Token (ghp_...): ")).trim();
|
|
8597
|
+
repo = (await rl.question(" Repository (owner/repo): ")).trim();
|
|
8598
|
+
} catch (e) {
|
|
8599
|
+
console.error(` \u2717 Eingabe fehlgeschlagen: ${e.message}`);
|
|
8600
|
+
process.exit(1);
|
|
8601
|
+
} finally {
|
|
8602
|
+
rl.close();
|
|
8603
|
+
}
|
|
8604
|
+
if (!token) {
|
|
8605
|
+
console.error(" \u2717 Kein Token eingegeben.");
|
|
8606
|
+
process.exit(1);
|
|
8607
|
+
}
|
|
8608
|
+
if (!token.startsWith("ghp_") && !token.startsWith("github_pat_") && !token.startsWith("gho_")) {
|
|
8609
|
+
console.warn(" \u26A0 Token-Format unbekannt \u2014 erwartet ghp_*, github_pat_*, oder gho_*");
|
|
8610
|
+
}
|
|
8611
|
+
const repoParts = repo.split("/");
|
|
8612
|
+
if (repoParts.length !== 2 || repoParts[0].length === 0 || repoParts[1].length === 0) {
|
|
8613
|
+
console.error(` \u2717 Ung\xFCltiges Repo-Format: "${repo}" \u2014 erwartet: owner/repo`);
|
|
8614
|
+
process.exit(1);
|
|
8615
|
+
}
|
|
8616
|
+
setCredentials("github", { GITHUB_TOKEN: token });
|
|
8617
|
+
saveGitHubConfig(forgehiveDir, { repo });
|
|
8618
|
+
console.log(" \u2713 Token gespeichert");
|
|
8619
|
+
console.log(" \u2713 Repo gespeichert in .forgehive/github.yaml");
|
|
8620
|
+
} else if (subcommand === "sync") {
|
|
8621
|
+
let config;
|
|
8622
|
+
try {
|
|
8623
|
+
config = loadGitHubConfig(forgehiveDir);
|
|
8624
|
+
} catch (e) {
|
|
8625
|
+
console.error(` \u2717 ${e.message}`);
|
|
8626
|
+
process.exit(1);
|
|
8627
|
+
}
|
|
8628
|
+
const creds = getCredentials("github");
|
|
8629
|
+
if (!creds?.GITHUB_TOKEN) {
|
|
8630
|
+
console.error(" \u2717 Kein GitHub-Token gefunden. F\xFChre zuerst aus: fh github setup");
|
|
8631
|
+
process.exit(1);
|
|
8632
|
+
}
|
|
8633
|
+
const [owner, repo] = config.repo.split("/");
|
|
8634
|
+
console.log(` Lade Issues von ${config.repo}...`);
|
|
8635
|
+
let issues;
|
|
8636
|
+
try {
|
|
8637
|
+
issues = await fetchOpenIssues(owner, repo, creds.GITHUB_TOKEN);
|
|
8638
|
+
} catch (e) {
|
|
8639
|
+
console.error(` \u2717 ${e.message}`);
|
|
8640
|
+
process.exit(1);
|
|
8641
|
+
}
|
|
8642
|
+
let created = 0;
|
|
8643
|
+
let skipped = 0;
|
|
8644
|
+
for (const issue of issues) {
|
|
8645
|
+
if (issueAlreadySynced(storiesDir, issue.number)) {
|
|
8646
|
+
console.log(` \u2013 #${issue.number} "${issue.title}" \u2192 bereits vorhanden (\xFCbersprungen)`);
|
|
8647
|
+
skipped++;
|
|
8648
|
+
continue;
|
|
8649
|
+
}
|
|
8650
|
+
const story = createStory(storiesDir, issue.title);
|
|
8651
|
+
appendGitHubRef(storiesDir, story.id, issue, config.repo);
|
|
8652
|
+
console.log(` \u2713 #${issue.number} "${issue.title}" \u2192 ${story.id} erstellt`);
|
|
8653
|
+
created++;
|
|
8654
|
+
}
|
|
8655
|
+
console.log(`
|
|
8656
|
+
${created} ${created === 1 ? "Story" : "Stories"} erstellt, ${skipped} \xFCbersprungen.`);
|
|
8657
|
+
} else if (subcommand === "pr") {
|
|
8658
|
+
const prNumberRaw = rest[0];
|
|
8659
|
+
if (!prNumberRaw || isNaN(parseInt(prNumberRaw, 10))) {
|
|
8660
|
+
console.error(" Usage: fh github pr <number>");
|
|
8661
|
+
process.exit(1);
|
|
8662
|
+
}
|
|
8663
|
+
const prNumber = parseInt(prNumberRaw, 10);
|
|
8664
|
+
let config;
|
|
8665
|
+
try {
|
|
8666
|
+
config = loadGitHubConfig(forgehiveDir);
|
|
8667
|
+
} catch (e) {
|
|
8668
|
+
console.error(` \u2717 ${e.message}`);
|
|
8669
|
+
process.exit(1);
|
|
8670
|
+
}
|
|
8671
|
+
const creds = getCredentials("github");
|
|
8672
|
+
if (!creds?.GITHUB_TOKEN) {
|
|
8673
|
+
console.error(" \u2717 Kein GitHub-Token gefunden. F\xFChre zuerst aus: fh github setup");
|
|
8674
|
+
process.exit(1);
|
|
8675
|
+
}
|
|
8676
|
+
const [owner, repo] = config.repo.split("/");
|
|
8677
|
+
console.log(` Lade PR #${prNumber} von ${config.repo}...`);
|
|
8678
|
+
let pr, files;
|
|
8679
|
+
try {
|
|
8680
|
+
[pr, files] = await Promise.all([
|
|
8681
|
+
fetchPR(owner, repo, prNumber, creds.GITHUB_TOKEN),
|
|
8682
|
+
fetchPRFiles(owner, repo, prNumber, creds.GITHUB_TOKEN)
|
|
8683
|
+
]);
|
|
8684
|
+
} catch (e) {
|
|
8685
|
+
console.error(` \u2717 ${e.message}`);
|
|
8686
|
+
process.exit(1);
|
|
8687
|
+
}
|
|
8688
|
+
const contextMd = formatPRContext(pr, files, config.repo);
|
|
8689
|
+
fs35.mkdirSync(forgehiveDir, { recursive: true });
|
|
8690
|
+
const outputPath = path36.join(forgehiveDir, `github-pr-${prNumber}.md`);
|
|
8691
|
+
fs35.writeFileSync(outputPath, contextMd, "utf8");
|
|
8692
|
+
console.log(` \u2713 PR-Kontext gespeichert: .forgehive/github-pr-${prNumber}.md`);
|
|
8693
|
+
console.log("");
|
|
8694
|
+
console.log(` PR context gespeichert. Starte Review: fh party run review`);
|
|
8695
|
+
} else {
|
|
8696
|
+
console.log("Verf\xFCgbar: fh github setup | fh github sync | fh github pr <number>");
|
|
8697
|
+
console.log("Hilfe: fh --help");
|
|
8698
|
+
}
|
|
7707
8699
|
} else {
|
|
7708
8700
|
console.error("Unbekannter Befehl: " + command);
|
|
7709
|
-
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");
|
|
8701
|
+
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> | github [setup|sync|pr] | cost | memory | skills | party | wire | mcp | security");
|
|
7710
8702
|
console.error("Hilfe: fh --help");
|
|
7711
8703
|
process.exit(1);
|
|
7712
8704
|
}
|