@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.
@@ -1,180 +1,76 @@
1
1
  #!/usr/bin/env node
2
2
  'use strict';
3
- const { execFileSync } = require('child_process');
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 { findBinary } = require('./find-binary');
8
- const { install, update, readManifest, getPluginVersion, checkScopeConflict } = require('./lifecycle');
9
- const { checkForUpdate, readState: readUpdateState } = require('./auto-update');
6
+ const {
7
+ install, update, readManifest, getPluginVersion, checkScopeConflict,
8
+ cleanupDisabledStatusline, isPluginInactive, readJson,
9
+ } = require('./lifecycle');
10
10
 
11
- let BIN = findBinary();
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
- execFileSync('npm', ['install', '-g', `@sdsrs/code-graph@${version}`], {
40
- timeout: 60000, stdio: 'pipe'
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
- // Clear cached path so findBinary picks up the new install
43
- try { fs.unlinkSync(path.join(os.homedir(), '.cache', 'code-graph', 'binary-path')); } catch {}
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
- process.stderr.write(
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
- // --- 1. Health check (always runs) ---
58
- let healthNodes = -1;
59
- if (BIN) {
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
- // --- 1a. Auto-index empty databases (fallback if MCP hasn't triggered indexing) ---
73
- if (BIN && healthNodes === 0) {
74
- const dbExists = fs.existsSync(path.join(process.cwd(), '.code-graph', 'index.db'));
75
- if (dbExists) {
76
- // DB exists but empty — MCP server likely hasn't received notifications/initialized yet.
77
- // Trigger CLI indexing as fallback so the index is ready before first tool call.
78
- try {
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
- // --- 1b. Suggest project_map as first action ---
99
- if (BIN) {
100
- process.stdout.write(
101
- '\n[code-graph] TIP: Call project_map first to get a full architecture overview ' +
102
- '(modules, dependencies, hot functions, entry points) in one call.\n'
103
- );
104
- }
49
+ function runSessionInit() {
50
+ if (isPluginInactive()) {
51
+ cleanupDisabledStatusline();
52
+ return { inactive: true, lifecycle: 'noop', autoUpdateLaunched: false };
53
+ }
105
54
 
106
- // --- 1c. Binary version sync (plugin may update before npm binary) ---
107
- if (BIN) {
108
- try {
109
- const binOut = execFileSync(BIN, ['--version'], { timeout: 2000, stdio: 'pipe' }).toString().trim();
110
- const binVersion = binOut.replace(/^code-graph-mcp\s+/, '');
111
- const pluginVersion = getPluginVersion();
112
- if (binVersion && pluginVersion && /^\d+\.\d+\.\d+$/.test(binVersion)) {
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
- // --- 2. Scope conflict warning ---
147
- const conflict = checkScopeConflict();
148
- if (conflict) {
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
- // --- 3. Lifecycle: install or update config (idempotent) ---
156
- const manifest = readManifest();
157
- const currentVersion = getPluginVersion();
68
+ module.exports = {
69
+ launchBackgroundAutoUpdate,
70
+ syncLifecycleConfig,
71
+ runSessionInit,
72
+ };
158
73
 
159
- if (!manifest.version) {
160
- install();
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 registry) {
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
- // Only show status in projects that have a code-graph index.
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
- if (!fs.existsSync(path.join(cwd, '.code-graph', 'index.db'))) {
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.27",
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.27",
37
- "@sdsrs/code-graph-linux-arm64": "0.5.27",
38
- "@sdsrs/code-graph-darwin-x64": "0.5.27",
39
- "@sdsrs/code-graph-darwin-arm64": "0.5.27",
40
- "@sdsrs/code-graph-win32-x64": "0.5.27"
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
  }