@sdsrs/code-graph 0.30.0 → 0.32.2
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/hooks/hooks.json +2 -58
- package/claude-plugin/scripts/adopt.js +15 -2
- package/claude-plugin/scripts/adopt.test.js +38 -0
- package/claude-plugin/scripts/auto-update.js +5 -4
- 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 +65 -34
- package/claude-plugin/scripts/hooks.test.js +151 -0
- package/claude-plugin/scripts/lifecycle.e2e.test.js +46 -0
- package/claude-plugin/scripts/lifecycle.js +185 -62
- package/claude-plugin/scripts/lifecycle.test.js +381 -6
- package/claude-plugin/scripts/mcp-launcher.js +73 -0
- package/claude-plugin/scripts/mcp-launcher.test.js +23 -3
- package/claude-plugin/scripts/pre-edit-guide.js +24 -4
- package/claude-plugin/scripts/pre-grep-guide.js +107 -9
- package/claude-plugin/scripts/pre-grep-guide.test.js +263 -1
- package/claude-plugin/scripts/pre-read-guide.js +2 -2
- package/claude-plugin/scripts/session-init.js +19 -2
- package/claude-plugin/scripts/tmp-dir.js +32 -0
- package/claude-plugin/scripts/tmp-dir.test.js +50 -0
- package/package.json +6 -6
|
@@ -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 */ }
|
|
@@ -238,28 +245,63 @@ function migrateOldPluginIds(settings) {
|
|
|
238
245
|
}
|
|
239
246
|
|
|
240
247
|
// --- Hook identity ---
|
|
241
|
-
//
|
|
242
|
-
//
|
|
243
|
-
//
|
|
244
|
-
//
|
|
245
|
-
//
|
|
248
|
+
//
|
|
249
|
+
// v0.32.0 ARCHITECTURE CORRECTION (see project_hooks_settings.md / feedback_pretooluse_dark_under_green_health.md):
|
|
250
|
+
//
|
|
251
|
+
// Empirical finding 2026-05-24: current Claude Code only loads SessionStart
|
|
252
|
+
// hooks from cache/<mp>/<plugin>/<ver>/hooks/hooks.json. PreToolUse, PostToolUse,
|
|
253
|
+
// UserPromptSubmit, Stop, SessionEnd entries in plugin-cache hooks.json are
|
|
254
|
+
// SILENTLY IGNORED. Only ~/.claude/settings.json entries reach CC for those events.
|
|
255
|
+
//
|
|
256
|
+
// Therefore lifecycle.js now ACTIVELY WRITES non-SessionStart hook entries to
|
|
257
|
+
// settings.json (with description markers for cleanup), and the shipped
|
|
258
|
+
// claude-plugin/hooks/hooks.json carries only SessionStart. SessionStart entries
|
|
259
|
+
// in claude-plugin/hooks/hooks.json continue to be CC-loaded as before.
|
|
260
|
+
//
|
|
261
|
+
// Pattern mirrors claude-mem-lite's install.mjs (cache hooks.json cleared
|
|
262
|
+
// to prevent duplicate registration).
|
|
263
|
+
|
|
264
|
+
const OUR_HOOK_SCRIPTS = [
|
|
265
|
+
'session-init.js',
|
|
266
|
+
'incremental-index.js',
|
|
267
|
+
'user-prompt-context.js',
|
|
268
|
+
'pre-edit-guide.js',
|
|
269
|
+
'pre-grep-guide.js', // v0.32.0 — was in plugin-cache only, never fired
|
|
270
|
+
'pre-read-guide.js', // v0.32.0 — was in plugin-cache only, never fired
|
|
271
|
+
];
|
|
272
|
+
|
|
273
|
+
// Description markers — primary cleanup discriminator (immune to env/path
|
|
274
|
+
// pollution per feedback_plugin_env_isolation.md). New v0.32.0 markers carry
|
|
275
|
+
// the version so older lifecycle.js still recognizes them as ours.
|
|
276
|
+
const SETTINGS_HOOK_DESC = {
|
|
277
|
+
preToolUse: '[code-graph-mcp v0.32+] PreToolUse re-routed via settings.json (cache hooks.json silently ignored for this event by current CC)',
|
|
278
|
+
postToolUseEdit: '[code-graph-mcp v0.32+] PostToolUse Write|Edit incremental-index update',
|
|
279
|
+
userPromptSubmit: '[code-graph-mcp v0.32+] UserPromptSubmit context push',
|
|
280
|
+
};
|
|
246
281
|
|
|
247
|
-
const OUR_HOOK_SCRIPTS = ['session-init.js', 'incremental-index.js', 'user-prompt-context.js', 'pre-edit-guide.js'];
|
|
248
282
|
const OUR_DESCRIPTIONS = [
|
|
283
|
+
// Legacy v0.7.x / 0.8.x descriptions — kept so very-old installs still get cleaned up.
|
|
249
284
|
'StatusLine self-heal, lifecycle sync, project map injection',
|
|
250
285
|
'Auto-inject impact analysis when editing functions with 2+ callers',
|
|
251
286
|
'Auto-update code graph index after file edits',
|
|
252
287
|
'Inject code-graph structural context based on user intent',
|
|
288
|
+
// v0.32.0 — new re-route markers
|
|
289
|
+
SETTINGS_HOOK_DESC.preToolUse,
|
|
290
|
+
SETTINGS_HOOK_DESC.postToolUseEdit,
|
|
291
|
+
SETTINGS_HOOK_DESC.userPromptSubmit,
|
|
253
292
|
];
|
|
254
293
|
|
|
255
294
|
function isOurHookEntry(entry) {
|
|
256
295
|
if (!entry || !entry.hooks) return false;
|
|
257
|
-
// Primary: match by description (
|
|
296
|
+
// Primary: match by description (immune to path pollution).
|
|
258
297
|
if (entry.description && OUR_DESCRIPTIONS.includes(entry.description)) return true;
|
|
259
|
-
// Fallback:
|
|
298
|
+
// Fallback: script name + MARKETPLACE_NAME in path. v0.32.1: tightened from
|
|
299
|
+
// bare 'code-graph' (which would claim a user's own ~/code-graph/foo.js) to
|
|
300
|
+
// the actual marketplace dir name 'code-graph-mcp' — Requirement 3 says
|
|
301
|
+
// foreign-entry strip is unacceptable, so be conservative.
|
|
260
302
|
return entry.hooks.some(h =>
|
|
261
303
|
h.command && OUR_HOOK_SCRIPTS.some(s => h.command.includes(s)) &&
|
|
262
|
-
h.command.includes(
|
|
304
|
+
h.command.includes(MARKETPLACE_NAME)
|
|
263
305
|
);
|
|
264
306
|
}
|
|
265
307
|
|
|
@@ -279,12 +321,68 @@ function removeHooksFromSettings(settings) {
|
|
|
279
321
|
return changed;
|
|
280
322
|
}
|
|
281
323
|
|
|
324
|
+
// --- v0.32.0: settings.json hook registration ---
|
|
325
|
+
|
|
326
|
+
// PLUGIN_ROOT (module-level, line 18) is the canonical __dirname-derived
|
|
327
|
+
// absolute path — never CLAUDE_PLUGIN_ROOT env (env leaks across plugins
|
|
328
|
+
// in settings.json hook execution context per feedback_plugin_env_isolation.md).
|
|
329
|
+
|
|
330
|
+
function buildSettingsHookEntries() {
|
|
331
|
+
const root = PLUGIN_ROOT;
|
|
332
|
+
const scriptCmd = (name, timeout) => ({
|
|
333
|
+
type: 'command',
|
|
334
|
+
command: `node "${path.join(root, 'scripts', name)}"`,
|
|
335
|
+
timeout,
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
return {
|
|
339
|
+
PreToolUse: [
|
|
340
|
+
{ description: SETTINGS_HOOK_DESC.preToolUse, matcher: 'Edit', hooks: [scriptCmd('pre-edit-guide.js', 4)] },
|
|
341
|
+
{ description: SETTINGS_HOOK_DESC.preToolUse, matcher: 'Bash', hooks: [scriptCmd('pre-grep-guide.js', 3)] },
|
|
342
|
+
{ description: SETTINGS_HOOK_DESC.preToolUse, matcher: 'Read', hooks: [scriptCmd('pre-read-guide.js', 3)] },
|
|
343
|
+
],
|
|
344
|
+
PostToolUse: [
|
|
345
|
+
{ description: SETTINGS_HOOK_DESC.postToolUseEdit, matcher: 'Write|Edit', hooks: [scriptCmd('incremental-index.js', 10)] },
|
|
346
|
+
],
|
|
347
|
+
UserPromptSubmit: [
|
|
348
|
+
{ description: SETTINGS_HOOK_DESC.userPromptSubmit, matcher: '', hooks: [scriptCmd('user-prompt-context.js', 5)] },
|
|
349
|
+
],
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// Idempotent two-pass: (1) evict ALL our entries (legacy v0.7+/0.8+ markers
|
|
354
|
+
// AND v0.32+ markers) across EVERY event — catches legacy SessionStart/
|
|
355
|
+
// PostToolUse entries in settings.json pointing to stale plugin-cache paths;
|
|
356
|
+
// (2) write fresh v0.32+ entries for the events we own. SessionStart stays
|
|
357
|
+
// in plugin-cache hooks.json (it's still loaded from there), so we don't
|
|
358
|
+
// re-write it to settings.json.
|
|
359
|
+
function registerHooksToSettings(settings) {
|
|
360
|
+
settings.hooks = settings.hooks || {};
|
|
361
|
+
const before = JSON.stringify(settings.hooks);
|
|
362
|
+
|
|
363
|
+
// Pass 1: evict our entries across every event.
|
|
364
|
+
for (const event of Object.keys(settings.hooks)) {
|
|
365
|
+
if (!Array.isArray(settings.hooks[event])) continue;
|
|
366
|
+
settings.hooks[event] = settings.hooks[event].filter(e => !isOurHookEntry(e));
|
|
367
|
+
if (settings.hooks[event].length === 0) delete settings.hooks[event];
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// Pass 2: write fresh entries for our desired events.
|
|
371
|
+
const desired = buildSettingsHookEntries();
|
|
372
|
+
for (const [event, desiredEntries] of Object.entries(desired)) {
|
|
373
|
+
const existing = Array.isArray(settings.hooks[event]) ? settings.hooks[event] : [];
|
|
374
|
+
settings.hooks[event] = [...existing, ...desiredEntries];
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
return before !== JSON.stringify(settings.hooks);
|
|
378
|
+
}
|
|
379
|
+
|
|
282
380
|
// --- Install (idempotent) ---
|
|
283
381
|
|
|
284
382
|
function install() {
|
|
285
383
|
const version = getPluginVersion();
|
|
286
384
|
const manifest = readManifest();
|
|
287
|
-
const settings = readJson(
|
|
385
|
+
const settings = readJson(settingsPath()) || {};
|
|
288
386
|
let settingsChanged = false;
|
|
289
387
|
|
|
290
388
|
// 0. Migrate from old plugin IDs
|
|
@@ -317,11 +415,12 @@ function install() {
|
|
|
317
415
|
// Register code-graph provider
|
|
318
416
|
registerStatuslineProvider('code-graph', codeGraphStatuslineCommand(), false);
|
|
319
417
|
|
|
320
|
-
// 2. Hooks —
|
|
321
|
-
//
|
|
322
|
-
//
|
|
323
|
-
|
|
324
|
-
|
|
418
|
+
// 2. Hooks — v0.32.0: actively write PreToolUse/PostToolUse/UserPromptSubmit
|
|
419
|
+
// to settings.json. Plugin-cache hooks.json is silently ignored by current
|
|
420
|
+
// Claude Code for these events (SessionStart still loads from cache).
|
|
421
|
+
// registerHooksToSettings is idempotent: strips priors then appends fresh.
|
|
422
|
+
const hooksRegistered = registerHooksToSettings(settings);
|
|
423
|
+
if (hooksRegistered) settingsChanged = true;
|
|
325
424
|
|
|
326
425
|
// NOTE: enabledPlugins is managed by Claude Code's plugin system, not by lifecycle.
|
|
327
426
|
// Do NOT add enabledPlugins entries here — it causes phantom plugin entries
|
|
@@ -329,7 +428,7 @@ function install() {
|
|
|
329
428
|
|
|
330
429
|
// 3. Write settings atomically if changed
|
|
331
430
|
if (settingsChanged) {
|
|
332
|
-
writeJsonAtomic(
|
|
431
|
+
writeJsonAtomic(settingsPath(), settings);
|
|
333
432
|
}
|
|
334
433
|
|
|
335
434
|
// 4. Write manifest with version
|
|
@@ -338,13 +437,13 @@ function install() {
|
|
|
338
437
|
manifest.updatedAt = new Date().toISOString();
|
|
339
438
|
writeManifest(manifest);
|
|
340
439
|
|
|
341
|
-
return { version, settingsChanged, statusLineClaimed: manifest.config.statusLine,
|
|
440
|
+
return { version, settingsChanged, statusLineClaimed: manifest.config.statusLine, hooksRegistered };
|
|
342
441
|
}
|
|
343
442
|
|
|
344
443
|
// --- Uninstall (clean all config) ---
|
|
345
444
|
|
|
346
445
|
function uninstall() {
|
|
347
|
-
const settings = readJson(
|
|
446
|
+
const settings = readJson(settingsPath());
|
|
348
447
|
let settingsChanged = false;
|
|
349
448
|
|
|
350
449
|
if (settings) {
|
|
@@ -370,12 +469,12 @@ function uninstall() {
|
|
|
370
469
|
|
|
371
470
|
// 4. Write settings if changed
|
|
372
471
|
if (settingsChanged) {
|
|
373
|
-
writeJsonAtomic(
|
|
472
|
+
writeJsonAtomic(settingsPath(), settings);
|
|
374
473
|
}
|
|
375
474
|
}
|
|
376
475
|
|
|
377
476
|
// 5. Remove all known IDs from installed_plugins.json
|
|
378
|
-
const installedPlugins = readJson(
|
|
477
|
+
const installedPlugins = readJson(installedPluginsPath());
|
|
379
478
|
if (installedPlugins && installedPlugins.plugins) {
|
|
380
479
|
let ipChanged = false;
|
|
381
480
|
for (const id of [PLUGIN_ID, ...OLD_PLUGIN_IDS]) {
|
|
@@ -384,17 +483,18 @@ function uninstall() {
|
|
|
384
483
|
ipChanged = true;
|
|
385
484
|
}
|
|
386
485
|
}
|
|
387
|
-
if (ipChanged) writeJsonAtomic(
|
|
486
|
+
if (ipChanged) writeJsonAtomic(installedPluginsPath(), installedPlugins);
|
|
388
487
|
}
|
|
389
488
|
|
|
390
489
|
// 6. Remove cache directory
|
|
391
490
|
try { fs.rmSync(CACHE_DIR, { recursive: true, force: true }); } catch { /* ok */ }
|
|
392
491
|
|
|
393
492
|
// 7. Remove plugin files from cache (all known paths, including parent dirs)
|
|
493
|
+
const cacheRoot = pluginsCacheDir();
|
|
394
494
|
const pluginCacheDirs = [
|
|
395
|
-
path.join(
|
|
396
|
-
path.join(
|
|
397
|
-
path.join(
|
|
495
|
+
path.join(cacheRoot, MARKETPLACE_NAME),
|
|
496
|
+
path.join(cacheRoot, 'sdsrss-code-graph'),
|
|
497
|
+
path.join(cacheRoot, 'sdsrss', 'code-graph'),
|
|
398
498
|
];
|
|
399
499
|
for (const dir of pluginCacheDirs) {
|
|
400
500
|
try { fs.rmSync(dir, { recursive: true, force: true }); } catch { /* ok */ }
|
|
@@ -409,7 +509,7 @@ function update() {
|
|
|
409
509
|
const version = getPluginVersion();
|
|
410
510
|
const manifest = readManifest();
|
|
411
511
|
const oldVersion = manifest.version;
|
|
412
|
-
const settings = readJson(
|
|
512
|
+
const settings = readJson(settingsPath()) || {};
|
|
413
513
|
let settingsChanged = false;
|
|
414
514
|
|
|
415
515
|
// 0. Migrate from old plugin IDs
|
|
@@ -429,16 +529,16 @@ function update() {
|
|
|
429
529
|
// 2. Update code-graph provider in registry
|
|
430
530
|
registerStatuslineProvider('code-graph', codeGraphStatuslineCommand(), false);
|
|
431
531
|
|
|
432
|
-
// 3. Hooks —
|
|
433
|
-
//
|
|
434
|
-
const
|
|
435
|
-
if (
|
|
532
|
+
// 3. Hooks — v0.32.0: register PreToolUse/PostToolUse/UserPromptSubmit in
|
|
533
|
+
// settings.json (idempotent; absolute paths re-anchor on every update).
|
|
534
|
+
const hooksRegistered = registerHooksToSettings(settings);
|
|
535
|
+
if (hooksRegistered) settingsChanged = true;
|
|
436
536
|
|
|
437
537
|
// NOTE: enabledPlugins is managed by Claude Code's plugin system, not by lifecycle.
|
|
438
538
|
|
|
439
539
|
// 4. Write settings if changed
|
|
440
540
|
if (settingsChanged) {
|
|
441
|
-
writeJsonAtomic(
|
|
541
|
+
writeJsonAtomic(settingsPath(), settings);
|
|
442
542
|
}
|
|
443
543
|
|
|
444
544
|
// 5. Clear update-check cache (force re-check after update)
|
|
@@ -455,7 +555,7 @@ function update() {
|
|
|
455
555
|
// cache dirs are inert disk clutter, not correctness risks.
|
|
456
556
|
cleanupOldCacheVersions(3);
|
|
457
557
|
|
|
458
|
-
return { oldVersion, version, settingsChanged,
|
|
558
|
+
return { oldVersion, version, settingsChanged, hooksRegistered };
|
|
459
559
|
}
|
|
460
560
|
|
|
461
561
|
/**
|
|
@@ -463,7 +563,7 @@ function update() {
|
|
|
463
563
|
* Cache layout: ~/.claude/plugins/cache/<marketplace>/<plugin>/<version>/
|
|
464
564
|
*/
|
|
465
565
|
function cleanupOldCacheVersions(keep = 3) {
|
|
466
|
-
const cacheParent = path.join(
|
|
566
|
+
const cacheParent = path.join(pluginsCacheDir(), MARKETPLACE_NAME);
|
|
467
567
|
try {
|
|
468
568
|
// List all subdirectories under the marketplace cache
|
|
469
569
|
const entries = fs.readdirSync(cacheParent, { withFileTypes: true });
|
|
@@ -495,10 +595,16 @@ function cleanupOldCacheVersions(keep = 3) {
|
|
|
495
595
|
|
|
496
596
|
// --- Health Check ---
|
|
497
597
|
// Validates all registered paths in settings.json point to existing scripts.
|
|
498
|
-
// Returns { healthy, issues, repaired }.
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
598
|
+
// Returns { healthy, issues, repaired, remaining }.
|
|
599
|
+
// issues: pre-repair detection list (what was wrong on entry)
|
|
600
|
+
// repaired: true only after a post-repair re-scan returned zero issues
|
|
601
|
+
// (was previously set blindly to true whenever install() ran,
|
|
602
|
+
// which would lie if install() couldn't actually fix something)
|
|
603
|
+
// remaining: post-repair detection list — present iff install() was invoked;
|
|
604
|
+
// empty array means repair succeeded
|
|
605
|
+
|
|
606
|
+
function scanForBrokenPaths() {
|
|
607
|
+
const settings = readJson(settingsPath()) || {};
|
|
502
608
|
const issues = [];
|
|
503
609
|
|
|
504
610
|
// Check statusLine path
|
|
@@ -535,26 +641,43 @@ function healthCheck() {
|
|
|
535
641
|
}
|
|
536
642
|
}
|
|
537
643
|
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
644
|
+
return issues;
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
function healthCheck() {
|
|
648
|
+
const issues = scanForBrokenPaths();
|
|
649
|
+
|
|
650
|
+
if (issues.length === 0) {
|
|
651
|
+
return { healthy: true, issues, repaired: false };
|
|
543
652
|
}
|
|
544
653
|
|
|
545
|
-
|
|
654
|
+
// Attempt auto-repair, then re-scan to confirm the issues actually went
|
|
655
|
+
// away. install() may legitimately fail to resolve a problem (binary path
|
|
656
|
+
// permanently gone, registry corrupted, etc.) and the previous code lied
|
|
657
|
+
// by always returning repaired:true.
|
|
658
|
+
install();
|
|
659
|
+
const remaining = scanForBrokenPaths();
|
|
660
|
+
return {
|
|
661
|
+
healthy: false,
|
|
662
|
+
issues,
|
|
663
|
+
repaired: remaining.length === 0,
|
|
664
|
+
remaining,
|
|
665
|
+
};
|
|
546
666
|
}
|
|
547
667
|
|
|
548
668
|
module.exports = {
|
|
549
|
-
install, uninstall, update, healthCheck, checkScopeConflict,
|
|
669
|
+
install, uninstall, update, healthCheck, scanForBrokenPaths, checkScopeConflict,
|
|
550
670
|
isPluginExplicitlyDisabled, isPluginInactive, cleanupDisabledStatusline,
|
|
551
671
|
readManifest, readJson, writeJsonAtomic,
|
|
552
672
|
readRegistry, writeRegistry,
|
|
553
673
|
getPluginVersion, cleanupOldCacheVersions,
|
|
554
674
|
removeHooksFromSettings, isOurHookEntry,
|
|
675
|
+
registerHooksToSettings, buildSettingsHookEntries, // v0.32.0
|
|
676
|
+
SETTINGS_HOOK_DESC, OUR_HOOK_SCRIPTS, OUR_DESCRIPTIONS, // v0.32.0 — for tests
|
|
677
|
+
PLUGIN_ROOT, // v0.32.1 — for tests / consumers
|
|
555
678
|
registerStatuslineProvider, unregisterStatuslineProvider,
|
|
556
679
|
PLUGIN_ID, OLD_PLUGIN_IDS, MARKETPLACE_NAME, CACHE_DIR, REGISTRY_FILE,
|
|
557
|
-
|
|
680
|
+
settingsPath, installedPluginsPath, providersBackupFile, pluginsCacheDir,
|
|
558
681
|
};
|
|
559
682
|
|
|
560
683
|
// CLI: node lifecycle.js <install|uninstall|update|health>
|