@sdsrs/code-graph 0.17.3 → 0.18.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/auto-update.js +15 -7
- package/claude-plugin/scripts/auto-update.test.js +17 -0
- package/claude-plugin/scripts/mcp-launcher.js +46 -19
- package/claude-plugin/scripts/version-utils.js +15 -6
- package/claude-plugin/scripts/version-utils.test.js +59 -3
- package/package.json +6 -6
|
@@ -38,6 +38,10 @@ function isSilentMode(argv = process.argv.slice(2), env = process.env) {
|
|
|
38
38
|
return argv.includes('--silent') || env.CODE_GRAPH_AUTO_UPDATE_SILENT === '1';
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
+
function isInstallMissingMode(argv = process.argv.slice(2)) {
|
|
42
|
+
return argv.includes('--install-missing');
|
|
43
|
+
}
|
|
44
|
+
|
|
41
45
|
// ── Platform → GitHub release asset name mapping ──────────
|
|
42
46
|
function getPlatformAssetName() {
|
|
43
47
|
const platform = os.platform();
|
|
@@ -327,10 +331,12 @@ async function downloadAndInstall(latest) {
|
|
|
327
331
|
|
|
328
332
|
// ── Main Entry ─────────────────────────────────────────────
|
|
329
333
|
|
|
330
|
-
async function checkForUpdate() {
|
|
334
|
+
async function checkForUpdate({ installMissing = false } = {}) {
|
|
331
335
|
try {
|
|
332
|
-
// Skip in dev mode
|
|
333
|
-
|
|
336
|
+
// Skip in dev mode — unless the launcher explicitly requested a missing-
|
|
337
|
+
// binary install, in which case we MUST proceed regardless of mode (the
|
|
338
|
+
// alternative is wedging the MCP server with no binary on disk).
|
|
339
|
+
if (!installMissing && isDevMode()) return null;
|
|
334
340
|
|
|
335
341
|
const state = readState();
|
|
336
342
|
// manifest.version is authoritative — /plugin update writes it directly and
|
|
@@ -414,23 +420,25 @@ async function checkForUpdate() {
|
|
|
414
420
|
|
|
415
421
|
module.exports = {
|
|
416
422
|
checkForUpdate, commandExists, isDevMode, readState, compareVersions,
|
|
417
|
-
getExtractedPluginVersion, readBinaryVersion, promoteVerifiedBinary,
|
|
423
|
+
getExtractedPluginVersion, readBinaryVersion, promoteVerifiedBinary,
|
|
424
|
+
isSilentMode, isInstallMissingMode,
|
|
418
425
|
requestJson, parseLatestRelease, fetchLatestRelease,
|
|
419
426
|
downloadBinary, cachedBinaryPath,
|
|
420
427
|
};
|
|
421
428
|
|
|
422
|
-
// CLI: node auto-update.js [check|status]
|
|
429
|
+
// CLI: node auto-update.js [check|status] [--silent] [--install-missing]
|
|
423
430
|
if (require.main === module) {
|
|
424
431
|
(async () => {
|
|
425
432
|
const argv = process.argv.slice(2);
|
|
426
433
|
const cmd = argv.find(arg => !arg.startsWith('--')) || 'check';
|
|
427
434
|
const silent = isSilentMode(argv);
|
|
435
|
+
const installMissing = isInstallMissingMode(argv);
|
|
428
436
|
if (cmd === 'status') {
|
|
429
437
|
const state = readState();
|
|
430
438
|
console.log(JSON.stringify(state, null, 2));
|
|
431
439
|
} else {
|
|
432
440
|
if (!silent) console.log('Checking for updates...');
|
|
433
|
-
const result = await checkForUpdate();
|
|
441
|
+
const result = await checkForUpdate({ installMissing });
|
|
434
442
|
if (silent) return;
|
|
435
443
|
if (result && result.updated) {
|
|
436
444
|
console.log(`Updated: v${result.from} → v${result.to} (binary: ${result.binaryUpdated ? 'yes' : 'no'})`);
|
|
@@ -438,7 +446,7 @@ if (require.main === module) {
|
|
|
438
446
|
console.log(`Update available: v${result.to} (auto-install failed)`);
|
|
439
447
|
} else if (result && result.binaryUpdated) {
|
|
440
448
|
console.log(`Repaired binary cache (v${result.to})`);
|
|
441
|
-
} else if (isDevMode()) {
|
|
449
|
+
} else if (!installMissing && isDevMode()) {
|
|
442
450
|
console.log('Dev mode — auto-update skipped');
|
|
443
451
|
} else {
|
|
444
452
|
const manifest = readManifest();
|
|
@@ -14,6 +14,8 @@ const {
|
|
|
14
14
|
promoteVerifiedBinary,
|
|
15
15
|
cachedBinaryPath,
|
|
16
16
|
downloadBinary,
|
|
17
|
+
isInstallMissingMode,
|
|
18
|
+
isSilentMode,
|
|
17
19
|
} = require('./auto-update');
|
|
18
20
|
|
|
19
21
|
function mkDir(t, prefix) {
|
|
@@ -113,6 +115,21 @@ test('downloadBinary returns false when latest is null', async () => {
|
|
|
113
115
|
assert.equal(result, false);
|
|
114
116
|
});
|
|
115
117
|
|
|
118
|
+
// ── Flag parsing ───────────────────────────────────────────
|
|
119
|
+
|
|
120
|
+
test('isInstallMissingMode detects --install-missing in argv', () => {
|
|
121
|
+
assert.equal(isInstallMissingMode(['--install-missing']), true);
|
|
122
|
+
assert.equal(isInstallMissingMode(['check', '--install-missing']), true);
|
|
123
|
+
assert.equal(isInstallMissingMode(['check']), false);
|
|
124
|
+
assert.equal(isInstallMissingMode([]), false);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
test('isSilentMode honors --silent flag and CODE_GRAPH_AUTO_UPDATE_SILENT env', () => {
|
|
128
|
+
assert.equal(isSilentMode(['--silent'], {}), true);
|
|
129
|
+
assert.equal(isSilentMode([], { CODE_GRAPH_AUTO_UPDATE_SILENT: '1' }), true);
|
|
130
|
+
assert.equal(isSilentMode([], {}), false);
|
|
131
|
+
});
|
|
132
|
+
|
|
116
133
|
test('fetchLatestRelease parses JSON without relying on global fetch', async () => {
|
|
117
134
|
const latest = await fetchLatestRelease(async () => ({
|
|
118
135
|
statusCode: 200,
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* Used by .mcp.json so the plugin controls binary discovery instead of
|
|
8
8
|
* relying on the binary being in PATH.
|
|
9
9
|
*/
|
|
10
|
-
const { spawn,
|
|
10
|
+
const { spawn, spawnSync } = require('child_process');
|
|
11
11
|
const path = require('path');
|
|
12
12
|
const fs = require('fs');
|
|
13
13
|
|
|
@@ -28,42 +28,69 @@ if (!binary) {
|
|
|
28
28
|
} catch { /* use latest */ }
|
|
29
29
|
|
|
30
30
|
process.stderr.write(`[code-graph] Binary not found, installing @sdsrs/code-graph@${version}...\n`);
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
31
|
+
const npmResult = spawnSync('npm', ['install', '-g', `@sdsrs/code-graph@${version}`], {
|
|
32
|
+
timeout: 60000, stdio: ['ignore', 'pipe', 'pipe'], encoding: 'utf8',
|
|
33
|
+
});
|
|
34
|
+
if (npmResult.error || npmResult.status !== 0) {
|
|
35
|
+
process.stderr.write('[code-graph] npm install failed.\n');
|
|
36
|
+
if (npmResult.stderr) {
|
|
37
|
+
process.stderr.write(npmResult.stderr.trim().split('\n').map(l => `[code-graph][npm] ${l}\n`).join(''));
|
|
38
|
+
}
|
|
39
|
+
} else {
|
|
35
40
|
clearCache();
|
|
36
41
|
binary = findBinary();
|
|
37
42
|
if (binary) {
|
|
38
43
|
process.stderr.write(`[code-graph] Installed at ${binary}\n`);
|
|
39
44
|
}
|
|
40
|
-
} catch {
|
|
41
|
-
process.stderr.write('[code-graph] npm install failed.\n');
|
|
42
45
|
}
|
|
43
46
|
}
|
|
44
47
|
|
|
45
48
|
// Fallback: npm install may have succeeded but optionalDependencies for the
|
|
46
49
|
// platform binary can fail silently (npm tolerates OS-mismatch + flaky
|
|
47
50
|
// registry). Pull the platform binary directly from the GitHub release.
|
|
51
|
+
//
|
|
52
|
+
// --install-missing bypasses auto-update.js's isDevMode() short-circuit. The
|
|
53
|
+
// marketplace ships the full repo (including Cargo.toml at the workspace root),
|
|
54
|
+
// so dev-mode heuristics that look for Cargo.toml were misclassifying every
|
|
55
|
+
// marketplace install as dev mode and skipping this fallback (issue #12).
|
|
48
56
|
if (!binary) {
|
|
49
57
|
process.stderr.write('[code-graph] Falling back to GitHub release download...\n');
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
58
|
+
const result = spawnSync(
|
|
59
|
+
process.execPath,
|
|
60
|
+
[path.join(__dirname, 'auto-update.js'), '--silent', '--install-missing'],
|
|
61
|
+
{ timeout: 90000, stdio: ['ignore', 'pipe', 'pipe'], encoding: 'utf8' }
|
|
62
|
+
);
|
|
63
|
+
if (result.stderr && result.stderr.trim()) {
|
|
64
|
+
process.stderr.write(result.stderr.trim().split('\n').map(l => `[code-graph][auto-update] ${l}\n`).join(''));
|
|
65
|
+
}
|
|
66
|
+
if (result.error) {
|
|
67
|
+
process.stderr.write(`[code-graph] auto-update spawn failed: ${result.error.message}\n`);
|
|
68
|
+
} else if (result.status !== 0) {
|
|
69
|
+
process.stderr.write(`[code-graph] auto-update exited with status ${result.status}\n`);
|
|
70
|
+
}
|
|
71
|
+
clearCache();
|
|
72
|
+
binary = findBinary();
|
|
73
|
+
if (binary) {
|
|
74
|
+
process.stderr.write(`[code-graph] Installed at ${binary}\n`);
|
|
75
|
+
}
|
|
60
76
|
}
|
|
61
77
|
|
|
62
78
|
if (!binary) {
|
|
79
|
+
const installedViaMarketplace = fs.existsSync(
|
|
80
|
+
path.join(__dirname, '..', '.claude-plugin', 'plugin.json')
|
|
81
|
+
);
|
|
82
|
+
process.stderr.write('[code-graph] Binary not found. Install manually:\n');
|
|
83
|
+
if (installedViaMarketplace) {
|
|
84
|
+
process.stderr.write(
|
|
85
|
+
' # Re-install the plugin via Claude Code marketplace:\n' +
|
|
86
|
+
' /plugin uninstall code-graph-mcp && /plugin install code-graph-mcp@code-graph-mcp\n' +
|
|
87
|
+
' # Or install the binary directly via npm:\n'
|
|
88
|
+
);
|
|
89
|
+
}
|
|
63
90
|
process.stderr.write(
|
|
64
|
-
'
|
|
91
|
+
' npm install -g @sdsrs/code-graph @sdsrs/code-graph-' + process.platform + '-' + process.arch + '\n' +
|
|
92
|
+
' # or, equivalent split form:\n' +
|
|
65
93
|
' npm install -g @sdsrs/code-graph\n' +
|
|
66
|
-
' # or\n' +
|
|
67
94
|
' npm install -g @sdsrs/code-graph-' + process.platform + '-' + process.arch + '\n'
|
|
68
95
|
);
|
|
69
96
|
process.exit(1);
|
|
@@ -18,13 +18,22 @@ function readBinaryVersion(binaryPath) {
|
|
|
18
18
|
}
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
function isDevMode() {
|
|
22
|
-
//
|
|
23
|
-
|
|
24
|
-
//
|
|
25
|
-
if (fs.existsSync(path.join(pluginRoot, '..', 'Cargo.toml'))) return true;
|
|
26
|
-
// Dev mode: plugin root is a symlink
|
|
21
|
+
function isDevMode(pluginRoot = path.resolve(__dirname, '..')) {
|
|
22
|
+
// Explicit opt-in always wins (also lets users force dev mode in any layout)
|
|
23
|
+
if (process.env.CODE_GRAPH_DEV === '1') return true;
|
|
24
|
+
// Plugin root is a symlink (e.g. `npm link`)
|
|
27
25
|
try { if (fs.lstatSync(pluginRoot).isSymbolicLink()) return true; } catch { /* ok */ }
|
|
26
|
+
// Source repo: Cargo.toml AND target/ at parent. Marketplace installs ship
|
|
27
|
+
// Cargo.toml (git-tracked) but NOT target/ (gitignored), so target/ is the
|
|
28
|
+
// discriminator — without it, a marketplace clone was being misclassified as
|
|
29
|
+
// dev mode and the launcher's GitHub-release fallback was unreachable
|
|
30
|
+
// (see GitHub issue #12). If a dev hasn't built yet, they fall through to
|
|
31
|
+
// the user-mode auto-install path, which still produces a working binary.
|
|
32
|
+
const parent = path.dirname(pluginRoot);
|
|
33
|
+
if (fs.existsSync(path.join(parent, 'Cargo.toml')) &&
|
|
34
|
+
fs.existsSync(path.join(parent, 'target'))) {
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
28
37
|
return false;
|
|
29
38
|
}
|
|
30
39
|
|
|
@@ -45,10 +45,66 @@ test('readBinaryVersion returns null for binary with unexpected output', (t) =>
|
|
|
45
45
|
|
|
46
46
|
// ── isDevMode ──
|
|
47
47
|
|
|
48
|
-
|
|
48
|
+
function makeFakePluginRoot(t, { withCargo = false, withTarget = false, asSymlink = false } = {}) {
|
|
49
|
+
const parent = mkDir(t, 'vu-dev-');
|
|
50
|
+
const pluginRoot = path.join(parent, 'claude-plugin');
|
|
51
|
+
fs.mkdirSync(pluginRoot, { recursive: true });
|
|
52
|
+
if (withCargo) fs.writeFileSync(path.join(parent, 'Cargo.toml'), '[package]\nname = "x"\n');
|
|
53
|
+
if (withTarget) fs.mkdirSync(path.join(parent, 'target'), { recursive: true });
|
|
54
|
+
|
|
55
|
+
if (asSymlink) {
|
|
56
|
+
const real = path.join(parent, 'real-plugin');
|
|
57
|
+
fs.mkdirSync(real, { recursive: true });
|
|
58
|
+
fs.rmSync(pluginRoot, { recursive: true, force: true });
|
|
59
|
+
fs.symlinkSync(real, pluginRoot);
|
|
60
|
+
}
|
|
61
|
+
return pluginRoot;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function withEnv(t, key, value) {
|
|
65
|
+
const original = process.env[key];
|
|
66
|
+
if (value === undefined) delete process.env[key]; else process.env[key] = value;
|
|
67
|
+
t.after(() => {
|
|
68
|
+
if (original === undefined) delete process.env[key]; else process.env[key] = original;
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
test('isDevMode returns true in source repo when Cargo.toml AND target/ both exist', (t) => {
|
|
73
|
+
const { isDevMode } = require('./version-utils');
|
|
74
|
+
withEnv(t, 'CODE_GRAPH_DEV', undefined);
|
|
75
|
+
const pluginRoot = makeFakePluginRoot(t, { withCargo: true, withTarget: true });
|
|
76
|
+
assert.equal(isDevMode(pluginRoot), true);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test('isDevMode returns false for marketplace clone (Cargo.toml without target/)', (t) => {
|
|
80
|
+
const { isDevMode } = require('./version-utils');
|
|
81
|
+
withEnv(t, 'CODE_GRAPH_DEV', undefined);
|
|
82
|
+
// Marketplace clones the full repo (Cargo.toml is git-tracked) but `target/`
|
|
83
|
+
// is gitignored, so a fresh marketplace install has no target/. This is the
|
|
84
|
+
// exact misclassification fixed for issue #12.
|
|
85
|
+
const pluginRoot = makeFakePluginRoot(t, { withCargo: true, withTarget: false });
|
|
86
|
+
assert.equal(isDevMode(pluginRoot), false);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test('isDevMode returns false for fully unrelated install (no Cargo.toml, no target/)', (t) => {
|
|
90
|
+
const { isDevMode } = require('./version-utils');
|
|
91
|
+
withEnv(t, 'CODE_GRAPH_DEV', undefined);
|
|
92
|
+
const pluginRoot = makeFakePluginRoot(t);
|
|
93
|
+
assert.equal(isDevMode(pluginRoot), false);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test('isDevMode honors CODE_GRAPH_DEV=1 even when neither Cargo.toml nor target/ exist', (t) => {
|
|
97
|
+
const { isDevMode } = require('./version-utils');
|
|
98
|
+
withEnv(t, 'CODE_GRAPH_DEV', '1');
|
|
99
|
+
const pluginRoot = makeFakePluginRoot(t);
|
|
100
|
+
assert.equal(isDevMode(pluginRoot), true);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test('isDevMode returns true when plugin root is a symlink', (t) => {
|
|
49
104
|
const { isDevMode } = require('./version-utils');
|
|
50
|
-
|
|
51
|
-
|
|
105
|
+
withEnv(t, 'CODE_GRAPH_DEV', undefined);
|
|
106
|
+
const pluginRoot = makeFakePluginRoot(t, { asSymlink: true });
|
|
107
|
+
assert.equal(isDevMode(pluginRoot), true);
|
|
52
108
|
});
|
|
53
109
|
|
|
54
110
|
// ── getNewestMtime ──
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sdsrs/code-graph",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.18.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.18.0",
|
|
39
|
+
"@sdsrs/code-graph-linux-arm64": "0.18.0",
|
|
40
|
+
"@sdsrs/code-graph-darwin-x64": "0.18.0",
|
|
41
|
+
"@sdsrs/code-graph-darwin-arm64": "0.18.0",
|
|
42
|
+
"@sdsrs/code-graph-win32-x64": "0.18.0"
|
|
43
43
|
}
|
|
44
44
|
}
|