Bläddra i källkod

feat(dist): self-contained bundle prototype (vendored Node + install channels)

Phase 3 of the node:sqlite migration: ship a vendored Node runtime so CodeGraph
runs with no system Node and no native build (node:sqlite is built in).

- scripts/build-bundle.sh: build a per-platform archive (official Node + dist +
  prod deps + launcher). Same recipe per platform; pins Node v24.16.0.
- install.sh: curl|sh installer (no Node required) — detects os/arch, pulls the
  archive from Releases, symlinks onto PATH; re-run to upgrade, --uninstall to
  remove. The VPS/SSH path.
- scripts/npm-shim.js: thin launcher for the npm channel — resolves the
  per-platform optionalDependency bundle and execs it, so `npm i -g` keeps
  working and the real work runs on the bundled Node regardless of the user's.
- BUNDLING.md: distribution design + release-pipeline TODO (CI matrix, platform
  packages, code signing, brew, retiring the Node-version gate).

Validated end-to-end: darwin-arm64 and linux-x64 bundles both run init + index +
status (Backend: node:sqlite, Journal: wal) + FTS query with NO system Node —
linux-x64 verified in a clean ubuntu:24.04 amd64 container. Release archives are
gitignored; CI will produce and upload them.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Colby McHenry 1 månad sedan
förälder
incheckning
c3d8e24ce9
5 ändrade filer med 259 tillägg och 0 borttagningar
  1. 1 0
      .gitignore
  2. 62 0
      BUNDLING.md
  3. 83 0
      install.sh
  4. 70 0
      scripts/build-bundle.sh
  5. 43 0
      scripts/npm-shim.js

+ 1 - 0
.gitignore

@@ -49,3 +49,4 @@ test_frameworks
 test-languages/
 
 nul
+release/

+ 62 - 0
BUNDLING.md

@@ -0,0 +1,62 @@
+# Distribution: self-contained bundles
+
+CodeGraph ships a **vendored Node runtime** alongside the app. Because Node 22.5+
+has a built-in real SQLite (`node:sqlite`, with WAL + FTS5), bundling Node means:
+
+- **No native build** — `better-sqlite3` is gone, so there are zero native addons
+  to compile or rebuild.
+- **No wasm fallback** — and therefore no more `database is locked` (issue #238).
+- **No Node-version dependence** — the app always runs on the bundled Node,
+  whatever the user has (or doesn't have) installed.
+
+## What's in a bundle
+
+Built by [`scripts/build-bundle.sh`](scripts/build-bundle.sh) — one archive per
+platform, identical recipe (only the Node download differs):
+
+```
+codegraph-<target>/
+  node                     # official Node runtime for <target>
+  lib/
+    dist/                  # compiled app (+ tree-sitter .wasm grammars, schema.sql)
+    node_modules/          # production deps only (pure JS / wasm — portable)
+  bin/
+    codegraph              # launcher: exec "$DIR/node" "$DIR/lib/dist/bin/codegraph.js" "$@"
+```
+
+Targets: `darwin-arm64`, `darwin-x64`, `linux-x64`, `linux-arm64` (Windows: TODO).
+
+```bash
+scripts/build-bundle.sh linux-x64            # -> release/codegraph-linux-x64.tar.gz
+scripts/build-bundle.sh darwin-arm64 v24.16.0
+```
+
+Note: the script does **not** cross-compile — it downloads the official Node
+binary for `<target>`, but to *run-test* a bundle you must be on that platform
+(or emulate it, e.g. `docker run --platform linux/amd64`).
+
+## Install channels (all deliver the same bundle)
+
+1. **`curl | sh`** ([`install.sh`](install.sh)) — no Node required; ideal for a
+   fresh Linux VPS over SSH. Detects os/arch, pulls the archive from GitHub
+   Releases, symlinks `codegraph` onto PATH. Re-run to upgrade; `--uninstall` to
+   remove.
+2. **npm** ([`scripts/npm-shim.js`](scripts/npm-shim.js)) — preserves
+   `npm i -g @colbymchenry/codegraph`. The main package is a tiny shim; the
+   bundles ship as per-platform `optionalDependencies`
+   (`@colbymchenry/codegraph-<target>` with `os`/`cpu`), so npm installs only the
+   matching one. The shim — run by the user's Node — execs the bundle, so the
+   real work runs on the bundled Node 24. Works even on old Node.
+3. **Homebrew / Scoop** — TODO (tap + cask pointing at the Release archives).
+
+## Release pipeline (TODO)
+
+- CI matrix (one runner per os/arch) runs `build-bundle.sh`, uploads each archive
+  to the GitHub Release.
+- Publish the npm main shim package + the per-platform packages.
+- **Code signing** is the main gap for "download & run": macOS Gatekeeper needs a
+  Developer ID + notarization; Windows needs Authenticode. Homebrew softens the
+  macOS case (handles quarantine).
+- Once bundles ship, retire the Node-version gate in `src/bin/codegraph.ts` — the
+  bundle always runs Node 24, and the npm shim does no tree-sitter work, so no
+  version check is needed.

+ 83 - 0
install.sh

@@ -0,0 +1,83 @@
+#!/bin/sh
+#
+# CodeGraph standalone installer.
+#
+# Downloads a self-contained bundle (a vendored Node runtime + the app) from
+# GitHub Releases. No Node.js, no build tools, no npm required — ideal for a
+# fresh Linux VPS over SSH.
+#
+#   curl -fsSL https://raw.githubusercontent.com/colbymchenry/codegraph/main/install.sh | sh
+#
+# Upgrade:   re-run the same command.
+# Uninstall: curl -fsSL .../install.sh | sh -s -- --uninstall
+#
+# Environment:
+#   CODEGRAPH_VERSION      release tag to install (default: latest)
+#   CODEGRAPH_INSTALL_DIR  bundle location   (default: ~/.codegraph)
+#   CODEGRAPH_BIN_DIR      symlink location  (default: ~/.local/bin)
+set -eu
+
+REPO="colbymchenry/codegraph"
+INSTALL_DIR="${CODEGRAPH_INSTALL_DIR:-$HOME/.codegraph}"
+BIN_DIR="${CODEGRAPH_BIN_DIR:-$HOME/.local/bin}"
+
+if [ "${1:-}" = "--uninstall" ]; then
+  rm -f "$BIN_DIR/codegraph"
+  rm -rf "$INSTALL_DIR"
+  echo "CodeGraph uninstalled (removed $INSTALL_DIR and $BIN_DIR/codegraph)."
+  exit 0
+fi
+
+# 1. Detect platform → target triple matching the release archives.
+os="$(uname -s)"
+arch="$(uname -m)"
+case "$os" in
+  Darwin) os="darwin" ;;
+  Linux)  os="linux" ;;
+  *) echo "codegraph: unsupported OS '$os'." >&2; exit 1 ;;
+esac
+case "$arch" in
+  arm64|aarch64) arch="arm64" ;;
+  x86_64|amd64)  arch="x64" ;;
+  *) echo "codegraph: unsupported architecture '$arch'." >&2; exit 1 ;;
+esac
+target="${os}-${arch}"
+
+# 2. Resolve the version (latest release unless pinned).
+version="${CODEGRAPH_VERSION:-}"
+if [ -z "$version" ]; then
+  version="$(curl -fsSL "https://api.github.com/repos/$REPO/releases/latest" \
+    | sed -n 's/.*"tag_name": *"\([^"]*\)".*/\1/p' | head -n1)"
+fi
+[ -n "$version" ] || { echo "codegraph: could not resolve latest version; set CODEGRAPH_VERSION." >&2; exit 1; }
+
+# 3. Download + extract the bundle.
+url="https://github.com/$REPO/releases/download/$version/codegraph-${target}.tar.gz"
+echo "Installing CodeGraph $version ($target)..."
+tmp="$(mktemp -d)"
+trap 'rm -rf "$tmp"' EXIT
+curl -fsSL "$url" -o "$tmp/cg.tar.gz" || { echo "codegraph: download failed: $url" >&2; exit 1; }
+
+dest="$INSTALL_DIR/versions/$version"
+rm -rf "$dest"
+mkdir -p "$dest"
+# Archives contain a top-level codegraph-<target>/ dir; strip it.
+tar -xzf "$tmp/cg.tar.gz" -C "$dest" --strip-components=1
+
+# 4. Symlink the launcher onto PATH and mark the current version.
+mkdir -p "$BIN_DIR"
+ln -sf "$dest/bin/codegraph" "$BIN_DIR/codegraph"
+ln -sfn "$dest" "$INSTALL_DIR/current"
+
+echo "Installed to $dest"
+echo "Linked     $BIN_DIR/codegraph"
+case ":$PATH:" in
+  *":$BIN_DIR:"*) ;;
+  *)
+    echo ""
+    echo "$BIN_DIR is not on your PATH. Add it:"
+    echo "  export PATH=\"$BIN_DIR:\$PATH\""
+    ;;
+esac
+echo ""
+echo "Done. Run: codegraph --help"

+ 70 - 0
scripts/build-bundle.sh

@@ -0,0 +1,70 @@
+#!/usr/bin/env bash
+#
+# Build a self-contained CodeGraph bundle: an official Node runtime + the
+# compiled app + its production deps, so CodeGraph runs with NO system Node and
+# NO native build — node:sqlite is built into the bundled Node. One archive per
+# platform; the recipe is identical across platforms (only the Node download
+# differs), so a CI matrix produces all of them.
+#
+# Usage:
+#   scripts/build-bundle.sh <target> [node-version]
+#     target:        darwin-arm64 | darwin-x64 | linux-x64 | linux-arm64
+#     node-version:  e.g. v24.16.0 (default below; pin for reproducible builds)
+#
+# Output: release/codegraph-<target>.tar.gz  (extracts to codegraph-<target>/)
+#
+# NOTE: does not cross-compile — the bundled Node binary is the official build
+# for <target>, but to *run-test* a bundle you must be on that platform.
+set -euo pipefail
+
+TARGET="${1:?usage: build-bundle.sh <target> [node-version]}"
+NODE_VERSION="${2:-v24.16.0}"
+
+ROOT="$(cd "$(dirname "$0")/.." && pwd)"
+OUT="$ROOT/release"
+WORK="$(mktemp -d)"
+trap 'rm -rf "$WORK"' EXIT
+
+NODE_DIST="node-${NODE_VERSION}-${TARGET}"
+NODE_URL="https://nodejs.org/dist/${NODE_VERSION}/${NODE_DIST}.tar.gz"
+
+echo "[bundle] target=${TARGET} node=${NODE_VERSION}"
+
+# 1. Download + extract the official Node runtime for the target platform.
+echo "[bundle] downloading ${NODE_URL}"
+curl -fsSL "$NODE_URL" -o "$WORK/node.tar.gz"
+tar -xzf "$WORK/node.tar.gz" -C "$WORK"
+NODE_BIN="$WORK/${NODE_DIST}/bin/node"
+[ -f "$NODE_BIN" ] || { echo "[bundle] error: node binary not found in tarball" >&2; exit 1; }
+
+# 2. Build the app (compiled JS + copied wasm/schema assets).
+echo "[bundle] building app"
+( cd "$ROOT" && npm run build >/dev/null )
+
+# 3. Stage: vendored node + app + production-only deps + launcher.
+STAGE="$WORK/codegraph-${TARGET}"
+mkdir -p "$STAGE/lib" "$STAGE/bin"
+cp "$NODE_BIN" "$STAGE/node"
+cp -R "$ROOT/dist" "$STAGE/lib/dist"
+cp "$ROOT/package.json" "$ROOT/package-lock.json" "$STAGE/lib/"
+echo "[bundle] installing production dependencies"
+( cd "$STAGE/lib" && npm ci --omit=dev --ignore-scripts >/dev/null 2>&1 )
+rm -f "$STAGE/lib/package-lock.json"
+
+# 4. Launcher: exec the vendored Node with the app entry. `exec` replaces the
+#    shell so there's a single process, and the absolute path means the bundled
+#    Node is used regardless of what's (or isn't) on the user's PATH.
+cat > "$STAGE/bin/codegraph" <<'LAUNCH'
+#!/bin/sh
+DIR="$(cd "$(dirname "$0")/.." && pwd)"
+exec "$DIR/node" "$DIR/lib/dist/bin/codegraph.js" "$@"
+LAUNCH
+chmod +x "$STAGE/bin/codegraph"
+
+# 5. Archive.
+mkdir -p "$OUT"
+ARCHIVE="$OUT/codegraph-${TARGET}.tar.gz"
+# --no-xattrs: don't embed macOS extended attributes (com.apple.provenance),
+# which make GNU tar warn noisily when the archive is extracted on Linux.
+tar --no-xattrs -czf "$ARCHIVE" -C "$WORK" "codegraph-${TARGET}"
+echo "[bundle] wrote ${ARCHIVE} ($(du -h "$ARCHIVE" | cut -f1))"

+ 43 - 0
scripts/npm-shim.js

@@ -0,0 +1,43 @@
+#!/usr/bin/env node
+'use strict';
+//
+// npm thin-installer launcher for CodeGraph.
+//
+// The heavy artifact (a vendored Node runtime + the app) ships as a per-platform
+// optionalDependency: @colbymchenry/codegraph-<platform>-<arch>. npm installs
+// only the one matching the host, via each package's `os`/`cpu` fields (the
+// esbuild pattern). This shim — run by the user's OWN Node — locates that bundle
+// and execs its launcher, so the real work always runs on the bundled Node 24
+// (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.
+//
+// Wired up at release time as the main package's `bin`:
+//   "bin": { "codegraph": "scripts/npm-shim.js" }
+// with the platform packages listed in `optionalDependencies`.
+
+var childProcess = require('child_process');
+
+var target = process.platform + '-' + process.arch; // e.g. darwin-arm64, linux-x64
+var pkg = '@colbymchenry/codegraph-' + target;
+var launcher = process.platform === 'win32' ? 'bin/codegraph.cmd' : 'bin/codegraph';
+
+var binPath;
+try {
+  binPath = require.resolve(pkg + '/' + launcher);
+} catch (e) {
+  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'
+  );
+  process.exit(1);
+}
+
+var res = childProcess.spawnSync(binPath, process.argv.slice(2), { stdio: 'inherit' });
+if (res.error) {
+  process.stderr.write('codegraph: ' + res.error.message + '\n');
+  process.exit(1);
+}
+process.exit(res.status === null ? 1 : res.status);