forgehive 0.7.9 → 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 +140 -11
- package/dist/cli.js +1126 -194
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -2751,8 +2751,8 @@ var init_harness = __esm({
|
|
|
2751
2751
|
|
|
2752
2752
|
// src/cli.ts
|
|
2753
2753
|
init_js_yaml();
|
|
2754
|
-
import
|
|
2755
|
-
import
|
|
2754
|
+
import fs35 from "node:fs";
|
|
2755
|
+
import path36 from "node:path";
|
|
2756
2756
|
import { spawnSync as spawnSync12 } from "node:child_process";
|
|
2757
2757
|
import { createInterface } from "node:readline";
|
|
2758
2758
|
|
|
@@ -5736,7 +5736,44 @@ function generateMap(projectRoot2) {
|
|
|
5736
5736
|
totalLines: files.reduce((sum, f) => sum + f.lines, 0)
|
|
5737
5737
|
};
|
|
5738
5738
|
}
|
|
5739
|
-
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) {
|
|
5740
5777
|
const lines = [];
|
|
5741
5778
|
lines.push("# Codebase Map");
|
|
5742
5779
|
lines.push(`Generated: ${map2.generatedAt}`);
|
|
@@ -5766,13 +5803,199 @@ function formatMap(map2) {
|
|
|
5766
5803
|
} else {
|
|
5767
5804
|
lines.push("No internal imports detected.");
|
|
5768
5805
|
}
|
|
5806
|
+
if (semantic) {
|
|
5807
|
+
lines.push("");
|
|
5808
|
+
lines.push(formatSemanticBlock(semantic, map2.projectRoot));
|
|
5809
|
+
}
|
|
5769
5810
|
return lines.join("\n");
|
|
5770
5811
|
}
|
|
5771
5812
|
|
|
5772
|
-
// src/
|
|
5773
|
-
init_js_yaml();
|
|
5813
|
+
// src/semantic-map.ts
|
|
5774
5814
|
import fs24 from "node:fs";
|
|
5775
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";
|
|
5776
5999
|
import { spawnSync as spawnSync7 } from "node:child_process";
|
|
5777
6000
|
function getRecentCommits(projectRoot2, n = 20) {
|
|
5778
6001
|
const result = spawnSync7("git", ["log", `--oneline`, `-${n}`], {
|
|
@@ -5783,30 +6006,30 @@ function getRecentCommits(projectRoot2, n = 20) {
|
|
|
5783
6006
|
return result.stdout.trim().split("\n").filter(Boolean);
|
|
5784
6007
|
}
|
|
5785
6008
|
function readCapabilities(forgehiveDir2) {
|
|
5786
|
-
const capPath =
|
|
5787
|
-
if (!
|
|
6009
|
+
const capPath = path26.join(forgehiveDir2, "capabilities.yaml");
|
|
6010
|
+
if (!fs25.existsSync(capPath)) return {};
|
|
5788
6011
|
try {
|
|
5789
|
-
return jsYaml.load(
|
|
6012
|
+
return jsYaml.load(fs25.readFileSync(capPath, "utf8")) ?? {};
|
|
5790
6013
|
} catch {
|
|
5791
6014
|
return {};
|
|
5792
6015
|
}
|
|
5793
6016
|
}
|
|
5794
6017
|
function readMemoryFiles(forgehiveDir2) {
|
|
5795
|
-
const memDir =
|
|
5796
|
-
if (!
|
|
6018
|
+
const memDir = path26.join(forgehiveDir2, "memory");
|
|
6019
|
+
if (!fs25.existsSync(memDir)) return {};
|
|
5797
6020
|
const result = {};
|
|
5798
|
-
for (const f of
|
|
5799
|
-
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");
|
|
5800
6023
|
}
|
|
5801
6024
|
return result;
|
|
5802
6025
|
}
|
|
5803
6026
|
function listAdrs2(forgehiveDir2) {
|
|
5804
|
-
const adrsDir =
|
|
5805
|
-
if (!
|
|
5806
|
-
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"));
|
|
5807
6030
|
}
|
|
5808
6031
|
function generateOnboardingDoc(projectRoot2, forgehiveDir2) {
|
|
5809
|
-
const projectName =
|
|
6032
|
+
const projectName = path26.basename(projectRoot2);
|
|
5810
6033
|
const caps = readCapabilities(forgehiveDir2);
|
|
5811
6034
|
const memFiles = readMemoryFiles(forgehiveDir2);
|
|
5812
6035
|
const commits = getRecentCommits(projectRoot2);
|
|
@@ -6021,13 +6244,13 @@ function getMetricsGitLog(projectRoot2, since) {
|
|
|
6021
6244
|
}
|
|
6022
6245
|
|
|
6023
6246
|
// src/sync.ts
|
|
6024
|
-
import
|
|
6025
|
-
import
|
|
6247
|
+
import fs26 from "node:fs";
|
|
6248
|
+
import path27 from "node:path";
|
|
6026
6249
|
import { spawnSync as spawnSync10 } from "node:child_process";
|
|
6027
6250
|
function getSyncStatus(forgehiveDir2) {
|
|
6028
|
-
const memDir =
|
|
6029
|
-
const files =
|
|
6030
|
-
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);
|
|
6031
6254
|
const configResult = spawnSync10(
|
|
6032
6255
|
"git",
|
|
6033
6256
|
["config", "--get", "forgehive.sync-remote"],
|
|
@@ -6043,18 +6266,18 @@ function getSyncStatus(forgehiveDir2) {
|
|
|
6043
6266
|
return { files, hasRemote: remote !== null, remote, branch };
|
|
6044
6267
|
}
|
|
6045
6268
|
function pushSync(forgehiveDir2, remote = "origin", branch = "forgehive-memory") {
|
|
6046
|
-
const projectRoot2 =
|
|
6047
|
-
const memDir =
|
|
6048
|
-
if (!
|
|
6269
|
+
const projectRoot2 = path27.dirname(forgehiveDir2);
|
|
6270
|
+
const memDir = path27.join(forgehiveDir2, "memory");
|
|
6271
|
+
if (!fs26.existsSync(memDir)) {
|
|
6049
6272
|
return { success: false, message: "Kein Memory-Verzeichnis gefunden.", filesCommitted: 0 };
|
|
6050
6273
|
}
|
|
6051
|
-
const files =
|
|
6274
|
+
const files = fs26.readdirSync(memDir).filter((f) => f.endsWith(".md"));
|
|
6052
6275
|
if (files.length === 0) {
|
|
6053
6276
|
return { success: false, message: "Keine Memory-Dateien gefunden.", filesCommitted: 0 };
|
|
6054
6277
|
}
|
|
6055
6278
|
const addResult = spawnSync10(
|
|
6056
6279
|
"git",
|
|
6057
|
-
["add",
|
|
6280
|
+
["add", path27.join(".forgehive", "memory")],
|
|
6058
6281
|
{ cwd: projectRoot2, encoding: "utf8" }
|
|
6059
6282
|
);
|
|
6060
6283
|
if (addResult.status !== 0) {
|
|
@@ -6082,9 +6305,9 @@ function pushSync(forgehiveDir2, remote = "origin", branch = "forgehive-memory")
|
|
|
6082
6305
|
return { success: true, message: `Memory gepusht nach ${remote}/${branch}`, filesCommitted: files.length };
|
|
6083
6306
|
}
|
|
6084
6307
|
function pullSync(forgehiveDir2, remote = "origin", branch = "forgehive-memory") {
|
|
6085
|
-
const projectRoot2 =
|
|
6086
|
-
const memDir =
|
|
6087
|
-
|
|
6308
|
+
const projectRoot2 = path27.dirname(forgehiveDir2);
|
|
6309
|
+
const memDir = path27.join(forgehiveDir2, "memory");
|
|
6310
|
+
fs26.mkdirSync(memDir, { recursive: true });
|
|
6088
6311
|
const fetchResult = spawnSync10(
|
|
6089
6312
|
"git",
|
|
6090
6313
|
["fetch", remote, branch],
|
|
@@ -6104,16 +6327,16 @@ function pullSync(forgehiveDir2, remote = "origin", branch = "forgehive-memory")
|
|
|
6104
6327
|
const remoteFiles = listResult.stdout.trim().split("\n").filter(Boolean);
|
|
6105
6328
|
const imported = [];
|
|
6106
6329
|
for (const remotePath of remoteFiles) {
|
|
6107
|
-
const filename =
|
|
6108
|
-
const localPath =
|
|
6109
|
-
if (!
|
|
6330
|
+
const filename = path27.basename(remotePath);
|
|
6331
|
+
const localPath = path27.join(memDir, filename);
|
|
6332
|
+
if (!fs26.existsSync(localPath)) {
|
|
6110
6333
|
const contentResult = spawnSync10(
|
|
6111
6334
|
"git",
|
|
6112
6335
|
["show", `${remote}/${branch}:${remotePath}`],
|
|
6113
6336
|
{ cwd: projectRoot2, encoding: "utf8" }
|
|
6114
6337
|
);
|
|
6115
6338
|
if (contentResult.status === 0) {
|
|
6116
|
-
|
|
6339
|
+
fs26.writeFileSync(localPath, contentResult.stdout, "utf8");
|
|
6117
6340
|
imported.push(filename);
|
|
6118
6341
|
}
|
|
6119
6342
|
}
|
|
@@ -6126,8 +6349,8 @@ function pullSync(forgehiveDir2, remote = "origin", branch = "forgehive-memory")
|
|
|
6126
6349
|
}
|
|
6127
6350
|
|
|
6128
6351
|
// src/background.ts
|
|
6129
|
-
import
|
|
6130
|
-
import
|
|
6352
|
+
import fs27 from "node:fs";
|
|
6353
|
+
import path28 from "node:path";
|
|
6131
6354
|
import { spawn } from "node:child_process";
|
|
6132
6355
|
var AGENT_ROLES = {
|
|
6133
6356
|
kai: "Senior Engineer \u2014 implements features and fixes bugs",
|
|
@@ -6174,23 +6397,23 @@ Instructions:
|
|
|
6174
6397
|
Work autonomously. Do not ask for clarification \u2014 use your best judgment based on the issue description and codebase context.`;
|
|
6175
6398
|
}
|
|
6176
6399
|
function runBackgroundAgent(forgehiveDir2, issueUrl, agentId) {
|
|
6177
|
-
const logsDir =
|
|
6178
|
-
|
|
6400
|
+
const logsDir = path28.join(forgehiveDir2, "background-runs");
|
|
6401
|
+
fs27.mkdirSync(logsDir, { recursive: true });
|
|
6179
6402
|
const timestamp2 = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
6180
|
-
const logFile =
|
|
6403
|
+
const logFile = path28.join(logsDir, `${agentId}-${timestamp2}.log`);
|
|
6181
6404
|
const prompt = buildAgentPrompt(issueUrl, agentId);
|
|
6182
|
-
const logStream =
|
|
6405
|
+
const logStream = fs27.openSync(logFile, "w");
|
|
6183
6406
|
const child = spawn(
|
|
6184
6407
|
"claude",
|
|
6185
6408
|
["-p", prompt, "--output-format", "text"],
|
|
6186
6409
|
{
|
|
6187
|
-
cwd:
|
|
6410
|
+
cwd: path28.dirname(forgehiveDir2),
|
|
6188
6411
|
detached: true,
|
|
6189
6412
|
stdio: ["ignore", logStream, logStream]
|
|
6190
6413
|
}
|
|
6191
6414
|
);
|
|
6192
6415
|
child.unref();
|
|
6193
|
-
|
|
6416
|
+
fs27.closeSync(logStream);
|
|
6194
6417
|
return {
|
|
6195
6418
|
pid: child.pid,
|
|
6196
6419
|
logFile,
|
|
@@ -6200,11 +6423,11 @@ function runBackgroundAgent(forgehiveDir2, issueUrl, agentId) {
|
|
|
6200
6423
|
|
|
6201
6424
|
// src/stories.ts
|
|
6202
6425
|
init_js_yaml();
|
|
6203
|
-
import
|
|
6204
|
-
import
|
|
6426
|
+
import fs28 from "node:fs";
|
|
6427
|
+
import path29 from "node:path";
|
|
6205
6428
|
function nextStoryId(storiesDir) {
|
|
6206
|
-
if (!
|
|
6207
|
-
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));
|
|
6208
6431
|
const max = existing.length > 0 ? Math.max(...existing) : 0;
|
|
6209
6432
|
return `US-${max + 1}`;
|
|
6210
6433
|
}
|
|
@@ -6236,7 +6459,7 @@ ${acLines || "- [ ] (noch nicht definiert)"}
|
|
|
6236
6459
|
}
|
|
6237
6460
|
function parseStoryFile(filePath) {
|
|
6238
6461
|
try {
|
|
6239
|
-
const content =
|
|
6462
|
+
const content = fs28.readFileSync(filePath, "utf8");
|
|
6240
6463
|
const match = content.match(/^---\n([\s\S]*?)\n---/);
|
|
6241
6464
|
if (!match) return null;
|
|
6242
6465
|
const data = jsYaml.load(match[1]);
|
|
@@ -6256,7 +6479,7 @@ function parseStoryFile(filePath) {
|
|
|
6256
6479
|
}
|
|
6257
6480
|
}
|
|
6258
6481
|
function createStory(storiesDir, title, epicId) {
|
|
6259
|
-
|
|
6482
|
+
fs28.mkdirSync(storiesDir, { recursive: true });
|
|
6260
6483
|
const id = nextStoryId(storiesDir);
|
|
6261
6484
|
const story = {
|
|
6262
6485
|
id,
|
|
@@ -6269,33 +6492,33 @@ function createStory(storiesDir, title, epicId) {
|
|
|
6269
6492
|
epicId: epicId ?? null,
|
|
6270
6493
|
status: "backlog"
|
|
6271
6494
|
};
|
|
6272
|
-
|
|
6495
|
+
fs28.writeFileSync(path29.join(storiesDir, `${id}.md`), storyToMarkdown(story), "utf8");
|
|
6273
6496
|
return story;
|
|
6274
6497
|
}
|
|
6275
6498
|
function listStories(storiesDir) {
|
|
6276
|
-
if (!
|
|
6277
|
-
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) => {
|
|
6278
6501
|
const na = parseInt(a.id.replace("US-", ""), 10);
|
|
6279
6502
|
const nb = parseInt(b.id.replace("US-", ""), 10);
|
|
6280
6503
|
return na - nb;
|
|
6281
6504
|
});
|
|
6282
6505
|
}
|
|
6283
6506
|
function getStory(storiesDir, id) {
|
|
6284
|
-
const filePath =
|
|
6285
|
-
if (!
|
|
6507
|
+
const filePath = path29.join(storiesDir, `${id}.md`);
|
|
6508
|
+
if (!fs28.existsSync(filePath)) return null;
|
|
6286
6509
|
return parseStoryFile(filePath);
|
|
6287
6510
|
}
|
|
6288
6511
|
function updateStoryPoints(storiesDir, id, points) {
|
|
6289
6512
|
const story = getStory(storiesDir, id);
|
|
6290
6513
|
if (!story) throw new Error(`Story ${id} nicht gefunden`);
|
|
6291
6514
|
story.points = points;
|
|
6292
|
-
|
|
6515
|
+
fs28.writeFileSync(path29.join(storiesDir, `${id}.md`), storyToMarkdown(story), "utf8");
|
|
6293
6516
|
}
|
|
6294
6517
|
function updateStoryStatus(storiesDir, id, status) {
|
|
6295
6518
|
const story = getStory(storiesDir, id);
|
|
6296
6519
|
if (!story) throw new Error(`Story ${id} nicht gefunden`);
|
|
6297
6520
|
story.status = status;
|
|
6298
|
-
|
|
6521
|
+
fs28.writeFileSync(path29.join(storiesDir, `${id}.md`), storyToMarkdown(story), "utf8");
|
|
6299
6522
|
}
|
|
6300
6523
|
function formatStoryCard(story) {
|
|
6301
6524
|
const points = story.points !== null ? ` \xB7 ${story.points} Punkte` : "";
|
|
@@ -6314,11 +6537,11 @@ function formatStoryCard(story) {
|
|
|
6314
6537
|
|
|
6315
6538
|
// src/epics.ts
|
|
6316
6539
|
init_js_yaml();
|
|
6317
|
-
import
|
|
6318
|
-
import
|
|
6540
|
+
import fs29 from "node:fs";
|
|
6541
|
+
import path30 from "node:path";
|
|
6319
6542
|
function nextEpicId(epicsDir) {
|
|
6320
|
-
if (!
|
|
6321
|
-
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));
|
|
6322
6545
|
const max = existing.length > 0 ? Math.max(...existing) : 0;
|
|
6323
6546
|
return `EPC-${max + 1}`;
|
|
6324
6547
|
}
|
|
@@ -6328,7 +6551,8 @@ function epicToMarkdown(epic) {
|
|
|
6328
6551
|
title: epic.title,
|
|
6329
6552
|
goal: epic.goal,
|
|
6330
6553
|
stories: epic.stories,
|
|
6331
|
-
status: epic.status
|
|
6554
|
+
status: epic.status,
|
|
6555
|
+
prdId: epic.prdId
|
|
6332
6556
|
});
|
|
6333
6557
|
const storyLines = epic.stories.map((s) => `- ${s}`).join("\n");
|
|
6334
6558
|
return `---
|
|
@@ -6345,7 +6569,7 @@ ${storyLines || "(noch keine Stories)"}
|
|
|
6345
6569
|
}
|
|
6346
6570
|
function parseEpicFile(filePath) {
|
|
6347
6571
|
try {
|
|
6348
|
-
const content =
|
|
6572
|
+
const content = fs29.readFileSync(filePath, "utf8");
|
|
6349
6573
|
const match = content.match(/^---\n([\s\S]*?)\n---/);
|
|
6350
6574
|
if (!match) return null;
|
|
6351
6575
|
const data = jsYaml.load(match[1]);
|
|
@@ -6354,30 +6578,31 @@ function parseEpicFile(filePath) {
|
|
|
6354
6578
|
title: data.title ?? "",
|
|
6355
6579
|
goal: data.goal ?? "",
|
|
6356
6580
|
stories: data.stories ?? [],
|
|
6357
|
-
status: data.status ?? "active"
|
|
6581
|
+
status: data.status ?? "active",
|
|
6582
|
+
prdId: data.prdId ?? null
|
|
6358
6583
|
};
|
|
6359
6584
|
} catch {
|
|
6360
6585
|
return null;
|
|
6361
6586
|
}
|
|
6362
6587
|
}
|
|
6363
|
-
function createEpic(epicsDir, title, goal) {
|
|
6364
|
-
|
|
6588
|
+
function createEpic(epicsDir, title, goal, prdId) {
|
|
6589
|
+
fs29.mkdirSync(epicsDir, { recursive: true });
|
|
6365
6590
|
const id = nextEpicId(epicsDir);
|
|
6366
|
-
const epic = { id, title, goal: goal ?? "", stories: [], status: "active" };
|
|
6367
|
-
|
|
6591
|
+
const epic = { id, title, goal: goal ?? "", stories: [], status: "active", prdId: prdId ?? null };
|
|
6592
|
+
fs29.writeFileSync(path30.join(epicsDir, `${id}.md`), epicToMarkdown(epic), "utf8");
|
|
6368
6593
|
return epic;
|
|
6369
6594
|
}
|
|
6370
6595
|
function listEpics(epicsDir) {
|
|
6371
|
-
if (!
|
|
6372
|
-
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) => {
|
|
6373
6598
|
const na = parseInt(a.id.replace("EPC-", ""), 10);
|
|
6374
6599
|
const nb = parseInt(b.id.replace("EPC-", ""), 10);
|
|
6375
6600
|
return na - nb;
|
|
6376
6601
|
});
|
|
6377
6602
|
}
|
|
6378
6603
|
function getEpic(epicsDir, id) {
|
|
6379
|
-
const filePath =
|
|
6380
|
-
if (!
|
|
6604
|
+
const filePath = path30.join(epicsDir, `${id}.md`);
|
|
6605
|
+
if (!fs29.existsSync(filePath)) return null;
|
|
6381
6606
|
return parseEpicFile(filePath);
|
|
6382
6607
|
}
|
|
6383
6608
|
function formatEpicCard(epic, stories) {
|
|
@@ -6399,25 +6624,188 @@ function formatEpicCard(epic, stories) {
|
|
|
6399
6624
|
return lines.join("\n");
|
|
6400
6625
|
}
|
|
6401
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
|
+
|
|
6402
6790
|
// src/velocity.ts
|
|
6403
|
-
import
|
|
6404
|
-
import
|
|
6791
|
+
import fs31 from "node:fs";
|
|
6792
|
+
import path32 from "node:path";
|
|
6405
6793
|
var HEADER = "# Sprint Velocity\n\n| Sprint | Datum | Committed | Delivered | Rate |\n|---|---|---|---|---|\n";
|
|
6406
6794
|
function recordVelocity(velocityFile, sprint, committed, delivered) {
|
|
6407
6795
|
const date = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
6408
6796
|
const rate = committed > 0 ? Math.round(delivered / committed * 100) : 0;
|
|
6409
6797
|
const row = `| Sprint ${sprint} | ${date} | ${committed} | ${delivered} | ${rate}% |
|
|
6410
6798
|
`;
|
|
6411
|
-
if (!
|
|
6412
|
-
|
|
6413
|
-
|
|
6799
|
+
if (!fs31.existsSync(velocityFile)) {
|
|
6800
|
+
fs31.mkdirSync(path32.dirname(velocityFile), { recursive: true });
|
|
6801
|
+
fs31.writeFileSync(velocityFile, HEADER + row, "utf8");
|
|
6414
6802
|
} else {
|
|
6415
|
-
|
|
6803
|
+
fs31.appendFileSync(velocityFile, row, "utf8");
|
|
6416
6804
|
}
|
|
6417
6805
|
}
|
|
6418
6806
|
function getVelocityHistory(velocityFile) {
|
|
6419
|
-
if (!
|
|
6420
|
-
const content =
|
|
6807
|
+
if (!fs31.existsSync(velocityFile)) return [];
|
|
6808
|
+
const content = fs31.readFileSync(velocityFile, "utf8");
|
|
6421
6809
|
const rows = content.split("\n").filter((l) => l.startsWith("| Sprint "));
|
|
6422
6810
|
return rows.map((row) => {
|
|
6423
6811
|
const cells = row.split("|").map((c) => c.trim()).filter(Boolean);
|
|
@@ -6458,22 +6846,22 @@ function formatVelocityReport(history) {
|
|
|
6458
6846
|
|
|
6459
6847
|
// src/docs.ts
|
|
6460
6848
|
init_js_yaml();
|
|
6461
|
-
import
|
|
6462
|
-
import
|
|
6849
|
+
import fs32 from "node:fs";
|
|
6850
|
+
import path33 from "node:path";
|
|
6463
6851
|
import { spawnSync as spawnSync11 } from "node:child_process";
|
|
6464
6852
|
var SOURCE_EXTS = [".ts", ".tsx", ".js", ".jsx", ".py", ".go"];
|
|
6465
|
-
var
|
|
6466
|
-
var
|
|
6853
|
+
var IGNORE_DIRS4 = ["node_modules", ".git", "dist", ".forgehive", "coverage", ".next", "build", "test", "__tests__", "spec"];
|
|
6854
|
+
var EXPORT_PATTERNS2 = [
|
|
6467
6855
|
/^export\s+(?:async\s+)?function\s+(\w+)/gm,
|
|
6468
6856
|
/^export\s+(?:const|let|var)\s+(\w+)/gm,
|
|
6469
6857
|
/^export\s+(?:class|interface|type|enum)\s+(\w+)/gm,
|
|
6470
6858
|
/^export\s+default\s+(?:function\s+)?(\w+)?/gm
|
|
6471
6859
|
];
|
|
6472
6860
|
function readCapabilities2(forgehiveDir2) {
|
|
6473
|
-
const capPath =
|
|
6474
|
-
if (!
|
|
6861
|
+
const capPath = path33.join(forgehiveDir2, "capabilities.yaml");
|
|
6862
|
+
if (!fs32.existsSync(capPath)) return {};
|
|
6475
6863
|
try {
|
|
6476
|
-
return jsYaml.load(
|
|
6864
|
+
return jsYaml.load(fs32.readFileSync(capPath, "utf8")) ?? {};
|
|
6477
6865
|
} catch {
|
|
6478
6866
|
return {};
|
|
6479
6867
|
}
|
|
@@ -6500,11 +6888,11 @@ function extractCapabilityInfo(caps) {
|
|
|
6500
6888
|
return { language, packageManager };
|
|
6501
6889
|
}
|
|
6502
6890
|
function readMemoryFiles2(forgehiveDir2) {
|
|
6503
|
-
const memDir =
|
|
6504
|
-
if (!
|
|
6891
|
+
const memDir = path33.join(forgehiveDir2, "memory");
|
|
6892
|
+
if (!fs32.existsSync(memDir)) return {};
|
|
6505
6893
|
const result = {};
|
|
6506
|
-
for (const f of
|
|
6507
|
-
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");
|
|
6508
6896
|
}
|
|
6509
6897
|
return result;
|
|
6510
6898
|
}
|
|
@@ -6515,7 +6903,7 @@ function getRecentCommits2(projectRoot2, n = 10) {
|
|
|
6515
6903
|
}
|
|
6516
6904
|
function extractExports(content) {
|
|
6517
6905
|
const exports = [];
|
|
6518
|
-
for (const pattern of
|
|
6906
|
+
for (const pattern of EXPORT_PATTERNS2) {
|
|
6519
6907
|
pattern.lastIndex = 0;
|
|
6520
6908
|
let m;
|
|
6521
6909
|
while ((m = pattern.exec(content)) !== null) {
|
|
@@ -6527,19 +6915,19 @@ function extractExports(content) {
|
|
|
6527
6915
|
function walkSourceFiles(dir) {
|
|
6528
6916
|
const results = [];
|
|
6529
6917
|
function walk(current) {
|
|
6530
|
-
if (!
|
|
6531
|
-
for (const entry of
|
|
6532
|
-
if (
|
|
6533
|
-
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);
|
|
6534
6922
|
if (entry.isDirectory()) walk(full);
|
|
6535
|
-
else if (entry.isFile() && SOURCE_EXTS.includes(
|
|
6923
|
+
else if (entry.isFile() && SOURCE_EXTS.includes(path33.extname(entry.name))) results.push(full);
|
|
6536
6924
|
}
|
|
6537
6925
|
}
|
|
6538
6926
|
walk(dir);
|
|
6539
6927
|
return results;
|
|
6540
6928
|
}
|
|
6541
6929
|
function generateUserGuide(projectRoot2, forgehiveDir2) {
|
|
6542
|
-
const projectName =
|
|
6930
|
+
const projectName = path33.basename(projectRoot2);
|
|
6543
6931
|
const caps = readCapabilities2(forgehiveDir2);
|
|
6544
6932
|
const { language: lang, packageManager, entryPoints } = extractCapabilityInfo(caps);
|
|
6545
6933
|
const memFiles = readMemoryFiles2(forgehiveDir2);
|
|
@@ -6618,8 +7006,8 @@ function generateUserGuide(projectRoot2, forgehiveDir2) {
|
|
|
6618
7006
|
return lines.join("\n");
|
|
6619
7007
|
}
|
|
6620
7008
|
function generateApiReference(projectRoot2) {
|
|
6621
|
-
const srcDir =
|
|
6622
|
-
const searchDir =
|
|
7009
|
+
const srcDir = path33.join(projectRoot2, "src");
|
|
7010
|
+
const searchDir = fs32.existsSync(srcDir) ? srcDir : projectRoot2;
|
|
6623
7011
|
const files = walkSourceFiles(searchDir);
|
|
6624
7012
|
const lines = [];
|
|
6625
7013
|
lines.push("# API Reference");
|
|
@@ -6633,13 +7021,13 @@ function generateApiReference(projectRoot2) {
|
|
|
6633
7021
|
for (const filePath of files) {
|
|
6634
7022
|
let content = "";
|
|
6635
7023
|
try {
|
|
6636
|
-
content =
|
|
7024
|
+
content = fs32.readFileSync(filePath, "utf8");
|
|
6637
7025
|
} catch {
|
|
6638
7026
|
continue;
|
|
6639
7027
|
}
|
|
6640
7028
|
const exports = extractExports(content);
|
|
6641
7029
|
if (exports.length === 0) continue;
|
|
6642
|
-
const relPath =
|
|
7030
|
+
const relPath = path33.relative(projectRoot2, filePath);
|
|
6643
7031
|
lines.push(`## \`${relPath}\``);
|
|
6644
7032
|
lines.push("");
|
|
6645
7033
|
lines.push("**Exports:**");
|
|
@@ -6651,27 +7039,314 @@ function generateApiReference(projectRoot2) {
|
|
|
6651
7039
|
}
|
|
6652
7040
|
function listExistingDocs(projectRoot2) {
|
|
6653
7041
|
const docs = [];
|
|
6654
|
-
const docsDir =
|
|
6655
|
-
if (
|
|
6656
|
-
for (const f of
|
|
6657
|
-
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));
|
|
6658
7046
|
}
|
|
6659
7047
|
}
|
|
6660
7048
|
const rootDocs = ["README.md", "CHANGELOG.md", "ONBOARDING.md", "CONTRIBUTING.md"];
|
|
6661
7049
|
for (const f of rootDocs) {
|
|
6662
|
-
const full =
|
|
6663
|
-
if (
|
|
7050
|
+
const full = path33.join(projectRoot2, f);
|
|
7051
|
+
if (fs32.existsSync(full)) docs.push(full);
|
|
6664
7052
|
}
|
|
6665
7053
|
return docs;
|
|
6666
7054
|
}
|
|
6667
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
|
+
|
|
6668
7343
|
// src/cli.ts
|
|
6669
7344
|
import { createRequire } from "node:module";
|
|
6670
7345
|
var require2 = createRequire(import.meta.url);
|
|
6671
7346
|
var { version } = require2("../package.json");
|
|
6672
7347
|
var [, , command, subcommand, ...rest] = process.argv;
|
|
6673
7348
|
var projectRoot = process.cwd();
|
|
6674
|
-
var forgehiveDir =
|
|
7349
|
+
var forgehiveDir = path36.join(projectRoot, ".forgehive");
|
|
6675
7350
|
if (command === "--version" || command === "-v") {
|
|
6676
7351
|
console.log(version);
|
|
6677
7352
|
process.exit(0);
|
|
@@ -6705,7 +7380,7 @@ CI
|
|
|
6705
7380
|
fh ci --init Generate GitHub Actions workflow
|
|
6706
7381
|
|
|
6707
7382
|
CODEBASE
|
|
6708
|
-
fh map
|
|
7383
|
+
fh map [--semantic] Codebase structure map + semantic graph
|
|
6709
7384
|
fh onboard [--output path] Generate ONBOARDING.md
|
|
6710
7385
|
fh changelog [--since tag] Semantic changelog from git
|
|
6711
7386
|
fh metrics [--since date] Developer productivity metrics
|
|
@@ -6716,6 +7391,11 @@ CODEBASE
|
|
|
6716
7391
|
fh docs changelog [--since tag] Generate changelog
|
|
6717
7392
|
fh docs adr "<title>" Create Architecture Decision Record
|
|
6718
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
|
+
|
|
6719
7399
|
SPRINT PLANNING
|
|
6720
7400
|
fh story create <title> [--epic EPC-N] [--points N]
|
|
6721
7401
|
fh story list [--epic EPC-N]
|
|
@@ -6729,8 +7409,10 @@ SPRINT PLANNING
|
|
|
6729
7409
|
fh velocity record <N> --committed N --delivered N
|
|
6730
7410
|
|
|
6731
7411
|
TEAM
|
|
7412
|
+
fh ask "<task>" Route a task to the right agent or party set
|
|
6732
7413
|
fh sync push|pull [--remote origin --branch forgehive-memory]
|
|
6733
7414
|
fh run <issue-url> [--agent name] [--label label]
|
|
7415
|
+
fh run "<task>" Freetext task \u2014 auto-routes like fh ask
|
|
6734
7416
|
fh memory show|clean|export|prune|snapshot
|
|
6735
7417
|
fh memory adr list|"<title>"
|
|
6736
7418
|
|
|
@@ -6751,15 +7433,15 @@ COST
|
|
|
6751
7433
|
process.exit(0);
|
|
6752
7434
|
}
|
|
6753
7435
|
function loadClaudeMdBlock() {
|
|
6754
|
-
const templatePath =
|
|
6755
|
-
|
|
7436
|
+
const templatePath = path36.join(
|
|
7437
|
+
path36.dirname(new URL(import.meta.url).pathname),
|
|
6756
7438
|
"..",
|
|
6757
7439
|
"forgehive",
|
|
6758
7440
|
"templates",
|
|
6759
7441
|
"claude-md.block.md"
|
|
6760
7442
|
);
|
|
6761
|
-
if (!
|
|
6762
|
-
return
|
|
7443
|
+
if (!fs35.existsSync(templatePath)) return "## forgehive\n\nSee .forgehive/ for configuration.";
|
|
7444
|
+
return fs35.readFileSync(templatePath, "utf8");
|
|
6763
7445
|
}
|
|
6764
7446
|
async function promptConfirm(question) {
|
|
6765
7447
|
if (!process.stdin.isTTY) return false;
|
|
@@ -6788,7 +7470,7 @@ if (command === "init") {
|
|
|
6788
7470
|
console.error(" Installation: https://git-scm.com");
|
|
6789
7471
|
process.exit(1);
|
|
6790
7472
|
}
|
|
6791
|
-
const forgehiveDirExists =
|
|
7473
|
+
const forgehiveDirExists = fs35.existsSync(forgehiveDir);
|
|
6792
7474
|
if (forgehiveDirExists && !rest.includes("--force")) {
|
|
6793
7475
|
console.log(`\u26A0 .forgehive/ existiert bereits in diesem Projekt.`);
|
|
6794
7476
|
console.log(` Nutze 'fh init --force' um neu zu initialisieren (\xFCberschreibt capabilities.yaml).`);
|
|
@@ -6806,9 +7488,9 @@ if (command === "init") {
|
|
|
6806
7488
|
const block = loadClaudeMdBlock();
|
|
6807
7489
|
writeForgehiveDir(projectRoot, scanResult, capMap, block);
|
|
6808
7490
|
const hash = computeHash(projectRoot);
|
|
6809
|
-
|
|
6810
|
-
const runtimeDir =
|
|
6811
|
-
|
|
7491
|
+
fs35.writeFileSync(path36.join(forgehiveDir, ".scan-hash"), hash, "utf8");
|
|
7492
|
+
const runtimeDir = path36.join(
|
|
7493
|
+
path36.dirname(new URL(import.meta.url).pathname),
|
|
6812
7494
|
"..",
|
|
6813
7495
|
"forgehive"
|
|
6814
7496
|
);
|
|
@@ -6850,7 +7532,7 @@ if (command === "init") {
|
|
|
6850
7532
|
process.exit(1);
|
|
6851
7533
|
}
|
|
6852
7534
|
} else if (command === "memory") {
|
|
6853
|
-
if (!
|
|
7535
|
+
if (!fs35.existsSync(forgehiveDir)) {
|
|
6854
7536
|
console.error("Fehler: .forgehive/ nicht gefunden \u2014 f\xFChre zuerst `fh init` aus");
|
|
6855
7537
|
process.exit(1);
|
|
6856
7538
|
}
|
|
@@ -6859,7 +7541,7 @@ if (command === "init") {
|
|
|
6859
7541
|
} else if (subcommand === "clean") {
|
|
6860
7542
|
cleanMemory(forgehiveDir);
|
|
6861
7543
|
} else if (subcommand === "export") {
|
|
6862
|
-
const outputPath = rest[0] ??
|
|
7544
|
+
const outputPath = rest[0] ?? path36.join(projectRoot, "forgehive-memory-export.md");
|
|
6863
7545
|
try {
|
|
6864
7546
|
exportMemory(forgehiveDir, outputPath);
|
|
6865
7547
|
} catch (err) {
|
|
@@ -6907,7 +7589,7 @@ Ausf\xFChren mit --remove um zu l\xF6schen: fh memory prune ${days} --remove`);
|
|
|
6907
7589
|
} else if (subcommand === "snapshot") {
|
|
6908
7590
|
const snapAction = rest[0];
|
|
6909
7591
|
if (snapAction === "export") {
|
|
6910
|
-
const outPath = rest[1] ??
|
|
7592
|
+
const outPath = rest[1] ?? path36.join(
|
|
6911
7593
|
projectRoot,
|
|
6912
7594
|
`forgehive-snapshot-${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}.json`
|
|
6913
7595
|
);
|
|
@@ -6947,11 +7629,11 @@ Ausf\xFChren mit --remove um zu l\xF6schen: fh memory prune ${days} --remove`);
|
|
|
6947
7629
|
process.exit(1);
|
|
6948
7630
|
}
|
|
6949
7631
|
} else if (command === "scan" && subcommand === "--update") {
|
|
6950
|
-
if (!
|
|
7632
|
+
if (!fs35.existsSync(forgehiveDir)) {
|
|
6951
7633
|
console.error("Fehler: .forgehive/ nicht gefunden \u2014 f\xFChre zuerst `fh init` aus");
|
|
6952
7634
|
process.exit(1);
|
|
6953
7635
|
}
|
|
6954
|
-
const savedHash =
|
|
7636
|
+
const savedHash = fs35.existsSync(path36.join(forgehiveDir, ".scan-hash")) ? fs35.readFileSync(path36.join(forgehiveDir, ".scan-hash"), "utf8").trim() : null;
|
|
6955
7637
|
const currentHash = computeHash(projectRoot);
|
|
6956
7638
|
if (savedHash === currentHash) {
|
|
6957
7639
|
console.log("\u2713 Keine \xC4nderungen erkannt \u2014 capabilities.yaml ist aktuell");
|
|
@@ -6959,7 +7641,7 @@ Ausf\xFChren mit --remove um zu l\xF6schen: fh memory prune ${days} --remove`);
|
|
|
6959
7641
|
}
|
|
6960
7642
|
console.log("\u{1F50D} \xC4nderungen erkannt \u2014 scanne erneut...\n");
|
|
6961
7643
|
const oldDoc = jsYaml.load(
|
|
6962
|
-
|
|
7644
|
+
fs35.readFileSync(path36.join(forgehiveDir, "capabilities.yaml"), "utf8")
|
|
6963
7645
|
);
|
|
6964
7646
|
const oldMap = { confirmed: oldDoc.capabilities.confirmed ?? [], inferred: [] };
|
|
6965
7647
|
const scanResult = scan(projectRoot);
|
|
@@ -6979,16 +7661,16 @@ Ausf\xFChren mit --remove um zu l\xF6schen: fh memory prune ${days} --remove`);
|
|
|
6979
7661
|
console.log();
|
|
6980
7662
|
const block = loadClaudeMdBlock();
|
|
6981
7663
|
writeForgehiveDir(projectRoot, scanResult, newMap, block);
|
|
6982
|
-
|
|
7664
|
+
fs35.writeFileSync(path36.join(forgehiveDir, ".scan-hash"), currentHash, "utf8");
|
|
6983
7665
|
console.log("\u2713 scan-result.yaml und capabilities.yaml aktualisiert");
|
|
6984
7666
|
console.log(" F\xFChre `fh confirm` aus um die \xC4nderungen zu best\xE4tigen");
|
|
6985
7667
|
}
|
|
6986
7668
|
} else if (command === "scan" && subcommand === "--check") {
|
|
6987
|
-
if (!
|
|
7669
|
+
if (!fs35.existsSync(path36.join(forgehiveDir, ".scan-hash"))) {
|
|
6988
7670
|
console.log("Warnung: Kein Scan-Hash gefunden. F\xFChre `fh init` aus.");
|
|
6989
7671
|
process.exit(1);
|
|
6990
7672
|
}
|
|
6991
|
-
const saved =
|
|
7673
|
+
const saved = fs35.readFileSync(path36.join(forgehiveDir, ".scan-hash"), "utf8").trim();
|
|
6992
7674
|
const current = computeHash(projectRoot);
|
|
6993
7675
|
if (saved !== current) {
|
|
6994
7676
|
console.log("\u26A0 Codebase hat sich seit letztem Scan ge\xE4ndert.");
|
|
@@ -6997,7 +7679,7 @@ Ausf\xFChren mit --remove um zu l\xF6schen: fh memory prune ${days} --remove`);
|
|
|
6997
7679
|
}
|
|
6998
7680
|
console.log("\u2713 capabilities.yaml ist aktuell");
|
|
6999
7681
|
} else if (command === "skills") {
|
|
7000
|
-
if (!
|
|
7682
|
+
if (!fs35.existsSync(forgehiveDir)) {
|
|
7001
7683
|
console.error("Fehler: .forgehive/ nicht gefunden \u2014 f\xFChre zuerst `fh init` aus");
|
|
7002
7684
|
process.exit(1);
|
|
7003
7685
|
}
|
|
@@ -7033,7 +7715,7 @@ Ausf\xFChren mit --remove um zu l\xF6schen: fh memory prune ${days} --remove`);
|
|
|
7033
7715
|
process.exit(1);
|
|
7034
7716
|
}
|
|
7035
7717
|
} else if (command === "party") {
|
|
7036
|
-
if (!
|
|
7718
|
+
if (!fs35.existsSync(forgehiveDir)) {
|
|
7037
7719
|
console.error("Fehler: .forgehive/ nicht gefunden \u2014 f\xFChre zuerst `fh init` aus");
|
|
7038
7720
|
process.exit(1);
|
|
7039
7721
|
}
|
|
@@ -7154,7 +7836,7 @@ Ausf\xFChren mit --remove um zu l\xF6schen: fh memory prune ${days} --remove`);
|
|
|
7154
7836
|
}
|
|
7155
7837
|
}
|
|
7156
7838
|
} else if (command === "wire") {
|
|
7157
|
-
if (!
|
|
7839
|
+
if (!fs35.existsSync(forgehiveDir)) {
|
|
7158
7840
|
console.error("Fehler: .forgehive/ nicht gefunden \u2014 f\xFChre zuerst `fh init` aus");
|
|
7159
7841
|
process.exit(1);
|
|
7160
7842
|
}
|
|
@@ -7193,7 +7875,7 @@ N\xE4chster Schritt: Setze die erforderlichen Umgebungsvariablen und starte Clau
|
|
|
7193
7875
|
const limitIdx = allArgs.indexOf("--limit");
|
|
7194
7876
|
const alertIdx = allArgs.indexOf("--alert");
|
|
7195
7877
|
if (limitIdx !== -1) {
|
|
7196
|
-
if (!
|
|
7878
|
+
if (!fs35.existsSync(forgehiveDir)) {
|
|
7197
7879
|
console.error("Fehler: .forgehive/ nicht gefunden \u2014 f\xFChre zuerst `fh init` aus");
|
|
7198
7880
|
process.exit(1);
|
|
7199
7881
|
}
|
|
@@ -7219,14 +7901,14 @@ N\xE4chster Schritt: Setze die erforderlichen Umgebungsvariablen und starte Clau
|
|
|
7219
7901
|
}
|
|
7220
7902
|
const sessions = parseCostSessions(projectRoot);
|
|
7221
7903
|
console.log(formatCostReport(sessions, range));
|
|
7222
|
-
if (
|
|
7904
|
+
if (fs35.existsSync(forgehiveDir)) {
|
|
7223
7905
|
const total = sessions.reduce((s, x) => s + x.estimatedCostUsd, 0);
|
|
7224
7906
|
const status = checkSpendStatus(forgehiveDir, total);
|
|
7225
7907
|
if (status.message) console.log("\n" + status.message);
|
|
7226
7908
|
}
|
|
7227
7909
|
}
|
|
7228
7910
|
} else if (command === "watch") {
|
|
7229
|
-
if (!
|
|
7911
|
+
if (!fs35.existsSync(forgehiveDir)) {
|
|
7230
7912
|
console.error("Fehler: .forgehive/ nicht gefunden \u2014 f\xFChre zuerst `fh init` aus");
|
|
7231
7913
|
process.exit(1);
|
|
7232
7914
|
}
|
|
@@ -7242,7 +7924,7 @@ N\xE4chster Schritt: Setze die erforderlichen Umgebungsvariablen und starte Clau
|
|
|
7242
7924
|
process.exit(0);
|
|
7243
7925
|
});
|
|
7244
7926
|
} else if (command === "mcp") {
|
|
7245
|
-
if (!
|
|
7927
|
+
if (!fs35.existsSync(forgehiveDir)) {
|
|
7246
7928
|
console.error("Fehler: .forgehive/ nicht gefunden \u2014 f\xFChre zuerst `fh init` aus");
|
|
7247
7929
|
process.exit(1);
|
|
7248
7930
|
}
|
|
@@ -7353,7 +8035,7 @@ Setze diese Umgebungsvariablen:`);
|
|
|
7353
8035
|
process.exit(1);
|
|
7354
8036
|
}
|
|
7355
8037
|
} else if (command === "security") {
|
|
7356
|
-
if (!
|
|
8038
|
+
if (!fs35.existsSync(forgehiveDir)) {
|
|
7357
8039
|
console.error("Fehler: .forgehive/ nicht gefunden \u2014 f\xFChre zuerst `fh init` aus");
|
|
7358
8040
|
process.exit(1);
|
|
7359
8041
|
}
|
|
@@ -7439,8 +8121,8 @@ Setze diese Umgebungsvariablen:`);
|
|
|
7439
8121
|
`);
|
|
7440
8122
|
const report = generateSecurityReport(projectRoot, forgehiveDir, mode);
|
|
7441
8123
|
const text = formatSecurityReport(report);
|
|
7442
|
-
const reportPath =
|
|
7443
|
-
|
|
8124
|
+
const reportPath = path36.join(forgehiveDir, "security-report.md");
|
|
8125
|
+
fs35.writeFileSync(reportPath, text, "utf8");
|
|
7444
8126
|
console.log(text);
|
|
7445
8127
|
console.log(`
|
|
7446
8128
|
\u2713 Report gespeichert: ${reportPath}`);
|
|
@@ -7452,8 +8134,8 @@ Setze diese Umgebungsvariablen:`);
|
|
|
7452
8134
|
} else if (subcommand === "permissions") {
|
|
7453
8135
|
const { writePermissions: writePermissions2 } = await Promise.resolve().then(() => (init_harness(), harness_exports));
|
|
7454
8136
|
writePermissions2(forgehiveDir);
|
|
7455
|
-
const permPath =
|
|
7456
|
-
console.log(
|
|
8137
|
+
const permPath = path36.join(forgehiveDir, "harness", "permissions.yaml");
|
|
8138
|
+
console.log(fs35.readFileSync(permPath, "utf8"));
|
|
7457
8139
|
} else {
|
|
7458
8140
|
console.error(`Unbekannter security-Subcommand: ${subcommand}`);
|
|
7459
8141
|
console.error("Verf\xFCgbar: scan | deps | audit | report [gdpr|soc2|hipaa|none] | permissions");
|
|
@@ -7465,36 +8147,62 @@ Setze diese Umgebungsvariablen:`);
|
|
|
7465
8147
|
const failOnArg = allCiArgs.includes("--fail-on") ? allCiArgs[allCiArgs.indexOf("--fail-on") + 1] : "high";
|
|
7466
8148
|
const initFlag = allCiArgs.includes("--init");
|
|
7467
8149
|
if (initFlag) {
|
|
7468
|
-
const ghDir =
|
|
7469
|
-
|
|
7470
|
-
const outPath =
|
|
7471
|
-
|
|
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");
|
|
7472
8154
|
console.log(`\u2714 GitHub Actions workflow geschrieben: ${outPath}`);
|
|
7473
8155
|
} else {
|
|
7474
8156
|
const report = generateCiReport(projectRoot, forgehiveDir, failOnArg);
|
|
7475
8157
|
const output = formatCiReport(report, format);
|
|
7476
8158
|
console.log(output);
|
|
7477
8159
|
if (format === "json") {
|
|
7478
|
-
|
|
7479
|
-
|
|
8160
|
+
fs35.mkdirSync(forgehiveDir, { recursive: true });
|
|
8161
|
+
fs35.writeFileSync(path36.join(forgehiveDir, "ci-report.json"), output, "utf8");
|
|
7480
8162
|
}
|
|
7481
8163
|
if (report.status === "fail") process.exit(1);
|
|
7482
8164
|
}
|
|
7483
8165
|
} else if (command === "map") {
|
|
8166
|
+
const semanticFlag = rest.includes("--semantic");
|
|
7484
8167
|
const map2 = generateMap(projectRoot);
|
|
7485
|
-
const
|
|
7486
|
-
const mapPath =
|
|
7487
|
-
|
|
7488
|
-
|
|
7489
|
-
|
|
7490
|
-
|
|
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(`
|
|
7491
8185
|
\u2714 Codebase-Map gespeichert: ${mapPath}`);
|
|
8186
|
+
}
|
|
7492
8187
|
} else if (command === "onboard") {
|
|
7493
8188
|
const outputArg = rest.includes("--output") ? rest[rest.indexOf("--output") + 1] : null;
|
|
7494
|
-
const outputPath = outputArg ??
|
|
8189
|
+
const outputPath = outputArg ?? path36.join(projectRoot, "ONBOARDING.md");
|
|
7495
8190
|
const doc = generateOnboardingDoc(projectRoot, forgehiveDir);
|
|
7496
|
-
|
|
8191
|
+
fs35.writeFileSync(outputPath, doc, "utf8");
|
|
7497
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();
|
|
7498
8206
|
} else if (command === "changelog") {
|
|
7499
8207
|
const sinceArg = rest.includes("--since") ? rest[rest.indexOf("--since") + 1] : null;
|
|
7500
8208
|
const outputArg = rest.includes("--output") ? rest[rest.indexOf("--output") + 1] : null;
|
|
@@ -7503,28 +8211,28 @@ Setze diese Umgebungsvariablen:`);
|
|
|
7503
8211
|
const commits = parseGitLog(rawLog);
|
|
7504
8212
|
let version2 = "unreleased";
|
|
7505
8213
|
try {
|
|
7506
|
-
const pkgPath =
|
|
7507
|
-
if (
|
|
7508
|
-
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, ""));
|
|
7509
8217
|
version2 = pkg.version ?? "unreleased";
|
|
7510
8218
|
}
|
|
7511
8219
|
} catch {
|
|
7512
8220
|
}
|
|
7513
8221
|
const md = formatChangelog(commits, version2);
|
|
7514
|
-
const outputPath = outputArg ??
|
|
8222
|
+
const outputPath = outputArg ?? path36.join(projectRoot, "CHANGELOG.md");
|
|
7515
8223
|
let existing = "";
|
|
7516
|
-
if (
|
|
7517
|
-
|
|
8224
|
+
if (fs35.existsSync(outputPath)) existing = fs35.readFileSync(outputPath, "utf8");
|
|
8225
|
+
fs35.writeFileSync(outputPath, md + "\n\n" + existing, "utf8");
|
|
7518
8226
|
console.log(`\u2714 CHANGELOG.md aktualisiert: ${outputPath}`);
|
|
7519
8227
|
console.log(` ${commits.length} Commits verarbeitet`);
|
|
7520
8228
|
} else if (command === "metrics") {
|
|
7521
8229
|
const sinceArg = rest.includes("--since") ? rest[rest.indexOf("--since") + 1] : void 0;
|
|
7522
8230
|
const rawLog = getMetricsGitLog(projectRoot, sinceArg);
|
|
7523
8231
|
const stats = parseCommitStats(rawLog);
|
|
7524
|
-
const md = formatMetrics(stats,
|
|
7525
|
-
const metricsPath =
|
|
7526
|
-
|
|
7527
|
-
|
|
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");
|
|
7528
8236
|
console.log(md);
|
|
7529
8237
|
console.log(`
|
|
7530
8238
|
\u2714 Metrics gespeichert: ${metricsPath}`);
|
|
@@ -7552,14 +8260,26 @@ Setze diese Umgebungsvariablen:`);
|
|
|
7552
8260
|
console.error("Usage: fh run <issue-url> [--agent <name>]");
|
|
7553
8261
|
process.exit(1);
|
|
7554
8262
|
}
|
|
7555
|
-
const
|
|
7556
|
-
|
|
7557
|
-
|
|
7558
|
-
|
|
7559
|
-
|
|
7560
|
-
|
|
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
|
+
}
|
|
7561
8281
|
} else if (command === "story") {
|
|
7562
|
-
const storiesDir =
|
|
8282
|
+
const storiesDir = path36.join(forgehiveDir, "memory", "stories");
|
|
7563
8283
|
if (subcommand === "create") {
|
|
7564
8284
|
const title = rest.filter((r) => !r.startsWith("--")).join(" ");
|
|
7565
8285
|
const epicArg = rest.includes("--epic") ? rest[rest.indexOf("--epic") + 1] : void 0;
|
|
@@ -7570,7 +8290,7 @@ Setze diese Umgebungsvariablen:`);
|
|
|
7570
8290
|
}
|
|
7571
8291
|
const story = createStory(storiesDir, title, epicArg);
|
|
7572
8292
|
if (pointsArg) updateStoryPoints(storiesDir, story.id, pointsArg);
|
|
7573
|
-
console.log(`\u2714 ${story.id} erstellt: ${
|
|
8293
|
+
console.log(`\u2714 ${story.id} erstellt: ${path36.join(storiesDir, story.id + ".md")}`);
|
|
7574
8294
|
console.log(` Bearbeite die Datei um Acceptance Criteria hinzuzuf\xFCgen.`);
|
|
7575
8295
|
} else if (subcommand === "list") {
|
|
7576
8296
|
const epicFilter = rest.includes("--epic") ? rest[rest.indexOf("--epic") + 1] : null;
|
|
@@ -7619,17 +8339,32 @@ Setze diese Umgebungsvariablen:`);
|
|
|
7619
8339
|
console.error("Verf\xFCgbar: fh story create | list | show | done");
|
|
7620
8340
|
}
|
|
7621
8341
|
} else if (command === "epic") {
|
|
7622
|
-
const epicsDir =
|
|
7623
|
-
const storiesDir =
|
|
8342
|
+
const epicsDir = path36.join(forgehiveDir, "memory", "epics");
|
|
8343
|
+
const storiesDir = path36.join(forgehiveDir, "memory", "stories");
|
|
7624
8344
|
if (subcommand === "create") {
|
|
7625
|
-
const title = rest.filter((r) => !r.startsWith("--")).join(" ");
|
|
7626
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(" ");
|
|
7627
8356
|
if (!title) {
|
|
7628
|
-
console.error("Usage: fh epic create <titel> [--goal <ziel>]");
|
|
8357
|
+
console.error("Usage: fh epic create <titel> [--goal <ziel>] [--prd PRD-N]");
|
|
7629
8358
|
process.exit(1);
|
|
7630
8359
|
}
|
|
7631
|
-
|
|
7632
|
-
|
|
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}`);
|
|
7633
8368
|
} else if (subcommand === "list") {
|
|
7634
8369
|
const epics = listEpics(epicsDir);
|
|
7635
8370
|
if (epics.length === 0) {
|
|
@@ -7654,8 +8389,94 @@ Setze diese Umgebungsvariablen:`);
|
|
|
7654
8389
|
} else {
|
|
7655
8390
|
console.error("Verf\xFCgbar: fh epic create | list | show");
|
|
7656
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
|
+
}
|
|
7657
8478
|
} else if (command === "velocity") {
|
|
7658
|
-
const velocityFile =
|
|
8479
|
+
const velocityFile = path36.join(forgehiveDir, "memory", "velocity.md");
|
|
7659
8480
|
if (subcommand === "record") {
|
|
7660
8481
|
const sprintNum = parseInt(rest[0] ?? "0", 10);
|
|
7661
8482
|
const committed = rest.includes("--committed") ? parseInt(rest[rest.indexOf("--committed") + 1], 10) : NaN;
|
|
@@ -7674,7 +8495,7 @@ Setze diese Umgebungsvariablen:`);
|
|
|
7674
8495
|
console.error("Verf\xFCgbar: fh velocity show | record <N> --committed N --delivered N");
|
|
7675
8496
|
}
|
|
7676
8497
|
} else if (command === "docs") {
|
|
7677
|
-
const docsDir =
|
|
8498
|
+
const docsDir = path36.join(projectRoot, "docs");
|
|
7678
8499
|
if (!subcommand || subcommand === "list") {
|
|
7679
8500
|
const existing = listExistingDocs(projectRoot);
|
|
7680
8501
|
if (existing.length === 0) {
|
|
@@ -7688,29 +8509,29 @@ Setze diese Umgebungsvariablen:`);
|
|
|
7688
8509
|
console.log(" fh docs adr <titel> \u2014 Architecture Decision Record");
|
|
7689
8510
|
} else {
|
|
7690
8511
|
console.log(`Vorhandene Dokumentation (${existing.length} Dateien):`);
|
|
7691
|
-
for (const d of existing) console.log(` ${
|
|
8512
|
+
for (const d of existing) console.log(` ${path36.relative(projectRoot, d)}`);
|
|
7692
8513
|
console.log("");
|
|
7693
8514
|
console.log("Aktualisieren: fh docs user | api | onboard | changelog");
|
|
7694
8515
|
}
|
|
7695
8516
|
} else if (subcommand === "user") {
|
|
7696
|
-
|
|
8517
|
+
fs35.mkdirSync(docsDir, { recursive: true });
|
|
7697
8518
|
const outputArg = rest.includes("--output") ? rest[rest.indexOf("--output") + 1] : null;
|
|
7698
|
-
const outputPath = outputArg ??
|
|
8519
|
+
const outputPath = outputArg ?? path36.join(docsDir, "user-guide.md");
|
|
7699
8520
|
const guide = generateUserGuide(projectRoot, forgehiveDir);
|
|
7700
|
-
|
|
8521
|
+
fs35.writeFileSync(outputPath, guide, "utf8");
|
|
7701
8522
|
console.log(`\u2714 User Guide geschrieben: ${outputPath}`);
|
|
7702
8523
|
} else if (subcommand === "api") {
|
|
7703
|
-
|
|
8524
|
+
fs35.mkdirSync(docsDir, { recursive: true });
|
|
7704
8525
|
const outputArg = rest.includes("--output") ? rest[rest.indexOf("--output") + 1] : null;
|
|
7705
|
-
const outputPath = outputArg ??
|
|
8526
|
+
const outputPath = outputArg ?? path36.join(docsDir, "api.md");
|
|
7706
8527
|
const ref = generateApiReference(projectRoot);
|
|
7707
|
-
|
|
8528
|
+
fs35.writeFileSync(outputPath, ref, "utf8");
|
|
7708
8529
|
console.log(`\u2714 API-Referenz geschrieben: ${outputPath}`);
|
|
7709
8530
|
} else if (subcommand === "onboard") {
|
|
7710
8531
|
const outputArg = rest.includes("--output") ? rest[rest.indexOf("--output") + 1] : null;
|
|
7711
|
-
const outputPath = outputArg ??
|
|
8532
|
+
const outputPath = outputArg ?? path36.join(projectRoot, "ONBOARDING.md");
|
|
7712
8533
|
const doc = generateOnboardingDoc(projectRoot, forgehiveDir);
|
|
7713
|
-
|
|
8534
|
+
fs35.writeFileSync(outputPath, doc, "utf8");
|
|
7714
8535
|
console.log(`\u2714 Onboarding-Dokument geschrieben: ${outputPath}`);
|
|
7715
8536
|
} else if (subcommand === "changelog") {
|
|
7716
8537
|
const sinceArg = rest.includes("--since") ? rest[rest.indexOf("--since") + 1] : null;
|
|
@@ -7720,15 +8541,15 @@ Setze diese Umgebungsvariablen:`);
|
|
|
7720
8541
|
const commits = parseGitLog(rawLog);
|
|
7721
8542
|
let pkg = {};
|
|
7722
8543
|
try {
|
|
7723
|
-
pkg = JSON.parse(
|
|
8544
|
+
pkg = JSON.parse(fs35.readFileSync(path36.join(projectRoot, "package.json"), "utf8"));
|
|
7724
8545
|
} catch {
|
|
7725
8546
|
}
|
|
7726
8547
|
const pkgVersion = pkg.version ?? "unreleased";
|
|
7727
8548
|
const md = formatChangelog(commits, pkgVersion);
|
|
7728
|
-
const outputPath = outputArg ??
|
|
8549
|
+
const outputPath = outputArg ?? path36.join(projectRoot, "CHANGELOG.md");
|
|
7729
8550
|
let existing = "";
|
|
7730
|
-
if (
|
|
7731
|
-
|
|
8551
|
+
if (fs35.existsSync(outputPath)) existing = fs35.readFileSync(outputPath, "utf8");
|
|
8552
|
+
fs35.writeFileSync(outputPath, md + "\n\n" + existing, "utf8");
|
|
7732
8553
|
console.log(`\u2714 CHANGELOG.md aktualisiert (${commits.length} Commits)`);
|
|
7733
8554
|
} else if (subcommand === "adr") {
|
|
7734
8555
|
const title = rest.join(" ");
|
|
@@ -7736,9 +8557,9 @@ Setze diese Umgebungsvariablen:`);
|
|
|
7736
8557
|
console.error("Usage: fh docs adr <titel>");
|
|
7737
8558
|
process.exit(1);
|
|
7738
8559
|
}
|
|
7739
|
-
const adrsDir =
|
|
7740
|
-
|
|
7741
|
-
const existing =
|
|
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;
|
|
7742
8563
|
const adrId = String(existing + 1).padStart(4, "0");
|
|
7743
8564
|
const slug = title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
7744
8565
|
const filename = `${adrId}-${slug}.md`;
|
|
@@ -7759,14 +8580,125 @@ Setze diese Umgebungsvariablen:`);
|
|
|
7759
8580
|
|
|
7760
8581
|
(Beschreibe die Auswirkungen dieser Entscheidung.)
|
|
7761
8582
|
`;
|
|
7762
|
-
|
|
8583
|
+
fs35.writeFileSync(path36.join(adrsDir, filename), content, "utf8");
|
|
7763
8584
|
console.log(`\u2714 ADR erstellt: .forgehive/memory/adrs/${filename}`);
|
|
7764
8585
|
} else {
|
|
7765
8586
|
console.error("Verf\xFCgbar: fh docs [list|user|api|onboard|changelog|adr <titel>]");
|
|
7766
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
|
+
}
|
|
7767
8699
|
} else {
|
|
7768
8700
|
console.error("Unbekannter Befehl: " + command);
|
|
7769
|
-
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");
|
|
7770
8702
|
console.error("Hilfe: fh --help");
|
|
7771
8703
|
process.exit(1);
|
|
7772
8704
|
}
|