@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
package/README.md
CHANGED
|
@@ -118,7 +118,7 @@ What you get:
|
|
|
118
118
|
- **Code Explorer Agent** — Deep code understanding expert via `code-explorer`
|
|
119
119
|
- **Auto-indexing Hook** — Incremental index on every file edit (PostToolUse)
|
|
120
120
|
- **StatusLine** — Real-time health display (nodes, files, watch status) — compatible with other plugins' StatusLine via composite multiplexer
|
|
121
|
-
- **Auto-update** — Checks for new versions every
|
|
121
|
+
- **Auto-update** — Checks for new versions every 6h, updates silently
|
|
122
122
|
|
|
123
123
|
#### Manual Update
|
|
124
124
|
|
package/bin/cli.js
CHANGED
|
@@ -1,62 +1,23 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
const {
|
|
3
|
+
const { spawn } = require("child_process");
|
|
4
4
|
const path = require("path");
|
|
5
|
-
const fs = require("fs");
|
|
6
|
-
const os = require("os");
|
|
7
5
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
function findBinary() {
|
|
13
|
-
const binaryName = getBinaryName();
|
|
14
|
-
|
|
15
|
-
// 1. Check platform-specific npm package (code-graph-<os>-<arch>)
|
|
16
|
-
const platformPkg = `@sdsrs/code-graph-${os.platform()}-${os.arch()}`;
|
|
17
|
-
try {
|
|
18
|
-
const pkgPath = require.resolve(`${platformPkg}/package.json`);
|
|
19
|
-
const pkgDir = path.dirname(pkgPath);
|
|
20
|
-
const platBinary = path.join(pkgDir, binaryName);
|
|
21
|
-
if (fs.existsSync(platBinary)) {
|
|
22
|
-
return platBinary;
|
|
23
|
-
}
|
|
24
|
-
} catch {
|
|
25
|
-
// Platform package not installed
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
// 2. Check bundled binary in the same directory
|
|
29
|
-
const bundled = path.join(__dirname, binaryName);
|
|
30
|
-
if (fs.existsSync(bundled)) {
|
|
31
|
-
return bundled;
|
|
32
|
-
}
|
|
6
|
+
// Tell find-binary.js our package root so it can locate bundled binaries
|
|
7
|
+
// and detect dev mode from bin/ → repo root (one level up)
|
|
8
|
+
process.env._FIND_BINARY_ROOT = path.resolve(__dirname, "..");
|
|
33
9
|
|
|
34
|
-
|
|
35
|
-
const cargoRelease = path.join(__dirname, "..", "target", "release", binaryName);
|
|
36
|
-
if (fs.existsSync(cargoRelease)) {
|
|
37
|
-
return cargoRelease;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
// 4. Check if available in PATH
|
|
41
|
-
try {
|
|
42
|
-
const which = os.platform() === "win32" ? "where" : "which";
|
|
43
|
-
const result = execFileSync(which, [binaryName], { encoding: "utf8" }).trim();
|
|
44
|
-
if (result) return result;
|
|
45
|
-
} catch {
|
|
46
|
-
// not in PATH
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
return null;
|
|
50
|
-
}
|
|
10
|
+
const { findBinary } = require("../claude-plugin/scripts/find-binary");
|
|
51
11
|
|
|
52
12
|
const binary = findBinary();
|
|
53
13
|
|
|
54
14
|
if (!binary) {
|
|
55
15
|
console.error(
|
|
56
16
|
"Error: code-graph-mcp binary not found.\n\n" +
|
|
17
|
+
"To install:\n" +
|
|
18
|
+
" npm install -g @sdsrs/code-graph\n\n" +
|
|
57
19
|
"To build from source:\n" +
|
|
58
|
-
" cargo build --release
|
|
59
|
-
"Or install the platform-specific binary."
|
|
20
|
+
" cargo build --release\n"
|
|
60
21
|
);
|
|
61
22
|
process.exit(1);
|
|
62
23
|
}
|
|
@@ -66,10 +66,10 @@
|
|
|
66
66
|
{
|
|
67
67
|
"type": "command",
|
|
68
68
|
"command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/session-init.js\"",
|
|
69
|
-
"timeout":
|
|
69
|
+
"timeout": 5
|
|
70
70
|
}
|
|
71
71
|
],
|
|
72
|
-
"description": "
|
|
72
|
+
"description": "StatusLine self-heal, lifecycle sync, and background update launch at session start"
|
|
73
73
|
}
|
|
74
74
|
]
|
|
75
75
|
}
|
|
@@ -2,18 +2,39 @@
|
|
|
2
2
|
'use strict';
|
|
3
3
|
const { execFileSync } = require('child_process');
|
|
4
4
|
const fs = require('fs');
|
|
5
|
+
const https = require('https');
|
|
5
6
|
const path = require('path');
|
|
6
7
|
const os = require('os');
|
|
7
8
|
const { CACHE_DIR, PLUGIN_ID, MARKETPLACE_NAME, readManifest, readJson, writeJsonAtomic } = require('./lifecycle');
|
|
9
|
+
const { clearCache: clearBinaryCache } = require('./find-binary');
|
|
8
10
|
|
|
9
11
|
// ── Configuration ──────────────────────────────────────────
|
|
10
12
|
const GITHUB_REPO = 'sdsrss/code-graph-mcp';
|
|
11
|
-
const NPM_PACKAGE = '@sdsrs/code-graph';
|
|
12
13
|
const STATE_FILE = path.join(CACHE_DIR, 'update-state.json');
|
|
13
|
-
const
|
|
14
|
+
const BINARY_CACHE_DIR = path.join(CACHE_DIR, 'bin');
|
|
15
|
+
const CHECK_INTERVAL_MS = 6 * 60 * 60 * 1000; // 6h
|
|
14
16
|
const RATE_LIMIT_INTERVAL_MS = 24 * 60 * 60 * 1000; // 24h if rate-limited
|
|
15
|
-
const POST_UPDATE_INTERVAL_MS = 1 * 60 * 60 * 1000; // 1h after update (verify success)
|
|
16
17
|
const FETCH_TIMEOUT_MS = 3000;
|
|
18
|
+
const VERSION_OUTPUT_RE = /^code-graph-mcp\s+(\d+\.\d+\.\d+)$/;
|
|
19
|
+
|
|
20
|
+
function isSilentMode(argv = process.argv.slice(2), env = process.env) {
|
|
21
|
+
return argv.includes('--silent') || env.CODE_GRAPH_AUTO_UPDATE_SILENT === '1';
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// ── Platform → GitHub release asset name mapping ──────────
|
|
25
|
+
function getPlatformAssetName() {
|
|
26
|
+
const platform = os.platform();
|
|
27
|
+
const arch = os.arch();
|
|
28
|
+
const key = `${platform}-${arch}`;
|
|
29
|
+
const map = {
|
|
30
|
+
'linux-x64': 'code-graph-mcp-linux-x64',
|
|
31
|
+
'linux-arm64': 'code-graph-mcp-linux-arm64',
|
|
32
|
+
'darwin-x64': 'code-graph-mcp-darwin-x64',
|
|
33
|
+
'darwin-arm64': 'code-graph-mcp-darwin-arm64',
|
|
34
|
+
'win32-x64': 'code-graph-mcp-win32-x64.exe',
|
|
35
|
+
};
|
|
36
|
+
return map[key] || null;
|
|
37
|
+
}
|
|
17
38
|
|
|
18
39
|
// ── State Persistence ──────────────────────────────────────
|
|
19
40
|
|
|
@@ -43,9 +64,7 @@ function isDevMode() {
|
|
|
43
64
|
function shouldCheck(state) {
|
|
44
65
|
if (!state.lastCheck) return true;
|
|
45
66
|
const elapsed = Date.now() - new Date(state.lastCheck).getTime();
|
|
46
|
-
|
|
47
|
-
if (state.rateLimited) interval = RATE_LIMIT_INTERVAL_MS;
|
|
48
|
-
else if (state.pendingBinaryUpdate) interval = POST_UPDATE_INTERVAL_MS;
|
|
67
|
+
const interval = state.rateLimited ? RATE_LIMIT_INTERVAL_MS : CHECK_INTERVAL_MS;
|
|
49
68
|
return elapsed >= interval;
|
|
50
69
|
}
|
|
51
70
|
|
|
@@ -63,30 +82,67 @@ function compareVersions(a, b) {
|
|
|
63
82
|
|
|
64
83
|
// ── GitHub API ─────────────────────────────────────────────
|
|
65
84
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
|
|
85
|
+
function requestJson(url, timeoutMs = FETCH_TIMEOUT_MS) {
|
|
86
|
+
return new Promise((resolve, reject) => {
|
|
87
|
+
const req = https.request(url, {
|
|
88
|
+
method: 'GET',
|
|
71
89
|
headers: {
|
|
72
90
|
'Accept': 'application/vnd.github+json',
|
|
73
91
|
'User-Agent': 'code-graph-auto-update/1.0',
|
|
74
92
|
},
|
|
93
|
+
}, (res) => {
|
|
94
|
+
let body = '';
|
|
95
|
+
res.setEncoding('utf8');
|
|
96
|
+
res.on('data', (chunk) => { body += chunk; });
|
|
97
|
+
res.on('end', () => {
|
|
98
|
+
if (!res.statusCode) {
|
|
99
|
+
reject(new Error('missing status code'));
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
resolve({ statusCode: res.statusCode, body });
|
|
103
|
+
});
|
|
75
104
|
});
|
|
76
105
|
|
|
77
|
-
|
|
78
|
-
|
|
106
|
+
req.setTimeout(timeoutMs, () => req.destroy(new Error('request timeout')));
|
|
107
|
+
req.on('error', reject);
|
|
108
|
+
req.end();
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function parseLatestRelease(data, assetName = getPlatformAssetName()) {
|
|
113
|
+
if (!data || typeof data.tag_name !== 'string' || typeof data.tarball_url !== 'string') {
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
let binaryUrl = null;
|
|
118
|
+
if (assetName && Array.isArray(data.assets)) {
|
|
119
|
+
const asset = data.assets.find((entry) => entry && entry.name === assetName);
|
|
120
|
+
if (asset && typeof asset.browser_download_url === 'string') {
|
|
121
|
+
binaryUrl = asset.browser_download_url;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return {
|
|
126
|
+
version: data.tag_name.replace(/^v/, ''),
|
|
127
|
+
tarballUrl: data.tarball_url,
|
|
128
|
+
binaryUrl,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async function fetchLatestRelease(requestJsonFn = requestJson) {
|
|
133
|
+
const url = `https://api.github.com/repos/${GITHUB_REPO}/releases/latest`;
|
|
134
|
+
try {
|
|
135
|
+
const res = await requestJsonFn(url, FETCH_TIMEOUT_MS);
|
|
136
|
+
|
|
137
|
+
if (res.statusCode === 403) {
|
|
79
138
|
const state = readState();
|
|
80
139
|
saveState({ ...state, rateLimited: true });
|
|
81
140
|
return null;
|
|
82
141
|
}
|
|
83
|
-
if (
|
|
142
|
+
if (res.statusCode < 200 || res.statusCode >= 300) return null;
|
|
84
143
|
|
|
85
|
-
const data =
|
|
86
|
-
return
|
|
87
|
-
version: data.tag_name.replace(/^v/, ''),
|
|
88
|
-
tarballUrl: data.tarball_url,
|
|
89
|
-
};
|
|
144
|
+
const data = JSON.parse(res.body);
|
|
145
|
+
return parseLatestRelease(data);
|
|
90
146
|
} catch { return null; }
|
|
91
147
|
}
|
|
92
148
|
|
|
@@ -105,14 +161,60 @@ function copyDirSync(src, dst) {
|
|
|
105
161
|
}
|
|
106
162
|
}
|
|
107
163
|
|
|
164
|
+
function getExtractedPluginVersion(pluginSrc) {
|
|
165
|
+
const manifest = readJson(path.join(pluginSrc, '.claude-plugin', 'plugin.json'));
|
|
166
|
+
return manifest && typeof manifest.version === 'string' ? manifest.version : null;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function readBinaryVersion(binaryPath) {
|
|
170
|
+
try {
|
|
171
|
+
const out = execFileSync(binaryPath, ['--version'], {
|
|
172
|
+
timeout: 2000,
|
|
173
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
174
|
+
}).toString().trim();
|
|
175
|
+
const match = out.match(VERSION_OUTPUT_RE);
|
|
176
|
+
return match ? match[1] : null;
|
|
177
|
+
} catch {
|
|
178
|
+
return null;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function promoteVerifiedBinary(binaryTmp, binaryDst, expectedVersion) {
|
|
183
|
+
try {
|
|
184
|
+
const stat = fs.statSync(binaryTmp);
|
|
185
|
+
if (stat.size <= 1_000_000) return false;
|
|
186
|
+
|
|
187
|
+
const actualVersion = readBinaryVersion(binaryTmp);
|
|
188
|
+
if (!actualVersion || (expectedVersion && actualVersion !== expectedVersion)) {
|
|
189
|
+
return false;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
fs.renameSync(binaryTmp, binaryDst);
|
|
193
|
+
if (os.platform() !== 'win32') {
|
|
194
|
+
fs.chmodSync(binaryDst, 0o755);
|
|
195
|
+
}
|
|
196
|
+
clearBinaryCache();
|
|
197
|
+
return true;
|
|
198
|
+
} catch {
|
|
199
|
+
return false;
|
|
200
|
+
} finally {
|
|
201
|
+
try {
|
|
202
|
+
if (fs.existsSync(binaryTmp)) fs.unlinkSync(binaryTmp);
|
|
203
|
+
} catch { /* ok */ }
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
108
207
|
// ── Download & Install ─────────────────────────────────────
|
|
109
208
|
|
|
110
209
|
async function downloadAndInstall(latest) {
|
|
111
210
|
const tmpDir = path.join(os.tmpdir(), `code-graph-update-${Date.now()}`);
|
|
211
|
+
let pluginUpdated = false;
|
|
212
|
+
let binaryUpdated = false;
|
|
213
|
+
|
|
112
214
|
try {
|
|
113
215
|
fs.mkdirSync(tmpDir, { recursive: true });
|
|
114
216
|
|
|
115
|
-
// 1
|
|
217
|
+
// ── Step 1: Download and install plugin files from tarball ──
|
|
116
218
|
const tarballPath = path.join(tmpDir, 'release.tar.gz');
|
|
117
219
|
execFileSync('curl', [
|
|
118
220
|
'-sL', '-o', tarballPath,
|
|
@@ -120,23 +222,22 @@ async function downloadAndInstall(latest) {
|
|
|
120
222
|
latest.tarballUrl,
|
|
121
223
|
], { timeout: 30000, stdio: 'pipe' });
|
|
122
224
|
|
|
123
|
-
// 2. Extract tarball
|
|
124
225
|
execFileSync('tar', [
|
|
125
226
|
'xzf', tarballPath, '-C', tmpDir, '--strip-components=1',
|
|
126
227
|
], { timeout: 15000, stdio: 'pipe' });
|
|
127
228
|
|
|
128
|
-
// 3. Copy plugin files to cache (cross-platform)
|
|
129
229
|
const pluginSrc = path.join(tmpDir, 'claude-plugin');
|
|
130
230
|
const pluginDst = path.join(
|
|
131
231
|
os.homedir(), '.claude', 'plugins', 'cache', MARKETPLACE_NAME, 'code-graph-mcp', latest.version
|
|
132
232
|
);
|
|
133
233
|
|
|
134
|
-
if (fs.existsSync(pluginSrc)) {
|
|
234
|
+
if (fs.existsSync(pluginSrc) && getExtractedPluginVersion(pluginSrc) === latest.version) {
|
|
135
235
|
fs.mkdirSync(pluginDst, { recursive: true });
|
|
136
236
|
copyDirSync(pluginSrc, pluginDst);
|
|
237
|
+
pluginUpdated = true;
|
|
137
238
|
}
|
|
138
239
|
|
|
139
|
-
//
|
|
240
|
+
// Update installed_plugins.json to point to new version
|
|
140
241
|
const installedPath = path.join(os.homedir(), '.claude', 'plugins', 'installed_plugins.json');
|
|
141
242
|
try {
|
|
142
243
|
const installed = readJson(installedPath);
|
|
@@ -146,34 +247,41 @@ async function downloadAndInstall(latest) {
|
|
|
146
247
|
installed.plugins[PLUGIN_ID][0].lastUpdated = new Date().toISOString();
|
|
147
248
|
writeJsonAtomic(installedPath, installed);
|
|
148
249
|
}
|
|
149
|
-
} catch { /*
|
|
250
|
+
} catch { /* not fatal */ }
|
|
150
251
|
|
|
151
|
-
//
|
|
252
|
+
// Update install manifest
|
|
152
253
|
try {
|
|
153
254
|
const manifest = readManifest();
|
|
154
255
|
manifest.version = latest.version;
|
|
155
256
|
manifest.updatedAt = new Date().toISOString();
|
|
156
257
|
writeJsonAtomic(path.join(CACHE_DIR, 'install-manifest.json'), manifest);
|
|
157
|
-
} catch { /*
|
|
158
|
-
|
|
159
|
-
//
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
258
|
+
} catch { /* not fatal */ }
|
|
259
|
+
|
|
260
|
+
// ── Step 2: Download platform binary directly from GitHub release ──
|
|
261
|
+
if (latest.binaryUrl) {
|
|
262
|
+
try {
|
|
263
|
+
const binaryName = os.platform() === 'win32' ? 'code-graph-mcp.exe' : 'code-graph-mcp';
|
|
264
|
+
const binaryDst = path.join(BINARY_CACHE_DIR, binaryName);
|
|
265
|
+
const binaryTmp = binaryDst + '.tmp.' + process.pid;
|
|
266
|
+
|
|
267
|
+
fs.mkdirSync(BINARY_CACHE_DIR, { recursive: true });
|
|
268
|
+
execFileSync('curl', [
|
|
269
|
+
'-sL', '-o', binaryTmp,
|
|
270
|
+
latest.binaryUrl,
|
|
271
|
+
], { timeout: 60000, stdio: 'pipe' });
|
|
272
|
+
|
|
273
|
+
if (promoteVerifiedBinary(binaryTmp, binaryDst, latest.version)) {
|
|
274
|
+
binaryUpdated = true;
|
|
275
|
+
}
|
|
276
|
+
} catch {
|
|
277
|
+
// Binary download failed — plugin update still counts as success
|
|
278
|
+
}
|
|
172
279
|
}
|
|
173
280
|
|
|
174
|
-
return
|
|
175
|
-
} catch {
|
|
176
|
-
|
|
281
|
+
return { pluginUpdated, binaryUpdated };
|
|
282
|
+
} catch {
|
|
283
|
+
return { pluginUpdated: false, binaryUpdated: false };
|
|
284
|
+
} finally {
|
|
177
285
|
try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch { /* ok */ }
|
|
178
286
|
}
|
|
179
287
|
}
|
|
@@ -189,7 +297,6 @@ async function checkForUpdate() {
|
|
|
189
297
|
|
|
190
298
|
// Time-based throttle
|
|
191
299
|
if (!shouldCheck(state)) {
|
|
192
|
-
// Report pending update from previous check
|
|
193
300
|
if (state.updateAvailable && state.latestVersion) {
|
|
194
301
|
return { updateAvailable: true, from: state.installedVersion, to: state.latestVersion };
|
|
195
302
|
}
|
|
@@ -209,8 +316,8 @@ async function checkForUpdate() {
|
|
|
209
316
|
const hasUpdate = compareVersions(latest.version, currentVersion) > 0;
|
|
210
317
|
|
|
211
318
|
if (hasUpdate) {
|
|
212
|
-
|
|
213
|
-
const success =
|
|
319
|
+
const result = await downloadAndInstall(latest);
|
|
320
|
+
const success = result.pluginUpdated;
|
|
214
321
|
const newState = {
|
|
215
322
|
lastCheck: new Date().toISOString(),
|
|
216
323
|
installedVersion: success ? latest.version : currentVersion,
|
|
@@ -218,12 +325,14 @@ async function checkForUpdate() {
|
|
|
218
325
|
updateAvailable: !success,
|
|
219
326
|
lastUpdate: success ? new Date().toISOString() : state.lastUpdate,
|
|
220
327
|
rateLimited: false,
|
|
328
|
+
binaryUpdated: result.binaryUpdated,
|
|
221
329
|
};
|
|
222
330
|
saveState(newState);
|
|
223
331
|
|
|
224
332
|
return {
|
|
225
333
|
updateAvailable: !success,
|
|
226
334
|
updated: success,
|
|
335
|
+
binaryUpdated: result.binaryUpdated,
|
|
227
336
|
from: currentVersion,
|
|
228
337
|
to: latest.version,
|
|
229
338
|
};
|
|
@@ -244,20 +353,27 @@ async function checkForUpdate() {
|
|
|
244
353
|
}
|
|
245
354
|
}
|
|
246
355
|
|
|
247
|
-
module.exports = {
|
|
356
|
+
module.exports = {
|
|
357
|
+
checkForUpdate, isDevMode, readState, compareVersions,
|
|
358
|
+
getExtractedPluginVersion, readBinaryVersion, promoteVerifiedBinary, isSilentMode,
|
|
359
|
+
requestJson, parseLatestRelease, fetchLatestRelease,
|
|
360
|
+
};
|
|
248
361
|
|
|
249
362
|
// CLI: node auto-update.js [check|status]
|
|
250
363
|
if (require.main === module) {
|
|
251
364
|
(async () => {
|
|
252
|
-
const
|
|
365
|
+
const argv = process.argv.slice(2);
|
|
366
|
+
const cmd = argv.find(arg => !arg.startsWith('--')) || 'check';
|
|
367
|
+
const silent = isSilentMode(argv);
|
|
253
368
|
if (cmd === 'status') {
|
|
254
369
|
const state = readState();
|
|
255
370
|
console.log(JSON.stringify(state, null, 2));
|
|
256
371
|
} else {
|
|
257
|
-
console.log('Checking for updates...');
|
|
372
|
+
if (!silent) console.log('Checking for updates...');
|
|
258
373
|
const result = await checkForUpdate();
|
|
374
|
+
if (silent) return;
|
|
259
375
|
if (result && result.updated) {
|
|
260
|
-
console.log(`Updated: v${result.from} → v${result.to}`);
|
|
376
|
+
console.log(`Updated: v${result.from} → v${result.to} (binary: ${result.binaryUpdated ? 'yes' : 'no'})`);
|
|
261
377
|
} else if (result && result.updateAvailable) {
|
|
262
378
|
console.log(`Update available: v${result.to} (auto-install failed)`);
|
|
263
379
|
} else if (isDevMode()) {
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const test = require('node:test');
|
|
3
|
+
const assert = require('node:assert/strict');
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const os = require('os');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
|
|
8
|
+
const {
|
|
9
|
+
fetchLatestRelease,
|
|
10
|
+
getExtractedPluginVersion,
|
|
11
|
+
parseLatestRelease,
|
|
12
|
+
readBinaryVersion,
|
|
13
|
+
promoteVerifiedBinary,
|
|
14
|
+
} = require('./auto-update');
|
|
15
|
+
|
|
16
|
+
function mkDir(prefix) {
|
|
17
|
+
return fs.mkdtempSync(path.join(os.tmpdir(), prefix));
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
test('getExtractedPluginVersion reads extracted plugin manifest version', () => {
|
|
21
|
+
const root = mkDir('code-graph-plugin-');
|
|
22
|
+
const manifest = path.join(root, '.claude-plugin', 'plugin.json');
|
|
23
|
+
fs.mkdirSync(path.dirname(manifest), { recursive: true });
|
|
24
|
+
fs.writeFileSync(manifest, JSON.stringify({ version: '1.2.3' }, null, 2));
|
|
25
|
+
assert.equal(getExtractedPluginVersion(root), '1.2.3');
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
function writeFakeBinary(filePath, version) {
|
|
29
|
+
const script = [
|
|
30
|
+
'#!/usr/bin/env bash',
|
|
31
|
+
'if [ "$1" = "--version" ]; then',
|
|
32
|
+
` echo "code-graph-mcp ${version}"`,
|
|
33
|
+
' exit 0',
|
|
34
|
+
'fi',
|
|
35
|
+
'exit 0',
|
|
36
|
+
`# ${'x'.repeat(1_100_000)}`,
|
|
37
|
+
'',
|
|
38
|
+
].join('\n');
|
|
39
|
+
fs.writeFileSync(filePath, script);
|
|
40
|
+
fs.chmodSync(filePath, 0o755);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
test('promoteVerifiedBinary accepts a runnable binary with the expected version', () => {
|
|
44
|
+
const dir = mkDir('code-graph-bin-');
|
|
45
|
+
const tmp = path.join(dir, 'code-graph-mcp.tmp');
|
|
46
|
+
const dst = path.join(dir, 'code-graph-mcp');
|
|
47
|
+
writeFakeBinary(tmp, '1.2.3');
|
|
48
|
+
|
|
49
|
+
assert.equal(readBinaryVersion(tmp), '1.2.3');
|
|
50
|
+
assert.equal(promoteVerifiedBinary(tmp, dst, '1.2.3'), true);
|
|
51
|
+
assert.equal(fs.existsSync(tmp), false);
|
|
52
|
+
assert.equal(fs.existsSync(dst), true);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test('promoteVerifiedBinary rejects binaries with mismatched version', () => {
|
|
56
|
+
const dir = mkDir('code-graph-bin-');
|
|
57
|
+
const tmp = path.join(dir, 'code-graph-mcp.tmp');
|
|
58
|
+
const dst = path.join(dir, 'code-graph-mcp');
|
|
59
|
+
writeFakeBinary(tmp, '1.2.2');
|
|
60
|
+
|
|
61
|
+
assert.equal(promoteVerifiedBinary(tmp, dst, '1.2.3'), false);
|
|
62
|
+
assert.equal(fs.existsSync(tmp), false);
|
|
63
|
+
assert.equal(fs.existsSync(dst), false);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test('parseLatestRelease selects the matching platform asset', () => {
|
|
67
|
+
const latest = parseLatestRelease({
|
|
68
|
+
tag_name: 'v1.2.3',
|
|
69
|
+
tarball_url: 'https://example.com/tarball.tgz',
|
|
70
|
+
assets: [
|
|
71
|
+
{ name: 'code-graph-mcp-linux-x64', browser_download_url: 'https://example.com/linux-x64' },
|
|
72
|
+
{ name: 'other', browser_download_url: 'https://example.com/other' },
|
|
73
|
+
],
|
|
74
|
+
}, 'code-graph-mcp-linux-x64');
|
|
75
|
+
|
|
76
|
+
assert.deepEqual(latest, {
|
|
77
|
+
version: '1.2.3',
|
|
78
|
+
tarballUrl: 'https://example.com/tarball.tgz',
|
|
79
|
+
binaryUrl: 'https://example.com/linux-x64',
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test('fetchLatestRelease parses JSON without relying on global fetch', async () => {
|
|
84
|
+
const latest = await fetchLatestRelease(async () => ({
|
|
85
|
+
statusCode: 200,
|
|
86
|
+
body: JSON.stringify({
|
|
87
|
+
tag_name: 'v2.0.0',
|
|
88
|
+
tarball_url: 'https://example.com/release.tgz',
|
|
89
|
+
assets: [
|
|
90
|
+
{ name: 'code-graph-mcp-linux-x64', browser_download_url: 'https://example.com/bin' },
|
|
91
|
+
],
|
|
92
|
+
}),
|
|
93
|
+
}));
|
|
94
|
+
|
|
95
|
+
assert.equal(latest.version, '2.0.0');
|
|
96
|
+
assert.equal(latest.tarballUrl, 'https://example.com/release.tgz');
|
|
97
|
+
});
|