@sdsrs/code-graph 0.30.0 → 0.31.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/claude-plugin/.claude-plugin/plugin.json +1 -1
- package/claude-plugin/scripts/adopt.js +15 -2
- package/claude-plugin/scripts/adopt.test.js +38 -0
- package/claude-plugin/scripts/auto-update.js +3 -3
- package/claude-plugin/scripts/claude-config.js +21 -0
- package/claude-plugin/scripts/claude-config.test.js +58 -0
- package/claude-plugin/scripts/doctor.js +5 -5
- package/claude-plugin/scripts/lifecycle.e2e.test.js +46 -0
- package/claude-plugin/scripts/lifecycle.js +40 -32
- package/claude-plugin/scripts/session-init.js +2 -2
- package/package.json +6 -6
|
@@ -269,9 +269,14 @@ const TARGET_NAME = 'plugin_code_graph_mcp.md';
|
|
|
269
269
|
// Claude Code slug convention: every non-alphanumeric-non-hyphen char → `-`.
|
|
270
270
|
// `/mnt/data_ssd/dev/proj` → `-mnt-data-ssd-dev-proj`
|
|
271
271
|
// `/home/sds/.claude/x` → `-home-sds--claude-x` (double-dash from `/.`)
|
|
272
|
+
//
|
|
273
|
+
// `home` is the OS home dir (default `os.homedir()`). When `CLAUDE_CONFIG_DIR`
|
|
274
|
+
// is set it overrides `home/.claude`, so multi-account users (personal vs work)
|
|
275
|
+
// land in the directory Claude Code itself is using for `projects/`.
|
|
272
276
|
function memoryDir(cwd = process.cwd(), home = os.homedir()) {
|
|
273
277
|
const slug = cwd.replace(/[^a-zA-Z0-9-]/g, '-');
|
|
274
|
-
|
|
278
|
+
const claudeDir = process.env.CLAUDE_CONFIG_DIR || path.join(home, '.claude');
|
|
279
|
+
return path.join(claudeDir, 'projects', slug, 'memory');
|
|
275
280
|
}
|
|
276
281
|
|
|
277
282
|
function escapeRegex(s) {
|
|
@@ -421,9 +426,17 @@ function needsRefresh({ cwd, home, templatePath } = {}) {
|
|
|
421
426
|
// 检测脚本是否从 Claude Code 插件 cache 运行。
|
|
422
427
|
// 走 __dirname 而非 CLAUDE_PLUGIN_ROOT — 后者在多插件共存时会互相污染
|
|
423
428
|
// (见 feedback_plugin_env_isolation.md)。
|
|
429
|
+
// 默认匹配 `.claude/plugins/` 路径;CLAUDE_CONFIG_DIR 自定义目录时
|
|
430
|
+
// 走 startsWith(CLAUDE_CONFIG_DIR/plugins/) 兜底。
|
|
424
431
|
function isPluginModeInstall(scriptPath = __dirname) {
|
|
425
432
|
const sep = path.sep;
|
|
426
|
-
|
|
433
|
+
if (scriptPath.includes(`${sep}.claude${sep}plugins${sep}`)) return true;
|
|
434
|
+
const envDir = process.env.CLAUDE_CONFIG_DIR;
|
|
435
|
+
if (envDir) {
|
|
436
|
+
const marker = path.join(envDir, 'plugins') + sep;
|
|
437
|
+
if (scriptPath.startsWith(marker)) return true;
|
|
438
|
+
}
|
|
439
|
+
return false;
|
|
427
440
|
}
|
|
428
441
|
|
|
429
442
|
// C' 上下文感知默认(v0.9.0):插件模式下首次 SessionStart 静默 adopt。
|
|
@@ -278,6 +278,44 @@ test('isPluginModeInstall rejects npx cache paths', () => {
|
|
|
278
278
|
assert.strictEqual(isPluginModeInstall(npxPath), false);
|
|
279
279
|
});
|
|
280
280
|
|
|
281
|
+
test('memoryDir honors CLAUDE_CONFIG_DIR override (multi-account isolation)', () => {
|
|
282
|
+
const prev = process.env.CLAUDE_CONFIG_DIR;
|
|
283
|
+
process.env.CLAUDE_CONFIG_DIR = '/home/alice/work-claude';
|
|
284
|
+
try {
|
|
285
|
+
// home arg is irrelevant when env var is set — projects live under the
|
|
286
|
+
// configured claude dir, not home/.claude.
|
|
287
|
+
assert.strictEqual(
|
|
288
|
+
memoryDir('/home/alice/proj', '/home/alice'),
|
|
289
|
+
'/home/alice/work-claude/projects/-home-alice-proj/memory'
|
|
290
|
+
);
|
|
291
|
+
} finally {
|
|
292
|
+
if (prev === undefined) delete process.env.CLAUDE_CONFIG_DIR;
|
|
293
|
+
else process.env.CLAUDE_CONFIG_DIR = prev;
|
|
294
|
+
}
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
test('isPluginModeInstall recognizes CLAUDE_CONFIG_DIR/plugins/... paths', () => {
|
|
298
|
+
const prev = process.env.CLAUDE_CONFIG_DIR;
|
|
299
|
+
process.env.CLAUDE_CONFIG_DIR = '/home/alice/work-claude';
|
|
300
|
+
try {
|
|
301
|
+
const pluginPath = '/home/alice/work-claude/plugins/cache/code-graph-mcp@0.31.0/scripts';
|
|
302
|
+
assert.strictEqual(isPluginModeInstall(pluginPath), true);
|
|
303
|
+
// Legacy ~/.claude/plugins/ path still works even with env var set.
|
|
304
|
+
assert.strictEqual(
|
|
305
|
+
isPluginModeInstall('/home/user/.claude/plugins/cache/code-graph-mcp/scripts'),
|
|
306
|
+
true
|
|
307
|
+
);
|
|
308
|
+
// Unrelated path under same prefix is still rejected.
|
|
309
|
+
assert.strictEqual(
|
|
310
|
+
isPluginModeInstall('/home/alice/work-claude/projects/foo/memory'),
|
|
311
|
+
false
|
|
312
|
+
);
|
|
313
|
+
} finally {
|
|
314
|
+
if (prev === undefined) delete process.env.CLAUDE_CONFIG_DIR;
|
|
315
|
+
else process.env.CLAUDE_CONFIG_DIR = prev;
|
|
316
|
+
}
|
|
317
|
+
});
|
|
318
|
+
|
|
281
319
|
test('maybeAutoAdopt skips when CODE_GRAPH_NO_AUTO_ADOPT=1', () => {
|
|
282
320
|
const sb = makeSandbox();
|
|
283
321
|
try {
|
|
@@ -5,7 +5,7 @@ const fs = require('fs');
|
|
|
5
5
|
const https = require('https');
|
|
6
6
|
const path = require('path');
|
|
7
7
|
const os = require('os');
|
|
8
|
-
const { CACHE_DIR, PLUGIN_ID, MARKETPLACE_NAME, readManifest, readJson, writeJsonAtomic } = require('./lifecycle');
|
|
8
|
+
const { CACHE_DIR, PLUGIN_ID, MARKETPLACE_NAME, readManifest, readJson, writeJsonAtomic, installedPluginsPath, pluginsCacheDir } = require('./lifecycle');
|
|
9
9
|
const { clearCache: clearBinaryCache } = require('./find-binary');
|
|
10
10
|
const { readBinaryVersion, isDevMode } = require('./version-utils');
|
|
11
11
|
|
|
@@ -272,7 +272,7 @@ async function downloadAndInstall(latest) {
|
|
|
272
272
|
|
|
273
273
|
const pluginSrc = path.join(tmpDir, 'claude-plugin');
|
|
274
274
|
const pluginDst = path.join(
|
|
275
|
-
|
|
275
|
+
pluginsCacheDir(), MARKETPLACE_NAME, 'code-graph-mcp', latest.version
|
|
276
276
|
);
|
|
277
277
|
|
|
278
278
|
if (fs.existsSync(pluginSrc) && getExtractedPluginVersion(pluginSrc) === latest.version) {
|
|
@@ -282,7 +282,7 @@ async function downloadAndInstall(latest) {
|
|
|
282
282
|
}
|
|
283
283
|
|
|
284
284
|
// Update installed_plugins.json to point to new version
|
|
285
|
-
const installedPath =
|
|
285
|
+
const installedPath = installedPluginsPath();
|
|
286
286
|
try {
|
|
287
287
|
const installed = readJson(installedPath);
|
|
288
288
|
if (installed && installed.plugins && installed.plugins[PLUGIN_ID]) {
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const os = require('os');
|
|
4
|
+
|
|
5
|
+
// Resolve Claude Code's config directory. Honors CLAUDE_CONFIG_DIR — when set
|
|
6
|
+
// (commonly used to keep multiple accounts isolated, e.g. personal vs work),
|
|
7
|
+
// Claude Code reads `settings.json`, `plugins/`, `projects/`, etc. from there
|
|
8
|
+
// instead of `~/.claude/`. Our plugin must follow the same override so its
|
|
9
|
+
// hook registrations, statusline, adoption files, and cache cleanup land in
|
|
10
|
+
// the directory Claude Code is actually using.
|
|
11
|
+
//
|
|
12
|
+
// Read fresh on every call so per-process env mutation (tests, child procs
|
|
13
|
+
// spawned with a different env) takes effect immediately. Unlike
|
|
14
|
+
// CLAUDE_PLUGIN_ROOT (which leaks across plugins — see
|
|
15
|
+
// feedback_plugin_env_isolation.md), CLAUDE_CONFIG_DIR is user-set and
|
|
16
|
+
// process-wide, so reading it is safe.
|
|
17
|
+
function claudeHome() {
|
|
18
|
+
return process.env.CLAUDE_CONFIG_DIR || path.join(os.homedir(), '.claude');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
module.exports = { claudeHome };
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const test = require('node:test');
|
|
3
|
+
const assert = require('node:assert');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const os = require('os');
|
|
6
|
+
const { claudeHome } = require('./claude-config');
|
|
7
|
+
|
|
8
|
+
test('claudeHome defaults to ~/.claude when CLAUDE_CONFIG_DIR unset', () => {
|
|
9
|
+
const prev = process.env.CLAUDE_CONFIG_DIR;
|
|
10
|
+
delete process.env.CLAUDE_CONFIG_DIR;
|
|
11
|
+
try {
|
|
12
|
+
assert.strictEqual(claudeHome(), path.join(os.homedir(), '.claude'));
|
|
13
|
+
} finally {
|
|
14
|
+
if (prev !== undefined) process.env.CLAUDE_CONFIG_DIR = prev;
|
|
15
|
+
}
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
test('claudeHome honors CLAUDE_CONFIG_DIR when set', () => {
|
|
19
|
+
const prev = process.env.CLAUDE_CONFIG_DIR;
|
|
20
|
+
process.env.CLAUDE_CONFIG_DIR = '/tmp/work-claude';
|
|
21
|
+
try {
|
|
22
|
+
assert.strictEqual(claudeHome(), '/tmp/work-claude');
|
|
23
|
+
} finally {
|
|
24
|
+
if (prev === undefined) delete process.env.CLAUDE_CONFIG_DIR;
|
|
25
|
+
else process.env.CLAUDE_CONFIG_DIR = prev;
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test('claudeHome re-reads env on every call (not cached)', () => {
|
|
30
|
+
const prev = process.env.CLAUDE_CONFIG_DIR;
|
|
31
|
+
delete process.env.CLAUDE_CONFIG_DIR;
|
|
32
|
+
try {
|
|
33
|
+
const before = claudeHome();
|
|
34
|
+
process.env.CLAUDE_CONFIG_DIR = '/tmp/account-A';
|
|
35
|
+
const during = claudeHome();
|
|
36
|
+
delete process.env.CLAUDE_CONFIG_DIR;
|
|
37
|
+
const after = claudeHome();
|
|
38
|
+
assert.strictEqual(before, path.join(os.homedir(), '.claude'));
|
|
39
|
+
assert.strictEqual(during, '/tmp/account-A');
|
|
40
|
+
assert.strictEqual(after, path.join(os.homedir(), '.claude'));
|
|
41
|
+
} finally {
|
|
42
|
+
if (prev !== undefined) process.env.CLAUDE_CONFIG_DIR = prev;
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test('claudeHome ignores empty CLAUDE_CONFIG_DIR (falls back to ~/.claude)', () => {
|
|
47
|
+
// Empty string is falsy in JS — sanity-check the `||` fallback path so an
|
|
48
|
+
// accidentally `CLAUDE_CONFIG_DIR=` (unset-style) shell line does not strand
|
|
49
|
+
// us writing to the literal repository root `/`.
|
|
50
|
+
const prev = process.env.CLAUDE_CONFIG_DIR;
|
|
51
|
+
process.env.CLAUDE_CONFIG_DIR = '';
|
|
52
|
+
try {
|
|
53
|
+
assert.strictEqual(claudeHome(), path.join(os.homedir(), '.claude'));
|
|
54
|
+
} finally {
|
|
55
|
+
if (prev === undefined) delete process.env.CLAUDE_CONFIG_DIR;
|
|
56
|
+
else process.env.CLAUDE_CONFIG_DIR = prev;
|
|
57
|
+
}
|
|
58
|
+
});
|
|
@@ -8,6 +8,7 @@ const { readBinaryVersion, isDevMode, getNewestMtime } = require('./version-util
|
|
|
8
8
|
const {
|
|
9
9
|
getPluginVersion, readJson, healthCheck, CACHE_DIR,
|
|
10
10
|
removeHooksFromSettings, isOurHookEntry, writeJsonAtomic,
|
|
11
|
+
settingsPath,
|
|
11
12
|
} = require('./lifecycle');
|
|
12
13
|
const { findBinary, clearCache: clearBinaryCache } = require('./find-binary');
|
|
13
14
|
|
|
@@ -190,8 +191,7 @@ function runDiagnostics() {
|
|
|
190
191
|
// cache/<ver>/hooks/hooks.json is now authoritative. Duplicates cause
|
|
191
192
|
// every hook to fire twice until settings.json is cleaned.
|
|
192
193
|
try {
|
|
193
|
-
const
|
|
194
|
-
const settings = readJson(SETTINGS_PATH) || {};
|
|
194
|
+
const settings = readJson(settingsPath()) || {};
|
|
195
195
|
const legacyCount = countLegacyHookEntries(settings);
|
|
196
196
|
if (legacyCount === 0) {
|
|
197
197
|
results.push({ name: 'Legacy hooks', status: 'ok', detail: 'settings.json is clean' });
|
|
@@ -377,10 +377,10 @@ function runRepairs(results) {
|
|
|
377
377
|
|
|
378
378
|
case 'legacy-hooks-in-settings': {
|
|
379
379
|
console.log('\n Removing legacy code-graph hooks from settings.json...');
|
|
380
|
-
const
|
|
381
|
-
const settings = readJson(
|
|
380
|
+
const settingsFile = settingsPath();
|
|
381
|
+
const settings = readJson(settingsFile) || {};
|
|
382
382
|
if (removeHooksFromSettings(settings)) {
|
|
383
|
-
writeJsonAtomic(
|
|
383
|
+
writeJsonAtomic(settingsFile, settings);
|
|
384
384
|
console.log(' \u2705 settings.json cleaned — restart Claude Code to apply');
|
|
385
385
|
fixed++;
|
|
386
386
|
} else {
|
|
@@ -95,3 +95,49 @@ test('lifecycle CLI handles install, disable self-heal, re-enable, and uninstall
|
|
|
95
95
|
assert.equal(fs.existsSync(cacheDir), false);
|
|
96
96
|
});
|
|
97
97
|
|
|
98
|
+
test('lifecycle install writes to CLAUDE_CONFIG_DIR instead of ~/.claude when set', (t) => {
|
|
99
|
+
// Multi-account isolation: a user with CLAUDE_CONFIG_DIR=~/work-claude
|
|
100
|
+
// expects all plugin config (settings.json, installed_plugins.json,
|
|
101
|
+
// statusline-providers backup) to land under that directory, not the
|
|
102
|
+
// default ~/.claude. Default path must remain untouched.
|
|
103
|
+
const homeDir = mkHome(t);
|
|
104
|
+
const configDir = fs.mkdtempSync(path.join(os.tmpdir(), 'code-graph-cfgdir-'));
|
|
105
|
+
t.after(() => fs.rmSync(configDir, { recursive: true, force: true }));
|
|
106
|
+
|
|
107
|
+
const cfgSettings = path.join(configDir, 'settings.json');
|
|
108
|
+
const cfgInstalled = path.join(configDir, 'plugins', 'installed_plugins.json');
|
|
109
|
+
const cfgBackup = path.join(configDir, 'statusline-providers.json');
|
|
110
|
+
const defaultSettings = path.join(homeDir, '.claude', 'settings.json');
|
|
111
|
+
|
|
112
|
+
writeJson(cfgSettings, {
|
|
113
|
+
statusLine: { type: 'command', command: 'echo prior-work-status' },
|
|
114
|
+
enabledPlugins: { 'code-graph-mcp@code-graph-mcp': true },
|
|
115
|
+
});
|
|
116
|
+
writeJson(cfgInstalled, {
|
|
117
|
+
plugins: {
|
|
118
|
+
'code-graph-mcp@code-graph-mcp': [{
|
|
119
|
+
installPath: pluginRoot,
|
|
120
|
+
version: currentVersion,
|
|
121
|
+
scope: 'user',
|
|
122
|
+
}],
|
|
123
|
+
},
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
// Run install with CLAUDE_CONFIG_DIR set; HOME points elsewhere.
|
|
127
|
+
const env = { ...process.env, HOME: homeDir, CLAUDE_CONFIG_DIR: configDir };
|
|
128
|
+
delete env.CLAUDE_PLUGIN_ROOT;
|
|
129
|
+
execFileSync(process.execPath, [lifecycleCli, 'install'], {
|
|
130
|
+
cwd: repoRoot, env, stdio: ['pipe', 'pipe', 'pipe'],
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
// Config landed in the override dir...
|
|
134
|
+
const settings = readJson(cfgSettings);
|
|
135
|
+
assert.match(settings.statusLine.command, /statusline-composite\.js/);
|
|
136
|
+
assert.equal(fs.existsSync(cfgBackup), true,
|
|
137
|
+
'statusline-providers backup should land in CLAUDE_CONFIG_DIR');
|
|
138
|
+
|
|
139
|
+
// ...and default ~/.claude was never touched.
|
|
140
|
+
assert.equal(fs.existsSync(defaultSettings), false,
|
|
141
|
+
'default ~/.claude/settings.json must not be written when override is set');
|
|
142
|
+
});
|
|
143
|
+
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
const fs = require('fs');
|
|
4
4
|
const path = require('path');
|
|
5
5
|
const os = require('os');
|
|
6
|
+
const { claudeHome } = require('./claude-config');
|
|
6
7
|
|
|
7
8
|
const PLUGIN_ID = 'code-graph-mcp@code-graph-mcp';
|
|
8
9
|
const OLD_PLUGIN_IDS = [
|
|
@@ -16,13 +17,18 @@ const CACHE_DIR = path.join(os.homedir(), '.cache', 'code-graph');
|
|
|
16
17
|
// to its own marketplace path, polluting all subsequent settings.json hook processes).
|
|
17
18
|
const PLUGIN_ROOT = path.resolve(__dirname, '..');
|
|
18
19
|
const MANIFEST_FILE = path.join(CACHE_DIR, 'install-manifest.json');
|
|
19
|
-
const SETTINGS_PATH = path.join(os.homedir(), '.claude', 'settings.json');
|
|
20
|
-
const INSTALLED_PLUGINS_PATH = path.join(os.homedir(), '.claude', 'plugins', 'installed_plugins.json');
|
|
21
20
|
const REGISTRY_FILE = path.join(CACHE_DIR, 'statusline-registry.json');
|
|
21
|
+
|
|
22
|
+
// Lazy resolvers — Claude Code's config dir can be overridden by CLAUDE_CONFIG_DIR
|
|
23
|
+
// (multi-account isolation). Re-read every call so test subprocesses with a
|
|
24
|
+
// different env see the right path.
|
|
25
|
+
function settingsPath() { return path.join(claudeHome(), 'settings.json'); }
|
|
26
|
+
function installedPluginsPath() { return path.join(claudeHome(), 'plugins', 'installed_plugins.json'); }
|
|
22
27
|
// Durable mirror outside ~/.cache/ — survives cache cleanup. Captures the
|
|
23
28
|
// `_previous` snapshot (pre-install statusline) and any third-party providers
|
|
24
29
|
// (GSD, etc.). readRegistry() self-heals from this file when primary is missing.
|
|
25
|
-
|
|
30
|
+
function providersBackupFile() { return path.join(claudeHome(), 'statusline-providers.json'); }
|
|
31
|
+
function pluginsCacheDir() { return path.join(claudeHome(), 'plugins', 'cache'); }
|
|
26
32
|
|
|
27
33
|
// --- Helpers ---
|
|
28
34
|
|
|
@@ -65,7 +71,7 @@ function hasOwn(obj, key) {
|
|
|
65
71
|
}
|
|
66
72
|
|
|
67
73
|
function hasInstalledPluginRecord() {
|
|
68
|
-
const installed = readJson(
|
|
74
|
+
const installed = readJson(installedPluginsPath());
|
|
69
75
|
return !!(installed && installed.plugins && Array.isArray(installed.plugins[PLUGIN_ID]) && installed.plugins[PLUGIN_ID].length > 0);
|
|
70
76
|
}
|
|
71
77
|
|
|
@@ -83,7 +89,7 @@ function readRegistry() {
|
|
|
83
89
|
if (primary && Array.isArray(primary) && primary.length > 0) return primary;
|
|
84
90
|
// Self-heal: primary missing or empty (e.g. user cleaned ~/.cache/code-graph/).
|
|
85
91
|
// Durable backup in ~/.claude/ retains `_previous` + third-party providers.
|
|
86
|
-
const backup = readJson(
|
|
92
|
+
const backup = readJson(providersBackupFile());
|
|
87
93
|
if (backup && Array.isArray(backup) && backup.length > 0) {
|
|
88
94
|
try { writeJsonAtomic(REGISTRY_FILE, backup); } catch { /* ok */ }
|
|
89
95
|
return backup;
|
|
@@ -94,13 +100,13 @@ function readRegistry() {
|
|
|
94
100
|
function writeRegistry(registry) {
|
|
95
101
|
if (!registry || registry.length === 0) {
|
|
96
102
|
try { fs.unlinkSync(REGISTRY_FILE); } catch { /* ok */ }
|
|
97
|
-
try { fs.unlinkSync(
|
|
103
|
+
try { fs.unlinkSync(providersBackupFile()); } catch { /* ok */ }
|
|
98
104
|
return;
|
|
99
105
|
}
|
|
100
106
|
writeJsonAtomic(REGISTRY_FILE, registry);
|
|
101
107
|
// Mirror to durable location so cache cleanup doesn't strand `_previous`
|
|
102
108
|
// or third-party provider entries.
|
|
103
|
-
try { writeJsonAtomic(
|
|
109
|
+
try { writeJsonAtomic(providersBackupFile(), registry); } catch { /* ok */ }
|
|
104
110
|
}
|
|
105
111
|
|
|
106
112
|
function registerStatuslineProvider(id, command, needsStdin) {
|
|
@@ -126,18 +132,18 @@ function unregisterStatuslineProvider(id) {
|
|
|
126
132
|
return true;
|
|
127
133
|
}
|
|
128
134
|
|
|
129
|
-
function isPluginExplicitlyDisabled(settings = readJson(
|
|
135
|
+
function isPluginExplicitlyDisabled(settings = readJson(settingsPath()) || {}) {
|
|
130
136
|
return hasOwn(settings.enabledPlugins, PLUGIN_ID) && settings.enabledPlugins[PLUGIN_ID] === false;
|
|
131
137
|
}
|
|
132
138
|
|
|
133
|
-
function isPluginInactive(settings = readJson(
|
|
139
|
+
function isPluginInactive(settings = readJson(settingsPath()) || {}) {
|
|
134
140
|
if (isPluginExplicitlyDisabled(settings)) return true;
|
|
135
141
|
|
|
136
142
|
const hasComposite = isOurComposite(settings);
|
|
137
143
|
const hasCodeGraphRegistry = readRegistry().some((provider) => provider.id === 'code-graph');
|
|
138
144
|
if (!hasComposite && !hasCodeGraphRegistry) return false;
|
|
139
145
|
|
|
140
|
-
const installed = readJson(
|
|
146
|
+
const installed = readJson(installedPluginsPath());
|
|
141
147
|
if (!installed || !installed.plugins) return false;
|
|
142
148
|
return !hasInstalledPluginRecord();
|
|
143
149
|
}
|
|
@@ -165,7 +171,7 @@ function detachStatuslineIntegration(settings) {
|
|
|
165
171
|
}
|
|
166
172
|
|
|
167
173
|
function cleanupDisabledStatusline() {
|
|
168
|
-
const settings = readJson(
|
|
174
|
+
const settings = readJson(settingsPath());
|
|
169
175
|
if (!settings || !isPluginInactive(settings)) {
|
|
170
176
|
return { cleaned: false, settingsChanged: false };
|
|
171
177
|
}
|
|
@@ -173,7 +179,7 @@ function cleanupDisabledStatusline() {
|
|
|
173
179
|
let settingsChanged = detachStatuslineIntegration(settings);
|
|
174
180
|
if (removeHooksFromSettings(settings)) settingsChanged = true;
|
|
175
181
|
if (settingsChanged) {
|
|
176
|
-
writeJsonAtomic(
|
|
182
|
+
writeJsonAtomic(settingsPath(), settings);
|
|
177
183
|
}
|
|
178
184
|
|
|
179
185
|
return { cleaned: true, settingsChanged };
|
|
@@ -182,7 +188,7 @@ function cleanupDisabledStatusline() {
|
|
|
182
188
|
// --- Scope Conflict Detection ---
|
|
183
189
|
|
|
184
190
|
function checkScopeConflict() {
|
|
185
|
-
const installed = readJson(
|
|
191
|
+
const installed = readJson(installedPluginsPath());
|
|
186
192
|
if (!installed || !installed.plugins) return null;
|
|
187
193
|
for (const [key, entries] of Object.entries(installed.plugins)) {
|
|
188
194
|
if (key === PLUGIN_ID) continue;
|
|
@@ -207,10 +213,10 @@ function migrateOldPluginIds(settings) {
|
|
|
207
213
|
}
|
|
208
214
|
|
|
209
215
|
// Clean old ID from installed_plugins.json
|
|
210
|
-
const installed = readJson(
|
|
216
|
+
const installed = readJson(installedPluginsPath());
|
|
211
217
|
if (installed && installed.plugins && oldId in installed.plugins) {
|
|
212
218
|
delete installed.plugins[oldId];
|
|
213
|
-
writeJsonAtomic(
|
|
219
|
+
writeJsonAtomic(installedPluginsPath(), installed);
|
|
214
220
|
}
|
|
215
221
|
}
|
|
216
222
|
|
|
@@ -225,10 +231,11 @@ function migrateOldPluginIds(settings) {
|
|
|
225
231
|
}
|
|
226
232
|
|
|
227
233
|
// Clean old cache paths
|
|
234
|
+
const cacheRoot = pluginsCacheDir();
|
|
228
235
|
const oldCacheDirs = [
|
|
229
|
-
path.join(
|
|
230
|
-
path.join(
|
|
231
|
-
path.join(
|
|
236
|
+
path.join(cacheRoot, 'sdsrss', 'code-graph'),
|
|
237
|
+
path.join(cacheRoot, 'sdsrss-code-graph', 'code-graph'),
|
|
238
|
+
path.join(cacheRoot, 'sdsrss-code-graph'),
|
|
232
239
|
];
|
|
233
240
|
for (const dir of oldCacheDirs) {
|
|
234
241
|
try { fs.rmSync(dir, { recursive: true, force: true }); } catch { /* ok */ }
|
|
@@ -284,7 +291,7 @@ function removeHooksFromSettings(settings) {
|
|
|
284
291
|
function install() {
|
|
285
292
|
const version = getPluginVersion();
|
|
286
293
|
const manifest = readManifest();
|
|
287
|
-
const settings = readJson(
|
|
294
|
+
const settings = readJson(settingsPath()) || {};
|
|
288
295
|
let settingsChanged = false;
|
|
289
296
|
|
|
290
297
|
// 0. Migrate from old plugin IDs
|
|
@@ -329,7 +336,7 @@ function install() {
|
|
|
329
336
|
|
|
330
337
|
// 3. Write settings atomically if changed
|
|
331
338
|
if (settingsChanged) {
|
|
332
|
-
writeJsonAtomic(
|
|
339
|
+
writeJsonAtomic(settingsPath(), settings);
|
|
333
340
|
}
|
|
334
341
|
|
|
335
342
|
// 4. Write manifest with version
|
|
@@ -344,7 +351,7 @@ function install() {
|
|
|
344
351
|
// --- Uninstall (clean all config) ---
|
|
345
352
|
|
|
346
353
|
function uninstall() {
|
|
347
|
-
const settings = readJson(
|
|
354
|
+
const settings = readJson(settingsPath());
|
|
348
355
|
let settingsChanged = false;
|
|
349
356
|
|
|
350
357
|
if (settings) {
|
|
@@ -370,12 +377,12 @@ function uninstall() {
|
|
|
370
377
|
|
|
371
378
|
// 4. Write settings if changed
|
|
372
379
|
if (settingsChanged) {
|
|
373
|
-
writeJsonAtomic(
|
|
380
|
+
writeJsonAtomic(settingsPath(), settings);
|
|
374
381
|
}
|
|
375
382
|
}
|
|
376
383
|
|
|
377
384
|
// 5. Remove all known IDs from installed_plugins.json
|
|
378
|
-
const installedPlugins = readJson(
|
|
385
|
+
const installedPlugins = readJson(installedPluginsPath());
|
|
379
386
|
if (installedPlugins && installedPlugins.plugins) {
|
|
380
387
|
let ipChanged = false;
|
|
381
388
|
for (const id of [PLUGIN_ID, ...OLD_PLUGIN_IDS]) {
|
|
@@ -384,17 +391,18 @@ function uninstall() {
|
|
|
384
391
|
ipChanged = true;
|
|
385
392
|
}
|
|
386
393
|
}
|
|
387
|
-
if (ipChanged) writeJsonAtomic(
|
|
394
|
+
if (ipChanged) writeJsonAtomic(installedPluginsPath(), installedPlugins);
|
|
388
395
|
}
|
|
389
396
|
|
|
390
397
|
// 6. Remove cache directory
|
|
391
398
|
try { fs.rmSync(CACHE_DIR, { recursive: true, force: true }); } catch { /* ok */ }
|
|
392
399
|
|
|
393
400
|
// 7. Remove plugin files from cache (all known paths, including parent dirs)
|
|
401
|
+
const cacheRoot = pluginsCacheDir();
|
|
394
402
|
const pluginCacheDirs = [
|
|
395
|
-
path.join(
|
|
396
|
-
path.join(
|
|
397
|
-
path.join(
|
|
403
|
+
path.join(cacheRoot, MARKETPLACE_NAME),
|
|
404
|
+
path.join(cacheRoot, 'sdsrss-code-graph'),
|
|
405
|
+
path.join(cacheRoot, 'sdsrss', 'code-graph'),
|
|
398
406
|
];
|
|
399
407
|
for (const dir of pluginCacheDirs) {
|
|
400
408
|
try { fs.rmSync(dir, { recursive: true, force: true }); } catch { /* ok */ }
|
|
@@ -409,7 +417,7 @@ function update() {
|
|
|
409
417
|
const version = getPluginVersion();
|
|
410
418
|
const manifest = readManifest();
|
|
411
419
|
const oldVersion = manifest.version;
|
|
412
|
-
const settings = readJson(
|
|
420
|
+
const settings = readJson(settingsPath()) || {};
|
|
413
421
|
let settingsChanged = false;
|
|
414
422
|
|
|
415
423
|
// 0. Migrate from old plugin IDs
|
|
@@ -438,7 +446,7 @@ function update() {
|
|
|
438
446
|
|
|
439
447
|
// 4. Write settings if changed
|
|
440
448
|
if (settingsChanged) {
|
|
441
|
-
writeJsonAtomic(
|
|
449
|
+
writeJsonAtomic(settingsPath(), settings);
|
|
442
450
|
}
|
|
443
451
|
|
|
444
452
|
// 5. Clear update-check cache (force re-check after update)
|
|
@@ -463,7 +471,7 @@ function update() {
|
|
|
463
471
|
* Cache layout: ~/.claude/plugins/cache/<marketplace>/<plugin>/<version>/
|
|
464
472
|
*/
|
|
465
473
|
function cleanupOldCacheVersions(keep = 3) {
|
|
466
|
-
const cacheParent = path.join(
|
|
474
|
+
const cacheParent = path.join(pluginsCacheDir(), MARKETPLACE_NAME);
|
|
467
475
|
try {
|
|
468
476
|
// List all subdirectories under the marketplace cache
|
|
469
477
|
const entries = fs.readdirSync(cacheParent, { withFileTypes: true });
|
|
@@ -498,7 +506,7 @@ function cleanupOldCacheVersions(keep = 3) {
|
|
|
498
506
|
// Returns { healthy, issues, repaired }.
|
|
499
507
|
|
|
500
508
|
function healthCheck() {
|
|
501
|
-
const settings = readJson(
|
|
509
|
+
const settings = readJson(settingsPath()) || {};
|
|
502
510
|
const issues = [];
|
|
503
511
|
|
|
504
512
|
// Check statusLine path
|
|
@@ -554,7 +562,7 @@ module.exports = {
|
|
|
554
562
|
removeHooksFromSettings, isOurHookEntry,
|
|
555
563
|
registerStatuslineProvider, unregisterStatuslineProvider,
|
|
556
564
|
PLUGIN_ID, OLD_PLUGIN_IDS, MARKETPLACE_NAME, CACHE_DIR, REGISTRY_FILE,
|
|
557
|
-
|
|
565
|
+
settingsPath, installedPluginsPath, providersBackupFile, pluginsCacheDir,
|
|
558
566
|
};
|
|
559
567
|
|
|
560
568
|
// CLI: node lifecycle.js <install|uninstall|update|health>
|
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
'use strict';
|
|
3
3
|
const { spawn, execSync, execFileSync } = require('child_process');
|
|
4
4
|
const path = require('path');
|
|
5
|
-
const os = require('os');
|
|
6
5
|
const fs = require('fs');
|
|
7
6
|
const {
|
|
8
7
|
install, update, readManifest, getPluginVersion, checkScopeConflict,
|
|
9
8
|
cleanupDisabledStatusline, isPluginInactive, readJson, CACHE_DIR,
|
|
9
|
+
settingsPath,
|
|
10
10
|
} = require('./lifecycle');
|
|
11
11
|
const { readBinaryVersion, isDevMode, getNewestMtime } = require('./version-utils');
|
|
12
12
|
const { maybeAutoAdopt, isAdopted } = require('./adopt');
|
|
@@ -58,7 +58,7 @@ function syncLifecycleConfig() {
|
|
|
58
58
|
// Self-heal: version matches but statusLine may have been lost or path corrupted
|
|
59
59
|
// (e.g. plugin removed and reinstalled, or CLAUDE_PLUGIN_ROOT leaked from another plugin).
|
|
60
60
|
// install() is idempotent — isOurComposite guard prevents duplicate work.
|
|
61
|
-
const settings = readJson(
|
|
61
|
+
const settings = readJson(settingsPath()) || {};
|
|
62
62
|
if (!settings.statusLine || !settings.statusLine.command ||
|
|
63
63
|
!settings.statusLine.command.includes('statusline-composite')) {
|
|
64
64
|
install();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sdsrs/code-graph",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.31.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": {
|
|
@@ -35,10 +35,10 @@
|
|
|
35
35
|
"node": ">=16"
|
|
36
36
|
},
|
|
37
37
|
"optionalDependencies": {
|
|
38
|
-
"@sdsrs/code-graph-linux-x64": "0.
|
|
39
|
-
"@sdsrs/code-graph-linux-arm64": "0.
|
|
40
|
-
"@sdsrs/code-graph-darwin-x64": "0.
|
|
41
|
-
"@sdsrs/code-graph-darwin-arm64": "0.
|
|
42
|
-
"@sdsrs/code-graph-win32-x64": "0.
|
|
38
|
+
"@sdsrs/code-graph-linux-x64": "0.31.0",
|
|
39
|
+
"@sdsrs/code-graph-linux-arm64": "0.31.0",
|
|
40
|
+
"@sdsrs/code-graph-darwin-x64": "0.31.0",
|
|
41
|
+
"@sdsrs/code-graph-darwin-arm64": "0.31.0",
|
|
42
|
+
"@sdsrs/code-graph-win32-x64": "0.31.0"
|
|
43
43
|
}
|
|
44
44
|
}
|