forgedock 1.7.0 → 1.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -231,7 +231,8 @@ npx forgedock # checks your environment, installs commands into ~/.claude/comman
231
231
  ```
232
232
 
233
233
  For OpenCode, install namespaced native commands without changing your provider
234
- or `opencode.json`:
234
+ or user-owned settings. When migrating an older ForgeDock adapter, it removes
235
+ only exact ForgeDock-managed legacy entries from `opencode.json`:
235
236
 
236
237
  ```bash
237
238
  npx forgedock opencode install
@@ -12,6 +12,7 @@ import { join } from "node:path";
12
12
  import { runIssue, DEFAULT_MAX_ATTEMPTS } from "./engine.mjs";
13
13
  import { makeProjector } from "./engine/projector.mjs";
14
14
  import { readLog, deriveState } from "./engine/runlog.mjs";
15
+ import { assertOpenCodeNativeRuntime } from "./runner.mjs";
15
16
 
16
17
  /**
17
18
  * Workflow labels that mark an issue as "in the pipeline" (not yet terminal).
@@ -340,6 +341,7 @@ export function lastLocalRun(dir) {
340
341
  export async function runFromCli(argv, deps = {}) {
341
342
  const issue = parseInt(argv[0], 10);
342
343
  if (!Number.isInteger(issue)) throw new Error("usage: forgedock run-issue <issue-number> --lane <lane>");
344
+ assertOpenCodeNativeRuntime({ operation: "forgedock run-issue" });
343
345
  const lane = flag(argv, "--lane");
344
346
  if (!lane) throw new Error("--lane is required: e.g. --lane main or --lane staging. No default to prevent accidental production targeting.");
345
347
  const repo = flag(argv, "--repo");
package/bin/forgedock.mjs CHANGED
@@ -416,6 +416,40 @@ function getVersion() {
416
416
  }
417
417
  }
418
418
 
419
+ /**
420
+ * Read the branch that owns the source currently being executed.
421
+ *
422
+ * This is deliberately best-effort: diagnostics should still identify the
423
+ * source path and version when the checkout has no readable branch ref.
424
+ *
425
+ * @param {string} [dir]
426
+ * @returns {string|null}
427
+ */
428
+ function getGitBranch(dir = FORGE_HOME) {
429
+ try {
430
+ const branch = execFileSync(
431
+ "git",
432
+ ["rev-parse", "--abbrev-ref", "HEAD"],
433
+ { cwd: dir, encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] },
434
+ ).trim();
435
+ return branch || null;
436
+ } catch {
437
+ return null;
438
+ }
439
+ }
440
+
441
+ /**
442
+ * Describe the source path the current CLI process resolves from.
443
+ *
444
+ * @param {string|null} [branch]
445
+ * @returns {string}
446
+ */
447
+ function formatSourceIdentity(branch = null) {
448
+ const version = getVersion() || "unknown";
449
+ const branchLabel = branch ? `, branch ${branch}` : "";
450
+ return `${FORGE_HOME} (v${version}${branchLabel})`;
451
+ }
452
+
419
453
  /**
420
454
  * Compare two dotted version strings component-wise (numeric, not
421
455
  * lexicographic — "1.9.0" must compare as older than "1.10.0").
@@ -1330,11 +1364,51 @@ async function uninstall() {
1330
1364
  console.log("");
1331
1365
  }
1332
1366
 
1333
- // Reinstall = relink commands + re-register the hook. Never the full
1334
- // journey: update must not reach read/review, which could overwrite a
1335
- // curated forge.yaml (and re-runs AI enrichment on every update). Also
1336
- // idempotent, so it's the repair path for a configured repo whose
1337
- // symlinks or hook registration got out of sync.
1367
+ // Reinstall = relink commands + re-register the hook + refresh an already
1368
+ // installed OpenCode adapter. Never the full journey: update must not reach
1369
+ // read/review, which could overwrite a curated forge.yaml (and re-runs AI
1370
+ // enrichment on every update). Also idempotent, so it's the repair path for a
1371
+ // configured repo whose symlinks, hook registration, or managed OpenCode files
1372
+ // got out of sync.
1373
+ async function refreshManagedOpenCodeAdapter() {
1374
+ try {
1375
+ const { getOpenCodeAdapterStatus, installOpenCodeAdapter } = await import("./opencode-adapter.mjs");
1376
+ const status = await getOpenCodeAdapterStatus({ home: HOME, env: process.env });
1377
+ if (!status.installed) return;
1378
+
1379
+ // npm/npx payloads must be persisted before the generated adapter points at
1380
+ // them; git-clone installs are already stable and persistHome returns them
1381
+ // unchanged.
1382
+ const persisted = await persistHome(ctx());
1383
+ const adapterForgeHome = persisted.forgeHome || FORGE_HOME;
1384
+ if (adapterForgeHome === FORGE_HOME && isEphemeralCachePath(FORGE_HOME)) {
1385
+ console.log(
1386
+ ` ${YELLOW}OpenCode adapter refresh skipped: the current ForgeDock payload is ephemeral.${RESET}`,
1387
+ );
1388
+ return;
1389
+ }
1390
+
1391
+ const result = await installOpenCodeAdapter({
1392
+ forgeHome: adapterForgeHome,
1393
+ home: HOME,
1394
+ env: process.env,
1395
+ includeExtras: status.manifest?.includeExtras === true,
1396
+ });
1397
+ console.log(
1398
+ ` ${GREEN}Refreshed managed OpenCode adapter (${result.commandCount} commands, ${result.skillCount} skills).${RESET}`,
1399
+ );
1400
+ for (const warning of result.migration.warnings) {
1401
+ console.log(` ${YELLOW}OpenCode adapter warning: ${warning}${RESET}`);
1402
+ }
1403
+ } catch (error) {
1404
+ // OpenCode refresh is best-effort, matching the existing update/relink
1405
+ // behavior: a permissions or ownership problem must not block Claude users.
1406
+ console.log(
1407
+ ` ${YELLOW}Could not refresh the managed OpenCode adapter: ${error?.message || String(error)}${RESET}`,
1408
+ );
1409
+ }
1410
+ }
1411
+
1338
1412
  async function relinkAndHint() {
1339
1413
  const c = ctx();
1340
1414
  const forged = await forge(c);
@@ -1346,6 +1420,7 @@ async function relinkAndHint() {
1346
1420
  // (that path calls statusScreen(), never relinkAndHint()) — see the
1347
1421
  // writeInstallReceipt() JSDoc in journey.mjs for the full picture.
1348
1422
  await writeInstallReceipt(c, { forged });
1423
+ await refreshManagedOpenCodeAdapter();
1349
1424
  if (!existsSync(join(c.cwd, "forge.yaml"))) {
1350
1425
  const dim = (s) => (c.mode === "none" ? s : `\x1b[2m${s}\x1b[22m`);
1351
1426
  c.stdout.write(" " + dim("Configure this repo: npx forgedock init") + "\n");
@@ -1808,24 +1883,18 @@ async function update() {
1808
1883
  const separateGlobalInstall = findSeparateGlobalInstall();
1809
1884
  if (separateGlobalInstall) {
1810
1885
  console.log(
1811
- ` ${YELLOW}Note: this updates the git clone above. Your global npm install at ${separateGlobalInstall} was NOT touched.${RESET}`,
1886
+ ` ${YELLOW}Note: this command resolves to the git clone above. Your global npm install at ${separateGlobalInstall} is NOT touched.${RESET}`,
1812
1887
  );
1813
1888
  console.log(
1814
1889
  ` ${YELLOW}Run 'npm update -g forgedock', or 'npx forgedock update' from outside a clone, to update it.${RESET}`,
1815
1890
  );
1816
1891
  }
1817
1892
  try {
1818
- const branch = execSync("git rev-parse --abbrev-ref HEAD", {
1819
- cwd: FORGE_HOME,
1820
- encoding: "utf-8",
1821
- }).trim();
1893
+ const branch = getGitBranch();
1894
+ const branchLabel = branch || "unknown branch";
1895
+ console.log(` Source: ${formatSourceIdentity(branch)}`);
1822
1896
 
1823
- // If on a non-main branch, switch to main for the update then restore.
1824
- // Detached HEAD ("HEAD") is treated as non-restorable — we leave on main
1825
- // after the update and warn the user.
1826
- const isDetached = branch === "HEAD";
1827
- const needsCheckout = branch !== "main";
1828
- if (needsCheckout) {
1897
+ if (branch !== "main") {
1829
1898
  // Dirty-tree guard: refuse to move HEAD in this clone if there are
1830
1899
  // uncommitted TRACKED changes. Untracked files are intentionally
1831
1900
  // excluded (--untracked-files=no) so a clean checkout with stray
@@ -1837,7 +1906,7 @@ async function update() {
1837
1906
  ).trim();
1838
1907
  if (porcelain) {
1839
1908
  console.log(
1840
- ` ${YELLOW}Working tree has uncommitted changes on branch ${branch} — skipping git-based update.${RESET}`,
1909
+ ` ${YELLOW}Working tree has uncommitted changes on branch ${branchLabel} — skipping git-based update.${RESET}`,
1841
1910
  );
1842
1911
 
1843
1912
  // forge#2460 — distinguish "developing on this checkout" from a
@@ -1882,16 +1951,13 @@ async function update() {
1882
1951
  return;
1883
1952
  }
1884
1953
 
1885
- if (isDetached) {
1886
- console.log(
1887
- ` ${YELLOW}Detached HEAD state — switching to main for update.${RESET}`,
1888
- );
1889
- } else {
1890
- console.log(
1891
- ` On branch ${CYAN}${branch}${RESET} — switching to ${CYAN}main${RESET} for update...`,
1892
- );
1893
- }
1894
- execFileSync("git", ["checkout", "main", "--quiet"], { cwd: FORGE_HOME });
1954
+ console.log(
1955
+ ` ${YELLOW}Source checkout is on ${branchLabel}, not main; skipping update to preserve this source.${RESET}`,
1956
+ );
1957
+ console.log(
1958
+ ` Run 'npx forgedock update' from a main-branch checkout (or use 'npx forgedock@latest') to refresh ForgeDock.`,
1959
+ );
1960
+ return;
1895
1961
  }
1896
1962
 
1897
1963
  const before = execSync("git rev-parse HEAD", {
@@ -1914,20 +1980,6 @@ async function update() {
1914
1980
  printGitCloneChangelogSummary(before, after);
1915
1981
  }
1916
1982
  await relinkAndHint();
1917
-
1918
- // Restore original branch after a successful update.
1919
- if (needsCheckout && !isDetached) {
1920
- try {
1921
- execFileSync("git", ["checkout", branch, "--quiet"], { cwd: FORGE_HOME });
1922
- console.log(
1923
- ` Restored branch ${CYAN}${branch}${RESET}.`,
1924
- );
1925
- } catch {
1926
- console.log(
1927
- ` ${YELLOW}⚠ Could not restore branch ${branch} — you are now on main.${RESET}`,
1928
- );
1929
- }
1930
- }
1931
1983
  } catch (err) {
1932
1984
  console.log(
1933
1985
  ` ${YELLOW}Cannot fast-forward — local changes exist. Skipping.${RESET}`,
@@ -2190,7 +2242,14 @@ async function doctor(fix = false) {
2190
2242
  console.log("");
2191
2243
 
2192
2244
  const { targetDir: TARGET_DIR } = detectInstallPaths();
2193
- console.log(` Mode: global (~/.claude)`);
2245
+ const isGitCloneInstall = existsSync(join(FORGE_HOME, ".git"));
2246
+ const sourceBranch = isGitCloneInstall ? getGitBranch() : null;
2247
+ console.log(
2248
+ isGitCloneInstall
2249
+ ? ` Mode: git clone at ${CYAN}${FORGE_HOME}${RESET}`
2250
+ : ` Mode: global (~/.claude)`,
2251
+ );
2252
+ console.log(` Source: ${formatSourceIdentity(sourceBranch)}`);
2194
2253
  console.log("");
2195
2254
 
2196
2255
  let failures = 0;
@@ -2717,7 +2776,6 @@ async function doctor(fix = false) {
2717
2776
  // absence that isn't a problem should not read as one.
2718
2777
  {
2719
2778
  try {
2720
- const isGitCloneInstall = existsSync(join(FORGE_HOME, ".git"));
2721
2779
  if (isGitCloneInstall) {
2722
2780
  pass("Persisted toolset home (~/.forge)", "skipped — git-clone install links directly from the clone");
2723
2781
  } else {
@@ -3825,7 +3883,7 @@ switch (command) {
3825
3883
  includeExtras: restArgs.includes("--extras"),
3826
3884
  });
3827
3885
  process.stdout.write(
3828
- `Installed ${result.commandCount} OpenCode commands under ${result.configDir}.\n` +
3886
+ `Installed ${result.commandCount} OpenCode commands and ${result.skillCount} skills under ${result.configDir}.\n` +
3829
3887
  "Restart OpenCode, then run /forge/work-on <issue>.\n",
3830
3888
  );
3831
3889
  if (result.migration.removedInstructionsFile || result.migration.removedConfigEntries > 0) {
@@ -3836,7 +3894,12 @@ switch (command) {
3836
3894
  for (const warning of result.migration.warnings) process.stderr.write(`Warning: ${warning}\n`);
3837
3895
  } else if (action === "status") {
3838
3896
  const result = await getOpenCodeAdapterStatus({ home: HOME });
3839
- if (!result.installed) {
3897
+ if (result.legacy) {
3898
+ process.stdout.write(
3899
+ "A legacy ForgeDock OpenCode adapter is installed. Re-run: npx forgedock opencode install\n",
3900
+ );
3901
+ exitCode = 1;
3902
+ } else if (!result.installed) {
3840
3903
  process.stdout.write(`OpenCode adapter is not installed under ${result.configDir}.\n`);
3841
3904
  exitCode = 1;
3842
3905
  } else if (!result.healthy) {
@@ -3847,7 +3910,7 @@ switch (command) {
3847
3910
  exitCode = 1;
3848
3911
  } else {
3849
3912
  process.stdout.write(
3850
- `OpenCode adapter is healthy (${result.manifest.commandCount} commands) under ${result.configDir}.\n`,
3913
+ `OpenCode adapter is healthy (${result.manifest.commandCount} commands, ${result.manifest.skillCount ?? 0} skills) under ${result.configDir}.\n`,
3851
3914
  );
3852
3915
  }
3853
3916
  } else if (action === "uninstall") {