@sdsrs/code-graph 0.74.2 → 0.74.4
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 +30 -0
- package/claude-plugin/.claude-plugin/plugin.json +1 -1
- package/claude-plugin/scripts/lifecycle.js +25 -2
- package/claude-plugin/scripts/lifecycle.test.js +33 -0
- package/claude-plugin/scripts/session-init.js +27 -4
- package/claude-plugin/scripts/session-init.test.js +47 -0
- package/claude-plugin/scripts/statusline.js +26 -3
- package/package.json +6 -6
package/bin/cli.js
CHANGED
|
@@ -36,6 +36,36 @@ if (sub === "adopt" || sub === "unadopt") {
|
|
|
36
36
|
process.exit(result.ok === false ? 1 : 0);
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
+
// Intercept `uninstall` — full local teardown (restore prior statusline, strip
|
|
40
|
+
// code-graph hooks from settings.json, delete ~/.cache/code-graph, and unadopt the
|
|
41
|
+
// CURRENT project). Node-only; no Rust counterpart. CC's `/plugin uninstall` fires
|
|
42
|
+
// no uninstall hook, so this is the user's one-shot CLI teardown. Guard `--help`
|
|
43
|
+
// before the destructive work (same discipline as adopt/unadopt above).
|
|
44
|
+
if (sub === "uninstall") {
|
|
45
|
+
if (process.argv.slice(3).some((a) => a === "--help" || a === "-h")) {
|
|
46
|
+
process.stdout.write(
|
|
47
|
+
"code-graph-mcp uninstall — remove code-graph config + cache from this machine\n\n" +
|
|
48
|
+
"USAGE:\n code-graph-mcp uninstall\n\n" +
|
|
49
|
+
"Restores your prior statusline, strips code-graph hooks from settings.json,\n" +
|
|
50
|
+
"deletes ~/.cache/code-graph, and removes this project's CLAUDE.md adoption\n" +
|
|
51
|
+
"block. Also run `/plugin uninstall code-graph-mcp` in Claude Code to sync its\n" +
|
|
52
|
+
"UI, and `code-graph-mcp unadopt` in any OTHER adopted project.\n");
|
|
53
|
+
process.exit(0);
|
|
54
|
+
}
|
|
55
|
+
const lifecycle = require("../claude-plugin/scripts/lifecycle");
|
|
56
|
+
const { unadopt } = require("../claude-plugin/scripts/adopt");
|
|
57
|
+
const r = lifecycle.uninstall();
|
|
58
|
+
let ua = { ok: false };
|
|
59
|
+
try { ua = unadopt(); } catch { /* best-effort — settings/cache already cleaned */ }
|
|
60
|
+
const projectUnadopted = !!(ua && (ua.blockPruned || ua.fileRemoved || ua.claudeMdRemoved));
|
|
61
|
+
process.stdout.write(
|
|
62
|
+
`Uninstalled code-graph-mcp | settings cleaned=${r.settingsChanged}` +
|
|
63
|
+
` | this project unadopted=${projectUnadopted}\n` +
|
|
64
|
+
" Also run `/plugin uninstall code-graph-mcp` in Claude Code, and\n" +
|
|
65
|
+
" `code-graph-mcp unadopt` in any other adopted project.\n");
|
|
66
|
+
process.exit(0);
|
|
67
|
+
}
|
|
68
|
+
|
|
39
69
|
const { findBinary, unsupportedPlatformHint } = require("../claude-plugin/scripts/find-binary");
|
|
40
70
|
|
|
41
71
|
const binary = findBinary();
|
|
@@ -871,9 +871,31 @@ function healthCheck() {
|
|
|
871
871
|
};
|
|
872
872
|
}
|
|
873
873
|
|
|
874
|
+
// True when the plugin has been UNINSTALLED (removed from installed_plugins.json),
|
|
875
|
+
// as opposed to merely toggled OFF (isPluginExplicitlyDisabled — the user may
|
|
876
|
+
// re-enable). The distinction matters because the uninstall teardown below is
|
|
877
|
+
// destructive (deletes the cached binary, unwinds project adoption); doing that
|
|
878
|
+
// on a temporary disable would force a re-download + re-adopt on re-enable.
|
|
879
|
+
function isPluginUninstalled(settings = readJson(settingsPath()) || {}) {
|
|
880
|
+
if (isPluginExplicitlyDisabled(settings)) return false;
|
|
881
|
+
return isPluginInactive(settings);
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
// Remove the ~/.cache/code-graph residue (the ~40MB binary, update-state,
|
|
885
|
+
// statusline-registry, install-manifest). The settings-only self-heal
|
|
886
|
+
// (cleanupDisabledStatusline) leaves this behind; the SessionStart teardown calls
|
|
887
|
+
// this so a CC `/plugin uninstall` (which fires no uninstall hook) still reclaims
|
|
888
|
+
// the disk. Idempotent: rm is force, so repeat SessionStarts are no-ops. Does NOT
|
|
889
|
+
// touch the plugin-cache script dirs — those are CC-managed and may be executing.
|
|
890
|
+
function removeCacheResidue() {
|
|
891
|
+
try { fs.rmSync(CACHE_DIR, { recursive: true, force: true }); return true; }
|
|
892
|
+
catch { return false; }
|
|
893
|
+
}
|
|
894
|
+
|
|
874
895
|
module.exports = {
|
|
875
896
|
install, uninstall, update, healthCheck, scanForBrokenPaths, checkScopeConflict,
|
|
876
|
-
isPluginExplicitlyDisabled, isPluginInactive,
|
|
897
|
+
isPluginExplicitlyDisabled, isPluginInactive, isPluginUninstalled, removeCacheResidue,
|
|
898
|
+
cleanupDisabledStatusline,
|
|
877
899
|
readManifest, readJson, writeJsonAtomic,
|
|
878
900
|
readRegistry, writeRegistry,
|
|
879
901
|
getPluginVersion, cleanupOldCacheVersions,
|
|
@@ -898,7 +920,8 @@ if (require.main === module) {
|
|
|
898
920
|
} else if (cmd === 'uninstall') {
|
|
899
921
|
const r = uninstall();
|
|
900
922
|
console.log(`Uninstalled | settings cleaned=${r.settingsChanged}`);
|
|
901
|
-
console.log(' Note: also run `/plugin uninstall code-graph-mcp` inside Claude Code to sync its UI state
|
|
923
|
+
console.log(' Note: also run `/plugin uninstall code-graph-mcp` inside Claude Code to sync its UI state,');
|
|
924
|
+
console.log(' and `code-graph-mcp unadopt` in each adopted project to remove its CLAUDE.md block.');
|
|
902
925
|
} else if (cmd === 'update') {
|
|
903
926
|
const r = update();
|
|
904
927
|
console.log(`Updated ${r.oldVersion} → ${r.version} | settings=${r.settingsChanged}`);
|
|
@@ -99,6 +99,39 @@ test('cleanupDisabledStatusline also heals orphaned statusline after uninstall',
|
|
|
99
99
|
assert.equal(fs.existsSync(registryPath), false);
|
|
100
100
|
});
|
|
101
101
|
|
|
102
|
+
test('isPluginUninstalled distinguishes a genuine uninstall from a temporary disable', (t) => {
|
|
103
|
+
// Orphaned composite (installed_plugins exists, no code-graph record) = uninstalled.
|
|
104
|
+
const uninstalledHome = mkHome(t);
|
|
105
|
+
seedOrphanedComposite(uninstalledHome);
|
|
106
|
+
// enabledPlugins[id]=false = user toggled it off; may re-enable → NOT uninstalled.
|
|
107
|
+
const disabledHome = mkHome(t);
|
|
108
|
+
seedDisabledComposite(disabledHome);
|
|
109
|
+
|
|
110
|
+
const probe = (home) => JSON.parse(execFileSync(process.execPath, ['-e', `
|
|
111
|
+
const { isPluginUninstalled } = require(${JSON.stringify(lifecyclePath)});
|
|
112
|
+
process.stdout.write(JSON.stringify(isPluginUninstalled()));
|
|
113
|
+
`], { env: { ...process.env, HOME: home } }).toString());
|
|
114
|
+
|
|
115
|
+
assert.equal(probe(uninstalledHome), true, 'orphaned/no-record → uninstalled');
|
|
116
|
+
assert.equal(probe(disabledHome), false, 'explicit disable → not uninstalled (re-enable safe)');
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test('removeCacheResidue deletes ~/.cache/code-graph and is idempotent', (t) => {
|
|
120
|
+
const homeDir = mkHome(t);
|
|
121
|
+
const cacheDir = path.join(homeDir, '.cache', 'code-graph');
|
|
122
|
+
writeJson(path.join(cacheDir, 'bin', 'marker.json'), { v: 1 });
|
|
123
|
+
fs.writeFileSync(path.join(cacheDir, 'update-state.json'), '{}');
|
|
124
|
+
|
|
125
|
+
const run = () => execFileSync(process.execPath, ['-e', `
|
|
126
|
+
const { removeCacheResidue } = require(${JSON.stringify(lifecyclePath)});
|
|
127
|
+
process.stdout.write(JSON.stringify(removeCacheResidue()));
|
|
128
|
+
`], { env: { ...process.env, HOME: homeDir } }).toString();
|
|
129
|
+
|
|
130
|
+
assert.equal(run(), 'true');
|
|
131
|
+
assert.equal(fs.existsSync(cacheDir), false, 'cache dir removed');
|
|
132
|
+
assert.equal(run(), 'true', 'second call is a no-op success (idempotent force-rm)');
|
|
133
|
+
});
|
|
134
|
+
|
|
102
135
|
function legacyHooksFromPlugin() {
|
|
103
136
|
return {
|
|
104
137
|
SessionStart: [{
|
|
@@ -5,11 +5,11 @@ const path = require('path');
|
|
|
5
5
|
const fs = require('fs');
|
|
6
6
|
const {
|
|
7
7
|
install, update, readManifest, getPluginVersion, checkScopeConflict,
|
|
8
|
-
cleanupDisabledStatusline, isPluginInactive,
|
|
9
|
-
settingsPath, isStaleRelicContext,
|
|
8
|
+
cleanupDisabledStatusline, isPluginInactive, isPluginUninstalled, removeCacheResidue,
|
|
9
|
+
readJson, CACHE_DIR, settingsPath, isStaleRelicContext,
|
|
10
10
|
} = require('./lifecycle');
|
|
11
11
|
const { readBinaryVersion, isDevMode, getNewestMtime } = require('./version-utils');
|
|
12
|
-
const { maybeAutoAdopt, isAdopted } = require('./adopt');
|
|
12
|
+
const { maybeAutoAdopt, isAdopted, unadopt } = require('./adopt');
|
|
13
13
|
const { isNonProjectCwd } = require('./project-detect');
|
|
14
14
|
|
|
15
15
|
// v0.17.0 — quietHooks: unconditional quiet 默认。
|
|
@@ -420,8 +420,31 @@ function consistencyCheck(binary) {
|
|
|
420
420
|
|
|
421
421
|
function runSessionInit({ source } = {}) {
|
|
422
422
|
if (isPluginInactive()) {
|
|
423
|
+
// Capture the uninstalled-vs-disabled verdict BEFORE cleanupDisabledStatusline()
|
|
424
|
+
// runs — it removes our composite + registry entry, which are the very signals
|
|
425
|
+
// isPluginUninstalled()/isPluginInactive() read, so calling it afterwards would
|
|
426
|
+
// always see "no composite/registry" and report not-uninstalled (teardown skipped).
|
|
427
|
+
const uninstalled = isPluginUninstalled();
|
|
423
428
|
cleanupDisabledStatusline();
|
|
424
|
-
|
|
429
|
+
// Genuine uninstall (not a temporary disable) leaves residue the settings-only
|
|
430
|
+
// self-heal can't reach: ~/.cache/code-graph (the ~40MB binary + state) and the
|
|
431
|
+
// current project's CLAUDE.md adoption block. CC fires no uninstall hook, so this
|
|
432
|
+
// SessionStart is the only automated teardown — symmetric to install's auto-adopt.
|
|
433
|
+
// Per-project: only the cwd we're in; other adopted projects self-clean when next
|
|
434
|
+
// opened, or via `code-graph-mcp unadopt`.
|
|
435
|
+
let teardown = null;
|
|
436
|
+
if (uninstalled) {
|
|
437
|
+
const cacheRemoved = removeCacheResidue();
|
|
438
|
+
let unadopted = false;
|
|
439
|
+
try {
|
|
440
|
+
if (!isNonProjectCwd(process.cwd())) {
|
|
441
|
+
const r = unadopt({ cwd: process.cwd() });
|
|
442
|
+
unadopted = !!(r && (r.blockPruned || r.fileRemoved || r.claudeMdRemoved));
|
|
443
|
+
}
|
|
444
|
+
} catch { /* best-effort — never let teardown break SessionStart */ }
|
|
445
|
+
teardown = { cacheRemoved, unadopted };
|
|
446
|
+
}
|
|
447
|
+
return { inactive: true, lifecycle: 'noop', autoUpdateLaunched: false, teardown };
|
|
425
448
|
}
|
|
426
449
|
|
|
427
450
|
// Non-project cwd (no .git/manifest — e.g. /tmp, where claude-mem-lite
|
|
@@ -99,6 +99,53 @@ test('runSessionInit no-ops (nonProject) in a non-project cwd', (t) => {
|
|
|
99
99
|
}
|
|
100
100
|
});
|
|
101
101
|
|
|
102
|
+
test('runSessionInit tears down cache + adoption on a genuine uninstall (order regression)', (t) => {
|
|
103
|
+
// Subprocess isolation: lifecycle.js evaluates CACHE_DIR from os.homedir() at
|
|
104
|
+
// MODULE LOAD, so HOME/CLAUDE_CONFIG_DIR must be set before require — only a
|
|
105
|
+
// fresh child honors them. This locks the order bug: isPluginUninstalled() MUST be
|
|
106
|
+
// read BEFORE cleanupDisabledStatusline() wipes the composite/registry signals it
|
|
107
|
+
// depends on — otherwise teardown is skipped (was null pre-fix).
|
|
108
|
+
const os = require('os');
|
|
109
|
+
const { execFileSync } = require('child_process');
|
|
110
|
+
const sb = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-si-teardown-'));
|
|
111
|
+
t.after(() => fs.rmSync(sb, { recursive: true, force: true }));
|
|
112
|
+
const home = sb, cfg = path.join(sb, '.claude'), proj = path.join(sb, 'proj');
|
|
113
|
+
fs.mkdirSync(path.join(cfg, 'plugins'), { recursive: true });
|
|
114
|
+
fs.mkdirSync(proj, { recursive: true });
|
|
115
|
+
fs.writeFileSync(path.join(proj, 'package.json'), '{"name":"p","version":"1.0.0"}');
|
|
116
|
+
fs.writeFileSync(path.join(proj, 'CLAUDE.md'), '# P\n\nKEEP THIS USER LINE.\n');
|
|
117
|
+
fs.writeFileSync(path.join(cfg, 'settings.json'), '{"statusLine":{"type":"command","command":"/bin/prior.sh"}}');
|
|
118
|
+
const env = { ...process.env, HOME: home, CLAUDE_CONFIG_DIR: cfg };
|
|
119
|
+
const lc = path.join(__dirname, 'lifecycle.js'), ad = path.join(__dirname, 'adopt.js');
|
|
120
|
+
const si = path.join(__dirname, 'session-init.js');
|
|
121
|
+
|
|
122
|
+
// install + adopt, then simulate a downloaded binary + a post-/plugin-uninstall
|
|
123
|
+
// installed_plugins.json (record for some OTHER plugin, none for code-graph).
|
|
124
|
+
execFileSync(process.execPath, [lc, 'install'], { env, cwd: proj, stdio: 'ignore' });
|
|
125
|
+
execFileSync(process.execPath, ['-e',
|
|
126
|
+
`require(${JSON.stringify(ad)}).adopt({cwd:process.cwd()})`], { env, cwd: proj, stdio: 'ignore' });
|
|
127
|
+
fs.mkdirSync(path.join(home, '.cache', 'code-graph', 'bin'), { recursive: true });
|
|
128
|
+
fs.writeFileSync(path.join(home, '.cache', 'code-graph', 'bin', 'code-graph-mcp'), 'x');
|
|
129
|
+
fs.writeFileSync(path.join(cfg, 'plugins', 'installed_plugins.json'),
|
|
130
|
+
JSON.stringify({ plugins: { 'other@mkt': [{ version: '1.0.0', installPath: '/x' }] } }));
|
|
131
|
+
assert.ok(fs.readFileSync(path.join(proj, 'CLAUDE.md'), 'utf8').includes('code-graph'), 'adopt injected block');
|
|
132
|
+
|
|
133
|
+
const res = JSON.parse(execFileSync(process.execPath, ['-e',
|
|
134
|
+
`process.stdout.write(JSON.stringify(require(${JSON.stringify(si)}).runSessionInit({source:'startup'})))`],
|
|
135
|
+
{ env, cwd: proj }).toString());
|
|
136
|
+
|
|
137
|
+
assert.equal(res.inactive, true);
|
|
138
|
+
assert.ok(res.teardown, 'teardown ran (null pre-fix = order bug)');
|
|
139
|
+
assert.equal(res.teardown.cacheRemoved, true);
|
|
140
|
+
assert.equal(res.teardown.unadopted, true);
|
|
141
|
+
assert.equal(fs.existsSync(path.join(home, '.cache', 'code-graph')), false, 'cache residue gone');
|
|
142
|
+
const md = fs.readFileSync(path.join(proj, 'CLAUDE.md'), 'utf8');
|
|
143
|
+
assert.ok(!md.includes('code-graph'), 'adopt block removed');
|
|
144
|
+
assert.ok(md.includes('KEEP THIS USER LINE'), 'user content preserved');
|
|
145
|
+
const settings = JSON.parse(fs.readFileSync(path.join(cfg, 'settings.json'), 'utf8'));
|
|
146
|
+
assert.equal(settings.statusLine.command, '/bin/prior.sh', 'prior statusline restored');
|
|
147
|
+
});
|
|
148
|
+
|
|
102
149
|
test('consistencyCheck returns empty array when binary version matches plugin', () => {
|
|
103
150
|
const result = consistencyCheck('/tmp/nonexistent-binary');
|
|
104
151
|
assert.ok(Array.isArray(result));
|
|
@@ -2,11 +2,25 @@
|
|
|
2
2
|
'use strict';
|
|
3
3
|
const { execFileSync } = require('child_process');
|
|
4
4
|
const fs = require('fs');
|
|
5
|
+
const os = require('os');
|
|
5
6
|
const path = require('path');
|
|
6
7
|
const { findBinary } = require('./find-binary');
|
|
7
8
|
const lifecycle = require('./lifecycle');
|
|
8
9
|
const cleanupDisabledStatusline = lifecycle.cleanupDisabledStatusline || (() => ({ cleaned: false }));
|
|
9
10
|
|
|
11
|
+
// True when auto-update has a newer release queued or in flight (the background
|
|
12
|
+
// downloader in session-init.js hasn't promoted the new binary yet). Used to show
|
|
13
|
+
// a transient "updating" state instead of the alarming "offline" during that window.
|
|
14
|
+
function updatePending() {
|
|
15
|
+
try {
|
|
16
|
+
const st = JSON.parse(fs.readFileSync(
|
|
17
|
+
path.join(os.homedir(), '.cache', 'code-graph', 'update-state.json'), 'utf8'));
|
|
18
|
+
if (st.updateAvailable) return true;
|
|
19
|
+
if (st.latestVersion && st.installedVersion && st.latestVersion !== st.installedVersion) return true;
|
|
20
|
+
} catch { /* no state file or unreadable — treat as no pending update */ }
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
|
|
10
24
|
const disabledCleanup = cleanupDisabledStatusline();
|
|
11
25
|
if (disabledCleanup.cleaned) process.exit(0);
|
|
12
26
|
|
|
@@ -38,7 +52,9 @@ if (!fs.existsSync(path.join(codeGraphDir, 'index.db'))) {
|
|
|
38
52
|
|
|
39
53
|
const bin = findBinary();
|
|
40
54
|
if (!bin) {
|
|
41
|
-
|
|
55
|
+
// No usable binary yet. If an update is queued, the background downloader is
|
|
56
|
+
// still fetching it \u2014 that is "updating", not a broken "offline" state.
|
|
57
|
+
process.stdout.write(updatePending() ? 'code-graph: \u21bb updating' : 'code-graph: offline');
|
|
42
58
|
process.exit(0);
|
|
43
59
|
}
|
|
44
60
|
|
|
@@ -53,6 +69,13 @@ try {
|
|
|
53
69
|
`code-graph: ${icon} ${s.nodes} nodes | ${s.files} files` +
|
|
54
70
|
(s.watching ? ' | watching' : '')
|
|
55
71
|
);
|
|
56
|
-
} catch {
|
|
57
|
-
|
|
72
|
+
} catch (e) {
|
|
73
|
+
// A schema-too-new error means the resolved binary is OLDER than the index it
|
|
74
|
+
// is reading \u2014 the classic post-update window where the new binary is still
|
|
75
|
+
// downloading. That, or any pending update, is transient: show "updating" so
|
|
76
|
+
// the user knows it self-heals, rather than the misleading "offline".
|
|
77
|
+
const errOut = ((e && (e.stderr || e.stdout)) || '').toString();
|
|
78
|
+
const binaryOutdated = /schema version/i.test(errOut);
|
|
79
|
+
process.stdout.write(
|
|
80
|
+
(binaryOutdated || updatePending()) ? 'code-graph: \u21bb updating' : 'code-graph: offline');
|
|
58
81
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sdsrs/code-graph",
|
|
3
|
-
"version": "0.74.
|
|
3
|
+
"version": "0.74.4",
|
|
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": {
|
|
@@ -35,10 +35,10 @@
|
|
|
35
35
|
"node": ">=16"
|
|
36
36
|
},
|
|
37
37
|
"optionalDependencies": {
|
|
38
|
-
"@sdsrs/code-graph-linux-x64": "0.74.
|
|
39
|
-
"@sdsrs/code-graph-linux-arm64": "0.74.
|
|
40
|
-
"@sdsrs/code-graph-darwin-x64": "0.74.
|
|
41
|
-
"@sdsrs/code-graph-darwin-arm64": "0.74.
|
|
42
|
-
"@sdsrs/code-graph-win32-x64": "0.74.
|
|
38
|
+
"@sdsrs/code-graph-linux-x64": "0.74.4",
|
|
39
|
+
"@sdsrs/code-graph-linux-arm64": "0.74.4",
|
|
40
|
+
"@sdsrs/code-graph-darwin-x64": "0.74.4",
|
|
41
|
+
"@sdsrs/code-graph-darwin-arm64": "0.74.4",
|
|
42
|
+
"@sdsrs/code-graph-win32-x64": "0.74.4"
|
|
43
43
|
}
|
|
44
44
|
}
|