@sdsrs/code-graph 0.45.0 → 0.45.1

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.1",
8
8
  "keywords": [
9
9
  "code-graph",
10
10
  "ast",
@@ -182,6 +182,21 @@ 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
+
185
200
  /**
186
201
  * Download just the platform binary from a GitHub release into the cache.
187
202
  * Used in two paths:
@@ -222,15 +237,21 @@ function promoteVerifiedBinary(binaryTmp, binaryDst, expectedVersion) {
222
237
  const stat = fs.statSync(binaryTmp);
223
238
  if (stat.size <= 1_000_000) return false;
224
239
 
240
+ // chmod BEFORE reading the version. readBinaryVersion executes the binary
241
+ // (`--version`), which requires the exec bit; `curl -o` writes the tmp file
242
+ // as 0644 (no exec bit), so reading the version first fails with EACCES →
243
+ // null → false, which silently wedged every download path. rename preserves
244
+ // the mode, so the promoted dst ends up 0755.
245
+ if (os.platform() !== 'win32') {
246
+ fs.chmodSync(binaryTmp, 0o755);
247
+ }
248
+
225
249
  const actualVersion = readBinaryVersion(binaryTmp);
226
250
  if (!actualVersion || (expectedVersion && actualVersion !== expectedVersion)) {
227
251
  return false;
228
252
  }
229
253
 
230
254
  fs.renameSync(binaryTmp, binaryDst);
231
- if (os.platform() !== 'win32') {
232
- fs.chmodSync(binaryDst, 0o755);
233
- }
234
255
  clearBinaryCache();
235
256
  return true;
236
257
  } catch {
@@ -392,12 +413,13 @@ async function checkForUpdate({ installMissing = false } = {}) {
392
413
  };
393
414
  }
394
415
 
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.
416
+ // No plugin-shell update — but self-heal the native binary if it is missing
417
+ // OR stale. The shell version (manifest.version) can match latest while the
418
+ // cached binary lags (a previous download failed silently, the cache was
419
+ // wiped, an `npm install -g` optionalDependency dropped the platform package,
420
+ // or the marketplace bumped the shell without re-fetching the binary).
399
421
  let selfHealedBinary = false;
400
- if (latest.binaryUrl && !fs.existsSync(cachedBinaryPath())) {
422
+ if (cachedBinaryNeedsUpdate(latest)) {
401
423
  selfHealedBinary = await downloadBinary(latest);
402
424
  }
403
425
 
@@ -424,7 +446,7 @@ module.exports = {
424
446
  getExtractedPluginVersion, readBinaryVersion, promoteVerifiedBinary,
425
447
  isSilentMode, isInstallMissingMode,
426
448
  requestJson, parseLatestRelease, fetchLatestRelease,
427
- downloadBinary, cachedBinaryPath,
449
+ downloadBinary, cachedBinaryPath, cachedBinaryNeedsUpdate,
428
450
  };
429
451
 
430
452
  // CLI: node auto-update.js [check|status] [--silent] [--install-missing]
@@ -13,6 +13,7 @@ const {
13
13
  readBinaryVersion,
14
14
  promoteVerifiedBinary,
15
15
  cachedBinaryPath,
16
+ cachedBinaryNeedsUpdate,
16
17
  downloadBinary,
17
18
  isInstallMissingMode,
18
19
  isSilentMode,
@@ -32,7 +33,7 @@ test('getExtractedPluginVersion reads extracted plugin manifest version', (t) =>
32
33
  assert.equal(getExtractedPluginVersion(root), '1.2.3');
33
34
  });
34
35
 
35
- function writeFakeBinary(filePath, version) {
36
+ function writeFakeBinary(filePath, version, mode = 0o755) {
36
37
  const script = [
37
38
  '#!/usr/bin/env bash',
38
39
  'if [ "$1" = "--version" ]; then',
@@ -44,7 +45,7 @@ function writeFakeBinary(filePath, version) {
44
45
  '',
45
46
  ].join('\n');
46
47
  fs.writeFileSync(filePath, script);
47
- fs.chmodSync(filePath, 0o755);
48
+ fs.chmodSync(filePath, mode);
48
49
  }
49
50
 
50
51
  test('promoteVerifiedBinary accepts a runnable binary with the expected version', (t) => {
@@ -70,6 +71,51 @@ test('promoteVerifiedBinary rejects binaries with mismatched version', (t) => {
70
71
  assert.equal(fs.existsSync(dst), false);
71
72
  });
72
73
 
74
+ test('promoteVerifiedBinary promotes a non-executable (0644) download — curl -o regression', (t) => {
75
+ // `curl -o` writes the tmp file as 0644 (no exec bit). promoteVerifiedBinary
76
+ // must chmod before reading the version (readBinaryVersion executes the
77
+ // binary), otherwise the version read fails with EACCES → null → false and
78
+ // every download path silently wedges. Regression for the binary-stuck-at-old
79
+ // -version deadlock.
80
+ if (process.platform === 'win32') return; // no exec bit on win32
81
+ const dir = mkDir(t, 'code-graph-bin-');
82
+ const tmp = path.join(dir, 'code-graph-mcp.tmp');
83
+ const dst = path.join(dir, 'code-graph-mcp');
84
+ writeFakeBinary(tmp, '1.2.3', 0o644);
85
+
86
+ assert.equal(readBinaryVersion(tmp), null, 'precondition: 0644 binary is not executable');
87
+ assert.equal(promoteVerifiedBinary(tmp, dst, '1.2.3'), true);
88
+ assert.equal(fs.existsSync(dst), true);
89
+ assert.equal(fs.statSync(dst).mode & 0o111, 0o111, 'promoted binary is executable');
90
+ assert.equal(readBinaryVersion(dst), '1.2.3');
91
+ });
92
+
93
+ test('cachedBinaryNeedsUpdate is version-aware, not existence-only', (t) => {
94
+ const dir = mkDir(t, 'code-graph-heal-');
95
+ const binaryPath = path.join(dir, 'code-graph-mcp');
96
+ const latest = { version: '0.45.0', binaryUrl: 'https://example.com/bin' };
97
+
98
+ // missing binary → needs update
99
+ assert.equal(cachedBinaryNeedsUpdate(latest, { binaryPath }), true);
100
+
101
+ // present but stale (the actual deadlock: shell at 0.45.0, binary at 0.16.6)
102
+ fs.writeFileSync(binaryPath, 'x');
103
+ assert.equal(
104
+ cachedBinaryNeedsUpdate(latest, { binaryPath, readVersion: () => '0.16.6' }),
105
+ true,
106
+ );
107
+
108
+ // present and current → no update
109
+ assert.equal(
110
+ cachedBinaryNeedsUpdate(latest, { binaryPath, readVersion: () => '0.45.0' }),
111
+ false,
112
+ );
113
+
114
+ // no binaryUrl / null latest → no-op (nothing to download)
115
+ assert.equal(cachedBinaryNeedsUpdate({ version: '0.45.0', binaryUrl: null }, { binaryPath }), false);
116
+ assert.equal(cachedBinaryNeedsUpdate(null, { binaryPath }), false);
117
+ });
118
+
73
119
  test('parseLatestRelease selects the matching platform asset', () => {
74
120
  const latest = parseLatestRelease({
75
121
  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.1",
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.1",
39
+ "@sdsrs/code-graph-linux-arm64": "0.45.1",
40
+ "@sdsrs/code-graph-darwin-x64": "0.45.1",
41
+ "@sdsrs/code-graph-darwin-arm64": "0.45.1",
42
+ "@sdsrs/code-graph-win32-x64": "0.45.1"
43
43
  }
44
44
  }