@sdsrs/code-graph 0.81.1 → 0.82.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -285,7 +285,7 @@ function runDiagnostics() {
|
|
|
285
285
|
|
|
286
286
|
const STATUS_ICONS = { ok: '\u2705', warn: '\u26a0\ufe0f', error: '\u274c', skip: '\u2796' };
|
|
287
287
|
|
|
288
|
-
function formatReport(results) {
|
|
288
|
+
function formatReport(results, { checkOnly = false } = {}) {
|
|
289
289
|
const pluginVersion = getPluginVersion();
|
|
290
290
|
const lines = [`\ud83d\udd0d code-graph doctor v${pluginVersion}`, ''];
|
|
291
291
|
|
|
@@ -302,7 +302,15 @@ function formatReport(results) {
|
|
|
302
302
|
lines.push(' All checks passed.');
|
|
303
303
|
} else {
|
|
304
304
|
const fixable = issues.filter(r => r.fixId);
|
|
305
|
-
|
|
305
|
+
// `--check-only` is read-only (it never reaches runRepairs), so it must not
|
|
306
|
+
// claim "Fixing..." — that contradicts the documented contract and alarms
|
|
307
|
+
// the user into thinking their settings.json/MEMORY.md was just rewritten.
|
|
308
|
+
const suffix = fixable.length === 0
|
|
309
|
+
? ''
|
|
310
|
+
: checkOnly
|
|
311
|
+
? ' Run without --check-only to fix.'
|
|
312
|
+
: ' Fixing...';
|
|
313
|
+
lines.push(` ${issues.length} issue(s) found.${suffix}`);
|
|
306
314
|
}
|
|
307
315
|
|
|
308
316
|
return lines.join('\n');
|
|
@@ -526,7 +534,7 @@ function runRepairs(results) {
|
|
|
526
534
|
|
|
527
535
|
function runDoctor(opts = {}) {
|
|
528
536
|
const results = runDiagnostics();
|
|
529
|
-
console.log(formatReport(results));
|
|
537
|
+
console.log(formatReport(results, { checkOnly: opts.checkOnly }));
|
|
530
538
|
|
|
531
539
|
const issues = results.filter(r => r.status === 'warn' || r.status === 'error');
|
|
532
540
|
|
|
@@ -48,6 +48,22 @@ test('formatReport shows issue count when problems exist', () => {
|
|
|
48
48
|
assert.ok(output.includes('issue'));
|
|
49
49
|
});
|
|
50
50
|
|
|
51
|
+
test('formatReport: --check-only never says "Fixing..." (it does not repair)', () => {
|
|
52
|
+
const results = [
|
|
53
|
+
{ name: 'Hook coverage', status: 'warn', detail: 'missing', fixId: 'hooks' },
|
|
54
|
+
];
|
|
55
|
+
// Default (repair mode) announces the fix.
|
|
56
|
+
assert.ok(formatReport(results).includes('Fixing...'),
|
|
57
|
+
'repair mode should announce Fixing...');
|
|
58
|
+
// --check-only is read-only: it must NOT claim to fix, and should point the
|
|
59
|
+
// user at the repair command instead.
|
|
60
|
+
const checkOnly = formatReport(results, { checkOnly: true });
|
|
61
|
+
assert.ok(!checkOnly.includes('Fixing...'),
|
|
62
|
+
`--check-only must not say "Fixing..."; got: ${checkOnly}`);
|
|
63
|
+
assert.ok(checkOnly.includes('--check-only'),
|
|
64
|
+
`--check-only should hint how to fix; got: ${checkOnly}`);
|
|
65
|
+
});
|
|
66
|
+
|
|
51
67
|
test('formatReport shows all-clear when no problems', () => {
|
|
52
68
|
const results = [
|
|
53
69
|
{ name: 'Binary version', status: 'ok', detail: 'v0.7.16' },
|
|
@@ -756,10 +756,13 @@ function update() {
|
|
|
756
756
|
manifest.updatedAt = new Date().toISOString();
|
|
757
757
|
writeManifest(manifest);
|
|
758
758
|
|
|
759
|
-
// 7. Clean up old cached versions (keep
|
|
760
|
-
//
|
|
761
|
-
//
|
|
762
|
-
|
|
759
|
+
// 7. Clean up old cached versions (keep the newest few). NOTE: older cache
|
|
760
|
+
// dirs are NOT always inert — a running MCP server's launcher path
|
|
761
|
+
// (<version>/scripts/mcp-launcher.js) is resolved + cached by Claude Code
|
|
762
|
+
// for the whole session, so pruning the version a live process is bound to
|
|
763
|
+
// breaks `/mcp` reconnect with -32000 (MODULE_NOT_FOUND). cleanupOldCacheVersions
|
|
764
|
+
// therefore skips any version still referenced by a live process cmdline.
|
|
765
|
+
cleanupOldCacheVersions(5);
|
|
763
766
|
|
|
764
767
|
return { oldVersion, version, settingsChanged, hooksRegistered };
|
|
765
768
|
}
|
|
@@ -768,8 +771,16 @@ function update() {
|
|
|
768
771
|
* Remove old plugin cache versions, keeping the N most recent.
|
|
769
772
|
* Cache layout: ~/.claude/plugins/cache/<marketplace>/<plugin>/<version>/
|
|
770
773
|
*/
|
|
771
|
-
function cleanupOldCacheVersions(
|
|
772
|
-
|
|
774
|
+
function cleanupOldCacheVersions(
|
|
775
|
+
keep = 5,
|
|
776
|
+
getActiveCmdlines = readActiveProcessCmdlines,
|
|
777
|
+
cacheParent = path.join(pluginsCacheDir(), MARKETPLACE_NAME),
|
|
778
|
+
) {
|
|
779
|
+
// Command lines of all live processes — used by the in-use guard below to
|
|
780
|
+
// avoid deleting a version a running MCP server is still bound to. Failure to
|
|
781
|
+
// enumerate ⇒ [] ⇒ pruning falls back to recency-only (pre-guard behavior).
|
|
782
|
+
let cmdlines;
|
|
783
|
+
try { cmdlines = getActiveCmdlines() || []; } catch { cmdlines = []; }
|
|
773
784
|
try {
|
|
774
785
|
// List all subdirectories under the marketplace cache
|
|
775
786
|
const entries = fs.readdirSync(cacheParent, { withFileTypes: true });
|
|
@@ -788,17 +799,51 @@ function cleanupOldCacheVersions(keep = 3) {
|
|
|
788
799
|
|
|
789
800
|
if (versions.length <= keep) continue;
|
|
790
801
|
|
|
791
|
-
const
|
|
792
|
-
|
|
802
|
+
for (const v of versions.slice(keep)) {
|
|
803
|
+
// In-use guard: never delete a version dir a live process is running
|
|
804
|
+
// from. Claude Code caches the resolved launcher path
|
|
805
|
+
// (<version>/scripts/mcp-launcher.js) for the session; deleting that
|
|
806
|
+
// dir breaks `/mcp` reconnect with -32000. Trailing separator stops
|
|
807
|
+
// `0.8` from matching `0.80.x`.
|
|
808
|
+
if (cmdlines.some(c => c.includes(v.path + path.sep))) continue;
|
|
793
809
|
try {
|
|
794
810
|
fs.rmSync(v.path, { recursive: true, force: true });
|
|
795
|
-
} catch { /* permission error
|
|
811
|
+
} catch { /* permission error — skip */ }
|
|
796
812
|
}
|
|
797
813
|
} catch { /* can't read plugin dir — skip */ }
|
|
798
814
|
}
|
|
799
815
|
} catch { /* cache dir doesn't exist — nothing to clean */ }
|
|
800
816
|
}
|
|
801
817
|
|
|
818
|
+
/**
|
|
819
|
+
* Best-effort list of running process command lines, for cleanupOldCacheVersions'
|
|
820
|
+
* in-use guard. Linux reads /proc/<pid>/cmdline; macOS/BSD shells out to `ps`;
|
|
821
|
+
* any other platform or failure returns [] (pruning then falls back to
|
|
822
|
+
* recency-only — the same behavior as before the guard existed).
|
|
823
|
+
*/
|
|
824
|
+
function readActiveProcessCmdlines() {
|
|
825
|
+
try {
|
|
826
|
+
if (process.platform === 'linux' && fs.existsSync('/proc')) {
|
|
827
|
+
const out = [];
|
|
828
|
+
for (const pid of fs.readdirSync('/proc')) {
|
|
829
|
+
if (!/^\d+$/.test(pid)) continue;
|
|
830
|
+
try {
|
|
831
|
+
const raw = fs.readFileSync(path.join('/proc', pid, 'cmdline'), 'utf8');
|
|
832
|
+
if (raw) out.push(raw.replace(/\0/g, ' '));
|
|
833
|
+
} catch { /* pid exited or unreadable — skip */ }
|
|
834
|
+
}
|
|
835
|
+
return out;
|
|
836
|
+
}
|
|
837
|
+
} catch { /* fall through to ps */ }
|
|
838
|
+
try {
|
|
839
|
+
const { execFileSync } = require('child_process');
|
|
840
|
+
return execFileSync('ps', ['-axww', '-o', 'command='], {
|
|
841
|
+
encoding: 'utf8', maxBuffer: 8 * 1024 * 1024,
|
|
842
|
+
}).split('\n').filter(Boolean);
|
|
843
|
+
} catch { /* unsupported platform — caller falls back to recency-only */ }
|
|
844
|
+
return [];
|
|
845
|
+
}
|
|
846
|
+
|
|
802
847
|
// --- Health Check ---
|
|
803
848
|
// Validates all registered paths in settings.json point to existing scripts.
|
|
804
849
|
// Returns { healthy, issues, repaired, remaining }.
|
|
@@ -733,3 +733,54 @@ test('isStaleRelicContext: relic in plugins cache defers to a different active i
|
|
|
733
733
|
existsSync: () => false,
|
|
734
734
|
}), false);
|
|
735
735
|
});
|
|
736
|
+
|
|
737
|
+
test('cleanupOldCacheVersions keeps an in-use version even beyond the keep window', (t) => {
|
|
738
|
+
const { cleanupOldCacheVersions } = require('./lifecycle.js');
|
|
739
|
+
const cacheParent = fs.mkdtempSync(path.join(os.tmpdir(), 'code-graph-cache-'));
|
|
740
|
+
t.after(() => fs.rmSync(cacheParent, { recursive: true, force: true }));
|
|
741
|
+
const pluginDir = path.join(cacheParent, 'code-graph-mcp');
|
|
742
|
+
// Seven versions, oldest -> newest by mtime.
|
|
743
|
+
const vers = ['0.78.0', '0.80.2', '0.80.3', '0.81.0', '0.81.1', '0.81.2', '0.81.3'];
|
|
744
|
+
vers.forEach((v, i) => {
|
|
745
|
+
const scripts = path.join(pluginDir, v, 'scripts');
|
|
746
|
+
fs.mkdirSync(scripts, { recursive: true });
|
|
747
|
+
fs.writeFileSync(path.join(scripts, 'mcp-launcher.js'), '// stub');
|
|
748
|
+
const ts = (i + 1) * 3600; // distinct, increasing mtimes
|
|
749
|
+
fs.utimesSync(path.join(pluginDir, v), ts, ts);
|
|
750
|
+
});
|
|
751
|
+
// A live MCP server is running from the OLDEST version (beyond keep=5) — this
|
|
752
|
+
// is the v0.80.2 reconnect-(-32000) scenario.
|
|
753
|
+
const inUse = path.join(pluginDir, '0.78.0');
|
|
754
|
+
const fakeCmdlines = [`node ${path.join(inUse, 'scripts', 'mcp-launcher.js')} `];
|
|
755
|
+
|
|
756
|
+
cleanupOldCacheVersions(5, () => fakeCmdlines, cacheParent);
|
|
757
|
+
|
|
758
|
+
assert.equal(fs.existsSync(inUse), true,
|
|
759
|
+
'in-use version must survive prune even when it is the oldest');
|
|
760
|
+
assert.equal(fs.existsSync(path.join(pluginDir, '0.80.2')), false,
|
|
761
|
+
'a non-in-use version beyond the keep window is still pruned');
|
|
762
|
+
assert.equal(fs.existsSync(path.join(pluginDir, '0.81.3')), true,
|
|
763
|
+
'newest version (within keep window) is kept');
|
|
764
|
+
});
|
|
765
|
+
|
|
766
|
+
test('cleanupOldCacheVersions prunes beyond keep when nothing is in use', (t) => {
|
|
767
|
+
const { cleanupOldCacheVersions } = require('./lifecycle.js');
|
|
768
|
+
const cacheParent = fs.mkdtempSync(path.join(os.tmpdir(), 'code-graph-cache-'));
|
|
769
|
+
t.after(() => fs.rmSync(cacheParent, { recursive: true, force: true }));
|
|
770
|
+
const pluginDir = path.join(cacheParent, 'code-graph-mcp');
|
|
771
|
+
const vers = ['0.78.0', '0.80.2', '0.80.3', '0.81.0', '0.81.1', '0.81.2', '0.81.3'];
|
|
772
|
+
vers.forEach((v, i) => {
|
|
773
|
+
fs.mkdirSync(path.join(pluginDir, v), { recursive: true });
|
|
774
|
+
const ts = (i + 1) * 3600;
|
|
775
|
+
fs.utimesSync(path.join(pluginDir, v), ts, ts);
|
|
776
|
+
});
|
|
777
|
+
// No live process references any version → recency-only pruning (pre-guard).
|
|
778
|
+
cleanupOldCacheVersions(5, () => [], cacheParent);
|
|
779
|
+
|
|
780
|
+
assert.equal(fs.existsSync(path.join(pluginDir, '0.78.0')), false, 'oldest pruned');
|
|
781
|
+
assert.equal(fs.existsSync(path.join(pluginDir, '0.80.2')), false, '2nd-oldest pruned');
|
|
782
|
+
assert.equal(fs.existsSync(path.join(pluginDir, '0.80.3')), true, 'within keep window kept');
|
|
783
|
+
assert.equal(
|
|
784
|
+
fs.readdirSync(pluginDir).filter(n => fs.statSync(path.join(pluginDir, n)).isDirectory()).length,
|
|
785
|
+
5, 'exactly keep=5 versions remain');
|
|
786
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sdsrs/code-graph",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.82.1",
|
|
4
4
|
"description": "MCP server that indexes codebases into an AST knowledge graph with semantic search, call graph traversal, and HTTP route tracing",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -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.82.1",
|
|
39
|
+
"@sdsrs/code-graph-linux-arm64": "0.82.1",
|
|
40
|
+
"@sdsrs/code-graph-darwin-x64": "0.82.1",
|
|
41
|
+
"@sdsrs/code-graph-darwin-arm64": "0.82.1",
|
|
42
|
+
"@sdsrs/code-graph-win32-x64": "0.82.1"
|
|
43
43
|
}
|
|
44
44
|
}
|