@sdsrs/code-graph 0.45.0 → 0.45.2

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.
@@ -4,7 +4,7 @@
4
4
  "author": {
5
5
  "name": "sdsrs"
6
6
  },
7
- "version": "0.45.0",
7
+ "version": "0.45.2",
8
8
  "keywords": [
9
9
  "code-graph",
10
10
  "ast",
@@ -182,6 +182,35 @@ function cachedBinaryPath() {
182
182
  return path.join(BINARY_CACHE_DIR, name);
183
183
  }
184
184
 
185
+ /**
186
+ * Decide whether the cached native binary must be (re)downloaded: true when it
187
+ * is missing OR its actual version differs from the latest release. Version-aware
188
+ * rather than existence-only — a stale-but-present binary must still self-heal
189
+ * even when the plugin shell version already matches latest. manifest.version
190
+ * tracks the plugin shell (the marketplace bumps it independently of the native
191
+ * binary), so an existence-only check leaves the engine permanently pinned to an
192
+ * old binary while the updater reports "up to date".
193
+ */
194
+ function cachedBinaryNeedsUpdate(latest, { binaryPath = cachedBinaryPath(), readVersion = readBinaryVersion } = {}) {
195
+ if (!latest || !latest.binaryUrl) return false;
196
+ if (!fs.existsSync(binaryPath)) return true;
197
+ return readVersion(binaryPath) !== latest.version;
198
+ }
199
+
200
+ /**
201
+ * Throttle-bypass predicate: is a *present* cached binary stale relative to the
202
+ * last known latest release (`state.latestVersion`, set on the previous fetch —
203
+ * no network here)? Used so a present-but-stale binary skips the time-based
204
+ * throttle instead of staying pinned for up to a full check interval. Returns
205
+ * false when there is no prior latestVersion (first run fetches anyway) or the
206
+ * binary is missing (handled by the separate `binaryMissing` bypass).
207
+ */
208
+ function cachedBinaryStaleVsState(state, { binaryPath = cachedBinaryPath(), readVersion = readBinaryVersion } = {}) {
209
+ if (!state || !state.latestVersion) return false;
210
+ if (!fs.existsSync(binaryPath)) return false;
211
+ return readVersion(binaryPath) !== state.latestVersion;
212
+ }
213
+
185
214
  /**
186
215
  * Download just the platform binary from a GitHub release into the cache.
187
216
  * Used in two paths:
@@ -222,15 +251,21 @@ function promoteVerifiedBinary(binaryTmp, binaryDst, expectedVersion) {
222
251
  const stat = fs.statSync(binaryTmp);
223
252
  if (stat.size <= 1_000_000) return false;
224
253
 
254
+ // chmod BEFORE reading the version. readBinaryVersion executes the binary
255
+ // (`--version`), which requires the exec bit; `curl -o` writes the tmp file
256
+ // as 0644 (no exec bit), so reading the version first fails with EACCES →
257
+ // null → false, which silently wedged every download path. rename preserves
258
+ // the mode, so the promoted dst ends up 0755.
259
+ if (os.platform() !== 'win32') {
260
+ fs.chmodSync(binaryTmp, 0o755);
261
+ }
262
+
225
263
  const actualVersion = readBinaryVersion(binaryTmp);
226
264
  if (!actualVersion || (expectedVersion && actualVersion !== expectedVersion)) {
227
265
  return false;
228
266
  }
229
267
 
230
268
  fs.renameSync(binaryTmp, binaryDst);
231
- if (os.platform() !== 'win32') {
232
- fs.chmodSync(binaryDst, 0o755);
233
- }
234
269
  clearBinaryCache();
235
270
  return true;
236
271
  } catch {
@@ -344,11 +379,14 @@ async function checkForUpdate({ installMissing = false } = {}) {
344
379
  // bypasses auto-update.js, so re-sync state.installedVersion every call.
345
380
  const installedVersion = readManifest().version || '0.0.0';
346
381
 
347
- // Time-based throttle. A missing cache binary is a hard failure (launcher
348
- // cannot start) so it overrides the throttle without this bypass the
349
- // session wedges for up to 6h waiting for the next check window.
382
+ // Time-based throttle. Two conditions override it: a missing cache binary
383
+ // (launcher cannot start) and a present-but-stale binary (otherwise it stays
384
+ // pinned to the old version for up to a full check interval the binary
385
+ // self-heal would never run inside the throttle window). Both bypass to the
386
+ // fetch + self-heal path below.
350
387
  const binaryMissing = !fs.existsSync(cachedBinaryPath());
351
- if (!binaryMissing && !shouldCheck(state)) {
388
+ const binaryStale = cachedBinaryStaleVsState(state);
389
+ if (!binaryMissing && !binaryStale && !shouldCheck(state)) {
352
390
  if (state.installedVersion !== installedVersion) {
353
391
  saveState({ ...state, installedVersion });
354
392
  }
@@ -392,12 +430,13 @@ async function checkForUpdate({ installMissing = false } = {}) {
392
430
  };
393
431
  }
394
432
 
395
- // No update needed — but self-heal if cache binary is missing.
396
- // State file alone is not authoritative; previous download may have failed
397
- // silently, cache may have been wiped, or `npm install -g` optionalDependency
398
- // may have dropped the platform package.
433
+ // No plugin-shell update — but self-heal the native binary if it is missing
434
+ // OR stale. The shell version (manifest.version) can match latest while the
435
+ // cached binary lags (a previous download failed silently, the cache was
436
+ // wiped, an `npm install -g` optionalDependency dropped the platform package,
437
+ // or the marketplace bumped the shell without re-fetching the binary).
399
438
  let selfHealedBinary = false;
400
- if (latest.binaryUrl && !fs.existsSync(cachedBinaryPath())) {
439
+ if (cachedBinaryNeedsUpdate(latest)) {
401
440
  selfHealedBinary = await downloadBinary(latest);
402
441
  }
403
442
 
@@ -424,7 +463,7 @@ module.exports = {
424
463
  getExtractedPluginVersion, readBinaryVersion, promoteVerifiedBinary,
425
464
  isSilentMode, isInstallMissingMode,
426
465
  requestJson, parseLatestRelease, fetchLatestRelease,
427
- downloadBinary, cachedBinaryPath,
466
+ downloadBinary, cachedBinaryPath, cachedBinaryNeedsUpdate, cachedBinaryStaleVsState,
428
467
  };
429
468
 
430
469
  // CLI: node auto-update.js [check|status] [--silent] [--install-missing]
@@ -13,6 +13,8 @@ const {
13
13
  readBinaryVersion,
14
14
  promoteVerifiedBinary,
15
15
  cachedBinaryPath,
16
+ cachedBinaryNeedsUpdate,
17
+ cachedBinaryStaleVsState,
16
18
  downloadBinary,
17
19
  isInstallMissingMode,
18
20
  isSilentMode,
@@ -32,7 +34,7 @@ test('getExtractedPluginVersion reads extracted plugin manifest version', (t) =>
32
34
  assert.equal(getExtractedPluginVersion(root), '1.2.3');
33
35
  });
34
36
 
35
- function writeFakeBinary(filePath, version) {
37
+ function writeFakeBinary(filePath, version, mode = 0o755) {
36
38
  const script = [
37
39
  '#!/usr/bin/env bash',
38
40
  'if [ "$1" = "--version" ]; then',
@@ -44,7 +46,7 @@ function writeFakeBinary(filePath, version) {
44
46
  '',
45
47
  ].join('\n');
46
48
  fs.writeFileSync(filePath, script);
47
- fs.chmodSync(filePath, 0o755);
49
+ fs.chmodSync(filePath, mode);
48
50
  }
49
51
 
50
52
  test('promoteVerifiedBinary accepts a runnable binary with the expected version', (t) => {
@@ -70,6 +72,77 @@ test('promoteVerifiedBinary rejects binaries with mismatched version', (t) => {
70
72
  assert.equal(fs.existsSync(dst), false);
71
73
  });
72
74
 
75
+ test('promoteVerifiedBinary promotes a non-executable (0644) download — curl -o regression', (t) => {
76
+ // `curl -o` writes the tmp file as 0644 (no exec bit). promoteVerifiedBinary
77
+ // must chmod before reading the version (readBinaryVersion executes the
78
+ // binary), otherwise the version read fails with EACCES → null → false and
79
+ // every download path silently wedges. Regression for the binary-stuck-at-old
80
+ // -version deadlock.
81
+ if (process.platform === 'win32') return; // no exec bit on win32
82
+ const dir = mkDir(t, 'code-graph-bin-');
83
+ const tmp = path.join(dir, 'code-graph-mcp.tmp');
84
+ const dst = path.join(dir, 'code-graph-mcp');
85
+ writeFakeBinary(tmp, '1.2.3', 0o644);
86
+
87
+ assert.equal(readBinaryVersion(tmp), null, 'precondition: 0644 binary is not executable');
88
+ assert.equal(promoteVerifiedBinary(tmp, dst, '1.2.3'), true);
89
+ assert.equal(fs.existsSync(dst), true);
90
+ assert.equal(fs.statSync(dst).mode & 0o111, 0o111, 'promoted binary is executable');
91
+ assert.equal(readBinaryVersion(dst), '1.2.3');
92
+ });
93
+
94
+ test('cachedBinaryNeedsUpdate is version-aware, not existence-only', (t) => {
95
+ const dir = mkDir(t, 'code-graph-heal-');
96
+ const binaryPath = path.join(dir, 'code-graph-mcp');
97
+ const latest = { version: '0.45.0', binaryUrl: 'https://example.com/bin' };
98
+
99
+ // missing binary → needs update
100
+ assert.equal(cachedBinaryNeedsUpdate(latest, { binaryPath }), true);
101
+
102
+ // present but stale (the actual deadlock: shell at 0.45.0, binary at 0.16.6)
103
+ fs.writeFileSync(binaryPath, 'x');
104
+ assert.equal(
105
+ cachedBinaryNeedsUpdate(latest, { binaryPath, readVersion: () => '0.16.6' }),
106
+ true,
107
+ );
108
+
109
+ // present and current → no update
110
+ assert.equal(
111
+ cachedBinaryNeedsUpdate(latest, { binaryPath, readVersion: () => '0.45.0' }),
112
+ false,
113
+ );
114
+
115
+ // no binaryUrl / null latest → no-op (nothing to download)
116
+ assert.equal(cachedBinaryNeedsUpdate({ version: '0.45.0', binaryUrl: null }, { binaryPath }), false);
117
+ assert.equal(cachedBinaryNeedsUpdate(null, { binaryPath }), false);
118
+ });
119
+
120
+ test('cachedBinaryStaleVsState bypasses throttle only for a present-but-stale binary', (t) => {
121
+ const dir = mkDir(t, 'code-graph-throttle-');
122
+ const binaryPath = path.join(dir, 'code-graph-mcp');
123
+ fs.writeFileSync(binaryPath, 'x'); // present
124
+
125
+ // no prior latestVersion → don't bypass (first run fetches anyway)
126
+ assert.equal(cachedBinaryStaleVsState({}, { binaryPath }), false);
127
+ assert.equal(cachedBinaryStaleVsState(null, { binaryPath }), false);
128
+
129
+ // present + stale vs last known latest → bypass throttle (the 6h-gap fix)
130
+ assert.equal(
131
+ cachedBinaryStaleVsState({ latestVersion: '0.45.1' }, { binaryPath, readVersion: () => '0.16.6' }),
132
+ true,
133
+ );
134
+
135
+ // present + current → stay throttled
136
+ assert.equal(
137
+ cachedBinaryStaleVsState({ latestVersion: '0.45.1' }, { binaryPath, readVersion: () => '0.45.1' }),
138
+ false,
139
+ );
140
+
141
+ // missing binary → false here (the separate binaryMissing bypass handles it)
142
+ fs.rmSync(binaryPath);
143
+ assert.equal(cachedBinaryStaleVsState({ latestVersion: '0.45.1' }, { binaryPath }), false);
144
+ });
145
+
73
146
  test('parseLatestRelease selects the matching platform asset', () => {
74
147
  const latest = parseLatestRelease({
75
148
  tag_name: 'v1.2.3',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sdsrs/code-graph",
3
- "version": "0.45.0",
3
+ "version": "0.45.2",
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.45.0",
39
- "@sdsrs/code-graph-linux-arm64": "0.45.0",
40
- "@sdsrs/code-graph-darwin-x64": "0.45.0",
41
- "@sdsrs/code-graph-darwin-arm64": "0.45.0",
42
- "@sdsrs/code-graph-win32-x64": "0.45.0"
38
+ "@sdsrs/code-graph-linux-x64": "0.45.2",
39
+ "@sdsrs/code-graph-linux-arm64": "0.45.2",
40
+ "@sdsrs/code-graph-darwin-x64": "0.45.2",
41
+ "@sdsrs/code-graph-darwin-arm64": "0.45.2",
42
+ "@sdsrs/code-graph-win32-x64": "0.45.2"
43
43
  }
44
44
  }