@sdsrs/code-graph 0.102.0 → 0.103.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/cli.js +15 -5
- package/claude-plugin/.claude-plugin/plugin.json +1 -1
- package/claude-plugin/scripts/auto-update.js +4 -16
- package/claude-plugin/scripts/doctor.js +12 -4
- package/claude-plugin/scripts/find-binary.js +16 -21
- package/claude-plugin/scripts/lifecycle.js +113 -26
- package/claude-plugin/scripts/mcp-launcher.js +4 -0
- package/claude-plugin/scripts/mcp-stub.js +25 -3
- package/claude-plugin/scripts/session-init.js +8 -2
- package/claude-plugin/scripts/version-utils.js +32 -1
- package/package.json +6 -6
package/bin/cli.js
CHANGED
|
@@ -45,16 +45,21 @@ if (sub === "uninstall") {
|
|
|
45
45
|
if (process.argv.slice(3).some((a) => a === "--help" || a === "-h")) {
|
|
46
46
|
process.stdout.write(
|
|
47
47
|
"code-graph-mcp uninstall — remove code-graph config + cache from this machine\n\n" +
|
|
48
|
-
"USAGE:\n code-graph-mcp uninstall\n\n" +
|
|
48
|
+
"USAGE:\n code-graph-mcp uninstall [--unadopt-all] [--purge-global]\n\n" +
|
|
49
49
|
"Restores your prior statusline, strips code-graph hooks from settings.json,\n" +
|
|
50
50
|
"deletes ~/.cache/code-graph, and removes this project's CLAUDE.md adoption\n" +
|
|
51
|
-
"block.
|
|
52
|
-
"
|
|
51
|
+
"block. --unadopt-all also removes the managed block + detail file from every\n" +
|
|
52
|
+
"registered adopted project; --purge-global removes the globally-installed\n" +
|
|
53
|
+
"@sdsrs npm packages even without the plugin-install marker. Also run\n" +
|
|
54
|
+
"`/plugin uninstall code-graph-mcp` in Claude Code to sync its UI.\n");
|
|
53
55
|
process.exit(0);
|
|
54
56
|
}
|
|
55
57
|
const lifecycle = require("../claude-plugin/scripts/lifecycle");
|
|
56
58
|
const { unadopt } = require("../claude-plugin/scripts/adopt");
|
|
57
|
-
const r = lifecycle.uninstall({
|
|
59
|
+
const r = lifecycle.uninstall({
|
|
60
|
+
purgeGlobal: process.argv.slice(3).includes("--purge-global"),
|
|
61
|
+
unadoptAll: process.argv.slice(3).includes("--unadopt-all"),
|
|
62
|
+
});
|
|
58
63
|
let ua = { ok: false };
|
|
59
64
|
try { ua = unadopt(); } catch { /* best-effort — settings/cache already cleaned */ }
|
|
60
65
|
const projectUnadopted = !!(ua && (ua.blockPruned || ua.fileRemoved || ua.claudeMdRemoved));
|
|
@@ -69,9 +74,14 @@ if (sub === "uninstall") {
|
|
|
69
74
|
` Remove with: npm uninstall -g ${r.globalPkgsRemaining.join(" ")}` +
|
|
70
75
|
(r.pluginInstalledGlobals ? "\n" : " (or re-run with --purge-global)\n");
|
|
71
76
|
}
|
|
77
|
+
if (r.unadopted.length) {
|
|
78
|
+
const cleaned = r.unadopted.filter((u) => u.cleaned).length;
|
|
79
|
+
out += ` Unadopted ${cleaned}/${r.unadopted.length} registered project(s) (--unadopt-all).\n`;
|
|
80
|
+
}
|
|
72
81
|
const otherAdopted = r.adoptedProjects.filter((p) => p !== process.cwd());
|
|
73
82
|
if (otherAdopted.length) {
|
|
74
|
-
out += " Other adopted project(s) — run
|
|
83
|
+
out += " Other adopted project(s) — re-run with --unadopt-all, or in each:" +
|
|
84
|
+
" `code-graph-mcp unadopt` + `rm -rf .code-graph`\n" +
|
|
75
85
|
otherAdopted.map((p) => ` ${p}\n`).join("");
|
|
76
86
|
}
|
|
77
87
|
out += " Also run `/plugin uninstall code-graph-mcp` in Claude Code to sync its UI state.\n";
|
|
@@ -10,7 +10,7 @@ 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
12
|
const { clearCache: clearBinaryCache, globalNodeModulesCandidates, PLATFORM_PKG, detectLibc } = require('./find-binary');
|
|
13
|
-
const { readBinaryVersion, isDevMode } = require('./version-utils');
|
|
13
|
+
const { readBinaryVersion, compareVersions, isDevMode } = require('./version-utils');
|
|
14
14
|
const { cgTmpDir } = require('./tmp-dir');
|
|
15
15
|
const { npmSpawnOpts } = require('./npm-exec');
|
|
16
16
|
const { acquireLock } = require('./install-lock');
|
|
@@ -112,21 +112,9 @@ function shouldCheck(state, { force = false } = {}) {
|
|
|
112
112
|
return elapsed >= interval;
|
|
113
113
|
}
|
|
114
114
|
|
|
115
|
-
// ── Version Comparison
|
|
116
|
-
|
|
117
|
-
//
|
|
118
|
-
// tag (e.g. "1.2.4-rc1") is NOT semver-ordered: `Number("4-rc1")` is NaN → coerced
|
|
119
|
-
// to 0, dropping that segment's number (so "1.2.4-rc1" wrongly sorts below "1.2.3").
|
|
120
|
-
// Revisit with a real semver compare only if the release process adopts pre-releases.
|
|
121
|
-
function compareVersions(a, b) {
|
|
122
|
-
const pa = a.split('.').map(Number);
|
|
123
|
-
const pb = b.split('.').map(Number);
|
|
124
|
-
for (let i = 0; i < 3; i++) {
|
|
125
|
-
if ((pa[i] || 0) > (pb[i] || 0)) return 1;
|
|
126
|
-
if ((pa[i] || 0) < (pb[i] || 0)) return -1;
|
|
127
|
-
}
|
|
128
|
-
return 0;
|
|
129
|
-
}
|
|
115
|
+
// ── Version Comparison ─────────────────────────────────────
|
|
116
|
+
// compareVersions is imported from version-utils.js (single canonical,
|
|
117
|
+
// pre-release-aware implementation) and re-exported below.
|
|
130
118
|
|
|
131
119
|
// ── GitHub API ─────────────────────────────────────────────
|
|
132
120
|
|
|
@@ -292,12 +292,20 @@ function runDiagnostics() {
|
|
|
292
292
|
.filter((p) => p.version);
|
|
293
293
|
if (found.length) {
|
|
294
294
|
const marker = !!readJson(GLOBAL_INSTALL_MARKER);
|
|
295
|
+
// Heal-exhausted is otherwise invisible: selfHealGlobalPkgs stops after
|
|
296
|
+
// 3 failed npm runs per target version and stays silent until the next
|
|
297
|
+
// release re-arms the counter — a drifted CLI shim just sits there.
|
|
298
|
+
const state = readJson(path.join(CACHE_DIR, 'update-state.json')) || {};
|
|
299
|
+
const healGaveUp = (state.globalPkgHealAttempts || 0) >= 3;
|
|
295
300
|
results.push({
|
|
296
301
|
name: 'Global npm packages',
|
|
297
|
-
status: 'ok',
|
|
298
|
-
detail: found.map((p) => `${p.name}@${p.version}`).join(', ') + (
|
|
299
|
-
?
|
|
300
|
-
|
|
302
|
+
status: healGaveUp ? 'warn' : 'ok',
|
|
303
|
+
detail: found.map((p) => `${p.name}@${p.version}`).join(', ') + (healGaveUp
|
|
304
|
+
? ` — self-heal gave up after ${state.globalPkgHealAttempts} failed npm runs targeting v${state.globalPkgHealVersion}; ` +
|
|
305
|
+
`your npm env likely can't install globally (EACCES/system node). Run manually: npm install -g ${found.map((p) => `${p.name}@${state.globalPkgHealVersion}`).join(' ')}`
|
|
306
|
+
: (marker
|
|
307
|
+
? ' — plugin-installed; `node lifecycle.js uninstall` removes them'
|
|
308
|
+
: ` — no plugin-install marker; uninstall leaves them (remove: npm uninstall -g ${found.map((p) => p.name).join(' ')})`)),
|
|
301
309
|
});
|
|
302
310
|
}
|
|
303
311
|
} catch { /* probe failed — skip */ }
|
|
@@ -4,7 +4,7 @@ const { execFileSync } = require('child_process');
|
|
|
4
4
|
const path = require('path');
|
|
5
5
|
const fs = require('fs');
|
|
6
6
|
const os = require('os');
|
|
7
|
-
const { readBinaryVersion } = require('./version-utils');
|
|
7
|
+
const { readBinaryVersion, compareVersions } = require('./version-utils');
|
|
8
8
|
const { npmSpawnOpts } = require('./npm-exec');
|
|
9
9
|
|
|
10
10
|
const PLATFORM = os.platform();
|
|
@@ -66,23 +66,8 @@ function getPackageVersion() {
|
|
|
66
66
|
catch { return null; }
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
* Assumes plain numeric releases (the project's tag scheme); a pre-release tag
|
|
72
|
-
* (e.g. "1.2.3-rc1") is NOT semver-ordered — `parseInt("3-rc1", 10)` keeps the
|
|
73
|
-
* leading 3 and drops the suffix, so "1.2.3-rc1" compares EQUAL to "1.2.3".
|
|
74
|
-
* Revisit only if releases adopt pre-release tags.
|
|
75
|
-
*/
|
|
76
|
-
function compareVersions(a, b) {
|
|
77
|
-
const pa = String(a).split('.').map(s => parseInt(s, 10));
|
|
78
|
-
const pb = String(b).split('.').map(s => parseInt(s, 10));
|
|
79
|
-
for (let i = 0; i < 3; i++) {
|
|
80
|
-
const x = Number.isFinite(pa[i]) ? pa[i] : 0;
|
|
81
|
-
const y = Number.isFinite(pb[i]) ? pb[i] : 0;
|
|
82
|
-
if (x !== y) return x < y ? -1 : 1;
|
|
83
|
-
}
|
|
84
|
-
return 0;
|
|
85
|
-
}
|
|
69
|
+
// compareVersions lives in version-utils.js (single canonical implementation,
|
|
70
|
+
// pre-release-aware); re-exported below for existing consumers.
|
|
86
71
|
|
|
87
72
|
/**
|
|
88
73
|
* Candidate paths for npm global `node_modules`.
|
|
@@ -217,19 +202,29 @@ function isDevRepo(rootDir) {
|
|
|
217
202
|
* nvm/standard setups), so a working `npm install -g @sdsrs/code-graph` can
|
|
218
203
|
* still be invisible without the fallback.
|
|
219
204
|
*/
|
|
205
|
+
// Truncation gate for the npm platform-package tier ONLY: an interrupted npm
|
|
206
|
+
// install can leave a partial binary with the right name, and unlike the
|
|
207
|
+
// GitHub-download path (size + sha256 sidecar + version-exec before promote)
|
|
208
|
+
// nothing else checks this tier. Real release binaries are ~40MB; 1MB matches
|
|
209
|
+
// promoteVerifiedBinary's floor. Deliberately NOT inside isNativeBinary —
|
|
210
|
+
// dev builds, cargo installs, and test fixtures go through other tiers.
|
|
211
|
+
function isPlausibleReleaseBinary(candidate) {
|
|
212
|
+
try { return fs.statSync(candidate).size > 1_000_000; } catch { return false; }
|
|
213
|
+
}
|
|
214
|
+
|
|
220
215
|
function platformBinaryCandidates() {
|
|
221
216
|
const out = [];
|
|
222
217
|
// Fast path: standard module resolution.
|
|
223
218
|
try {
|
|
224
219
|
const pkgPath = require.resolve(`${PLATFORM_PKG}/package.json`);
|
|
225
220
|
const bin = path.join(path.dirname(pkgPath), BINARY_NAME);
|
|
226
|
-
if (isNativeBinary(bin)) out.push(bin);
|
|
221
|
+
if (isNativeBinary(bin) && isPlausibleReleaseBinary(bin)) out.push(bin);
|
|
227
222
|
} catch { /* not in node_modules walk-up */ }
|
|
228
223
|
|
|
229
224
|
// Slow path: explicit global node_modules probe.
|
|
230
225
|
for (const globalRoot of globalNodeModulesCandidates()) {
|
|
231
226
|
const bin = path.join(globalRoot, '@sdsrs', `code-graph-${PLATFORM}-${ARCH}`, BINARY_NAME);
|
|
232
|
-
if (isNativeBinary(bin)) out.push(bin);
|
|
227
|
+
if (isNativeBinary(bin) && isPlausibleReleaseBinary(bin)) out.push(bin);
|
|
233
228
|
}
|
|
234
229
|
|
|
235
230
|
return out;
|
|
@@ -372,7 +367,7 @@ function clearCache() {
|
|
|
372
367
|
|
|
373
368
|
module.exports = {
|
|
374
369
|
findBinary, findBinaryUncached, clearCache,
|
|
375
|
-
globalNodeModulesCandidates, findPlatformBinary, createVersionGate,
|
|
370
|
+
globalNodeModulesCandidates, findPlatformBinary, platformBinaryCandidates, createVersionGate,
|
|
376
371
|
getPackageVersion, compareVersions, isCachedBinaryFresh,
|
|
377
372
|
detectLibc, unsupportedPlatformHint,
|
|
378
373
|
CACHE_FILE, BINARY_NAME, PLATFORM_PKG,
|
|
@@ -197,25 +197,41 @@ function isPluginInactive(settings = readJson(settingsPath()) || {}) {
|
|
|
197
197
|
return !hasInstalledPluginRecord();
|
|
198
198
|
}
|
|
199
199
|
|
|
200
|
-
function detachStatuslineIntegration(settings) {
|
|
200
|
+
function detachStatuslineIntegration(settings, { compositeDoomed = true } = {}) {
|
|
201
201
|
let settingsChanged = false;
|
|
202
202
|
|
|
203
203
|
unregisterStatuslineProvider('code-graph');
|
|
204
|
-
const
|
|
204
|
+
const registry = readRegistry();
|
|
205
|
+
const previous = registry.find(p => p.id === '_previous' && p.command);
|
|
206
|
+
// Third-party providers registered through our registry (e.g. gsd). They
|
|
207
|
+
// must not be silently orphaned: with the composite gone from settings
|
|
208
|
+
// their segments stop rendering while the registry entries dangle.
|
|
209
|
+
const thirdParty = registry.filter(p => p.id !== '_previous' && p.id !== 'code-graph' && p.command);
|
|
205
210
|
|
|
206
211
|
// If our composite is still configured while the plugin is disabled/uninstalled,
|
|
207
|
-
//
|
|
208
|
-
// truly stops affecting Claude Code.
|
|
212
|
+
// stop affecting Claude Code — but keep surviving third parties rendering.
|
|
209
213
|
if (isOurComposite(settings)) {
|
|
210
|
-
if (
|
|
214
|
+
if (thirdParty.length > 0 && !compositeDoomed) {
|
|
215
|
+
// Temporary disable: the composite script survives on disk and keeps
|
|
216
|
+
// rendering the remaining providers — only our segment was unregistered.
|
|
217
|
+
} else if (thirdParty.length > 0) {
|
|
218
|
+
// Genuine uninstall: our composite runner dies with the plugin cache.
|
|
219
|
+
// Hand the slot to the first surviving third-party provider; the rest
|
|
220
|
+
// stay listed in the registry backup for manual re-wiring.
|
|
221
|
+
settings.statusLine = { type: 'command', command: thirdParty[0].command };
|
|
222
|
+
settingsChanged = true;
|
|
223
|
+
} else if (previous) {
|
|
211
224
|
settings.statusLine = { type: 'command', command: previous.command };
|
|
225
|
+
settingsChanged = true;
|
|
212
226
|
} else {
|
|
213
227
|
delete settings.statusLine;
|
|
228
|
+
settingsChanged = true;
|
|
214
229
|
}
|
|
215
|
-
settingsChanged = true;
|
|
216
230
|
}
|
|
217
231
|
|
|
218
|
-
|
|
232
|
+
// _previous only becomes removable once no third party still relies on the
|
|
233
|
+
// registry file (writeRegistry unlinks primary+backup when emptied).
|
|
234
|
+
if (thirdParty.length === 0) unregisterStatuslineProvider('_previous');
|
|
219
235
|
return settingsChanged;
|
|
220
236
|
}
|
|
221
237
|
|
|
@@ -229,7 +245,7 @@ function cleanupDisabledStatusline() {
|
|
|
229
245
|
// registry markers detachStatuslineIntegration is about to remove.
|
|
230
246
|
const uninstalled = isPluginUninstalled(settings);
|
|
231
247
|
|
|
232
|
-
let settingsChanged = detachStatuslineIntegration(settings);
|
|
248
|
+
let settingsChanged = detachStatuslineIntegration(settings, { compositeDoomed: uninstalled });
|
|
233
249
|
if (removeHooksFromSettings(settings)) settingsChanged = true;
|
|
234
250
|
if (settingsChanged) {
|
|
235
251
|
writeJsonAtomic(settingsPath(), settings);
|
|
@@ -393,11 +409,20 @@ function removeHooksFromSettings(settings) {
|
|
|
393
409
|
|
|
394
410
|
function buildSettingsHookEntries() {
|
|
395
411
|
const root = PLUGIN_ROOT;
|
|
396
|
-
const scriptCmd = (name, timeout) =>
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
412
|
+
const scriptCmd = (name, timeout) => {
|
|
413
|
+
const script = path.join(root, 'scripts', name);
|
|
414
|
+
// POSIX: existence-guarded. After `/plugin uninstall`, CC may delete the
|
|
415
|
+
// plugin-cache dir before our statusline teardown gets to strip these
|
|
416
|
+
// entries — in that window every Edit/Bash/Read/prompt errored on a dead
|
|
417
|
+
// path. The `if` form preserves node's own exit code (PreToolUse deny =
|
|
418
|
+
// exit 2); `&& … || exit 0` would swallow it. Windows keeps the bare
|
|
419
|
+
// command — the hook shell there is not reliably cmd, so `if exist`
|
|
420
|
+
// can't be assumed.
|
|
421
|
+
const command = process.platform === 'win32'
|
|
422
|
+
? `node "${script}"`
|
|
423
|
+
: `if [ -f "${script}" ]; then node "${script}"; fi`;
|
|
424
|
+
return { type: 'command', command, timeout };
|
|
425
|
+
};
|
|
401
426
|
|
|
402
427
|
return {
|
|
403
428
|
PreToolUse: [
|
|
@@ -604,7 +629,7 @@ function verifyHooksFire({ hooks, env, timeoutMs = 4000, tmpBase } = {}) {
|
|
|
604
629
|
|
|
605
630
|
// --- Install (idempotent) ---
|
|
606
631
|
|
|
607
|
-
function install() {
|
|
632
|
+
function install({ reclaimStatusline = false } = {}) {
|
|
608
633
|
const version = getPluginVersion();
|
|
609
634
|
const manifest = readManifest();
|
|
610
635
|
const settings = readJson(settingsPath()) || {};
|
|
@@ -620,14 +645,40 @@ function install() {
|
|
|
620
645
|
// b. Register code-graph as a provider
|
|
621
646
|
// c. Set statusLine to composite script
|
|
622
647
|
if (!isOurComposite(settings)) {
|
|
623
|
-
//
|
|
624
|
-
|
|
625
|
-
|
|
648
|
+
// Displacement tracking: we held the slot before (manifest.config.statusLine)
|
|
649
|
+
// but a foreign command sits there now — either another slot-claiming plugin
|
|
650
|
+
// (whose own self-heal re-takes it just like ours would → statusline
|
|
651
|
+
// ping-pong every session) or the user's deliberate choice. Either way,
|
|
652
|
+
// silently re-claiming forever is wrong: after >2 observed displacements
|
|
653
|
+
// stand down — stay registered as a provider, leave the slot alone.
|
|
654
|
+
// Explicit `lifecycle.js install` (or CODE_GRAPH_FORCE_STATUSLINE=1)
|
|
655
|
+
// resets the counter and re-claims.
|
|
656
|
+
const currentCmd = settings.statusLine && settings.statusLine.command;
|
|
657
|
+
if (reclaimStatusline || process.env.CODE_GRAPH_FORCE_STATUSLINE === '1') {
|
|
658
|
+
manifest.config.statuslineDisplaced = 0;
|
|
659
|
+
} else if (manifest.config.statusLine === true && currentCmd) {
|
|
660
|
+
manifest.config.statuslineDisplaced = (manifest.config.statuslineDisplaced || 0) + 1;
|
|
661
|
+
}
|
|
662
|
+
if ((manifest.config.statuslineDisplaced || 0) > 2) {
|
|
663
|
+
if (manifest.config.statusLine === true) {
|
|
664
|
+
// Transition into stand-down exactly once: release claimed ownership
|
|
665
|
+
// (stops the counter) and leave a breadcrumb.
|
|
666
|
+
manifest.config.statusLine = false;
|
|
667
|
+
process.stderr.write(
|
|
668
|
+
'[code-graph] statusLine slot keeps being re-claimed by another provider — standing down.\n' +
|
|
669
|
+
' Re-claim: CODE_GRAPH_FORCE_STATUSLINE=1 or `node lifecycle.js install`\n'
|
|
670
|
+
);
|
|
671
|
+
}
|
|
672
|
+
} else {
|
|
673
|
+
// Preserve existing statusline as first provider
|
|
674
|
+
if (currentCmd) {
|
|
675
|
+
registerStatuslineProvider('_previous', currentCmd, true);
|
|
676
|
+
}
|
|
677
|
+
// Set composite as the statusLine
|
|
678
|
+
settings.statusLine = { type: 'command', command: compositeCommand() };
|
|
679
|
+
settingsChanged = true;
|
|
680
|
+
manifest.config.statusLine = true;
|
|
626
681
|
}
|
|
627
|
-
// Set composite as the statusLine
|
|
628
|
-
settings.statusLine = { type: 'command', command: compositeCommand() };
|
|
629
|
-
settingsChanged = true;
|
|
630
|
-
manifest.config.statusLine = true;
|
|
631
682
|
} else {
|
|
632
683
|
// Composite exists — ensure path is correct (may have been polluted by env leak)
|
|
633
684
|
const cmd = compositeCommand();
|
|
@@ -635,6 +686,9 @@ function install() {
|
|
|
635
686
|
settings.statusLine.command = cmd;
|
|
636
687
|
settingsChanged = true;
|
|
637
688
|
}
|
|
689
|
+
// We hold the slot — any displacement episode is over.
|
|
690
|
+
if (manifest.config.statuslineDisplaced) manifest.config.statuslineDisplaced = 0;
|
|
691
|
+
manifest.config.statusLine = true;
|
|
638
692
|
}
|
|
639
693
|
|
|
640
694
|
// Register code-graph provider
|
|
@@ -690,7 +744,7 @@ function defaultRunNpm(args) {
|
|
|
690
744
|
} catch { return false; }
|
|
691
745
|
}
|
|
692
746
|
|
|
693
|
-
function uninstall({ purgeGlobal = false, runNpm = defaultRunNpm, scanGlobalPkgs = installedGlobalPkgs } = {}) {
|
|
747
|
+
function uninstall({ purgeGlobal = false, unadoptAll = false, runNpm = defaultRunNpm, scanGlobalPkgs = installedGlobalPkgs } = {}) {
|
|
694
748
|
const settings = readJson(settingsPath());
|
|
695
749
|
let settingsChanged = false;
|
|
696
750
|
|
|
@@ -742,6 +796,27 @@ function uninstall({ purgeGlobal = false, runNpm = defaultRunNpm, scanGlobalPkgs
|
|
|
742
796
|
const pluginInstalledGlobals = !!readJson(GLOBAL_INSTALL_MARKER);
|
|
743
797
|
let adoptedProjects = [];
|
|
744
798
|
try { adoptedProjects = require('./adopt').readAdoptedProjects(); } catch { /* POSIX-only helper — ok */ }
|
|
799
|
+
|
|
800
|
+
// 5.4. --unadopt-all: sweep every registered project's managed CLAUDE.md
|
|
801
|
+
// block + generated detail file (unadopt is marker-guarded, so user files
|
|
802
|
+
// are never touched; the .code-graph/ index dir is project DATA and stays —
|
|
803
|
+
// its removal is listed in the guidance instead of automated).
|
|
804
|
+
const unadopted = [];
|
|
805
|
+
if (unadoptAll && adoptedProjects.length) {
|
|
806
|
+
let unadoptFn = null;
|
|
807
|
+
try { unadoptFn = require('./adopt').unadopt; } catch { /* POSIX-only — skip */ }
|
|
808
|
+
if (unadoptFn) {
|
|
809
|
+
for (const project of adoptedProjects) {
|
|
810
|
+
try {
|
|
811
|
+
const r = unadoptFn({ cwd: project });
|
|
812
|
+
unadopted.push({ project, ok: !!(r && r.ok), cleaned: !!(r && (r.blockPruned || r.fileRemoved || r.claudeMdRemoved)) });
|
|
813
|
+
} catch (e) {
|
|
814
|
+
unadopted.push({ project, ok: false, error: (e && e.message) || String(e) });
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
try { adoptedProjects = require('./adopt').readAdoptedProjects(); } catch { /* ok */ }
|
|
818
|
+
}
|
|
819
|
+
}
|
|
745
820
|
let globalPkgsRemoved = [];
|
|
746
821
|
let globalPkgsRemaining = scanGlobalPkgs();
|
|
747
822
|
if (globalPkgsRemaining.length && (pluginInstalledGlobals || purgeGlobal)) {
|
|
@@ -765,7 +840,7 @@ function uninstall({ purgeGlobal = false, runNpm = defaultRunNpm, scanGlobalPkgs
|
|
|
765
840
|
try { fs.rmSync(dir, { recursive: true, force: true }); } catch { /* ok */ }
|
|
766
841
|
}
|
|
767
842
|
|
|
768
|
-
return { settingsChanged, pluginInstalledGlobals, globalPkgsRemoved, globalPkgsRemaining, adoptedProjects };
|
|
843
|
+
return { settingsChanged, pluginInstalledGlobals, globalPkgsRemoved, globalPkgsRemaining, adoptedProjects, unadopted };
|
|
769
844
|
}
|
|
770
845
|
|
|
771
846
|
// --- Update (refresh config points) ---
|
|
@@ -1020,11 +1095,23 @@ module.exports = {
|
|
|
1020
1095
|
if (require.main === module) {
|
|
1021
1096
|
const cmd = process.argv[2];
|
|
1022
1097
|
if (cmd === 'install') {
|
|
1023
|
-
|
|
1098
|
+
// Explicit CLI install = user intent: reset any statusline stand-down and re-claim.
|
|
1099
|
+
const r = install({ reclaimStatusline: true });
|
|
1024
1100
|
console.log(`Installed v${r.version} | settings=${r.settingsChanged} | statusLine=${r.statusLineClaimed}`);
|
|
1025
1101
|
} else if (cmd === 'uninstall') {
|
|
1026
|
-
const r = uninstall({
|
|
1102
|
+
const r = uninstall({
|
|
1103
|
+
purgeGlobal: process.argv.includes('--purge-global'),
|
|
1104
|
+
unadoptAll: process.argv.includes('--unadopt-all'),
|
|
1105
|
+
});
|
|
1027
1106
|
console.log(`Uninstalled | settings cleaned=${r.settingsChanged}`);
|
|
1107
|
+
if (r.unadopted.length) {
|
|
1108
|
+
const cleaned = r.unadopted.filter((u) => u.cleaned).length;
|
|
1109
|
+
console.log(` Unadopted ${cleaned}/${r.unadopted.length} registered project(s):`);
|
|
1110
|
+
for (const u of r.unadopted) {
|
|
1111
|
+
console.log(` ${u.ok ? (u.cleaned ? 'cleaned' : 'nothing-to-clean') : `FAILED (${u.error || 'unknown'})`} ${u.project}`);
|
|
1112
|
+
}
|
|
1113
|
+
console.log(' Their .code-graph/ index dirs are project data — remove per project with `rm -rf .code-graph` if unwanted.');
|
|
1114
|
+
}
|
|
1028
1115
|
if (r.globalPkgsRemoved.length) {
|
|
1029
1116
|
console.log(` Removed global npm package(s): ${r.globalPkgsRemoved.join(', ')}`);
|
|
1030
1117
|
}
|
|
@@ -1038,7 +1125,7 @@ if (require.main === module) {
|
|
|
1038
1125
|
if (r.adoptedProjects.length) {
|
|
1039
1126
|
console.log(' Adopted project(s) still carrying a managed CLAUDE.md block + .code-graph/ index:');
|
|
1040
1127
|
for (const p of r.adoptedProjects) console.log(` ${p}`);
|
|
1041
|
-
console.log('
|
|
1128
|
+
console.log(' Clean all at once: re-run with --unadopt-all, or per project `code-graph-mcp unadopt` + `rm -rf .code-graph`.');
|
|
1042
1129
|
}
|
|
1043
1130
|
console.log(' Note: also run `/plugin uninstall code-graph-mcp` inside Claude Code to sync its UI state.');
|
|
1044
1131
|
} else if (cmd === 'update') {
|
|
@@ -150,6 +150,10 @@ if (!binary) {
|
|
|
150
150
|
|
|
151
151
|
const stub = serveEmptyMcpStub({
|
|
152
152
|
upgrade: {
|
|
153
|
+
// Each probe is a full discovery walk (incl. `npm root -g`, up to 2s);
|
|
154
|
+
// offline the binary never appears, so back the poll off toward 60s.
|
|
155
|
+
// The install chain's onInstalled nudge below still upgrades instantly.
|
|
156
|
+
backoff: true,
|
|
153
157
|
shouldUpgrade: () => !!findBinary(),
|
|
154
158
|
spawnReal: () => {
|
|
155
159
|
const bin = findBinary();
|
|
@@ -52,6 +52,8 @@ function serveEmptyMcpStub(opts = {}) {
|
|
|
52
52
|
const queuedForChild = []; // client lines seen after spawn, before child is ready
|
|
53
53
|
let poller = null;
|
|
54
54
|
let upgradeFailures = 0; // consecutive failed upgrade attempts (see noteUpgradeFailure)
|
|
55
|
+
let backoffTicks = 0; // poll ticks to skip before the next probe (upgrade.backoff)
|
|
56
|
+
let backoffNext = 1;
|
|
55
57
|
|
|
56
58
|
function writeCc(obj) { output.write(JSON.stringify(obj) + '\n'); }
|
|
57
59
|
|
|
@@ -133,7 +135,20 @@ function serveEmptyMcpStub(opts = {}) {
|
|
|
133
135
|
|
|
134
136
|
function attemptUpgrade() {
|
|
135
137
|
if (child || !upgrade) return;
|
|
136
|
-
if (!upgrade.shouldUpgrade())
|
|
138
|
+
if (!upgrade.shouldUpgrade()) {
|
|
139
|
+
// Not upgradable yet — not a failure. But when the probe itself is
|
|
140
|
+
// expensive (missing-binary: full discovery walk incl. `npm root -g`,
|
|
141
|
+
// up to 2s), a flat 4s cadence for a whole offline session is pure
|
|
142
|
+
// subprocess churn. With { backoff:true } skip a doubling number of
|
|
143
|
+
// ticks between probes, capped near 60s; a manual attemptUpgrade()
|
|
144
|
+
// nudge (install chain's onInstalled) still probes immediately.
|
|
145
|
+
if (upgrade.backoff) {
|
|
146
|
+
const pollMs = upgrade.pollMs || DEFAULT_POLL_MS;
|
|
147
|
+
backoffTicks = backoffNext;
|
|
148
|
+
backoffNext = Math.min(backoffNext * 2, Math.max(1, Math.floor(60000 / pollMs) - 1));
|
|
149
|
+
}
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
137
152
|
const spawned = upgrade.spawnReal();
|
|
138
153
|
if (!spawned) { noteUpgradeFailure('binary-unresolved'); return; }
|
|
139
154
|
if (poller) { clearIv(poller); poller = null; }
|
|
@@ -141,6 +156,13 @@ function serveEmptyMcpStub(opts = {}) {
|
|
|
141
156
|
beginProxy();
|
|
142
157
|
}
|
|
143
158
|
|
|
159
|
+
// Poll-timer tick: honors the backoff skip counter; the exported
|
|
160
|
+
// attemptUpgrade stays direct so external nudges are never delayed.
|
|
161
|
+
function pollTick() {
|
|
162
|
+
if (backoffTicks > 0) { backoffTicks--; return; }
|
|
163
|
+
attemptUpgrade();
|
|
164
|
+
}
|
|
165
|
+
|
|
144
166
|
function fallBackToStub(reason) {
|
|
145
167
|
// Child spawned but died/errored before it was ready: answer anything the
|
|
146
168
|
// client queued (so it doesn't hang on those ids), resume polling, and count
|
|
@@ -153,7 +175,7 @@ function serveEmptyMcpStub(opts = {}) {
|
|
|
153
175
|
try { const req = JSON.parse(line); if (req && typeof req.method === 'string') answerAsStub(req); }
|
|
154
176
|
catch { /* ignore */ }
|
|
155
177
|
}
|
|
156
|
-
if (upgrade && !poller) poller = setIv(
|
|
178
|
+
if (upgrade && !poller) poller = setIv(pollTick, upgrade.pollMs || DEFAULT_POLL_MS);
|
|
157
179
|
noteUpgradeFailure(reason);
|
|
158
180
|
}
|
|
159
181
|
|
|
@@ -206,7 +228,7 @@ function serveEmptyMcpStub(opts = {}) {
|
|
|
206
228
|
});
|
|
207
229
|
}
|
|
208
230
|
|
|
209
|
-
if (upgrade) poller = setIv(
|
|
231
|
+
if (upgrade) poller = setIv(pollTick, upgrade.pollMs || DEFAULT_POLL_MS);
|
|
210
232
|
|
|
211
233
|
return { attemptUpgrade, _state: () => ({ hasChild: !!child, childReady }) };
|
|
212
234
|
}
|
|
@@ -553,9 +553,15 @@ function runSessionInit({ source } = {}) {
|
|
|
553
553
|
}
|
|
554
554
|
if (autoAdopt.attempted && autoAdopt.result && autoAdopt.result.ok) {
|
|
555
555
|
if (autoAdopt.reason === 'refreshed') {
|
|
556
|
+
// Name BOTH refreshed surfaces: the drift-refresh fully overwrites the
|
|
557
|
+
// generated detail doc, so a user who hand-edited it must learn why
|
|
558
|
+
// their edits vanished and how to lock the file.
|
|
559
|
+
const detailNote = autoAdopt.result.detailWritten
|
|
560
|
+
? ' + .claude/plugin_code_graph_mcp.md (manual edits to that generated file are overwritten)'
|
|
561
|
+
: '';
|
|
556
562
|
process.stderr.write(
|
|
557
|
-
|
|
558
|
-
' Lock
|
|
563
|
+
`[code-graph] Refreshed CLAUDE.md decision block to latest shipped version${detailNote}.\n` +
|
|
564
|
+
' Lock files: CODE_GRAPH_NO_TEMPLATE_REFRESH=1 in ~/.claude/settings.json env\n'
|
|
559
565
|
);
|
|
560
566
|
} else {
|
|
561
567
|
process.stderr.write(
|
|
@@ -26,6 +26,37 @@ function readBinaryVersion(binaryPath) {
|
|
|
26
26
|
}
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
+
/**
|
|
30
|
+
* Compare semver-ish version strings; returns -1, 0, or 1. Numeric triple
|
|
31
|
+
* compared ordinally (missing/non-numeric parts → 0); a pre-release suffix
|
|
32
|
+
* sorts BELOW its release ("1.2.3-rc1" < "1.2.3"), two pre-releases compare
|
|
33
|
+
* as plain strings. Single canonical implementation — auto-update.js and
|
|
34
|
+
* find-binary.js each carried a divergent copy whose pre-release semantics
|
|
35
|
+
* disagreed (Number("4-rc1")→NaN→0 vs parseInt("3-rc1")→3), a silent
|
|
36
|
+
* mis-ordering trap if release tags ever adopt "-rc" suffixes.
|
|
37
|
+
*/
|
|
38
|
+
function compareVersions(a, b) {
|
|
39
|
+
const parse = (v) => {
|
|
40
|
+
const s = String(v);
|
|
41
|
+
const dash = s.indexOf('-');
|
|
42
|
+
const core = dash === -1 ? s : s.slice(0, dash);
|
|
43
|
+
return {
|
|
44
|
+
nums: core.split('.').map((x) => parseInt(x, 10)),
|
|
45
|
+
pre: dash === -1 ? null : s.slice(dash + 1),
|
|
46
|
+
};
|
|
47
|
+
};
|
|
48
|
+
const pa = parse(a), pb = parse(b);
|
|
49
|
+
for (let i = 0; i < 3; i++) {
|
|
50
|
+
const x = Number.isFinite(pa.nums[i]) ? pa.nums[i] : 0;
|
|
51
|
+
const y = Number.isFinite(pb.nums[i]) ? pb.nums[i] : 0;
|
|
52
|
+
if (x !== y) return x < y ? -1 : 1;
|
|
53
|
+
}
|
|
54
|
+
if (pa.pre && !pb.pre) return -1;
|
|
55
|
+
if (!pa.pre && pb.pre) return 1;
|
|
56
|
+
if (pa.pre && pb.pre && pa.pre !== pb.pre) return pa.pre < pb.pre ? -1 : 1;
|
|
57
|
+
return 0;
|
|
58
|
+
}
|
|
59
|
+
|
|
29
60
|
function isDevMode(pluginRoot = path.resolve(__dirname, '..')) {
|
|
30
61
|
// Explicit opt-in always wins (also lets users force dev mode in any layout)
|
|
31
62
|
if (process.env.CODE_GRAPH_DEV === '1') return true;
|
|
@@ -63,4 +94,4 @@ function getNewestMtime(dir, ext = '.rs') {
|
|
|
63
94
|
return newest;
|
|
64
95
|
}
|
|
65
96
|
|
|
66
|
-
module.exports = { readBinaryVersion, isDevMode, getNewestMtime, VERSION_OUTPUT_RE };
|
|
97
|
+
module.exports = { readBinaryVersion, compareVersions, isDevMode, getNewestMtime, VERSION_OUTPUT_RE };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sdsrs/code-graph",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.103.0",
|
|
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": {
|
|
@@ -36,10 +36,10 @@
|
|
|
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.103.0",
|
|
40
|
+
"@sdsrs/code-graph-linux-arm64": "0.103.0",
|
|
41
|
+
"@sdsrs/code-graph-darwin-x64": "0.103.0",
|
|
42
|
+
"@sdsrs/code-graph-darwin-arm64": "0.103.0",
|
|
43
|
+
"@sdsrs/code-graph-win32-x64": "0.103.0"
|
|
44
44
|
}
|
|
45
45
|
}
|