@sdsrs/code-graph 0.103.0 → 0.104.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.
|
@@ -9,7 +9,7 @@ const path = require('path');
|
|
|
9
9
|
const os = require('os');
|
|
10
10
|
const { CACHE_DIR, PLUGIN_ID, MARKETPLACE_NAME, readManifest, readJson, writeJsonAtomic, installedPluginsPath, pluginsCacheDir } = require('./lifecycle');
|
|
11
11
|
const { claudeHome } = require('./claude-config');
|
|
12
|
-
const { clearCache: clearBinaryCache, globalNodeModulesCandidates, PLATFORM_PKG, detectLibc } = require('./find-binary');
|
|
12
|
+
const { clearCache: clearBinaryCache, globalNodeModulesCandidates, nvmNodeModulesDirs, PLATFORM_PKG, detectLibc } = require('./find-binary');
|
|
13
13
|
const { readBinaryVersion, compareVersions, isDevMode } = require('./version-utils');
|
|
14
14
|
const { cgTmpDir } = require('./tmp-dir');
|
|
15
15
|
const { npmSpawnOpts } = require('./npm-exec');
|
|
@@ -597,6 +597,31 @@ function staleGlobalPkgs(latestVersion, roots = null) {
|
|
|
597
597
|
return out;
|
|
598
598
|
}
|
|
599
599
|
|
|
600
|
+
/**
|
|
601
|
+
* Global installs of ours stranded under a NON-active node version. nvm keeps a
|
|
602
|
+
* separate global prefix per node; switching the default node leaves the old
|
|
603
|
+
* prefix's `@sdsrs/code-graph` behind — invisible to selfHealGlobalPkgs (which
|
|
604
|
+
* only sees, and can only `npm install -g` into, the ACTIVE node's prefix) yet
|
|
605
|
+
* still able to seed stale settings.json hooks / shadow PATH shims (the
|
|
606
|
+
* v24.11.1@0.46.0 relic firing beside the active install — RCA 2026-07-24).
|
|
607
|
+
* Detection-only: returns each relic's package + version + node prefix so doctor
|
|
608
|
+
* can surface it with manual remediation. `dirs`/`activeDir` injectable for tests.
|
|
609
|
+
*/
|
|
610
|
+
function inactiveNodeGlobalRelics({ dirs = null, activeDir = null } = {}) {
|
|
611
|
+
const active = path.resolve(activeDir
|
|
612
|
+
|| path.join(path.dirname(process.execPath), '..', 'lib', 'node_modules'));
|
|
613
|
+
const roots = dirs || nvmNodeModulesDirs();
|
|
614
|
+
const out = [];
|
|
615
|
+
for (const dir of roots) {
|
|
616
|
+
if (path.resolve(dir) === active) continue; // active prefix → not a relic
|
|
617
|
+
for (const name of [SHELL_PKG, PLATFORM_PKG]) {
|
|
618
|
+
const version = globalPkgVersion(name, [dir]);
|
|
619
|
+
if (version) out.push({ name, version, nodeModulesDir: dir });
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
return out;
|
|
623
|
+
}
|
|
624
|
+
|
|
600
625
|
/** One targeted `npm install -g` for the given specs. Resolves true on exit 0. */
|
|
601
626
|
function npmInstallGlobal(specs) {
|
|
602
627
|
return new Promise((resolve) => {
|
|
@@ -647,6 +672,21 @@ async function selfHealGlobalPkgs(latest, state, {
|
|
|
647
672
|
};
|
|
648
673
|
}
|
|
649
674
|
|
|
675
|
+
// Whether a THROTTLED checkForUpdate should still attempt the global-npm
|
|
676
|
+
// self-heal. The post-fetch heal below only runs on the non-throttle path, but
|
|
677
|
+
// the ONLY context that can SEE a user's nvm/global prefix is a CLI run under
|
|
678
|
+
// that node (globalNodeModulesCandidates is execPath-derived) — and such a run,
|
|
679
|
+
// once binary+shell are current, short-circuits at the throttle early-return and
|
|
680
|
+
// never reaches the heal. That gap stranded a global `code-graph-mcp` shim at
|
|
681
|
+
// 0.101.0 while the binary reached 0.103.0 (RCA 2026-07-24). Cheap local
|
|
682
|
+
// package.json read (readStale) gates the slow, lock-guarded npm path. Split out
|
|
683
|
+
// so the decision is unit-testable without the full checkForUpdate harness.
|
|
684
|
+
function shouldHealGlobalsOnThrottle(state, { readStale = staleGlobalPkgs } = {}) {
|
|
685
|
+
if (!state || !state.latestVersion) return false;
|
|
686
|
+
if (process.env.CODE_GRAPH_INSTALL_LOCK_HELD === '1') return false; // parent launcher holds it
|
|
687
|
+
return readStale(state.latestVersion).length > 0;
|
|
688
|
+
}
|
|
689
|
+
|
|
650
690
|
async function checkForUpdate({ installMissing = false, force = false } = {}) {
|
|
651
691
|
let installLock = null;
|
|
652
692
|
try {
|
|
@@ -671,6 +711,16 @@ async function checkForUpdate({ installMissing = false, force = false } = {}) {
|
|
|
671
711
|
if (state.installedVersion !== installedVersion) {
|
|
672
712
|
saveState({ ...state, installedVersion });
|
|
673
713
|
}
|
|
714
|
+
// Global-npm shell/platform self-heal reaches the throttle window too (see
|
|
715
|
+
// shouldHealGlobalsOnThrottle). Cheap local check first; only the actually-
|
|
716
|
+
// stale case takes the slow, lock-guarded npm path.
|
|
717
|
+
if (shouldHealGlobalsOnThrottle(state)) {
|
|
718
|
+
installLock = acquireLock(path.join(CACHE_DIR, 'install.lock'));
|
|
719
|
+
if (installLock) {
|
|
720
|
+
const globalHeal = await selfHealGlobalPkgs({ version: state.latestVersion }, state);
|
|
721
|
+
saveState({ ...readState(), ...globalHeal });
|
|
722
|
+
}
|
|
723
|
+
}
|
|
674
724
|
if (state.updateAvailable && state.latestVersion
|
|
675
725
|
&& compareVersions(state.latestVersion, installedVersion) > 0) {
|
|
676
726
|
return { updateAvailable: true, from: installedVersion, to: state.latestVersion };
|
|
@@ -776,6 +826,7 @@ module.exports = {
|
|
|
776
826
|
getPlatformAssetName,
|
|
777
827
|
selfHealStaleBinary,
|
|
778
828
|
selfHealGlobalPkgs, staleGlobalPkgs, globalPkgVersion, npmInstallGlobal,
|
|
829
|
+
shouldHealGlobalsOnThrottle, inactiveNodeGlobalRelics,
|
|
779
830
|
downloadAndInstall, refreshMarketplaceClone, marketplaceCloneDir,
|
|
780
831
|
};
|
|
781
832
|
|
|
@@ -285,11 +285,29 @@ function runDiagnostics() {
|
|
|
285
285
|
// `lifecycle.js uninstall` removes them; without it they are treated as
|
|
286
286
|
// user-installed and a plugin uninstall leaves them on PATH.
|
|
287
287
|
try {
|
|
288
|
-
const { globalPkgVersion } = require('./auto-update');
|
|
288
|
+
const { globalPkgVersion, inactiveNodeGlobalRelics } = require('./auto-update');
|
|
289
289
|
const { PLATFORM_PKG } = require('./find-binary');
|
|
290
290
|
const found = [SHELL_PKG, PLATFORM_PKG]
|
|
291
291
|
.map((name) => ({ name, version: globalPkgVersion(name) }))
|
|
292
292
|
.filter((p) => p.version);
|
|
293
|
+
|
|
294
|
+
// Relics stranded under a NON-active node version (nvm keeps a per-node
|
|
295
|
+
// global prefix). selfHealGlobalPkgs / the check above only see the active
|
|
296
|
+
// node, so these drift unseen for months and can seed stale settings.json
|
|
297
|
+
// hooks — the v24.11.1@0.46.0 relic behind the RCA. Report-only: `npm i -g`
|
|
298
|
+
// can't target another node's prefix, so hand the user the exact remediation.
|
|
299
|
+
const relics = inactiveNodeGlobalRelics();
|
|
300
|
+
if (relics.length) {
|
|
301
|
+
const home = require('os').homedir();
|
|
302
|
+
results.push({
|
|
303
|
+
name: 'Global npm relics',
|
|
304
|
+
status: 'warn',
|
|
305
|
+
detail: relics.map((r) => `${r.name}@${r.version} (${r.nodeModulesDir.replace(home, '~')})`).join('; ')
|
|
306
|
+
+ ' — installed under a non-active node version; auto-heal cannot reach another node\'s prefix. '
|
|
307
|
+
+ 'Remove each via `nvm use <that node> && npm rm -g <pkg>`, or uninstall the unused node (`nvm uninstall <ver>`).',
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
|
|
293
311
|
if (found.length) {
|
|
294
312
|
const marker = !!readJson(GLOBAL_INSTALL_MARKER);
|
|
295
313
|
// Heal-exhausted is otherwise invisible: selfHealGlobalPkgs stops after
|
|
@@ -119,6 +119,22 @@ function globalNodeModulesCandidates() {
|
|
|
119
119
|
return [...new Set(out)];
|
|
120
120
|
}
|
|
121
121
|
|
|
122
|
+
// Every nvm-managed node version's global node_modules dir (`~/.nvm/versions/
|
|
123
|
+
// node/*/lib/node_modules`). nvm keeps a SEPARATE global prefix per node
|
|
124
|
+
// version; switching the default node strands the previous version's globals —
|
|
125
|
+
// a global `@sdsrs/code-graph` there is invisible to globalNodeModulesCandidates
|
|
126
|
+
// (execPath-derived → only the ACTIVE node) yet still shadows PATH shims / seeds
|
|
127
|
+
// stale settings.json hooks (the v24.11.1@0.46.0 relic — RCA 2026-07-24). Used
|
|
128
|
+
// for detection/reporting only; `npm install -g` cannot target another node's
|
|
129
|
+
// prefix. `base` is injectable for hermetic tests (never the real ~/.nvm).
|
|
130
|
+
function nvmNodeModulesDirs(base = path.join(os.homedir(), '.nvm', 'versions', 'node')) {
|
|
131
|
+
let entries;
|
|
132
|
+
try { entries = fs.readdirSync(base); } catch { return []; }
|
|
133
|
+
return entries
|
|
134
|
+
.map((v) => path.join(base, v, 'lib', 'node_modules'))
|
|
135
|
+
.filter((d) => { try { return fs.statSync(d).isDirectory(); } catch { return false; } });
|
|
136
|
+
}
|
|
137
|
+
|
|
122
138
|
function isNativeBinary(candidate) {
|
|
123
139
|
if (!candidate) return false;
|
|
124
140
|
try {
|
|
@@ -367,7 +383,7 @@ function clearCache() {
|
|
|
367
383
|
|
|
368
384
|
module.exports = {
|
|
369
385
|
findBinary, findBinaryUncached, clearCache,
|
|
370
|
-
globalNodeModulesCandidates, findPlatformBinary, platformBinaryCandidates, createVersionGate,
|
|
386
|
+
globalNodeModulesCandidates, nvmNodeModulesDirs, findPlatformBinary, platformBinaryCandidates, createVersionGate,
|
|
371
387
|
getPackageVersion, compareVersions, isCachedBinaryFresh,
|
|
372
388
|
detectLibc, unsupportedPlatformHint,
|
|
373
389
|
CACHE_FILE, BINARY_NAME, PLATFORM_PKG,
|
|
@@ -375,13 +375,17 @@ function isOurHookEntry(entry) {
|
|
|
375
375
|
if (!entry || !entry.hooks) return false;
|
|
376
376
|
// Primary: match by description (immune to path pollution).
|
|
377
377
|
if (entry.description && OUR_DESCRIPTIONS.includes(entry.description)) return true;
|
|
378
|
-
// Fallback: script
|
|
379
|
-
//
|
|
380
|
-
// the
|
|
381
|
-
//
|
|
378
|
+
// Fallback: script basename + a delivery-surface marker in the path. TWO
|
|
379
|
+
// surfaces ship these scripts: the marketplace plugin-cache (dir
|
|
380
|
+
// 'code-graph-mcp') AND the global npm package (dir '@sdsrs/code-graph' — note
|
|
381
|
+
// NO '-mcp' suffix). v0.32.1 tightened from bare 'code-graph' (which would
|
|
382
|
+
// claim a user's own ~/code-graph/foo.js) to MARKETPLACE_NAME, but that alone
|
|
383
|
+
// missed the npm-global surface, so `npm i -g`-delivered hooks were never
|
|
384
|
+
// evicted and orphan-accumulated across node/version switches (RCA 2026-07-24).
|
|
385
|
+
// Both markers are specific enough not to claim a user's unrelated file.
|
|
382
386
|
return entry.hooks.some(h =>
|
|
383
387
|
h.command && OUR_HOOK_SCRIPTS.some(s => h.command.includes(s)) &&
|
|
384
|
-
h.command.includes(MARKETPLACE_NAME)
|
|
388
|
+
(h.command.includes(MARKETPLACE_NAME) || h.command.includes(SHELL_PKG))
|
|
385
389
|
);
|
|
386
390
|
}
|
|
387
391
|
|
|
@@ -448,6 +452,23 @@ function buildSettingsHookEntries() {
|
|
|
448
452
|
// re-write it to settings.json.
|
|
449
453
|
function registerHooksToSettings(settings) {
|
|
450
454
|
settings.hooks = settings.hooks || {};
|
|
455
|
+
|
|
456
|
+
// Idempotent across delivery surfaces: if every desired (event,matcher) is
|
|
457
|
+
// already present exactly once, pointing at a current, existing script
|
|
458
|
+
// (plugin-cache OR global-npm), do nothing. Stops the settings.json ping-pong
|
|
459
|
+
// where the cache session-init and the npm-global CLI doctor each rewrote the
|
|
460
|
+
// other's valid entry every run (RCA 2026-07-24). Any missing/stale/dead entry
|
|
461
|
+
// — or a duplicate ( oursCount > expected) — still triggers evict+rewrite.
|
|
462
|
+
const survey = surveyHookCoverage(settings);
|
|
463
|
+
let oursCount = 0;
|
|
464
|
+
for (const entries of Object.values(settings.hooks)) {
|
|
465
|
+
if (Array.isArray(entries)) oursCount += entries.filter(isOurHookEntry).length;
|
|
466
|
+
}
|
|
467
|
+
if (survey.missing.length === 0 && survey.stale.length === 0
|
|
468
|
+
&& oursCount === survey.expected.length) {
|
|
469
|
+
return false;
|
|
470
|
+
}
|
|
471
|
+
|
|
451
472
|
const before = JSON.stringify(settings.hooks);
|
|
452
473
|
|
|
453
474
|
// Pass 1: evict our entries across every event.
|
|
@@ -467,6 +488,22 @@ function registerHooksToSettings(settings) {
|
|
|
467
488
|
return before !== JSON.stringify(settings.hooks);
|
|
468
489
|
}
|
|
469
490
|
|
|
491
|
+
// Extract the .js script path a hook command invokes — bare (`node "…"`) or
|
|
492
|
+
// existence-guarded (`if [ -f "…" ]; then node "…"; fi`).
|
|
493
|
+
function hookCmdScript(cmd) {
|
|
494
|
+
const m = (cmd || '').match(/node "([^"]+\.js)"/) || (cmd || '').match(/"([^"]+\.js)"/);
|
|
495
|
+
return m ? m[1] : null;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
// Version encoded in a plugin-cache path (.../code-graph-mcp/code-graph-mcp/<ver>/scripts/…).
|
|
499
|
+
// Null for in-place installs (global npm), whose path never carries a version
|
|
500
|
+
// dir — npm overwrites the same path on upgrade, so such a path never goes
|
|
501
|
+
// version-stale (only dead-path-stale, caught separately by fs.existsSync).
|
|
502
|
+
function cacheDirVersion(scriptPath) {
|
|
503
|
+
const m = (scriptPath || '').match(/\/code-graph-mcp\/code-graph-mcp\/(\d+\.\d+\.\d+[^/]*)\//);
|
|
504
|
+
return m ? m[1] : null;
|
|
505
|
+
}
|
|
506
|
+
|
|
470
507
|
// Inventory of (event, matcher) tuples we expect to find in settings.json after
|
|
471
508
|
// install. Consumed by doctor (report + fix) and session-init (self-heal):
|
|
472
509
|
// `missing` = entry absent; `stale` = present but the registered command no
|
|
@@ -504,10 +541,34 @@ function surveyHookCoverage(settings) {
|
|
|
504
541
|
}
|
|
505
542
|
}
|
|
506
543
|
|
|
544
|
+
const { compareVersions } = require('./version-utils');
|
|
507
545
|
const missing = expected.filter(k => !present.has(k));
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
)
|
|
546
|
+
// Version/surface-tolerant staleness. Was an exact command-string compare,
|
|
547
|
+
// which made two registration authorities (plugin-cache session-init vs
|
|
548
|
+
// global-npm CLI doctor — different absolute paths) each flag the other's
|
|
549
|
+
// VALID CURRENT entry stale and rewrite it → settings.json ping-pong on every
|
|
550
|
+
// alternating run (RCA 2026-07-24). An entry is stale only when its script is
|
|
551
|
+
// a dead path OR resolves to an OLDER plugin-cache version dir than we'd write
|
|
552
|
+
// now. A present entry on a different but valid, current surface (npm in-place
|
|
553
|
+
// install: file exists, no version in path) is NOT stale.
|
|
554
|
+
const stale = expected.filter(k => {
|
|
555
|
+
if (!present.has(k) || !presentCmd[k]) return false;
|
|
556
|
+
const pScript = hookCmdScript(presentCmd[k]);
|
|
557
|
+
if (!pScript) return false;
|
|
558
|
+
if (!fs.existsSync(pScript)) return true; // dead path
|
|
559
|
+
const pv = cacheDirVersion(pScript);
|
|
560
|
+
if (pv) {
|
|
561
|
+
// Pinned to a plugin-cache version dir: stale iff older than us. Compare
|
|
562
|
+
// against the desired cache dir when we're the cache authority; the
|
|
563
|
+
// global-npm/dev authority's desired path carries no version dir, so
|
|
564
|
+
// fall back to our own plugin version — an old-version cache pin must be
|
|
565
|
+
// healed from EITHER surface, not only when the desired path happens to
|
|
566
|
+
// be cache-shaped. Newer-than-us stays (downgrade-war guard, §1.11).
|
|
567
|
+
const dv = cacheDirVersion(hookCmdScript(desiredCmd[k])) || getPluginVersion();
|
|
568
|
+
return compareVersions(pv, dv) < 0;
|
|
569
|
+
}
|
|
570
|
+
return false; // in-place/current surface
|
|
571
|
+
});
|
|
511
572
|
return { expected, present: [...present], missing, stale };
|
|
512
573
|
}
|
|
513
574
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sdsrs/code-graph",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.104.1",
|
|
4
4
|
"description": "MCP server that indexes codebases into an AST knowledge graph with semantic search, call graph traversal, and HTTP route tracing",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -29,17 +29,17 @@
|
|
|
29
29
|
],
|
|
30
30
|
"scripts": {
|
|
31
31
|
"build": "cargo build --release --no-default-features && node scripts/copy-binary.js",
|
|
32
|
-
"prepare": "git rev-parse --git-dir > /dev/null 2>&1 &&
|
|
32
|
+
"prepare": "git rev-parse --git-dir > /dev/null 2>&1 && git config core.hooksPath scripts/githooks || true",
|
|
33
33
|
"preuninstall": "node claude-plugin/scripts/lifecycle.js uninstall || true"
|
|
34
34
|
},
|
|
35
35
|
"engines": {
|
|
36
36
|
"node": ">=16"
|
|
37
37
|
},
|
|
38
38
|
"optionalDependencies": {
|
|
39
|
-
"@sdsrs/code-graph-linux-x64": "0.
|
|
40
|
-
"@sdsrs/code-graph-linux-arm64": "0.
|
|
41
|
-
"@sdsrs/code-graph-darwin-x64": "0.
|
|
42
|
-
"@sdsrs/code-graph-darwin-arm64": "0.
|
|
43
|
-
"@sdsrs/code-graph-win32-x64": "0.
|
|
39
|
+
"@sdsrs/code-graph-linux-x64": "0.104.1",
|
|
40
|
+
"@sdsrs/code-graph-linux-arm64": "0.104.1",
|
|
41
|
+
"@sdsrs/code-graph-darwin-x64": "0.104.1",
|
|
42
|
+
"@sdsrs/code-graph-darwin-arm64": "0.104.1",
|
|
43
|
+
"@sdsrs/code-graph-win32-x64": "0.104.1"
|
|
44
44
|
}
|
|
45
45
|
}
|