forgedock 1.5.0 → 1.6.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/bin/forgedock.mjs +72 -25
- package/bin/hooks/session-start.mjs +22 -1
- package/bin/journey.mjs +460 -399
- package/bin/registry.mjs +86 -0
- package/bin/runner.mjs +197 -34
- package/commands/orchestrate/phase-3-dependency.md +6 -1
- package/commands/orchestrate/phase-4-execution.md +113 -3
- package/commands/work-on/build/architect.md +28 -3
- package/package.json +1 -1
- package/scripts/build-knowledge-index.mjs +6 -2
- package/scripts/check-spec-markers.sh +1 -0
- package/scripts/derive-finding-milestone.sh +18 -8
package/bin/forgedock.mjs
CHANGED
|
@@ -59,8 +59,8 @@ import {
|
|
|
59
59
|
detectClaudeVersion,
|
|
60
60
|
loadBreakpoints,
|
|
61
61
|
hasFeature,
|
|
62
|
-
getPersistedHomeState,
|
|
63
62
|
setPersistedHomeState,
|
|
63
|
+
getInstalledCommandsDriftStatus,
|
|
64
64
|
} from "./registry.mjs";
|
|
65
65
|
import { renderMark, ember } from "./cinema.mjs";
|
|
66
66
|
import {
|
|
@@ -1513,6 +1513,51 @@ function resolveGlobalNpmForgedockDir() {
|
|
|
1513
1513
|
}
|
|
1514
1514
|
}
|
|
1515
1515
|
|
|
1516
|
+
/**
|
|
1517
|
+
* Compare two already-`resolve()`d absolute paths for equality or containment
|
|
1518
|
+
* (child equals parent, or sits strictly under it). On case-insensitive
|
|
1519
|
+
* filesystems (win32/darwin) the same physical directory can resolve with
|
|
1520
|
+
* differing letter casing (drive letter, path segments), so both operands are
|
|
1521
|
+
* lowercased before comparing there; on Linux the comparison stays
|
|
1522
|
+
* case-sensitive byte-for-byte (forge#2668). On the case-insensitive branch
|
|
1523
|
+
* both operands are also Unicode-normalized to NFC: macOS APFS/HFS+ enumerate
|
|
1524
|
+
* filenames NFD-normalized while CLI args/env vars typically arrive NFC, and
|
|
1525
|
+
* `resolve()` preserves the input form — so without normalization two paths
|
|
1526
|
+
* for the same physical directory containing accented characters can compare
|
|
1527
|
+
* unequal (forge#2674). NFC is idempotent for pure-ASCII and pre-composed
|
|
1528
|
+
* paths, so ASCII behavior is unchanged; win32 normalizes harmlessly. The
|
|
1529
|
+
* `+ sep` boundary keeps `/foo-bar` from matching parent `/foo`. Callers pass
|
|
1530
|
+
* paths that are already resolved — this helper deliberately does not
|
|
1531
|
+
* re-resolve.
|
|
1532
|
+
*
|
|
1533
|
+
* Known limitation: `caseInsensitive` is gated on `process.platform`, an
|
|
1534
|
+
* OS-level constant, not on the actual case-sensitivity of the volume
|
|
1535
|
+
* FORGE_HOME/the npm global root live on. This is the correct assumption for
|
|
1536
|
+
* the default filesystem format on each OS (NTFS on win32, APFS/HFS+ on
|
|
1537
|
+
* darwin), but a volume explicitly formatted case-sensitive (e.g. opt-in
|
|
1538
|
+
* case-sensitive APFS) would still be case-folded here, and two distinct
|
|
1539
|
+
* directories differing only by case could be treated as equal. This is an
|
|
1540
|
+
* accepted approximation, not a probe of real volume semantics — both call
|
|
1541
|
+
* sites (`isGlobalNpmInstall`, `findSeparateGlobalInstall`) already fail
|
|
1542
|
+
* closed / stay advisory-only, so a false match here degrades to their
|
|
1543
|
+
* pre-existing "detection inconclusive" behavior rather than causing any
|
|
1544
|
+
* destructive or incorrect action (forge#2675). Probing the actual volume
|
|
1545
|
+
* (e.g. an `existsSync` case-twist check) is deliberately not done — the
|
|
1546
|
+
* added complexity and syscall aren't warranted for a purely advisory path.
|
|
1547
|
+
*
|
|
1548
|
+
* @param {string} child - resolved absolute path being tested
|
|
1549
|
+
* @param {string} parent - resolved absolute path of the candidate ancestor
|
|
1550
|
+
* @returns {boolean}
|
|
1551
|
+
*/
|
|
1552
|
+
function samePathOrUnder(child, parent) {
|
|
1553
|
+
const caseInsensitive =
|
|
1554
|
+
process.platform === "win32" || process.platform === "darwin";
|
|
1555
|
+
const fold = (p) => (caseInsensitive ? p.normalize("NFC").toLowerCase() : p);
|
|
1556
|
+
const a = fold(child);
|
|
1557
|
+
const b = fold(parent);
|
|
1558
|
+
return a === b || a.startsWith(b + sep);
|
|
1559
|
+
}
|
|
1560
|
+
|
|
1516
1561
|
/**
|
|
1517
1562
|
* Detect whether FORGE_HOME resolves inside npm's global install tree, i.e.
|
|
1518
1563
|
* this process is running as a globally-installed `forgedock` package (not
|
|
@@ -1533,7 +1578,7 @@ function isGlobalNpmInstall() {
|
|
|
1533
1578
|
const globalPkgDir = resolveGlobalNpmForgedockDir();
|
|
1534
1579
|
if (!globalPkgDir) return false;
|
|
1535
1580
|
const home = resolve(FORGE_HOME);
|
|
1536
|
-
return home
|
|
1581
|
+
return samePathOrUnder(home, globalPkgDir);
|
|
1537
1582
|
}
|
|
1538
1583
|
|
|
1539
1584
|
/**
|
|
@@ -1558,7 +1603,7 @@ function findSeparateGlobalInstall() {
|
|
|
1558
1603
|
const globalPkgDir = resolveGlobalNpmForgedockDir();
|
|
1559
1604
|
if (!globalPkgDir) return null;
|
|
1560
1605
|
const home = resolve(FORGE_HOME);
|
|
1561
|
-
if (home
|
|
1606
|
+
if (samePathOrUnder(home, globalPkgDir)) {
|
|
1562
1607
|
// Current FORGE_HOME already IS the global install — nothing separate.
|
|
1563
1608
|
return null;
|
|
1564
1609
|
}
|
|
@@ -1804,9 +1849,10 @@ async function update() {
|
|
|
1804
1849
|
// dirty tree. HEAD is still never moved in either case.
|
|
1805
1850
|
const cwdResolved = resolve(process.cwd());
|
|
1806
1851
|
const forgeHomeResolved = resolve(FORGE_HOME);
|
|
1807
|
-
const isSourceRepoSelf =
|
|
1808
|
-
cwdResolved
|
|
1809
|
-
|
|
1852
|
+
const isSourceRepoSelf = samePathOrUnder(
|
|
1853
|
+
cwdResolved,
|
|
1854
|
+
forgeHomeResolved,
|
|
1855
|
+
);
|
|
1810
1856
|
|
|
1811
1857
|
if (isSourceRepoSelf) {
|
|
1812
1858
|
console.log(
|
|
@@ -2694,27 +2740,16 @@ async function doctor(fix = false) {
|
|
|
2694
2740
|
`incomplete (missing: ${missingDirs.join(", ")}). Run: npx forgedock install`,
|
|
2695
2741
|
);
|
|
2696
2742
|
} else {
|
|
2697
|
-
//
|
|
2698
|
-
//
|
|
2699
|
-
//
|
|
2700
|
-
//
|
|
2701
|
-
//
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
const recorded = getPersistedHomeState();
|
|
2705
|
-
let persistedVersion = recorded?.version || "";
|
|
2706
|
-
if (!persistedVersion) {
|
|
2707
|
-
try {
|
|
2708
|
-
persistedVersion = readFileSync(versionPath, "utf-8").trim();
|
|
2709
|
-
} catch {
|
|
2710
|
-
// version file missing/unreadable — reported below as "unknown"
|
|
2711
|
-
}
|
|
2712
|
-
}
|
|
2713
|
-
const sourceVersion = getVersion();
|
|
2714
|
-
if (persistedVersion && sourceVersion && compareVersions(persistedVersion, sourceVersion) < 0) {
|
|
2743
|
+
// Package-vs-installed-commands drift comparison is factored into
|
|
2744
|
+
// a single shared helper (bin/registry.mjs's
|
|
2745
|
+
// getInstalledCommandsDriftStatus, forge#2719) so doctor, the
|
|
2746
|
+
// `version` command, and the SessionStart hook all agree on one
|
|
2747
|
+
// source of truth instead of re-deriving this logic independently.
|
|
2748
|
+
const { persistedVersion, isStale, unknown } = getInstalledCommandsDriftStatus(FORGE_HOME);
|
|
2749
|
+
if (!unknown && isStale) {
|
|
2715
2750
|
warn(
|
|
2716
2751
|
"Persisted toolset home (~/.forge)",
|
|
2717
|
-
`v${persistedVersion} — source is v${
|
|
2752
|
+
`v${persistedVersion} — source is v${getVersion()}. Run: npx forgedock update`,
|
|
2718
2753
|
);
|
|
2719
2754
|
} else {
|
|
2720
2755
|
pass("Persisted toolset home (~/.forge)", `v${persistedVersion || "unknown"} at ${persistedHome}`);
|
|
@@ -3913,6 +3948,18 @@ switch (command) {
|
|
|
3913
3948
|
`Run ${CYAN}npx forgedock update${RESET} to fetch it.`,
|
|
3914
3949
|
);
|
|
3915
3950
|
}
|
|
3951
|
+
|
|
3952
|
+
// forge#2719 — surface package-vs-installed-commands drift (previously
|
|
3953
|
+
// only visible via `doctor` Check 5d). Uses the same shared helper doctor
|
|
3954
|
+
// calls, so both surfaces agree. Silent when versions match or when
|
|
3955
|
+
// either version is unknown (e.g. a git-clone dev install, which never
|
|
3956
|
+
// persists to ~/.forge — see registry.mjs's getInstalledCommandsDriftStatus).
|
|
3957
|
+
const driftStatus = getInstalledCommandsDriftStatus(FORGE_HOME);
|
|
3958
|
+
if (!driftStatus.unknown && driftStatus.isStale) {
|
|
3959
|
+
console.log(
|
|
3960
|
+
` ${YELLOW}Installed commands are stale (v${driftStatus.persistedVersion}) vs package v${driftStatus.sourceVersion}${RESET} — run: ${CYAN}npx forgedock update${RESET}`,
|
|
3961
|
+
);
|
|
3962
|
+
}
|
|
3916
3963
|
break;
|
|
3917
3964
|
}
|
|
3918
3965
|
case "help":
|
|
@@ -51,6 +51,8 @@ const FORGE_HOME = resolve(__dirname, "..", "..");
|
|
|
51
51
|
let resolveState;
|
|
52
52
|
let nudgeSeen;
|
|
53
53
|
let markNudgeSeen;
|
|
54
|
+
// Assigned alongside the other registry.mjs helpers below — see forge#2719.
|
|
55
|
+
let getInstalledCommandsDriftStatus;
|
|
54
56
|
|
|
55
57
|
// ---------------------------------------------------------------------------
|
|
56
58
|
// forge-utils helpers — populated inside the try block to honour fail-open
|
|
@@ -88,7 +90,7 @@ try {
|
|
|
88
90
|
// that import() rejects with ERR_UNSUPPORTED_ESM_URL_SCHEME.
|
|
89
91
|
/** @type {import('../registry.mjs')} */
|
|
90
92
|
(
|
|
91
|
-
{ resolveState, nudgeSeen, markNudgeSeen } = await import(
|
|
93
|
+
{ resolveState, nudgeSeen, markNudgeSeen, getInstalledCommandsDriftStatus } = await import(
|
|
92
94
|
pathToFileURL(join(FORGE_HOME, "bin", "registry.mjs")).href
|
|
93
95
|
)
|
|
94
96
|
);
|
|
@@ -126,6 +128,25 @@ try {
|
|
|
126
128
|
const cwd = process.cwd();
|
|
127
129
|
const state = resolveState(cwd);
|
|
128
130
|
|
|
131
|
+
// Surface package-vs-installed-commands drift as a single non-fatal
|
|
132
|
+
// advisory line (forge#2719). Uses the same shared helper doctor's
|
|
133
|
+
// Check 5d and the `version` command call, so all three surfaces agree.
|
|
134
|
+
// Best-effort: any failure here must never block the session, and
|
|
135
|
+
// managed-optedout directories stay completely silent per this hook's
|
|
136
|
+
// existing contract for that state (see the switch below).
|
|
137
|
+
if (state !== "managed-optedout") {
|
|
138
|
+
try {
|
|
139
|
+
const driftStatus = getInstalledCommandsDriftStatus(FORGE_HOME);
|
|
140
|
+
if (!driftStatus.unknown && driftStatus.isStale) {
|
|
141
|
+
console.log(
|
|
142
|
+
`ForgeDock: installed commands are stale (v${driftStatus.persistedVersion}) vs package v${driftStatus.sourceVersion} — run: npx forgedock update`,
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
} catch {
|
|
146
|
+
// Fail open — never let a drift-check error affect session start.
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
129
150
|
switch (state) {
|
|
130
151
|
case "managed-active":
|
|
131
152
|
await handleManagedActive(cwd);
|