forgedock 1.0.21 → 1.0.22
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 +134 -80
- package/bin/demo.mjs +386 -0
- package/bin/forgedock.mjs +357 -30
- package/bin/init-detect.mjs +95 -0
- package/bin/runner.mjs +780 -0
- package/commands/cleanup.md +85 -39
- package/commands/deploy-info.md +6 -2
- package/commands/forge-stats.md +1 -1
- package/commands/optimize.md +1 -1
- package/commands/orchestrate.md +320 -26
- package/commands/quality-gate.md +91 -49
- package/commands/review-pr-agents.md +62 -38
- package/commands/review-pr.md +33 -2
- package/commands/scope.md +11 -3
- package/commands/security-audit.md +24 -16
- package/commands/sync-ecosystem.md +2 -2
- package/commands/work-on/build/architect.md +47 -3
- package/commands/work-on/build/context.md +36 -5
- package/commands/work-on/build/validate.md +16 -7
- package/commands/work-on/close.md +129 -1
- package/commands/work-on/investigate.md +24 -1
- package/commands/work-on.md +226 -8
- package/docs/site/annotations-explained.md +55 -0
- package/docs/site/command-learning-path.md +104 -0
- package/docs/site/command-reference.md +3 -0
- package/docs/site/getting-started.md +36 -14
- package/docs/site/troubleshooting.md +208 -0
- package/docs/spec/forge-protocol-v1.md +768 -0
- package/examples/forgedock-demo/README.md +113 -0
- package/examples/forgedock-demo/bootstrap.sh +125 -0
- package/examples/forgedock-demo/issues/01-bug-delete-missing-auth.md +37 -0
- package/examples/forgedock-demo/issues/02-feature-filter-notes.md +37 -0
- package/examples/forgedock-demo/issues/03-refactor-router-table.md +33 -0
- package/examples/forgedock-demo/issues/04-perf-index-lookup.md +35 -0
- package/examples/forgedock-demo/issues/05-docs-api-reference.md +39 -0
- package/examples/forgedock-demo/labels.json +21 -0
- package/examples/forgedock-demo/package.json +16 -0
- package/examples/forgedock-demo/scripts/smoke.js +61 -0
- package/examples/forgedock-demo/src/auth.js +28 -0
- package/examples/forgedock-demo/src/db.js +66 -0
- package/examples/forgedock-demo/src/routes/notes.js +64 -0
- package/examples/forgedock-demo/src/server.js +79 -0
- package/package.json +5 -1
- package/templates/forge.yaml.minimal +44 -0
package/bin/forgedock.mjs
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
4
4
|
|
|
5
5
|
import { fileURLToPath, pathToFileURL } from "url";
|
|
6
|
-
import { dirname, join, relative, resolve } from "path";
|
|
6
|
+
import { basename, dirname, join, relative, resolve } from "path";
|
|
7
7
|
import { mkdir, symlink, readlink, lstat, readdir, stat, unlink } from "fs/promises";
|
|
8
8
|
import {
|
|
9
9
|
existsSync,
|
|
@@ -33,6 +33,7 @@ import {
|
|
|
33
33
|
YELLOW,
|
|
34
34
|
CYAN,
|
|
35
35
|
} from "./tui.mjs";
|
|
36
|
+
import { buildMinimalForgeYaml } from "./init-detect.mjs";
|
|
36
37
|
|
|
37
38
|
const __filename = fileURLToPath(import.meta.url);
|
|
38
39
|
const __dirname = dirname(__filename);
|
|
@@ -346,6 +347,9 @@ function writeSymlinkSentinel() {
|
|
|
346
347
|
const content =
|
|
347
348
|
`# ForgeDock command symlinks — DO NOT REPOINT\n` +
|
|
348
349
|
`# Source: ${COMMANDS_DIR}\n` +
|
|
350
|
+
// Machine-readable version line — read by install() on the next run to
|
|
351
|
+
// detect first-time vs. update installs and render a version diff (#1146).
|
|
352
|
+
`# Version: ${getVersion()}\n` +
|
|
349
353
|
`#\n` +
|
|
350
354
|
`# These symlinks are managed by ForgeDock (https://forgedock.com).\n` +
|
|
351
355
|
`# ForgeDock owns the global ~/.claude/commands/ namespace.\n` +
|
|
@@ -1050,6 +1054,28 @@ async function linkScripts(step) {
|
|
|
1050
1054
|
}
|
|
1051
1055
|
|
|
1052
1056
|
async function install() {
|
|
1057
|
+
// Detect first-time vs. update install by inspecting the namespace sentinel
|
|
1058
|
+
// BEFORE the install steps overwrite it. A missing sentinel means this is a
|
|
1059
|
+
// first install; a present one carries the previously installed version (if
|
|
1060
|
+
// it was written by a build that records `# Version:`). Reading is non-fatal:
|
|
1061
|
+
// any error is treated as a first install so guidance still renders. (#1146)
|
|
1062
|
+
const sentinelPath = join(TARGET_DIR, ".symlink-source");
|
|
1063
|
+
let priorSentinel = null;
|
|
1064
|
+
try {
|
|
1065
|
+
priorSentinel = readFileSync(sentinelPath, "utf-8");
|
|
1066
|
+
} catch {
|
|
1067
|
+
priorSentinel = null;
|
|
1068
|
+
}
|
|
1069
|
+
const isFirstInstall = priorSentinel === null;
|
|
1070
|
+
const priorVersion = priorSentinel
|
|
1071
|
+
? priorSentinel.match(/^# Version:\s*(.+)$/m)?.[1]?.trim() || null
|
|
1072
|
+
: null;
|
|
1073
|
+
const currentVersion = getVersion();
|
|
1074
|
+
|
|
1075
|
+
// Captured from the "Linking commands" step so the post-install summary can
|
|
1076
|
+
// report how many commands were installed/changed without recounting.
|
|
1077
|
+
let linkStats = { installed: 0, updated: 0, skipped: 0 };
|
|
1078
|
+
|
|
1053
1079
|
const result = await runSteps([
|
|
1054
1080
|
{
|
|
1055
1081
|
label: "Checking environment",
|
|
@@ -1086,6 +1112,8 @@ async function install() {
|
|
|
1086
1112
|
async run(step) {
|
|
1087
1113
|
const { installed, updated, skipped } = await linkCommands(step);
|
|
1088
1114
|
const total = installed + updated + skipped;
|
|
1115
|
+
// Hoist for the post-install summary box (#1146).
|
|
1116
|
+
linkStats = { installed, updated, skipped };
|
|
1089
1117
|
step.note(
|
|
1090
1118
|
`${green(String(installed))} installed, ${updated} updated, ${dim(String(skipped))} skipped (${total} commands total)`,
|
|
1091
1119
|
);
|
|
@@ -1231,18 +1259,65 @@ async function install() {
|
|
|
1231
1259
|
]);
|
|
1232
1260
|
|
|
1233
1261
|
if (result.ok) {
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1262
|
+
const totalCommands =
|
|
1263
|
+
linkStats.installed + linkStats.updated + linkStats.skipped;
|
|
1264
|
+
const changedCommands = linkStats.installed + linkStats.updated;
|
|
1265
|
+
const versionLabel = currentVersion ? `v${currentVersion}` : "";
|
|
1266
|
+
|
|
1267
|
+
if (isFirstInstall) {
|
|
1268
|
+
// First-time install — show the full guided next-steps box so the user
|
|
1269
|
+
// has a prioritized path from "installed" to "using it". (#1146)
|
|
1270
|
+
const cmd = (text) => cyan(text.padEnd(22));
|
|
1271
|
+
process.stderr.write(
|
|
1272
|
+
box(
|
|
1273
|
+
[
|
|
1274
|
+
`${green("✔")} ${bold(`ForgeDock ${versionLabel}`.trim())} installed`,
|
|
1275
|
+
`${green("✔")} ${totalCommands} commands → ${cyan(TARGET_DIR)}`,
|
|
1276
|
+
"",
|
|
1277
|
+
bold("What's next?"),
|
|
1278
|
+
"",
|
|
1279
|
+
dim("First time?"),
|
|
1280
|
+
` ${cmd("npx forgedock demo")}${dim("Try the pipeline risk-free")}`,
|
|
1281
|
+
"",
|
|
1282
|
+
dim("Setting up your repo?"),
|
|
1283
|
+
` ${cmd("npx forgedock init")}${dim("Configure forge.yaml")}`,
|
|
1284
|
+
` ${cmd("npx forgedock doctor")}${dim("Verify your setup")}`,
|
|
1285
|
+
"",
|
|
1286
|
+
dim("Ready to go?"),
|
|
1287
|
+
` ${cyan("/work-on #N".padEnd(22))}${dim("Run it in Claude Code")}`,
|
|
1288
|
+
],
|
|
1289
|
+
{ title: "ForgeDock Installed" },
|
|
1290
|
+
) + "\n",
|
|
1291
|
+
);
|
|
1292
|
+
} else {
|
|
1293
|
+
// Update install — show a compact version diff and what changed, with a
|
|
1294
|
+
// changelog link instead of the full first-run guidance. (#1146)
|
|
1295
|
+
let headline;
|
|
1296
|
+
if (priorVersion && currentVersion && priorVersion !== currentVersion) {
|
|
1297
|
+
headline = `Updated from ${bold(`v${priorVersion}`)} → ${bold(`v${currentVersion}`)}.`;
|
|
1298
|
+
} else if (currentVersion && priorVersion === currentVersion) {
|
|
1299
|
+
headline = `Reinstalled ${bold(`v${currentVersion}`)}.`;
|
|
1300
|
+
} else if (currentVersion) {
|
|
1301
|
+
headline = `Updated to ${bold(`v${currentVersion}`)}.`;
|
|
1302
|
+
} else {
|
|
1303
|
+
headline = "Update complete.";
|
|
1304
|
+
}
|
|
1305
|
+
const changeNote =
|
|
1306
|
+
changedCommands > 0
|
|
1307
|
+
? ` ${changedCommands} new/changed command${changedCommands === 1 ? "" : "s"}.`
|
|
1308
|
+
: " Commands already up to date.";
|
|
1309
|
+
process.stderr.write(
|
|
1310
|
+
box(
|
|
1311
|
+
[
|
|
1312
|
+
`${green("✔")} ${headline}${changeNote}`,
|
|
1313
|
+
"",
|
|
1314
|
+
`Changelog: ${cyan("https://github.com/RapierCraftStudios/ForgeDock/releases")}`,
|
|
1315
|
+
`Verify: ${cyan("npx forgedock doctor")}`,
|
|
1316
|
+
],
|
|
1317
|
+
{ title: "ForgeDock Updated" },
|
|
1318
|
+
) + "\n",
|
|
1319
|
+
);
|
|
1320
|
+
}
|
|
1246
1321
|
}
|
|
1247
1322
|
}
|
|
1248
1323
|
|
|
@@ -1476,13 +1551,15 @@ async function update() {
|
|
|
1476
1551
|
console.log("");
|
|
1477
1552
|
}
|
|
1478
1553
|
|
|
1479
|
-
async function init(fromInstall = false) {
|
|
1554
|
+
async function init(fromInstall = false, minimal = false) {
|
|
1480
1555
|
// When called from install() via runSteps(), suppress stdout to prevent
|
|
1481
1556
|
// interleaving with the TUI spinner on stderr. runSteps() provides the
|
|
1482
1557
|
// "Generating forge.yaml" step label as visual feedback. (#812)
|
|
1483
1558
|
const log = fromInstall ? () => {} : console.log;
|
|
1484
1559
|
log("");
|
|
1485
|
-
log(
|
|
1560
|
+
log(
|
|
1561
|
+
`${BOLD}ForgeDock${RESET} — Generating forge.yaml${minimal ? " (minimal)" : ""}`,
|
|
1562
|
+
);
|
|
1486
1563
|
log("");
|
|
1487
1564
|
|
|
1488
1565
|
const cwd = process.cwd();
|
|
@@ -1688,7 +1765,7 @@ async function init(fromInstall = false) {
|
|
|
1688
1765
|
`forge.yaml.bak.${new Date().toISOString().replace(/[:.]/g, "-")}`,
|
|
1689
1766
|
)
|
|
1690
1767
|
: baseBak;
|
|
1691
|
-
const backupName = backupPath
|
|
1768
|
+
const backupName = basename(backupPath);
|
|
1692
1769
|
renameSync(outputPath, backupPath);
|
|
1693
1770
|
log(` ${YELLOW}Backed up${RESET}: forge.yaml → ${backupName}`);
|
|
1694
1771
|
}
|
|
@@ -1707,7 +1784,7 @@ async function init(fromInstall = false) {
|
|
|
1707
1784
|
const safeDefaultBranch = defaultBranch.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
|
1708
1785
|
const safeStagingBranch = stagingBranch.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
|
1709
1786
|
|
|
1710
|
-
const
|
|
1787
|
+
const fullContent = `# forge.yaml — ForgeDock Configuration
|
|
1711
1788
|
#
|
|
1712
1789
|
# Auto-generated by: npx forgedock init
|
|
1713
1790
|
# Edit this file with your project details.
|
|
@@ -1825,9 +1902,26 @@ branches:
|
|
|
1825
1902
|
# # override_phrase: "OVERRIDE: shipping with test failures \u2014"
|
|
1826
1903
|
`;
|
|
1827
1904
|
|
|
1905
|
+
// When --minimal is requested, emit only the three required sections
|
|
1906
|
+
// (project, paths, branches) — no commented optional blocks. The minimal
|
|
1907
|
+
// output still passes `forgedock doctor` and drives `/work-on`, while staying
|
|
1908
|
+
// ~20 lines instead of ~200. (#1148)
|
|
1909
|
+
const content = minimal
|
|
1910
|
+
? buildMinimalForgeYaml({
|
|
1911
|
+
projectName,
|
|
1912
|
+
owner,
|
|
1913
|
+
repo,
|
|
1914
|
+
description,
|
|
1915
|
+
root: cwd,
|
|
1916
|
+
worktreeBase,
|
|
1917
|
+
defaultBranch,
|
|
1918
|
+
stagingBranch,
|
|
1919
|
+
})
|
|
1920
|
+
: fullContent;
|
|
1921
|
+
|
|
1828
1922
|
atomicWriteFile(outputPath, content);
|
|
1829
1923
|
|
|
1830
|
-
log(` ${GREEN}Created${RESET}: forge.yaml`);
|
|
1924
|
+
log(` ${GREEN}Created${RESET}: forge.yaml${minimal ? " (minimal)" : ""}`);
|
|
1831
1925
|
log("");
|
|
1832
1926
|
|
|
1833
1927
|
if (fromInstall) {
|
|
@@ -1857,9 +1951,15 @@ branches:
|
|
|
1857
1951
|
log(
|
|
1858
1952
|
` 2. Add ${CYAN}forge.yaml${RESET} to ${CYAN}.gitignore${RESET} if it contains sensitive paths`,
|
|
1859
1953
|
);
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1954
|
+
if (minimal) {
|
|
1955
|
+
log(
|
|
1956
|
+
` 3. Need more? Add optional sections from ${CYAN}forge.yaml.example${RESET} (see ${CYAN}docs/CONFIG.md${RESET})`,
|
|
1957
|
+
);
|
|
1958
|
+
} else {
|
|
1959
|
+
log(
|
|
1960
|
+
` 3. Run ${CYAN}/forgedock-init${RESET} inside Claude Code for guided AI-powered setup`,
|
|
1961
|
+
);
|
|
1962
|
+
}
|
|
1863
1963
|
log("");
|
|
1864
1964
|
}
|
|
1865
1965
|
}
|
|
@@ -2109,13 +2209,27 @@ async function doctor() {
|
|
|
2109
2209
|
symlinkOk = false;
|
|
2110
2210
|
}
|
|
2111
2211
|
} else {
|
|
2112
|
-
// Regular file —
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2212
|
+
// Regular file — copy-mode install (Windows without Developer Mode).
|
|
2213
|
+
// linkCommands() falls back to copyFile() on EPERM/EACCES, so a regular
|
|
2214
|
+
// file is valid as long as its content matches the source. <!-- Added: forge#1174 -->
|
|
2215
|
+
try {
|
|
2216
|
+
const srcContent = readFileSync(src, "utf-8");
|
|
2217
|
+
const tgtContent = readFileSync(tgt, "utf-8");
|
|
2218
|
+
if (srcContent !== tgtContent) {
|
|
2219
|
+
broken++;
|
|
2220
|
+
brokenLinks.push(`${rel} (stale copy)`);
|
|
2221
|
+
symlinkOk = false;
|
|
2222
|
+
}
|
|
2223
|
+
// else: content matches — valid copy-mode install, not broken
|
|
2224
|
+
} catch {
|
|
2225
|
+
// Could not read one of the files — treat as broken.
|
|
2226
|
+
broken++;
|
|
2227
|
+
brokenLinks.push(`${rel} (unreadable)`);
|
|
2228
|
+
symlinkOk = false;
|
|
2229
|
+
}
|
|
2116
2230
|
}
|
|
2117
2231
|
} catch {
|
|
2118
|
-
//
|
|
2232
|
+
// File missing
|
|
2119
2233
|
broken++;
|
|
2120
2234
|
brokenLinks.push(`${rel} (missing)`);
|
|
2121
2235
|
symlinkOk = false;
|
|
@@ -2123,15 +2237,15 @@ async function doctor() {
|
|
|
2123
2237
|
}
|
|
2124
2238
|
|
|
2125
2239
|
if (symlinkOk) {
|
|
2126
|
-
pass("Command
|
|
2240
|
+
pass("Command files", `${checked} files installed`);
|
|
2127
2241
|
} else {
|
|
2128
2242
|
fail(
|
|
2129
|
-
"Command
|
|
2243
|
+
"Command files",
|
|
2130
2244
|
`Run: npx forgedock install (${broken}/${checked} broken: ${brokenLinks.slice(0, 3).join(", ")}${brokenLinks.length > 3 ? "…" : ""})`,
|
|
2131
2245
|
);
|
|
2132
2246
|
}
|
|
2133
2247
|
} catch (err) {
|
|
2134
|
-
fail("Command
|
|
2248
|
+
fail("Command files", `Could not read commands directory: ${err.message}`);
|
|
2135
2249
|
}
|
|
2136
2250
|
}
|
|
2137
2251
|
|
|
@@ -2441,6 +2555,106 @@ async function doctor() {
|
|
|
2441
2555
|
}
|
|
2442
2556
|
}
|
|
2443
2557
|
|
|
2558
|
+
// ── Check 10: yq installed ────────────────────────────────────────────────
|
|
2559
|
+
// yq is a hard dependency: pipeline commands (work-on, review-pr, orchestrate)
|
|
2560
|
+
// read forge.yaml via yq. Without it, those commands fail. Fixed literal
|
|
2561
|
+
// command — no interpolation, so no injection surface (cf. #663/#789/#807).
|
|
2562
|
+
{
|
|
2563
|
+
let yqVersion = "";
|
|
2564
|
+
try {
|
|
2565
|
+
yqVersion = execSync("yq --version", {
|
|
2566
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
2567
|
+
encoding: "utf-8",
|
|
2568
|
+
timeout: 5000,
|
|
2569
|
+
}).trim();
|
|
2570
|
+
} catch {
|
|
2571
|
+
// yq not installed or not on PATH
|
|
2572
|
+
}
|
|
2573
|
+
|
|
2574
|
+
if (yqVersion) {
|
|
2575
|
+
pass("yq", yqVersion);
|
|
2576
|
+
} else {
|
|
2577
|
+
fail(
|
|
2578
|
+
"yq",
|
|
2579
|
+
"Install the YAML processor (used to read forge.yaml): https://github.com/mikefarah/yq#install",
|
|
2580
|
+
);
|
|
2581
|
+
}
|
|
2582
|
+
}
|
|
2583
|
+
|
|
2584
|
+
// ── Check 11: Claude Code installed + version compatible ───────────────────
|
|
2585
|
+
// The compatibility floor is the @anthropic-ai/claude-code peerDependency in
|
|
2586
|
+
// package.json (read dynamically so it never drifts from the declared floor).
|
|
2587
|
+
// Not-on-PATH is a warning, not a failure: Claude Code may run as the host
|
|
2588
|
+
// process without exposing the `claude` CLI on PATH, so a hard fail would be a
|
|
2589
|
+
// false negative. Only an installed-but-too-old version is a hard failure.
|
|
2590
|
+
{
|
|
2591
|
+
// Resolve the minimum compatible version from package.json peerDependencies.
|
|
2592
|
+
let minClaudeVersion = "2.0.0";
|
|
2593
|
+
try {
|
|
2594
|
+
const pkgRaw = readFileSync(join(FORGE_HOME, "package.json"), "utf-8");
|
|
2595
|
+
const pkg = JSON.parse(pkgRaw);
|
|
2596
|
+
const range = pkg?.peerDependencies?.["@anthropic-ai/claude-code"];
|
|
2597
|
+
if (typeof range === "string") {
|
|
2598
|
+
const m = range.match(/(\d+\.\d+\.\d+)/);
|
|
2599
|
+
if (m) minClaudeVersion = m[1];
|
|
2600
|
+
}
|
|
2601
|
+
} catch {
|
|
2602
|
+
// package.json unreadable — keep the hardcoded fallback floor.
|
|
2603
|
+
}
|
|
2604
|
+
|
|
2605
|
+
let claudeRaw = "";
|
|
2606
|
+
let claudeInstalled = false;
|
|
2607
|
+
try {
|
|
2608
|
+
claudeRaw = execSync("claude --version", {
|
|
2609
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
2610
|
+
encoding: "utf-8",
|
|
2611
|
+
timeout: 5000,
|
|
2612
|
+
}).trim();
|
|
2613
|
+
claudeInstalled = true;
|
|
2614
|
+
} catch {
|
|
2615
|
+
// claude CLI not on PATH
|
|
2616
|
+
}
|
|
2617
|
+
|
|
2618
|
+
if (!claudeInstalled) {
|
|
2619
|
+
warn(
|
|
2620
|
+
"Claude Code",
|
|
2621
|
+
`\`claude\` CLI not found on PATH (need >= v${minClaudeVersion}). If you run Claude Code without the CLI on PATH this is fine; otherwise install it: https://docs.anthropic.com/en/docs/claude-code`,
|
|
2622
|
+
);
|
|
2623
|
+
} else {
|
|
2624
|
+
const verMatch = claudeRaw.match(/(\d+)\.(\d+)\.(\d+)/);
|
|
2625
|
+
if (!verMatch) {
|
|
2626
|
+
warn(
|
|
2627
|
+
"Claude Code",
|
|
2628
|
+
`installed but version string could not be parsed ("${claudeRaw}"). Expected >= v${minClaudeVersion}.`,
|
|
2629
|
+
);
|
|
2630
|
+
} else {
|
|
2631
|
+
const found = [
|
|
2632
|
+
Number(verMatch[1]),
|
|
2633
|
+
Number(verMatch[2]),
|
|
2634
|
+
Number(verMatch[3]),
|
|
2635
|
+
];
|
|
2636
|
+
const floor = minClaudeVersion.split(".").map(Number);
|
|
2637
|
+
// Lexicographic semver comparison: found >= floor ?
|
|
2638
|
+
let compatible = true;
|
|
2639
|
+
for (let i = 0; i < 3; i++) {
|
|
2640
|
+
if (found[i] > floor[i]) break;
|
|
2641
|
+
if (found[i] < floor[i]) {
|
|
2642
|
+
compatible = false;
|
|
2643
|
+
break;
|
|
2644
|
+
}
|
|
2645
|
+
}
|
|
2646
|
+
if (compatible) {
|
|
2647
|
+
pass("Claude Code", `v${found.join(".")} (compatible, >= v${minClaudeVersion})`);
|
|
2648
|
+
} else {
|
|
2649
|
+
fail(
|
|
2650
|
+
"Claude Code",
|
|
2651
|
+
`v${found.join(".")} is below the supported floor v${minClaudeVersion}. Update Claude Code: https://docs.anthropic.com/en/docs/claude-code`,
|
|
2652
|
+
);
|
|
2653
|
+
}
|
|
2654
|
+
}
|
|
2655
|
+
}
|
|
2656
|
+
}
|
|
2657
|
+
|
|
2444
2658
|
// ── Summary ────────────────────────────────────────────────────────────────
|
|
2445
2659
|
console.log("");
|
|
2446
2660
|
if (failures === 0 && warnings === 0) {
|
|
@@ -2466,8 +2680,11 @@ function help() {
|
|
|
2466
2680
|
["npx forgedock", "Install commands (default)"],
|
|
2467
2681
|
["npx forgedock install", "Install commands"],
|
|
2468
2682
|
["npx forgedock init", "Generate forge.yaml config for your project"],
|
|
2683
|
+
["npx forgedock init --minimal", "Generate a minimal forge.yaml (required sections only)"],
|
|
2469
2684
|
["npx forgedock uninstall", "Remove commands"],
|
|
2470
2685
|
["npx forgedock update", "Pull latest & reinstall"],
|
|
2686
|
+
["npx forgedock run <cmd> [args]", "Run a command headlessly via the Anthropic API"],
|
|
2687
|
+
["npx forgedock demo", "Set up a risk-free demo repo and print next steps"],
|
|
2471
2688
|
["npx forgedock enable [dir]", "Mark directory as ForgeDock-managed"],
|
|
2472
2689
|
["npx forgedock disable [dir]", "Opt directory out of ForgeDock"],
|
|
2473
2690
|
["npx forgedock status [dir]", "Show resolved state for a directory"],
|
|
@@ -2480,6 +2697,110 @@ function help() {
|
|
|
2480
2697
|
);
|
|
2481
2698
|
}
|
|
2482
2699
|
|
|
2700
|
+
/**
|
|
2701
|
+
* `forgedock run <command> [args...]` — execute a ForgeDock command spec
|
|
2702
|
+
* directly via the Anthropic API, outside of Claude Code (headless / CI).
|
|
2703
|
+
*
|
|
2704
|
+
* Flags:
|
|
2705
|
+
* --dry-run Preview the assembled prompt + tool plan; no API call.
|
|
2706
|
+
* --model <id> Override the model (default: claude-sonnet-4-5 or $FORGEDOCK_MODEL).
|
|
2707
|
+
* --max-iterations <n> Bound the tool-use loop (default: 50).
|
|
2708
|
+
*
|
|
2709
|
+
* The live loop requires ANTHROPIC_API_KEY and the optional @anthropic-ai/sdk
|
|
2710
|
+
* dependency. The runtime itself lives in bin/runner.mjs.
|
|
2711
|
+
*
|
|
2712
|
+
* Env:
|
|
2713
|
+
* FORGEDOCK_MODEL Default model id when --model is omitted.
|
|
2714
|
+
* FORGEDOCK_SHELL Override the shell used by run_bash. Defaults to bash when
|
|
2715
|
+
* found (Git Bash / WSL on Windows, /bin/bash on POSIX),
|
|
2716
|
+
* falling back to the platform default shell otherwise.
|
|
2717
|
+
*/
|
|
2718
|
+
async function run() {
|
|
2719
|
+
const runArgs = args.slice(1);
|
|
2720
|
+
let dryRun = false;
|
|
2721
|
+
let model;
|
|
2722
|
+
let maxIterations;
|
|
2723
|
+
const positional = [];
|
|
2724
|
+
for (let i = 0; i < runArgs.length; i++) {
|
|
2725
|
+
const a = runArgs[i];
|
|
2726
|
+
if (a === "--dry-run") {
|
|
2727
|
+
dryRun = true;
|
|
2728
|
+
} else if (a === "--model") {
|
|
2729
|
+
model = runArgs[++i];
|
|
2730
|
+
} else if (a.startsWith("--model=")) {
|
|
2731
|
+
model = a.slice("--model=".length);
|
|
2732
|
+
} else if (a === "--max-iterations") {
|
|
2733
|
+
maxIterations = parseInt(runArgs[++i], 10);
|
|
2734
|
+
} else if (a.startsWith("--max-iterations=")) {
|
|
2735
|
+
maxIterations = parseInt(a.slice("--max-iterations=".length), 10);
|
|
2736
|
+
} else {
|
|
2737
|
+
positional.push(a);
|
|
2738
|
+
}
|
|
2739
|
+
}
|
|
2740
|
+
|
|
2741
|
+
const commandName = positional[0];
|
|
2742
|
+
const commandArgs = positional.slice(1);
|
|
2743
|
+
|
|
2744
|
+
if (!commandName) {
|
|
2745
|
+
process.stderr.write(
|
|
2746
|
+
`${RED}Usage: forgedock run <command> [args...] [--dry-run] [--model <id>] [--max-iterations <n>]${RESET}\n`,
|
|
2747
|
+
);
|
|
2748
|
+
process.exit(1);
|
|
2749
|
+
}
|
|
2750
|
+
|
|
2751
|
+
const { runCommand } = await import("./runner.mjs");
|
|
2752
|
+
try {
|
|
2753
|
+
const result = await runCommand({
|
|
2754
|
+
commandsDir: COMMANDS_DIR,
|
|
2755
|
+
commandName,
|
|
2756
|
+
args: commandArgs,
|
|
2757
|
+
cwd: process.cwd(),
|
|
2758
|
+
dryRun,
|
|
2759
|
+
...(model ? { model } : {}),
|
|
2760
|
+
...(Number.isInteger(maxIterations) && maxIterations > 0
|
|
2761
|
+
? { maxIterations }
|
|
2762
|
+
: {}),
|
|
2763
|
+
});
|
|
2764
|
+
// Treat a non-clean stop (iteration cap hit, or a max_tokens-truncated
|
|
2765
|
+
// turn) as a failed run so CI/headless callers notice.
|
|
2766
|
+
if (
|
|
2767
|
+
result &&
|
|
2768
|
+
(result.status === "max-iterations" || result.status === "incomplete")
|
|
2769
|
+
) {
|
|
2770
|
+
process.exitCode = 1;
|
|
2771
|
+
}
|
|
2772
|
+
} catch (err) {
|
|
2773
|
+
process.stderr.write(`${RED}${err.message}${RESET}\n`);
|
|
2774
|
+
process.exit(1);
|
|
2775
|
+
}
|
|
2776
|
+
}
|
|
2777
|
+
|
|
2778
|
+
/**
|
|
2779
|
+
* `forgedock demo` — one-command demo mode (issue #1145).
|
|
2780
|
+
*
|
|
2781
|
+
* Stands up a runnable ForgeDock demo at a predictable location (default
|
|
2782
|
+
* ~/forgedock-demo) with zero required decisions, then prints the exact next
|
|
2783
|
+
* steps. Clones the live demo repo when available, otherwise falls back to the
|
|
2784
|
+
* bundled scaffold at FORGE_HOME/examples/forgedock-demo. The runtime lives in
|
|
2785
|
+
* bin/demo.mjs.
|
|
2786
|
+
*/
|
|
2787
|
+
async function demo() {
|
|
2788
|
+
const { runDemo } = await import("./demo.mjs");
|
|
2789
|
+
try {
|
|
2790
|
+
const result = await runDemo({
|
|
2791
|
+
forgeHome: FORGE_HOME,
|
|
2792
|
+
args: args.slice(1),
|
|
2793
|
+
cwd: process.cwd(),
|
|
2794
|
+
});
|
|
2795
|
+
if (result && result.status === "error") {
|
|
2796
|
+
process.exitCode = 1;
|
|
2797
|
+
}
|
|
2798
|
+
} catch (err) {
|
|
2799
|
+
process.stderr.write(`${RED}${err.message}${RESET}\n`);
|
|
2800
|
+
process.exit(1);
|
|
2801
|
+
}
|
|
2802
|
+
}
|
|
2803
|
+
|
|
2483
2804
|
splash();
|
|
2484
2805
|
|
|
2485
2806
|
switch (command) {
|
|
@@ -2487,7 +2808,7 @@ switch (command) {
|
|
|
2487
2808
|
await install();
|
|
2488
2809
|
break;
|
|
2489
2810
|
case "init":
|
|
2490
|
-
await init();
|
|
2811
|
+
await init(false, args.includes("--minimal"));
|
|
2491
2812
|
break;
|
|
2492
2813
|
case "uninstall":
|
|
2493
2814
|
await uninstall();
|
|
@@ -2495,6 +2816,12 @@ switch (command) {
|
|
|
2495
2816
|
case "update":
|
|
2496
2817
|
await update();
|
|
2497
2818
|
break;
|
|
2819
|
+
case "run":
|
|
2820
|
+
await run();
|
|
2821
|
+
break;
|
|
2822
|
+
case "demo":
|
|
2823
|
+
await demo();
|
|
2824
|
+
break;
|
|
2498
2825
|
case "enable":
|
|
2499
2826
|
await enable(args[1]);
|
|
2500
2827
|
break;
|
package/bin/init-detect.mjs
CHANGED
|
@@ -323,3 +323,98 @@ export async function detectConfig(cwd = process.cwd()) {
|
|
|
323
323
|
meta: { remoteDetected },
|
|
324
324
|
};
|
|
325
325
|
}
|
|
326
|
+
|
|
327
|
+
// ---------------------------------------------------------------------------
|
|
328
|
+
// Minimal forge.yaml template builder
|
|
329
|
+
// ---------------------------------------------------------------------------
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Escape a value for embedding inside a YAML double-quoted scalar.
|
|
333
|
+
*
|
|
334
|
+
* Backslash MUST be escaped first to avoid double-escaping an already-escaped
|
|
335
|
+
* quote. This mirrors the escaping applied by the full-config generator in
|
|
336
|
+
* forgedock.mjs and prevents Windows paths (which contain `\`) from corrupting
|
|
337
|
+
* the emitted YAML. (Ref: forge#810 — init() did not escape backslashes.)
|
|
338
|
+
*
|
|
339
|
+
* @param {string} value
|
|
340
|
+
* @returns {string}
|
|
341
|
+
*/
|
|
342
|
+
function escapeYamlScalar(value) {
|
|
343
|
+
return String(value).replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Build a minimal forge.yaml document containing ONLY the three required
|
|
348
|
+
* sections — project, paths, branches — with no commented optional blocks.
|
|
349
|
+
*
|
|
350
|
+
* This is the `npx forgedock init --minimal` output. It is intentionally
|
|
351
|
+
* complete enough to pass `npx forgedock doctor` (which requires project,
|
|
352
|
+
* paths, AND branches) and to drive `/work-on` (which needs
|
|
353
|
+
* paths.worktree_base for git worktrees) — while staying ~20 lines instead of
|
|
354
|
+
* the ~200-line full template. Optional sections (repos, project_board,
|
|
355
|
+
* review, verification, …) fall back to sensible defaults and can be added
|
|
356
|
+
* later from forge.yaml.example / docs/CONFIG.md.
|
|
357
|
+
*
|
|
358
|
+
* Pure: returns a string, performs no I/O.
|
|
359
|
+
*
|
|
360
|
+
* @param {object} opts
|
|
361
|
+
* @param {string} opts.projectName - Human-readable project name.
|
|
362
|
+
* @param {string} opts.owner - GitHub org/user that owns the repo.
|
|
363
|
+
* @param {string} opts.repo - Repository name (no owner prefix).
|
|
364
|
+
* @param {string} [opts.description] - One-line project description (omitted if empty).
|
|
365
|
+
* @param {string} opts.root - Absolute path to the project root.
|
|
366
|
+
* @param {string} opts.worktreeBase - Directory where git worktrees are created.
|
|
367
|
+
* @param {string} opts.defaultBranch - Default branch (e.g. main).
|
|
368
|
+
* @param {string} opts.stagingBranch - Staging branch for fast-lane PRs.
|
|
369
|
+
* @returns {string} The minimal forge.yaml content.
|
|
370
|
+
*/
|
|
371
|
+
export function buildMinimalForgeYaml({
|
|
372
|
+
projectName,
|
|
373
|
+
owner,
|
|
374
|
+
repo,
|
|
375
|
+
description = "",
|
|
376
|
+
root,
|
|
377
|
+
worktreeBase,
|
|
378
|
+
defaultBranch,
|
|
379
|
+
stagingBranch,
|
|
380
|
+
}) {
|
|
381
|
+
const e = escapeYamlScalar;
|
|
382
|
+
const descLine = description
|
|
383
|
+
? `\n description: "${e(description)}"`
|
|
384
|
+
: "";
|
|
385
|
+
|
|
386
|
+
return `# forge.yaml — ForgeDock Configuration (minimal)
|
|
387
|
+
#
|
|
388
|
+
# Generated by: npx forgedock init --minimal
|
|
389
|
+
#
|
|
390
|
+
# Only project, paths, and branches are required — everything else is optional
|
|
391
|
+
# and falls back to sensible defaults. Start here and add sections as you need
|
|
392
|
+
# them. See forge.yaml.example and docs/CONFIG.md for the full reference.
|
|
393
|
+
|
|
394
|
+
# =============================================================================
|
|
395
|
+
# PROJECT (REQUIRED)
|
|
396
|
+
# =============================================================================
|
|
397
|
+
|
|
398
|
+
project:
|
|
399
|
+
name: "${e(projectName)}"
|
|
400
|
+
owner: "${e(owner)}"
|
|
401
|
+
repo: "${e(repo)}"${descLine}
|
|
402
|
+
|
|
403
|
+
# =============================================================================
|
|
404
|
+
# PATHS (REQUIRED)
|
|
405
|
+
# =============================================================================
|
|
406
|
+
|
|
407
|
+
paths:
|
|
408
|
+
root: "${e(root)}"
|
|
409
|
+
worktree_base: "${e(worktreeBase)}"
|
|
410
|
+
|
|
411
|
+
# =============================================================================
|
|
412
|
+
# BRANCHES (REQUIRED)
|
|
413
|
+
# =============================================================================
|
|
414
|
+
|
|
415
|
+
branches:
|
|
416
|
+
default: "${e(defaultBranch)}"
|
|
417
|
+
staging: "${e(stagingBranch)}"
|
|
418
|
+
feature_pattern: "milestone/{slug}"
|
|
419
|
+
`;
|
|
420
|
+
}
|