@sdsrs/code-graph 0.5.27 → 0.5.29
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/README.md +1 -1
- package/bin/cli.js +8 -47
- package/claude-plugin/.claude-plugin/plugin.json +1 -1
- package/claude-plugin/hooks/hooks.json +2 -2
- package/claude-plugin/scripts/auto-update.js +167 -51
- package/claude-plugin/scripts/auto-update.test.js +97 -0
- package/claude-plugin/scripts/find-binary.js +92 -29
- package/claude-plugin/scripts/lifecycle.e2e.test.js +91 -0
- package/claude-plugin/scripts/lifecycle.js +69 -16
- package/claude-plugin/scripts/lifecycle.test.js +97 -0
- package/claude-plugin/scripts/session-init.js +55 -159
- package/claude-plugin/scripts/session-init.test.js +35 -0
- package/claude-plugin/scripts/statusline-composite.js +13 -2
- package/claude-plugin/scripts/statusline.js +24 -2
- package/package.json +6 -6
|
@@ -1,180 +1,76 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
'use strict';
|
|
3
|
-
const {
|
|
4
|
-
const fs = require('fs');
|
|
3
|
+
const { spawn } = require('child_process');
|
|
5
4
|
const path = require('path');
|
|
6
5
|
const os = require('os');
|
|
7
|
-
const {
|
|
8
|
-
|
|
9
|
-
|
|
6
|
+
const {
|
|
7
|
+
install, update, readManifest, getPluginVersion, checkScopeConflict,
|
|
8
|
+
cleanupDisabledStatusline, isPluginInactive, readJson,
|
|
9
|
+
} = require('./lifecycle');
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
// --- 0b. Retry pending binary update from previous failed auto-update ---
|
|
14
|
-
{
|
|
15
|
-
const updateState = readUpdateState();
|
|
16
|
-
if (updateState.pendingBinaryUpdate) {
|
|
17
|
-
const pendingVer = updateState.pendingBinaryUpdate;
|
|
18
|
-
try {
|
|
19
|
-
execFileSync('npm', ['install', '-g', `@sdsrs/code-graph@${pendingVer}`], {
|
|
20
|
-
timeout: 30000, stdio: 'pipe'
|
|
21
|
-
});
|
|
22
|
-
try { fs.unlinkSync(path.join(os.homedir(), '.cache', 'code-graph', 'binary-path')); } catch {}
|
|
23
|
-
// Clear pending flag
|
|
24
|
-
const { writeJsonAtomic, CACHE_DIR } = require('./lifecycle');
|
|
25
|
-
const s = readUpdateState();
|
|
26
|
-
delete s.pendingBinaryUpdate;
|
|
27
|
-
writeJsonAtomic(path.join(CACHE_DIR, 'update-state.json'), s);
|
|
28
|
-
process.stderr.write(`[code-graph] Binary retry succeeded: v${pendingVer}\n`);
|
|
29
|
-
BIN = findBinary(); // refresh
|
|
30
|
-
} catch { /* npm still not available — will retry next session */ }
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
// --- 0. Auto-install binary if missing ---
|
|
35
|
-
if (!BIN) {
|
|
36
|
-
const version = getPluginVersion();
|
|
37
|
-
process.stderr.write(`[code-graph] Binary not found, installing @sdsrs/code-graph@${version}...\n`);
|
|
11
|
+
function launchBackgroundAutoUpdate(spawnFn = spawn, env = process.env) {
|
|
38
12
|
try {
|
|
39
|
-
|
|
40
|
-
|
|
13
|
+
const child = spawnFn(process.execPath, [path.join(__dirname, 'auto-update.js'), 'check', '--silent'], {
|
|
14
|
+
detached: true,
|
|
15
|
+
stdio: 'ignore',
|
|
16
|
+
env: { ...env, CODE_GRAPH_AUTO_UPDATE_SILENT: '1' },
|
|
41
17
|
});
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
BIN = findBinary();
|
|
45
|
-
if (BIN) {
|
|
46
|
-
process.stderr.write(`[code-graph] Installed v${version} at ${BIN}\n`);
|
|
47
|
-
} else {
|
|
48
|
-
process.stderr.write('[code-graph] Install succeeded but binary not found in PATH. Try: npx @sdsrs/code-graph@latest\n');
|
|
49
|
-
}
|
|
18
|
+
if (child && typeof child.unref === 'function') child.unref();
|
|
19
|
+
return true;
|
|
50
20
|
} catch {
|
|
51
|
-
|
|
52
|
-
`[code-graph] Auto-install failed. Run manually: npm install -g @sdsrs/code-graph@${version}\n`
|
|
53
|
-
);
|
|
21
|
+
return false;
|
|
54
22
|
}
|
|
55
23
|
}
|
|
56
24
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
try {
|
|
61
|
-
const out = execFileSync(BIN, ['health-check', '--format', 'oneline'], {
|
|
62
|
-
timeout: 2000,
|
|
63
|
-
stdio: ['pipe', 'pipe', 'pipe']
|
|
64
|
-
}).toString().trim();
|
|
65
|
-
if (out) process.stdout.write(out);
|
|
66
|
-
// Parse node count for empty-index detection
|
|
67
|
-
const m = out.match(/(\d+)\s*nodes/);
|
|
68
|
-
if (m) healthNodes = parseInt(m[1], 10);
|
|
69
|
-
} catch { /* timeout — silent */ }
|
|
70
|
-
}
|
|
25
|
+
function syncLifecycleConfig() {
|
|
26
|
+
const manifest = readManifest();
|
|
27
|
+
const currentVersion = getPluginVersion();
|
|
71
28
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
process.stderr.write('[code-graph] Empty index detected, running initial indexing...\n');
|
|
80
|
-
const result = execFileSync(BIN, ['incremental-index', '--quiet'], {
|
|
81
|
-
timeout: 15000, // 15s max (SessionStart hook has 20s budget)
|
|
82
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
83
|
-
}).toString().trim();
|
|
84
|
-
if (result) process.stderr.write(`[code-graph] ${result}\n`);
|
|
85
|
-
// Re-run health check to update statusline with new counts
|
|
86
|
-
try {
|
|
87
|
-
const out2 = execFileSync(BIN, ['health-check', '--format', 'oneline'], {
|
|
88
|
-
timeout: 2000, stdio: ['pipe', 'pipe', 'pipe']
|
|
89
|
-
}).toString().trim();
|
|
90
|
-
if (out2) process.stdout.write(`\n${out2}`);
|
|
91
|
-
} catch { /* ok */ }
|
|
92
|
-
} catch (e) {
|
|
93
|
-
process.stderr.write(`[code-graph] Auto-index failed: ${e.message || e}\n`);
|
|
94
|
-
}
|
|
29
|
+
if (!manifest.version) {
|
|
30
|
+
install();
|
|
31
|
+
return 'installed';
|
|
32
|
+
}
|
|
33
|
+
if (manifest.version !== currentVersion) {
|
|
34
|
+
update();
|
|
35
|
+
return 'updated';
|
|
95
36
|
}
|
|
37
|
+
// Self-heal: version matches but statusLine may have been lost
|
|
38
|
+
// (e.g. plugin removed and reinstalled without lifecycle uninstall).
|
|
39
|
+
// install() is idempotent — isOurComposite guard prevents duplicate work.
|
|
40
|
+
const settings = readJson(path.join(os.homedir(), '.claude', 'settings.json')) || {};
|
|
41
|
+
if (!settings.statusLine || !settings.statusLine.command ||
|
|
42
|
+
!settings.statusLine.command.includes('statusline-composite')) {
|
|
43
|
+
install();
|
|
44
|
+
return 'self-healed';
|
|
45
|
+
}
|
|
46
|
+
return 'noop';
|
|
96
47
|
}
|
|
97
48
|
|
|
98
|
-
|
|
99
|
-
if (
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
);
|
|
104
|
-
}
|
|
49
|
+
function runSessionInit() {
|
|
50
|
+
if (isPluginInactive()) {
|
|
51
|
+
cleanupDisabledStatusline();
|
|
52
|
+
return { inactive: true, lifecycle: 'noop', autoUpdateLaunched: false };
|
|
53
|
+
}
|
|
105
54
|
|
|
106
|
-
|
|
107
|
-
if (
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
const bv = binVersion.split('.').map(Number);
|
|
114
|
-
const pv = pluginVersion.split('.').map(Number);
|
|
115
|
-
const pluginNewer = (pv[0] > bv[0]) ||
|
|
116
|
-
(pv[0] === bv[0] && pv[1] > bv[1]) ||
|
|
117
|
-
(pv[0] === bv[0] && pv[1] === bv[1] && pv[2] > bv[2]);
|
|
118
|
-
if (pluginNewer) {
|
|
119
|
-
process.stderr.write(`[code-graph] Binary v${binVersion} < plugin v${pluginVersion}, updating...\n`);
|
|
120
|
-
let binarySynced = false;
|
|
121
|
-
try {
|
|
122
|
-
execFileSync('npm', ['install', '-g', `@sdsrs/code-graph@${pluginVersion}`], {
|
|
123
|
-
timeout: 30000, stdio: 'pipe'
|
|
124
|
-
});
|
|
125
|
-
// Clear cached binary path so next lookup finds the new binary
|
|
126
|
-
try { fs.unlinkSync(path.join(os.homedir(), '.cache', 'code-graph', 'binary-path')); } catch {}
|
|
127
|
-
process.stderr.write(`[code-graph] Binary updated to v${pluginVersion}\n`);
|
|
128
|
-
binarySynced = true;
|
|
129
|
-
} catch {
|
|
130
|
-
process.stderr.write(
|
|
131
|
-
`[code-graph] Auto-update failed. Run: npm install -g @sdsrs/code-graph@${pluginVersion}\n`
|
|
132
|
-
);
|
|
133
|
-
}
|
|
134
|
-
if (binarySynced) {
|
|
135
|
-
// MCP server is still running old binary — prompt user to reconnect
|
|
136
|
-
process.stdout.write(
|
|
137
|
-
`\n\u26A0\uFE0F [code-graph] Binary updated v${binVersion} \u2192 v${pluginVersion}. ` +
|
|
138
|
-
`Run /mcp to reconnect MCP server with new version.\n`
|
|
139
|
-
);
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
} catch { /* version check failed — not critical */ }
|
|
144
|
-
}
|
|
55
|
+
const conflict = checkScopeConflict();
|
|
56
|
+
if (conflict) {
|
|
57
|
+
process.stderr.write(
|
|
58
|
+
`[code-graph] Warning: conflicting install detected — ${conflict.existingId} (${conflict.scope || 'unknown'} scope). ` +
|
|
59
|
+
`Use /plugin to remove one to avoid config conflicts.\n`
|
|
60
|
+
);
|
|
61
|
+
}
|
|
145
62
|
|
|
146
|
-
|
|
147
|
-
const
|
|
148
|
-
|
|
149
|
-
process.stderr.write(
|
|
150
|
-
`[code-graph] Warning: conflicting install detected — ${conflict.existingId} (${conflict.scope || 'unknown'} scope). ` +
|
|
151
|
-
`Use /plugin to remove one to avoid config conflicts.\n`
|
|
152
|
-
);
|
|
63
|
+
const lifecycle = syncLifecycleConfig();
|
|
64
|
+
const autoUpdateLaunched = launchBackgroundAutoUpdate();
|
|
65
|
+
return { inactive: false, lifecycle, autoUpdateLaunched };
|
|
153
66
|
}
|
|
154
67
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
68
|
+
module.exports = {
|
|
69
|
+
launchBackgroundAutoUpdate,
|
|
70
|
+
syncLifecycleConfig,
|
|
71
|
+
runSessionInit,
|
|
72
|
+
};
|
|
158
73
|
|
|
159
|
-
if (
|
|
160
|
-
|
|
161
|
-
} else if (manifest.version !== currentVersion) {
|
|
162
|
-
update();
|
|
74
|
+
if (require.main === module) {
|
|
75
|
+
runSessionInit();
|
|
163
76
|
}
|
|
164
|
-
|
|
165
|
-
// --- 4. Auto-update (throttled, non-blocking) ---
|
|
166
|
-
(async () => {
|
|
167
|
-
const result = await checkForUpdate();
|
|
168
|
-
if (result && result.updated) {
|
|
169
|
-
process.stderr.write(`[code-graph] Updated: v${result.from} \u2192 v${result.to}\n`);
|
|
170
|
-
process.stdout.write(
|
|
171
|
-
`\n\uD83D\uDD04 [code-graph] Auto-updated v${result.from} \u2192 v${result.to}. ` +
|
|
172
|
-
`Run /mcp to use the new version.\n`
|
|
173
|
-
);
|
|
174
|
-
} else if (result && result.updateAvailable) {
|
|
175
|
-
process.stderr.write(
|
|
176
|
-
`[code-graph] Update available: v${result.from} \u2192 v${result.to}. ` +
|
|
177
|
-
`Run: npx @sdsrs/code-graph@latest\n`
|
|
178
|
-
);
|
|
179
|
-
}
|
|
180
|
-
})();
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const test = require('node:test');
|
|
3
|
+
const assert = require('node:assert/strict');
|
|
4
|
+
|
|
5
|
+
const { launchBackgroundAutoUpdate, syncLifecycleConfig } = require('./session-init');
|
|
6
|
+
|
|
7
|
+
test('syncLifecycleConfig is exported as a callable helper', () => {
|
|
8
|
+
assert.equal(typeof syncLifecycleConfig, 'function');
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
test('launchBackgroundAutoUpdate spawns detached silent updater', () => {
|
|
12
|
+
const calls = [];
|
|
13
|
+
|
|
14
|
+
const ok = launchBackgroundAutoUpdate((command, args, options) => {
|
|
15
|
+
const record = { command, args, options, unrefCalled: false };
|
|
16
|
+
calls.push(record);
|
|
17
|
+
return {
|
|
18
|
+
unref() {
|
|
19
|
+
record.unrefCalled = true;
|
|
20
|
+
},
|
|
21
|
+
};
|
|
22
|
+
}, { HOME: '/tmp/fake-home' });
|
|
23
|
+
|
|
24
|
+
assert.equal(ok, true);
|
|
25
|
+
assert.equal(calls.length, 1);
|
|
26
|
+
assert.equal(calls[0].command, process.execPath);
|
|
27
|
+
assert.match(calls[0].args[0], /auto-update\.js$/);
|
|
28
|
+
assert.equal(calls[0].args[1], 'check');
|
|
29
|
+
assert.equal(calls[0].args[2], '--silent');
|
|
30
|
+
assert.equal(calls[0].options.detached, true);
|
|
31
|
+
assert.equal(calls[0].options.stdio, 'ignore');
|
|
32
|
+
assert.equal(calls[0].options.env.CODE_GRAPH_AUTO_UPDATE_SILENT, '1');
|
|
33
|
+
assert.equal(calls[0].unrefCalled, true);
|
|
34
|
+
});
|
|
35
|
+
|
|
@@ -7,10 +7,13 @@
|
|
|
7
7
|
*/
|
|
8
8
|
const { execFileSync } = require('child_process');
|
|
9
9
|
const path = require('path');
|
|
10
|
-
const { readRegistry } = require('./lifecycle');
|
|
10
|
+
const { cleanupDisabledStatusline, readRegistry } = require('./lifecycle');
|
|
11
11
|
|
|
12
12
|
const SEPARATOR = ' \x1b[2m|\x1b[0m ';
|
|
13
13
|
|
|
14
|
+
const disabledCleanup = cleanupDisabledStatusline();
|
|
15
|
+
if (disabledCleanup.cleaned) process.exit(0);
|
|
16
|
+
|
|
14
17
|
// Collect stdin (Claude Code pipes JSON context)
|
|
15
18
|
let stdinData = '';
|
|
16
19
|
let ran = false;
|
|
@@ -28,8 +31,16 @@ function run(stdin) {
|
|
|
28
31
|
return;
|
|
29
32
|
}
|
|
30
33
|
|
|
34
|
+
// Display order: pre-existing statuslines (_previous) first, then our providers.
|
|
35
|
+
// This ensures plugins installed earlier appear before ours.
|
|
36
|
+
const sorted = registry.slice().sort((a, b) => {
|
|
37
|
+
if (a.id === '_previous') return -1;
|
|
38
|
+
if (b.id === '_previous') return 1;
|
|
39
|
+
return 0;
|
|
40
|
+
});
|
|
41
|
+
|
|
31
42
|
const outputs = [];
|
|
32
|
-
for (const provider of
|
|
43
|
+
for (const provider of sorted) {
|
|
33
44
|
const out = runProvider(provider.command, provider.needsStdin, stdin);
|
|
34
45
|
if (out) outputs.push(out);
|
|
35
46
|
}
|
|
@@ -4,12 +4,34 @@ const { execFileSync } = require('child_process');
|
|
|
4
4
|
const fs = require('fs');
|
|
5
5
|
const path = require('path');
|
|
6
6
|
const { findBinary } = require('./find-binary');
|
|
7
|
+
const { cleanupDisabledStatusline } = require('./lifecycle');
|
|
7
8
|
|
|
8
|
-
|
|
9
|
+
const disabledCleanup = cleanupDisabledStatusline();
|
|
10
|
+
if (disabledCleanup.cleaned) process.exit(0);
|
|
11
|
+
|
|
12
|
+
// Only show status in projects that have a code-graph directory.
|
|
9
13
|
// The statusLine config is global, so we must exit silently for
|
|
10
14
|
// directories that aren't code-graph projects.
|
|
11
15
|
const cwd = process.cwd();
|
|
12
|
-
|
|
16
|
+
const codeGraphDir = path.join(cwd, '.code-graph');
|
|
17
|
+
if (!fs.existsSync(codeGraphDir)) {
|
|
18
|
+
process.exit(0);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Check for background indexing progress file first
|
|
22
|
+
const progressFile = path.join(codeGraphDir, 'indexing-status.json');
|
|
23
|
+
try {
|
|
24
|
+
const raw = fs.readFileSync(progressFile, 'utf8');
|
|
25
|
+
const p = JSON.parse(raw);
|
|
26
|
+
if (p.s === 'indexing' && p.t > 0) {
|
|
27
|
+
const pct = Math.round((p.d / p.t) * 100);
|
|
28
|
+
process.stdout.write(`code-graph: \u21BB indexing ${p.d}/${p.t} (${pct}%)`);
|
|
29
|
+
process.exit(0);
|
|
30
|
+
}
|
|
31
|
+
} catch { /* no progress file or parse error — continue to health check */ }
|
|
32
|
+
|
|
33
|
+
// No indexing in progress — show normal health status
|
|
34
|
+
if (!fs.existsSync(path.join(codeGraphDir, 'index.db'))) {
|
|
13
35
|
process.exit(0);
|
|
14
36
|
}
|
|
15
37
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sdsrs/code-graph",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.29",
|
|
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": {
|
|
@@ -33,10 +33,10 @@
|
|
|
33
33
|
"node": ">=16"
|
|
34
34
|
},
|
|
35
35
|
"optionalDependencies": {
|
|
36
|
-
"@sdsrs/code-graph-linux-x64": "0.5.
|
|
37
|
-
"@sdsrs/code-graph-linux-arm64": "0.5.
|
|
38
|
-
"@sdsrs/code-graph-darwin-x64": "0.5.
|
|
39
|
-
"@sdsrs/code-graph-darwin-arm64": "0.5.
|
|
40
|
-
"@sdsrs/code-graph-win32-x64": "0.5.
|
|
36
|
+
"@sdsrs/code-graph-linux-x64": "0.5.29",
|
|
37
|
+
"@sdsrs/code-graph-linux-arm64": "0.5.29",
|
|
38
|
+
"@sdsrs/code-graph-darwin-x64": "0.5.29",
|
|
39
|
+
"@sdsrs/code-graph-darwin-arm64": "0.5.29",
|
|
40
|
+
"@sdsrs/code-graph-win32-x64": "0.5.29"
|
|
41
41
|
}
|
|
42
42
|
}
|