Explorar o código

fix: self-heal missing platform bundle from GitHub Releases (#303) (#335)

Installing from a registry mirror (npmmirror/cnpm) that hadn't mirrored the
per-platform optionalDependency left codegraph failing with "no prebuilt
bundle for <platform>" — npm treats an unfetchable optional dep as success and
silently skips it. The npm-shim now self-heals: when the bundle is missing it
downloads the matching archive from GitHub Releases (checksum-verified, with a
download timeout) and caches it, so a global install works on any registry.

release.yml now publishes SHA256SUMS and triggers an npmmirror sync after
publish. Adds hermetic tests for the shim (resolution, cache reuse, disable
knob, download + checksum match/mismatch/absent).

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Colby Mchenry hai 1 mes
pai
achega
15072aa29f
Modificáronse 5 ficheiros con 475 adicións e 30 borrados
  1. 25 2
      .github/workflows/release.yml
  2. 26 0
      CHANGELOG.md
  3. 208 0
      __tests__/npm-shim.test.ts
  4. 1 1
      package.json
  5. 215 27
      scripts/npm-shim.js

+ 25 - 2
.github/workflows/release.yml

@@ -36,6 +36,13 @@ jobs:
           done
           ls -lh release
 
+      - name: Generate SHA256SUMS
+        # Published as a release asset; the npm launcher verifies downloaded
+        # bundles against it (basenames only, so its path.basename match works).
+        run: |
+          ( cd release && sha256sum codegraph-* > SHA256SUMS )
+          cat release/SHA256SUMS
+
       - name: Resolve version
         id: ver
         run: echo "version=$(node -p "require('./package.json').version")" >> "$GITHUB_OUTPUT"
@@ -58,9 +65,9 @@ jobs:
           TAG="v${{ steps.ver.outputs.version }}"
           # Idempotent: create the release once, otherwise (re-run) refresh assets.
           if gh release view "$TAG" >/dev/null 2>&1; then
-            gh release upload "$TAG" release/codegraph-* --clobber
+            gh release upload "$TAG" release/codegraph-* release/SHA256SUMS --clobber
           else
-            gh release create "$TAG" release/codegraph-* --title "$TAG" --notes-file notes.md
+            gh release create "$TAG" release/codegraph-* release/SHA256SUMS --title "$TAG" --notes-file notes.md
           fi
 
       - name: Publish to npm
@@ -96,3 +103,19 @@ jobs:
             [ -n "$ok" ] || { echo "::error::$name@$V never appeared on the registry"; exit 1; }
             echo "verified $name@$V"
           done
+
+      - name: Sync packages to npmmirror
+        # npmmirror/cnpm mirror lazily and frequently never pull the per-platform
+        # optionalDependencies on their own, so `npm i` there fails with
+        # "no prebuilt bundle" (issue #303). Nudge a sync now so mirror users get
+        # the bundle without waiting. Best-effort — the launcher also self-heals
+        # from GitHub Releases — so a mirror hiccup never fails the release.
+        continue-on-error: true
+        run: |
+          for dir in release/npm/codegraph-* release/npm/main; do
+            name=$(node -p "require('./$dir/package.json').name")
+            enc=$(node -p "encodeURIComponent(require('./$dir/package.json').name)")
+            echo "sync $name"
+            curl -s -X PUT "https://registry.npmmirror.com/-/package/$enc/syncs" || true
+            echo
+          done

+ 26 - 0
CHANGELOG.md

@@ -7,6 +7,31 @@ a [GitHub Release](https://github.com/colbymchenry/codegraph/releases) tagged
 This project follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/)
 and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
 
+## [0.9.4] - 2026-05-22
+
+### Added
+- **Release archives now ship with a `SHA256SUMS` file**, and the npm launcher
+  verifies the bundle it downloads against it — a mismatch aborts before
+  anything runs. Releases published before this change have no checksum file, so
+  the verification is skipped (not failed) when none is available.
+
+### Fixed
+- **`codegraph: no prebuilt bundle for <platform>` after installing through a
+  registry mirror.** Installing `@colbymchenry/codegraph` from a registry that
+  hadn't mirrored the matching per-platform package — most often the
+  npmmirror/cnpm mirrors, but any lazily-syncing mirror or corporate proxy can
+  do it — left every command failing with `no prebuilt bundle for <platform>`.
+  The runtime ships as a per-platform `optionalDependency`, and npm treats an
+  optional package it can't fetch as a success and silently skips it, so the
+  bundle simply went missing. The launcher now self-heals: when the platform
+  bundle isn't installed, it downloads the same archive from GitHub Releases
+  (cached under `~/.codegraph/bundles/` for next time) and runs that — so a
+  global install works even on a mirror that never carried the platform package.
+  Set `CODEGRAPH_NO_DOWNLOAD=1` to disable the network fallback, or
+  `CODEGRAPH_DOWNLOAD_BASE=<url>` to point it at your own mirror of the release
+  archives; the standalone `install.sh` remains the no-Node alternative. Resolves
+  [#303](https://github.com/colbymchenry/codegraph/issues/303).
+
 ## [0.9.3] - 2026-05-22
 
 ### Added
@@ -132,6 +157,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
   find its bundle. The release pipeline now verifies every package reached the
   registry (and is idempotent), so a release can't pass green-but-broken again.
 
+[0.9.4]: https://github.com/colbymchenry/codegraph/releases/tag/v0.9.4
 [0.9.3]: https://github.com/colbymchenry/codegraph/releases/tag/v0.9.3
 [0.9.2]: https://github.com/colbymchenry/codegraph/releases/tag/v0.9.2
 [0.9.1]: https://github.com/colbymchenry/codegraph/releases/tag/v0.9.1

+ 208 - 0
__tests__/npm-shim.test.ts

@@ -0,0 +1,208 @@
+/**
+ * npm thin-installer launcher (`scripts/npm-shim.js`) tests.
+ *
+ * The shim runs on the user's own Node, locates the per-platform optionalDependency
+ * bundle, and — when a registry mirror failed to deliver it (issue #303) — falls
+ * back to downloading the bundle from GitHub Releases. These tests exercise that
+ * shim as a real subprocess from a temp "main package" dir (its own package.json
+ * + node_modules), so resolution and version lookup behave hermetically.
+ *
+ * The download/checksum paths run against a local self-signed HTTPS server via
+ * CODEGRAPH_DOWNLOAD_BASE — no real network, no published release needed. The
+ * shim is launched with async `spawn` (not spawnSync), so the test's event loop
+ * stays free to serve those requests.
+ *
+ * POSIX only: the fake bundle launcher is a shell script and extraction uses the
+ * system `tar`. Skipped on Windows (where the shim's exec path differs anyway).
+ */
+
+import { describe, it, expect, beforeAll, afterAll } from 'vitest';
+import { spawn, execSync } from 'child_process';
+import * as https from 'https';
+import * as fs from 'fs';
+import * as os from 'os';
+import * as path from 'path';
+import * as crypto from 'crypto';
+import type { AddressInfo } from 'net';
+
+const SHIM_SRC = path.join(__dirname, '..', 'scripts', 'npm-shim.js');
+const target = `${process.platform}-${process.arch}`;
+const asset = `codegraph-${target}.tar.gz`;
+const isWindows = process.platform === 'win32';
+
+function hasOpenssl(): boolean {
+  try { execSync('openssl version', { stdio: 'ignore' }); return true; } catch { return false; }
+}
+const CAN_NET = !isWindows && hasOpenssl();
+
+function mkTmp(label: string): string {
+  return fs.mkdtempSync(path.join(os.tmpdir(), `cg-shim-${label}-`));
+}
+
+// A temp dir standing in for the installed @colbymchenry/codegraph main package.
+function makePkg(version = '9.9.9-test'): string {
+  const dir = mkTmp('pkg');
+  fs.copyFileSync(SHIM_SRC, path.join(dir, 'npm-shim.js'));
+  fs.writeFileSync(path.join(dir, 'package.json'),
+    JSON.stringify({ name: '@colbymchenry/codegraph', version }) + '\n');
+  return dir;
+}
+
+// A fake bundle launcher that prints a marker + its args, so we can prove the
+// shim found and exec'd it (and passed args through).
+function writeLauncher(binDir: string): void {
+  fs.mkdirSync(binDir, { recursive: true });
+  const p = path.join(binDir, 'codegraph');
+  fs.writeFileSync(p, '#!/bin/sh\necho "FAKE_BUNDLE_RAN args:$*"\n');
+  fs.chmodSync(p, 0o755);
+}
+
+// Launch the shim with async spawn so the in-process HTTPS server can respond
+// while it runs (spawnSync would block this event loop and deadlock).
+function runShim(pkgDir: string, args: string[], env: Record<string, string>) {
+  return new Promise<{ status: number | null; stdout: string; stderr: string }>((resolve) => {
+    const child = spawn(process.execPath, [path.join(pkgDir, 'npm-shim.js'), ...args], {
+      env: { ...process.env, ...env },
+    });
+    let stdout = '', stderr = '';
+    child.stdout.on('data', (d) => { stdout += d.toString(); });
+    child.stderr.on('data', (d) => { stderr += d.toString(); });
+    child.on('close', (status) => resolve({ status, stdout, stderr }));
+  });
+}
+
+describe.skipIf(isWindows)('npm-shim launcher', () => {
+  it('runs the installed optional-dependency bundle without any download', async () => {
+    const pkg = makePkg();
+    const platformPkg = path.join(pkg, 'node_modules', '@colbymchenry', `codegraph-${target}`);
+    writeLauncher(path.join(platformPkg, 'bin'));
+    fs.writeFileSync(path.join(platformPkg, 'package.json'),
+      JSON.stringify({ name: `@colbymchenry/codegraph-${target}`, version: '9.9.9-test' }) + '\n');
+    const cache = mkTmp('cache');
+    const r = await runShim(pkg, ['--probe-abc'], { CODEGRAPH_INSTALL_DIR: cache });
+
+    expect(r.status).toBe(0);
+    expect(r.stdout).toContain('FAKE_BUNDLE_RAN');
+    expect(r.stdout).toContain('--probe-abc');     // args passed through
+    expect(r.stderr).not.toContain('downloading'); // never reached the fallback
+    expect(fs.existsSync(path.join(cache, 'bundles'))).toBe(false);
+  });
+
+  it('uses an already-cached bundle even when downloads are disabled', async () => {
+    const pkg = makePkg('1.2.3-cached');
+    const cache = mkTmp('cache');
+    writeLauncher(path.join(cache, 'bundles', `${target}-1.2.3-cached`, 'bin'));
+    const r = await runShim(pkg, ['--probe-xyz'], {
+      CODEGRAPH_INSTALL_DIR: cache,
+      CODEGRAPH_NO_DOWNLOAD: '1',
+    });
+
+    expect(r.status).toBe(0);
+    expect(r.stdout).toContain('FAKE_BUNDLE_RAN');
+    expect(r.stdout).toContain('--probe-xyz');
+    expect(r.stderr).toBe('');
+  });
+
+  it('prints actionable guidance and exits 1 when disabled with no bundle', async () => {
+    const pkg = makePkg();
+    const r = await runShim(pkg, ['--version'], {
+      CODEGRAPH_INSTALL_DIR: mkTmp('cache'),
+      CODEGRAPH_NO_DOWNLOAD: '1',
+    });
+
+    expect(r.status).toBe(1);
+    expect(r.stderr).toContain(`no prebuilt bundle for ${target}`);
+    expect(r.stderr).toContain(`@colbymchenry/codegraph-${target}`);
+    expect(r.stderr).toContain('--registry=https://registry.npmjs.org');
+    expect(r.stderr).toContain('install.sh');
+  });
+});
+
+describe.skipIf(!CAN_NET)('npm-shim download fallback (local HTTPS)', () => {
+  let server: https.Server;
+  let port = 0;
+  let fixtureBytes: Buffer;
+  let fixtureSha: string;
+  let sumsBody: string | null = null; // per-test: SHA256SUMS contents, or null for 404
+
+  beforeAll(async () => {
+    // Self-signed cert for the mock release host.
+    const cdir = mkTmp('tls');
+    const keyP = path.join(cdir, 'key.pem');
+    const certP = path.join(cdir, 'cert.pem');
+    execSync(
+      `openssl req -x509 -newkey rsa:2048 -nodes -keyout ${keyP} -out ${certP} -days 1 -subj "/CN=localhost"`,
+      { stdio: 'ignore' },
+    );
+
+    // Build a fake bundle archive (codegraph-<target>/bin/codegraph), like a real release asset.
+    const work = mkTmp('fixture');
+    writeLauncher(path.join(work, `codegraph-${target}`, 'bin'));
+    const archive = path.join(work, asset);
+    execSync(`tar -czf ${JSON.stringify(archive)} -C ${JSON.stringify(work)} codegraph-${target}`);
+    fixtureBytes = fs.readFileSync(archive);
+    fixtureSha = crypto.createHash('sha256').update(fixtureBytes).digest('hex');
+
+    server = https.createServer({ key: fs.readFileSync(keyP), cert: fs.readFileSync(certP) }, (req, res) => {
+      const url = req.url || '';
+      if (url.endsWith(`/${asset}`)) {
+        res.writeHead(200); res.end(fixtureBytes);
+      } else if (url.endsWith('/SHA256SUMS')) {
+        if (sumsBody === null) { res.writeHead(404); res.end('not found'); }
+        else { res.writeHead(200); res.end(sumsBody); }
+      } else {
+        res.writeHead(404); res.end('not found');
+      }
+    });
+    await new Promise<void>((resolve) => server.listen(0, '127.0.0.1', resolve));
+    port = (server.address() as AddressInfo).port;
+  }, 30000);
+
+  afterAll(() => { server?.close(); });
+
+  function netEnv(cache: string): Record<string, string> {
+    return {
+      CODEGRAPH_INSTALL_DIR: cache,
+      CODEGRAPH_DOWNLOAD_BASE: `https://127.0.0.1:${port}`,
+      NODE_TLS_REJECT_UNAUTHORIZED: '0',
+    };
+  }
+
+  it('downloads, verifies the checksum, extracts, and execs the bundle', async () => {
+    sumsBody = `${fixtureSha}  ${asset}\n`;
+    const pkg = makePkg('5.0.0-net');
+    const cache = mkTmp('cache');
+    const r = await runShim(pkg, ['--probe-net'], netEnv(cache));
+
+    expect(r.stderr).toContain('downloading');
+    expect(r.stderr).toContain('checksum verified');
+    expect(r.status).toBe(0);
+    expect(r.stdout).toContain('FAKE_BUNDLE_RAN');
+    expect(r.stdout).toContain('--probe-net');
+    expect(fs.existsSync(path.join(cache, 'bundles', `${target}-5.0.0-net`, 'bin', 'codegraph'))).toBe(true);
+  }, 20000);
+
+  it('aborts (exit 1) on a checksum mismatch and caches nothing', async () => {
+    sumsBody = `${'0'.repeat(64)}  ${asset}\n`;
+    const pkg = makePkg('5.0.0-bad');
+    const cache = mkTmp('cache');
+    const r = await runShim(pkg, ['--version'], netEnv(cache));
+
+    expect(r.status).toBe(1);
+    expect(r.stderr).toContain('checksum mismatch');
+    expect(r.stdout).not.toContain('FAKE_BUNDLE_RAN'); // never exec'd a tampered bundle
+    expect(fs.existsSync(path.join(cache, 'bundles', `${target}-5.0.0-bad`))).toBe(false);
+  }, 20000);
+
+  it('proceeds when no SHA256SUMS is published (older releases)', async () => {
+    sumsBody = null; // 404
+    const pkg = makePkg('5.0.0-nosums');
+    const cache = mkTmp('cache');
+    const r = await runShim(pkg, ['--version'], netEnv(cache));
+
+    expect(r.status).toBe(0);
+    expect(r.stderr).toContain('downloading');
+    expect(r.stderr).not.toContain('checksum verified'); // skipped, not failed
+    expect(r.stdout).toContain('FAKE_BUNDLE_RAN');
+  }, 20000);
+});

+ 1 - 1
package.json

@@ -1,6 +1,6 @@
 {
   "name": "@colbymchenry/codegraph",
-  "version": "0.9.3",
+  "version": "0.9.4",
   "description": "Supercharge Claude Code with semantic code intelligence. 94% fewer tool calls • 77% faster exploration • 100% local.",
   "main": "dist/index.js",
   "types": "dist/index.d.ts",

+ 215 - 27
scripts/npm-shim.js

@@ -11,48 +11,236 @@
 // (with node:sqlite), regardless of the user's Node version. The user's Node is
 // only ever a launcher; even an ancient version can run this file.
 //
+// Self-heal (issue #303): some registries — notably the npmmirror/cnpm mirrors,
+// and some corporate proxies — don't reliably mirror the per-platform
+// optionalDependencies. npm treats an unfetchable optional dep as success and
+// silently skips it, so the bundle goes missing and every command fails. When
+// the installed bundle can't be resolved, this shim falls back to downloading
+// the matching bundle straight from GitHub Releases — the very archive
+// install.sh uses — into a cache dir, then runs that. Knobs:
+//   CODEGRAPH_NO_DOWNLOAD=1     disable the network fallback (print guidance)
+//   CODEGRAPH_INSTALL_DIR=DIR   cache location (default: ~/.codegraph)
+//   CODEGRAPH_DOWNLOAD_BASE=URL release-download base (for mirrors/air-gapped)
+//
 // Wired up at release time as the main package's `bin`:
-//   "bin": { "codegraph": "scripts/npm-shim.js" }
+//   "bin": { "codegraph": "npm-shim.js" }
 // with the platform packages listed in `optionalDependencies`.
 
 var childProcess = require('child_process');
+var fs = require('fs');
+var os = require('os');
+var path = require('path');
 
 var target = process.platform + '-' + process.arch; // e.g. darwin-arm64, linux-x64
 var pkg = '@colbymchenry/codegraph-' + target;
 var isWindows = process.platform === 'win32';
+var REPO = 'colbymchenry/codegraph';
+
+main().catch(function (e) {
+  process.stderr.write('codegraph: ' + (e && e.message ? e.message : String(e)) + '\n');
+  process.exit(1);
+});
+
+async function main() {
+  // Happy path: the npm-installed optional dependency. Fall back to a download
+  // when the registry didn't deliver it.
+  var resolved = resolveInstalledBundle() || (await selfHealBundle());
+  var res = childProcess.spawnSync(resolved.command, resolved.args, { stdio: 'inherit' });
+  if (res.error) {
+    process.stderr.write('codegraph: ' + res.error.message + '\n');
+    process.exit(1);
+  }
+  process.exit(res.status === null ? 1 : res.status);
+}
 
-// On Windows the bundle's launcher is a .cmd batch file. Modern Node refuses to
-// spawn .cmd/.bat directly — spawnSync throws EINVAL (the CVE-2024-27980
-// hardening, observed on Node 24). So on Windows we skip the .cmd and invoke the
-// bundled node.exe against the app entry point directly. On unix the bin launcher
-// is a shell script that spawns cleanly.
-var command, args;
-try {
+// Resolve the launcher from the installed per-platform optionalDependency.
+// Returns {command, args} or null if the package isn't installed.
+function resolveInstalledBundle() {
+  try {
+    if (isWindows) {
+      // Modern Node refuses to spawn the bundle's .cmd directly (EINVAL, the
+      // CVE-2024-27980 hardening on Node 24), so invoke the bundled node.exe
+      // against the app entry point and pass --liftoff-only here.
+      var nodeExe = require.resolve(pkg + '/node.exe');
+      var entry = require.resolve(pkg + '/lib/dist/bin/codegraph.js');
+      return { command: nodeExe, args: liftoff(entry) };
+    }
+    return { command: require.resolve(pkg + '/bin/codegraph'), args: process.argv.slice(2) };
+  } catch (e) {
+    return null;
+  }
+}
+
+// Locate the launcher inside an extracted GitHub bundle directory (same
+// node/lib/bin layout as the npm platform package). Returns {command, args} or
+// null when the directory doesn't hold a usable bundle yet.
+function launcherIn(dir) {
   if (isWindows) {
-    command = require.resolve(pkg + '/node.exe');
-    var entry = require.resolve(pkg + '/lib/dist/bin/codegraph.js');
-    // --liftoff-only: keep tree-sitter's WASM grammars off V8's turboshaft tier
-    // to avoid the Zone OOM on Node >= 22 (issues #293/#298). The unix launcher
-    // passes this too; on Windows we invoke node.exe directly so add it here.
-    args = ['--liftoff-only', entry].concat(process.argv.slice(2));
+    var nodeExe = path.join(dir, 'node.exe');
+    var entry = path.join(dir, 'lib', 'dist', 'bin', 'codegraph.js');
+    if (fs.existsSync(nodeExe) && fs.existsSync(entry)) {
+      return { command: nodeExe, args: liftoff(entry) };
+    }
   } else {
-    command = require.resolve(pkg + '/bin/codegraph');
-    args = process.argv.slice(2);
+    var launcher = path.join(dir, 'bin', 'codegraph');
+    if (fs.existsSync(launcher)) return { command: launcher, args: process.argv.slice(2) };
+  }
+  return null;
+}
+
+// --liftoff-only keeps tree-sitter's WASM grammars off V8's turboshaft tier to
+// avoid the Zone OOM on Node >= 22 (issues #293/#298). The unix bin/codegraph
+// launcher already passes it; on Windows we invoke node.exe directly so add it.
+function liftoff(entry) {
+  return ['--liftoff-only', entry].concat(process.argv.slice(2));
+}
+
+// Download + cache the platform bundle from GitHub Releases. Returns
+// {command, args}; exits the process with guidance if it can't.
+async function selfHealBundle() {
+  var version = readVersion();
+  var bundlesDir = path.join(process.env.CODEGRAPH_INSTALL_DIR || path.join(os.homedir(), '.codegraph'), 'bundles');
+  var dest = path.join(bundlesDir, target + '-' + version);
+
+  // Already downloaded by a previous run? Use it even when downloads are
+  // disabled — CODEGRAPH_NO_DOWNLOAD blocks fetching, not a cached bundle.
+  var cached = launcherIn(dest);
+  if (cached) return cached;
+
+  if (process.env.CODEGRAPH_NO_DOWNLOAD) {
+    fail('the network fallback is disabled (CODEGRAPH_NO_DOWNLOAD is set).');
   }
-} catch (e) {
+
+  var asset = 'codegraph-' + target + (isWindows ? '.zip' : '.tar.gz');
+  var base = process.env.CODEGRAPH_DOWNLOAD_BASE || ('https://github.com/' + REPO + '/releases/download');
+  var url = base + '/v' + version + '/' + asset;
+
   process.stderr.write(
-    'codegraph: no prebuilt bundle for ' + target + '.\n' +
-    'Expected the optional package ' + pkg + ' to be installed.\n' +
-    'Try reinstalling:  npm i -g @colbymchenry/codegraph\n' +
-    'Or use the standalone installer (no Node required):\n' +
-    '  curl -fsSL https://raw.githubusercontent.com/colbymchenry/codegraph/main/install.sh | sh\n'
+    'codegraph: platform bundle missing (registry did not provide ' + pkg + ').\n' +
+    'codegraph: downloading ' + asset + ' from GitHub Releases (' + version + ')...\n'
   );
-  process.exit(1);
+
+  // Stage inside bundlesDir so the final rename is on the same filesystem (atomic,
+  // no EXDEV across tmpfs). Strip the archive's top-level codegraph-<target>/ dir.
+  fs.mkdirSync(bundlesDir, { recursive: true });
+  var stage = fs.mkdtempSync(path.join(bundlesDir, '.dl-'));
+  try {
+    var archivePath = path.join(stage, asset);
+    await download(url, archivePath, 6);
+    await verifyChecksum(archivePath, asset, base, version);
+    var extracted = path.join(stage, 'bundle');
+    fs.mkdirSync(extracted);
+    extract(archivePath, extracted);
+
+    var raced = launcherIn(dest); // another process may have finished meanwhile
+    if (raced) { rmrf(stage); return raced; }
+    try {
+      fs.renameSync(extracted, dest);
+    } catch (e) {
+      var other = launcherIn(dest); // lost the race but theirs is valid
+      if (other) { rmrf(stage); return other; }
+      throw e;
+    }
+  } catch (e) {
+    rmrf(stage);
+    fail('download failed (' + e.message + ').\n  URL: ' + url);
+  }
+  rmrf(stage);
+
+  var ready = launcherIn(dest);
+  if (!ready) fail('downloaded bundle is missing its launcher under ' + dest + '.');
+  process.stderr.write('codegraph: bundle ready.\n');
+  return ready;
+}
+
+function readVersion() {
+  try {
+    return require(path.join(__dirname, 'package.json')).version;
+  } catch (e) {
+    fail('could not read this package\'s version to locate a matching release.');
+  }
 }
 
-var res = childProcess.spawnSync(command, args, { stdio: 'inherit' });
-if (res.error) {
-  process.stderr.write('codegraph: ' + res.error.message + '\n');
+// GET with manual redirect following (GitHub release URLs redirect to a CDN).
+function download(url, dest, redirectsLeft) {
+  return new Promise(function (resolve, reject) {
+    var https = require('https');
+    // timeout is an idle/inactivity timeout — it won't kill a slow-but-progressing
+    // download, only a stalled connection (so a blocked mirror fails fast with
+    // guidance instead of hanging the user's command forever).
+    var req = https.get(url, { headers: { 'User-Agent': 'codegraph-npm-shim' }, timeout: 30000 }, function (res) {
+      var status = res.statusCode;
+      if (status >= 300 && status < 400 && res.headers.location) {
+        res.resume();
+        if (redirectsLeft <= 0) { reject(new Error('too many redirects')); return; }
+        download(new URL(res.headers.location, url).toString(), dest, redirectsLeft - 1).then(resolve, reject);
+        return;
+      }
+      if (status !== 200) { res.resume(); reject(new Error('HTTP ' + status)); return; }
+      var file = fs.createWriteStream(dest);
+      res.on('error', reject);
+      res.pipe(file);
+      file.on('error', reject);
+      file.on('finish', function () { file.close(function () { resolve(); }); });
+    });
+    req.on('timeout', function () { req.destroy(new Error('connection timed out')); });
+    req.on('error', reject);
+  });
+}
+
+// Best-effort integrity check. When the release publishes a SHA256SUMS file, the
+// downloaded archive MUST match its listed hash or we abort. When that file is
+// absent (older releases) or simply unreachable, we proceed — the archive still
+// arrived from GitHub over TLS. So tampering/corruption is caught, while a
+// missing checksum never breaks an install.
+async function verifyChecksum(archivePath, asset, base, version) {
+  var sumsPath = archivePath + '.SHA256SUMS';
+  try {
+    await download(base + '/v' + version + '/SHA256SUMS', sumsPath, 6);
+  } catch (e) {
+    return; // not published / unreachable → skip
+  }
+  var expected = null;
+  var lines = fs.readFileSync(sumsPath, 'utf8').split('\n');
+  for (var i = 0; i < lines.length; i++) {
+    var m = lines[i].trim().match(/^([0-9a-fA-F]{64})\s+\*?(.+)$/);
+    if (m && path.basename(m[2].trim()) === asset) { expected = m[1].toLowerCase(); break; }
+  }
+  if (!expected) return; // asset not listed → nothing to check
+  var actual = require('crypto').createHash('sha256').update(fs.readFileSync(archivePath)).digest('hex');
+  if (actual !== expected) {
+    throw new Error('checksum mismatch for ' + asset +
+      ' (expected ' + expected.slice(0, 12) + '…, got ' + actual.slice(0, 12) + '…)');
+  }
+  process.stderr.write('codegraph: checksum verified.\n');
+}
+
+// Extract via the system tar — present on macOS, Linux, and Windows 10+
+// (bsdtar reads .zip too). No third-party dependency in the shim.
+function extract(archive, destDir) {
+  var args = isWindows
+    ? ['-xf', archive, '-C', destDir, '--strip-components=1']
+    : ['-xzf', archive, '-C', destDir, '--strip-components=1'];
+  var res = childProcess.spawnSync('tar', args, { stdio: 'ignore' });
+  if (res.error) throw new Error('tar unavailable: ' + res.error.message);
+  if (res.status !== 0) throw new Error('tar exited ' + res.status);
+}
+
+function rmrf(p) {
+  try { fs.rmSync(p, { recursive: true, force: true }); } catch (e) { /* best effort */ }
+}
+
+function fail(reason) {
+  process.stderr.write(
+    'codegraph: no prebuilt bundle for ' + target + '.\n' +
+    (reason ? 'codegraph: ' + reason + '\n' : '') +
+    'Expected the optional package ' + pkg + ' to be installed.\n' +
+    'A registry mirror (e.g. npmmirror/cnpm) that did not mirror the per-platform\n' +
+    'package is the usual cause. Fixes:\n' +
+    '  - install from the official registry:\n' +
+    '      npm i -g @colbymchenry/codegraph --registry=https://registry.npmjs.org\n' +
+    '  - or use the standalone installer (no Node required):\n' +
+    '      curl -fsSL https://raw.githubusercontent.com/' + REPO + '/main/install.sh | sh\n'
+  );
   process.exit(1);
 }
-process.exit(res.status === null ? 1 : res.status);