Self-contained distribution: bundle Node + node:sqlite, drop better-sqlite3/wasm (closes #238) (#282)

* fix(db): eliminate concurrent-read "database is locked"; add node:sqlite backend (#238)

WAL + busy_timeout were already enabled, so the issue's suggested fix was a
no-op. The real causes, addressed here:

- busy_timeout is now set first (before journal_mode) and lowered 120s -> 5s,
  so open-time pragmas wait out a lock instead of hanging for two minutes.
- getCodeGraph no longer opens a second connection to the default project when
  a tool passes its own projectPath (the in-process lock amplifier).
- The wasm fallback (no WAL) gets a bounded read-retry on SQLITE_BUSY.
- New: node:sqlite backend, preferred over wasm, so installs whose native
  better-sqlite3 build fails land on a real-WAL backend instead of no-WAL wasm.
- codegraph status / codegraph_status now report the effective journal mode, so
  a lock report is triageable (wal vs delete).
- CLI hard-blocks Node < 20 to actually enforce the engines floor.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(db)!: node:sqlite is the sole backend; drop better-sqlite3 + wasm

Now that distribution will bundle a Node 24 runtime, node:sqlite (real SQLite
with WAL + FTS5) is always available. Collapse the three-backend adapter to
node:sqlite only and remove the machinery the other two needed:

- Remove better-sqlite3 (optionalDependency) and node-sqlite3-wasm (dependency).
- Remove WasmDatabaseAdapter, the named->positional param translation, the
  SQLITE_BUSY read-retry, the wasm fallback banner, the backend env override,
  and the native/node-sqlite/wasm selection chain.
- createDatabase now opens node:sqlite directly, with a clear error pointing at
  the bundled release / Node 22.5+ when the module is absent.
- NodeSqliteAdapter.close() is idempotent and pragma() supports { simple }, to
  match the better-sqlite3 behavior callers relied on.
- status (CLI + MCP) reports the single node:sqlite backend; journal-mode
  diagnostics and the getCodeGraph single-connection fix are retained.
- Tests repointed off better-sqlite3 onto node:sqlite.

Net -1044 lines. Running from source now requires Node 22.5+.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 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>

* feat(dist): add Windows PowerShell installer (install.ps1)

The `irm … | iex` one-liner for Windows, mirroring install.sh: detect arch,
pull the matching bundle from Releases, extract to %LOCALAPPDATA%\codegraph,
add it to user PATH. Re-run to upgrade. (Windows bundle production in
build-bundle.sh is still TODO.)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(dist): release workflow + npm packaging; README/CHANGELOG for bundled distro

- .github/workflows/release.yml: manually-triggered (workflow_dispatch) release
  matrix. Builds a self-contained bundle per platform on its own runner
  (darwin-arm64/x64, linux-x64/arm64), publishes a GitHub Release with all
  archives, and publishes the npm thin-installer (shim + per-platform packages).
  Windows targets are TODO (build-bundle.sh is unix-only).
- scripts/pack-npm.sh: assemble the npm packages from built bundles — per-platform
  packages tagged os/cpu + the main shim package with them as optionalDependencies
  (esbuild pattern). Proven locally: npm-install the tarballs, run via the shim,
  resolves the bundle and runs on the bundled Node 24 (node:sqlite / WAL).
- README: install section now leads with the no-Node one-liners (curl|sh, irm|iex)
  then npm/npx; "bundled · none required" badge.
- CHANGELOG: standout headline for the self-contained release, plus Added/Changed/
  Removed for the install channels, node:sqlite backend, and dropped deps.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(dist): Windows bundles + single-trigger release workflow

- build-bundle.sh: add win32-x64 / win32-arm64 targets — download Node's Windows
  zip, bundle node.exe + a .cmd launcher, output a .zip. Verified structurally
  (PE32+ node.exe, CRLF .cmd, portable node_modules). Since there are no native
  addons, any target builds on any OS, so the whole matrix builds on one runner.
- pack-npm.sh: handle .zip bundles and win32 packages (os: win32, node.exe).
- release.yml: simplified to your spec — manual trigger reads the version from
  package.json, builds all platform bundles, creates the GitHub Release with notes
  pulled from CHANGELOG.md, and publishes the npm shim + platform packages.
- BUNDLING.md: Windows + build-anywhere notes; release pipeline documented.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Colby Mchenry
2026-05-21 15:48:04 -05:00
committed by GitHub
co-authored by Claude Opus 4.7
parent b8aec39abd
commit ac52fd76c0
24 changed files with 1054 additions and 794 deletions
+72
View File
@@ -0,0 +1,72 @@
name: Release
# Manually triggered ("Run workflow"). On trigger it:
# 1. reads the version from package.json,
# 2. builds a self-contained bundle for every platform (one runner — there's no
# native compilation, so cross-packaging is fine),
# 3. creates the GitHub Release (tag v<version>) with all archives, using the
# release notes from CHANGELOG.md,
# 4. publishes the npm thin-installer (shim + per-platform packages).
#
# Before triggering: bump package.json and make sure CHANGELOG.md has the matching
# section (## [<version>], or ## [Unreleased]). Set the NPM_TOKEN repo secret.
on:
workflow_dispatch: {}
permissions:
contents: write # create the GitHub Release + tag
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
registry-url: https://registry.npmjs.org
- run: npm ci
- name: Ensure zip/unzip
run: sudo apt-get update -qq && sudo apt-get install -y -qq zip unzip
- name: Build all platform bundles
run: |
for t in darwin-arm64 darwin-x64 linux-x64 linux-arm64 win32-x64 win32-arm64; do
bash scripts/build-bundle.sh "$t"
done
ls -lh release
- name: Resolve version
id: ver
run: echo "version=$(node -p "require('./package.json').version")" >> "$GITHUB_OUTPUT"
- name: Release notes from CHANGELOG.md
run: |
V="${{ steps.ver.outputs.version }}"
node scripts/extract-release-notes.mjs "$V" > notes.md 2>/dev/null \
|| node scripts/extract-release-notes.mjs Unreleased > notes.md 2>/dev/null || true
if [ ! -s notes.md ]; then
echo "::error::No release notes in CHANGELOG.md for [$V] or [Unreleased]."
exit 1
fi
echo "----- release notes -----"; cat notes.md
- name: Create GitHub Release
env:
GH_TOKEN: ${{ github.token }}
run: |
gh release create "v${{ steps.ver.outputs.version }}" \
release/codegraph-* \
--title "v${{ steps.ver.outputs.version }}" \
--notes-file notes.md
- name: Publish to npm
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
run: |
bash scripts/pack-npm.sh "${{ steps.ver.outputs.version }}"
# Platform packages first, then the main shim (which depends on them).
for dir in release/npm/codegraph-* release/npm/main; do
echo "publishing $dir"
( cd "$dir" && npm publish --access public )
done
+1
View File
@@ -49,3 +49,4 @@ test_frameworks
test-languages/
nul
release/
+72
View File
@@ -0,0 +1,72 @@
# 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 | node.exe # 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 | codegraph.cmd # launcher → runs the bundled Node with the app
```
Targets: `darwin-arm64`, `darwin-x64`, `linux-x64`, `linux-arm64`, `win32-x64`,
`win32-arm64`. Unix targets produce `.tar.gz` (shell launcher); Windows produces
`.zip` (`node.exe` + a `.cmd` launcher).
```bash
scripts/build-bundle.sh linux-x64 # -> release/codegraph-linux-x64.tar.gz
scripts/build-bundle.sh win32-x64 # -> release/codegraph-win32-x64.zip
```
Because dropping better-sqlite3 left **zero native addons**, building a bundle is
pure file-packaging — **any** target builds on **any** OS (the whole matrix builds
on one Linux runner). Cross-compilation isn't a concern; only *run-testing* a
bundle needs the target platform (or emulation, 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. **Windows** ([`install.ps1`](install.ps1)) — `irm … | iex`; same flow as
install.sh (detect arch, pull the `.zip` from Releases, add to PATH).
4. **Homebrew / Scoop** — TODO (tap + cask pointing at the Release archives).
## Release pipeline
[`.github/workflows/release.yml`](.github/workflows/release.yml) — manually
triggered. Reads the version from `package.json`, builds every platform bundle on
one runner, creates the GitHub Release (notes from `CHANGELOG.md`), and publishes
the npm shim + per-platform packages. Requires the `NPM_TOKEN` repo secret.
Still TODO:
- **Code signing** — the main gap for "download & run": macOS Gatekeeper needs a
Developer ID + notarization; Windows needs Authenticode. Homebrew softens the
macOS case (handles quarantine).
- Retire the now-vestigial Node-version gate in `src/bin/codegraph.ts` — the
bundle always runs Node 24, and the npm shim does no tree-sitter work.
- Re-wire `npm uninstall` cleanup (the agent-config `preuninstall`) through the
shim — the generated main package doesn't carry it.
+47
View File
@@ -9,7 +9,37 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
### 🎉 Self-contained: CodeGraph bundles its own runtime — install anywhere, on any Node (or none)
**No more `database is locked`. No more native build failures. No more "WASM fallback active."**
CodeGraph used to need `better-sqlite3`, a native module compiled against your exact
Node version. When that build failed (common on Windows and locked-down machines) it
silently dropped to a slow WASM SQLite build with **no WAL** — the root cause of the
intermittent `database is locked` errors on concurrent MCP tool calls
([#238](https://github.com/colbymchenry/codegraph/issues/238)). That entire class of
problem is **gone**: CodeGraph now ships a self-contained Node runtime and uses Node's
built-in `node:sqlite` (real SQLite, full WAL + FTS5).
- ✅ **Zero native compilation** — nothing to build, ever; nothing to rebuild when Node changes.
- ✅ **Runs on any Node version — or with no Node at all.** Install via the standalone installers with no Node present, or keep using `npm`/`npx` on any version (your Node only launches the bundled runtime).
- ✅ **`database is locked` fixed at the root** — real WAL means readers never block on a writer.
- ⚡ **5–10× faster** than the old WASM fallback for anyone who was stuck on it.
```bash
# macOS / Linux — no Node required
curl -fsSL https://raw.githubusercontent.com/colbymchenry/codegraph/main/install.sh | sh
# Windows (PowerShell) — no Node required
irm https://raw.githubusercontent.com/colbymchenry/codegraph/main/install.ps1 | iex
# or, if you have Node (any version):
npm i -g @colbymchenry/codegraph
```
### Added
- **Standalone installers** — one-line install with no Node.js required:
`curl -fsSL .../install.sh | sh` (macOS/Linux) and `irm .../install.ps1 | iex`
(Windows). They fetch the matching self-contained bundle from GitHub Releases
and put `codegraph` on your PATH.
- **Lua**: CodeGraph now indexes Lua (`.lua`) — functions, methods (table `t.f`
and `t:m` definitions become methods with a `t::f` receiver-qualified name),
local variables, `require(...)` imports, and the call edges between them.
@@ -21,6 +51,23 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
signatures, generics, and Roblox instance-path `require(script.Parent.X)`
imports.
### Changed
- **SQLite backend is now Node's built-in `node:sqlite`** (real SQLite, WAL +
FTS5), shipped inside a bundled Node runtime. This fixes the concurrent-read
`database is locked` errors ([#238](https://github.com/colbymchenry/codegraph/issues/238))
at the root and removes the native build step entirely.
- **`npm i -g` / `npx` now install a self-contained bundle.** The main package is
a tiny shim; the runtime ships as per-platform `optionalDependencies`, so the
install works on any Node version (your Node only launches the bundle).
- **`codegraph status`** now reports the effective journal mode (`wal` vs not),
so a `database is locked` report is triageable at a glance.
### Removed
- **`better-sqlite3`** (optional native dependency) and **`node-sqlite3-wasm`**
(WASM fallback) — along with the native-build banner, the WASM fallback path,
and the no-WAL lock retries they required. The dependency tree now has zero
native addons.
### Fixed
- **Installer**: re-running `codegraph install` now removes the broken
auto-sync hooks that pre-0.8 versions wrote to Claude Code's
+20 -6
View File
@@ -8,7 +8,7 @@
[![npm version](https://img.shields.io/npm/v/@colbymchenry/codegraph.svg)](https://www.npmjs.com/package/@colbymchenry/codegraph)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
[![Node.js](https://img.shields.io/badge/Node.js-20--24-green.svg)](https://nodejs.org/)
[![Self-contained](https://img.shields.io/badge/Node.js-bundled%20%C2%B7%20none%20required-brightgreen.svg)](https://nodejs.org/)
[![Windows](https://img.shields.io/badge/Windows-supported-blue.svg)](#)
[![macOS](https://img.shields.io/badge/macOS-supported-blue.svg)](#)
@@ -23,11 +23,24 @@
### Get Started
**No Node.js required** — one command grabs the right build for your OS:
```bash
npx @colbymchenry/codegraph
# macOS / Linux
curl -fsSL https://raw.githubusercontent.com/colbymchenry/codegraph/main/install.sh | sh
# Windows (PowerShell)
irm https://raw.githubusercontent.com/colbymchenry/codegraph/main/install.ps1 | iex
```
<sub>Interactive installer auto-configures your agent(s) — Claude Code, Cursor, Codex CLI, opencode</sub>
Already have Node? Use npm instead (works on any version):
```bash
npx @colbymchenry/codegraph # zero-install, or:
npm i -g @colbymchenry/codegraph
```
<sub>CodeGraph bundles its own runtime — nothing to compile, no native build, works the same everywhere.<br />The interactive installer auto-configures your agent(s) — Claude Code, Cursor, Codex CLI, opencode.</sub>
#### Initialize Projects
@@ -456,10 +469,10 @@ The `.codegraph/config.json` file controls indexing:
**Indexing is slow** — Check that `node_modules` and other large directories are excluded. Use `--quiet` to reduce output overhead.
**Indexing is slow / MCP `database is locked` / WASM fallback active** — `codegraph` ships with a WASM SQLite fallback for environments where `better-sqlite3` (a native module, declared as `optionalDependencies`) can't install. The fallback is 5-10x slower than the native backend and uses a journal mode that lets writers block readers, so MCP queries can also hit `database is locked` while indexing runs. Run `codegraph status` and look at the `Backend:` line:
**Indexing is slow, or MCP hits `database is locked`** — both trace to the SQLite backend. `codegraph` picks the best available, in order: native `better-sqlite3` (fastest; an `optionalDependencies` native module), then Node's built-in `node:sqlite` (Node ≥ 22.5), then a bundled WASM build. Run `codegraph status` and read the **`Backend:`** and **`Journal:`** lines:
- `Backend: native` — you're on the fast path, nothing to do.
- `Backend: wasm` — you're on the slow fallback. Common causes: missing C build tools, prebuilt binary unavailable for your Node version, or your Node version changed after install. Fix:
- `Backend: native` or `node:sqlite` with `Journal: wal` — fast path with lock-free concurrent reads; nothing to do.
- `Backend: wasm` — the native module didn't load *and* `node:sqlite` is unavailable (Node < 22.5). WASM is 5-10x slower and has no WAL, so heavy concurrent use can briefly hit `database is locked`. The simplest fix is Node ≥ 22.5 (you get `node:sqlite` automatically); otherwise restore the native backend:
```bash
# macOS
@@ -479,6 +492,7 @@ The `.codegraph/config.json` file controls indexing:
```
After the fix, `codegraph status` should show `Backend: native`.
- `Journal:` shows anything other than `wal` on a `native` / `node:sqlite` backend — WAL couldn't be enabled on this filesystem (common on network shares and WSL2 `/mnt`), so reads can block on writes. Move the project (with its `.codegraph/` folder) onto a local disk.
**MCP server not connecting** — Ensure the project is initialized/indexed, verify the path in your MCP config, and check that `codegraph serve --mcp` works from the command line.
+152
View File
@@ -0,0 +1,152 @@
/**
* Issue #238 — "database is locked" on concurrent MCP tool calls.
*
* With node:sqlite (real WAL) as the backend, the fixes that remain relevant:
* 1. busy_timeout is a bounded few-second wait (not a 2-minute hang) and WAL is
* active — so a reader never blocks on a concurrent writer.
* 2. The MCP ToolHandler reuses the default instance when a tool passes a
* projectPath pointing at the default project, instead of opening a SECOND
* connection to the same DB.
*/
import { describe, it, expect, beforeAll, afterAll, vi } from 'vitest';
import * as fs from 'fs';
import * as path from 'path';
import * as os from 'os';
import CodeGraph from '../src';
import { ToolHandler } from '../src/mcp/tools';
import { DatabaseConnection } from '../src/db';
/** Normalize a PRAGMA read across return shapes (array | object | scalar). */
function pragmaValue(raw: unknown, key: string): unknown {
const row = Array.isArray(raw) ? raw[0] : raw;
if (row !== null && typeof row === 'object') return (row as Record<string, unknown>)[key];
return row;
}
describe('issue #238 — connection PRAGMAs (#1)', () => {
let dir: string;
let conn: DatabaseConnection;
beforeAll(() => {
dir = fs.mkdtempSync(path.join(os.tmpdir(), 'cg238-pragma-'));
conn = DatabaseConnection.initialize(path.join(dir, 'codegraph.db'));
});
afterAll(() => {
conn.close();
fs.rmSync(dir, { recursive: true, force: true });
});
it('uses a bounded busy_timeout, not the old 2-minute hang', () => {
const ms = Number(pragmaValue(conn.getDb().pragma('busy_timeout'), 'timeout'));
expect(ms).toBeGreaterThan(0);
expect(ms).toBeLessThanOrEqual(30000); // far below the old 120000
});
it('runs in WAL mode — the mode that lets readers proceed during a write', () => {
const mode = String(pragmaValue(conn.getDb().pragma('journal_mode'), 'journal_mode')).toLowerCase();
expect(mode).toBe('wal');
});
it('getJournalMode() surfaces the effective mode for status triage', () => {
expect(conn.getJournalMode()).toBe('wal');
});
});
describe('issue #238 — WAL lets a reader proceed during a writer', () => {
let dir: string;
beforeAll(() => {
dir = fs.mkdtempSync(path.join(os.tmpdir(), 'cg238-wal-'));
});
afterAll(() => {
fs.rmSync(dir, { recursive: true, force: true });
});
it('a read on a 2nd connection succeeds while a writer holds the lock', () => {
const dbPath = path.join(dir, 'codegraph.db');
const writer = DatabaseConnection.initialize(dbPath);
// The property only holds under WAL; skip if the filesystem couldn't enable it.
if (writer.getJournalMode() !== 'wal') {
writer.close();
return;
}
const reader = DatabaseConnection.open(dbPath);
try {
writer.getDb().prepare('BEGIN EXCLUSIVE').run(); // hard write lock, held open
const t0 = Date.now();
const row = reader.getDb().prepare('SELECT COUNT(*) AS c FROM nodes').get() as { c: number };
const waited = Date.now() - t0;
expect(row.c).toBe(0);
expect(waited).toBeLessThan(1000); // proceeds immediately, no busy wait
} finally {
try { writer.getDb().prepare('COMMIT').run(); } catch { /* ignore */ }
reader.close();
writer.close();
}
});
});
describe('issue #238 — ToolHandler reuses the default instance (#2)', () => {
let dir: string;
let cg: CodeGraph;
let root: string;
let handler: ToolHandler;
beforeAll(async () => {
dir = fs.mkdtempSync(path.join(os.tmpdir(), 'cg238-tools-'));
fs.writeFileSync(path.join(dir, 'a.ts'), 'export function helper(): number { return 1; }\n');
fs.writeFileSync(
path.join(dir, 'b.ts'),
"import { helper } from './a';\nexport function main(): number { return helper(); }\n"
);
cg = await CodeGraph.init(dir, { index: true });
root = cg.getProjectRoot();
handler = new ToolHandler(cg);
});
afterAll(() => {
cg.close();
fs.rmSync(dir, { recursive: true, force: true });
});
it('getCodeGraph(defaultRoot) returns the default instance, not a new connection', () => {
const openSpy = vi.spyOn(CodeGraph, 'openSync');
try {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const resolved = (handler as any).getCodeGraph(root);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const nested = (handler as any).getCodeGraph(path.join(root, 'does', 'not', 'exist'));
expect(resolved).toBe(cg);
expect(nested).toBe(cg); // a sub-path resolves up to the same default project
expect(openSpy).not.toHaveBeenCalled(); // no second connection opened
} finally {
openSpy.mockRestore();
}
});
it('concurrent read tool calls (mixed projectPath) all succeed without "database is locked"', async () => {
const openSpy = vi.spyOn(CodeGraph, 'openSync');
try {
const calls: Promise<{ content: Array<{ text: string }>; isError?: boolean }>[] = [
handler.execute('codegraph_search', { query: 'helper' }),
handler.execute('codegraph_search', { query: 'helper', projectPath: root }),
handler.execute('codegraph_callers', { symbol: 'helper', projectPath: root }),
handler.execute('codegraph_callees', { symbol: 'main' }),
handler.execute('codegraph_files', { projectPath: root }),
handler.execute('codegraph_status', { projectPath: root }),
];
const results = await Promise.all(calls);
for (const r of results) {
expect(r.isError).not.toBe(true);
expect(r.content[0]?.text ?? '').not.toMatch(/database is locked/i);
}
// Passing the default project's own path must not open a second connection.
expect(openSpy).not.toHaveBeenCalled();
} finally {
openSpy.mockRestore();
}
});
});
+71
View File
@@ -0,0 +1,71 @@
/**
* node:sqlite backend (issue #238 follow-up).
*
* node:sqlite (Node's built-in real SQLite) is now the sole backend. This drives
* a real index + queries through it, so WAL, FTS5 search, and @named-param
* writes are all exercised end-to-end.
*
* Skipped on Node < 22.5 where node:sqlite doesn't exist.
*/
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import * as fs from 'fs';
import * as path from 'path';
import * as os from 'os';
import CodeGraph from '../src';
let nodeSqliteAvailable = false;
try {
// eslint-disable-next-line @typescript-eslint/no-require-imports
require('node:sqlite');
nodeSqliteAvailable = true;
} catch {
nodeSqliteAvailable = false;
}
describe.skipIf(!nodeSqliteAvailable)('node:sqlite backend — real index + queries', () => {
let dir: string;
let cg: CodeGraph;
beforeAll(async () => {
dir = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-nodesqlite-'));
fs.writeFileSync(path.join(dir, 'a.ts'), 'export function helper(): number { return 1; }\n');
fs.writeFileSync(
path.join(dir, 'b.ts'),
"import { helper } from './a';\nexport function main(): number { return helper(); }\n"
);
cg = await CodeGraph.init(dir, { index: true });
});
afterAll(() => {
cg?.close();
fs.rmSync(dir, { recursive: true, force: true });
});
it('uses the node:sqlite backend', () => {
expect(cg.getBackend()).toBe('node-sqlite');
});
it('runs in WAL mode — the whole reason it beats the wasm fallback', () => {
expect(cg.getJournalMode()).toBe('wal');
});
it('indexed the project (write path: @named-param INSERTs via node:sqlite)', () => {
const stats = cg.getStats();
expect(stats.fileCount).toBe(2);
expect(stats.nodeCount).toBeGreaterThan(0);
});
it('FTS5 search returns the indexed symbol (read path)', () => {
const results = cg.searchNodes('helper');
const names = results.map(r => r.node.name);
expect(names).toContain('helper');
});
it('graph traversal resolves the cross-file caller', () => {
const helper = cg.searchNodes('helper').find(r => r.node.name === 'helper');
expect(helper).toBeTruthy();
const callers = cg.getCallers(helper!.node.id);
expect(callers.map(c => c.node.name)).toContain('main');
});
});
+27 -1
View File
@@ -7,7 +7,7 @@
*/
import { describe, it, expect } from 'vitest';
import { buildNode25BlockBanner } from '../src/bin/node-version-check';
import { buildNode25BlockBanner, buildNodeTooOldBanner, MIN_NODE_MAJOR } from '../src/bin/node-version-check';
describe('buildNode25BlockBanner', () => {
it('embeds the reported Node version in the header', () => {
@@ -41,3 +41,29 @@ describe('buildNode25BlockBanner', () => {
);
});
});
describe('buildNodeTooOldBanner', () => {
it('embeds the reported Node version in the header', () => {
expect(buildNodeTooOldBanner('18.20.0')).toContain(
'Unsupported Node.js version: 18.20.0'
);
});
it('states the supported floor matching MIN_NODE_MAJOR', () => {
expect(MIN_NODE_MAJOR).toBe(20);
expect(buildNodeTooOldBanner('18.0.0')).toContain(
`requires Node.js ${MIN_NODE_MAJOR} or newer`
);
});
it('points users to Node 22 LTS via nvm and Homebrew', () => {
const banner = buildNodeTooOldBanner('16.0.0');
expect(banner).toContain('Node.js 22 LTS');
expect(banner).toContain('nvm install 22');
expect(banner).toContain('brew install node@22');
});
it('documents the CODEGRAPH_ALLOW_UNSAFE_NODE override', () => {
expect(buildNodeTooOldBanner('18.0.0')).toContain('CODEGRAPH_ALLOW_UNSAFE_NODE=1');
});
});
+3 -3
View File
@@ -45,11 +45,11 @@ function cleanupTempDir(dir: string): void {
}
}
// Check if better-sqlite3 native bindings are available
// Check if the node:sqlite backend is available (Node >= 22.5)
function hasSqliteBindings(): boolean {
try {
const Database = require('better-sqlite3');
const db = new Database(':memory:');
const { DatabaseSync } = require('node:sqlite');
const db = new DatabaseSync(':memory:');
db.close();
return true;
} catch {
+9 -51
View File
@@ -1,59 +1,18 @@
/**
* SQLite backend visibility tests
* SQLite backend reporting.
*
* Pins the WASM-fallback banner content + the per-instance backend
* tracking. Closes the visibility gap behind issue #138.
* node:sqlite (Node's built-in real SQLite) is the sole backend. Pin that
* DatabaseConnection / CodeGraph report it and come up in WAL.
*/
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import * as fs from 'fs';
import * as path from 'path';
import * as os from 'os';
import {
buildWasmFallbackBanner,
WASM_FALLBACK_FIX_RECIPE,
} from '../src/db/sqlite-adapter';
import { DatabaseConnection } from '../src/db';
import { CodeGraph } from '../src';
describe('buildWasmFallbackBanner — fix-recipe content', () => {
it('includes the macOS / Linux / cross-platform fix commands', () => {
const banner = buildWasmFallbackBanner();
expect(banner).toContain('WASM SQLite fallback active');
expect(banner).toContain('5-10x slower');
expect(banner).toContain('xcode-select --install');
expect(banner).toContain('apt install build-essential');
expect(banner).toContain('npm rebuild better-sqlite3');
expect(banner).toContain('npm install better-sqlite3 --save');
expect(banner).toContain('codegraph status');
});
it('appends the native load error when one is provided', () => {
const banner = buildWasmFallbackBanner(
"Cannot find module 'better-sqlite3'"
);
expect(banner).toContain(
"Native load error: Cannot find module 'better-sqlite3'"
);
});
it('omits the load-error block when no error is supplied', () => {
const banner = buildWasmFallbackBanner();
expect(banner).not.toContain('Native load error:');
});
});
describe('WASM_FALLBACK_FIX_RECIPE — single source of truth', () => {
it('mentions the three recovery commands', () => {
expect(WASM_FALLBACK_FIX_RECIPE).toContain('xcode-select --install');
expect(WASM_FALLBACK_FIX_RECIPE).toContain('npm rebuild better-sqlite3');
expect(WASM_FALLBACK_FIX_RECIPE).toContain(
'npm install better-sqlite3 --save'
);
});
});
describe('DatabaseConnection — per-instance backend reporting', () => {
describe('DatabaseConnection — backend reporting', () => {
let dir: string;
beforeEach(() => {
@@ -66,11 +25,10 @@ describe('DatabaseConnection — per-instance backend reporting', () => {
}
});
it('reports a concrete backend (native or wasm) for an initialized DB', () => {
const dbPath = path.join(dir, 'test.db');
const conn = DatabaseConnection.initialize(dbPath);
const backend = conn.getBackend();
expect(['native', 'wasm']).toContain(backend);
it('reports the node-sqlite backend in WAL for an initialized DB', () => {
const conn = DatabaseConnection.initialize(path.join(dir, 'test.db'));
expect(conn.getBackend()).toBe('node-sqlite');
expect(conn.getJournalMode()).toBe('wal');
conn.close();
});
@@ -78,7 +36,7 @@ describe('DatabaseConnection — per-instance backend reporting', () => {
fs.writeFileSync(path.join(dir, 'x.ts'), `export function x(): void {}\n`);
const cg = await CodeGraph.init(dir, { index: true });
try {
expect(['native', 'wasm']).toContain(cg.getBackend());
expect(cg.getBackend()).toBe('node-sqlite');
} finally {
cg.destroy();
}
+2 -2
View File
@@ -25,8 +25,8 @@ beforeAll(async () => {
function hasSqliteBindings(): boolean {
try {
const Database = require('better-sqlite3');
const db = new Database(':memory:');
const { DatabaseSync } = require('node:sqlite');
const db = new DatabaseSync(':memory:');
db.close();
return true;
} catch {
+59
View File
@@ -0,0 +1,59 @@
# CodeGraph standalone installer for Windows (PowerShell).
#
# Downloads a self-contained bundle (a vendored Node runtime + the app) from
# GitHub Releases. No Node.js, no build tools required.
#
# irm https://raw.githubusercontent.com/colbymchenry/codegraph/main/install.ps1 | iex
#
# Re-run to upgrade. To uninstall: remove $env:LOCALAPPDATA\codegraph and drop
# its \current\bin entry from your user PATH.
#
# Environment:
# CODEGRAPH_VERSION release tag to install (default: latest)
# CODEGRAPH_INSTALL_DIR install location (default: %LOCALAPPDATA%\codegraph)
$ErrorActionPreference = 'Stop'
$repo = 'colbymchenry/codegraph'
$installDir = if ($env:CODEGRAPH_INSTALL_DIR) { $env:CODEGRAPH_INSTALL_DIR } else { Join-Path $env:LOCALAPPDATA 'codegraph' }
# 1. Detect architecture -> target matching the release archives.
$arch = if ([System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture -eq 'Arm64') { 'arm64' } else { 'x64' }
$target = "win32-$arch"
# 2. Resolve the version (latest release unless pinned).
$version = $env:CODEGRAPH_VERSION
if (-not $version) {
$version = (Invoke-RestMethod "https://api.github.com/repos/$repo/releases/latest").tag_name
}
if (-not $version) { throw "codegraph: could not resolve latest version; set CODEGRAPH_VERSION." }
# 3. Download + extract the bundle into a stable 'current' dir (overwritten on upgrade).
$url = "https://github.com/$repo/releases/download/$version/codegraph-$target.zip"
Write-Host "Installing CodeGraph $version ($target)..."
$tmp = Join-Path $env:TEMP ("cg-" + [guid]::NewGuid().ToString())
New-Item -ItemType Directory -Force -Path $tmp | Out-Null
$zip = Join-Path $tmp 'cg.zip'
Invoke-WebRequest -Uri $url -OutFile $zip
$dest = Join-Path $installDir 'current'
if (Test-Path $dest) { Remove-Item -Recurse -Force $dest }
New-Item -ItemType Directory -Force -Path $dest | Out-Null
Expand-Archive -Path $zip -DestinationPath $dest -Force
# Archives contain a top-level codegraph-<target>\ dir; flatten it.
$inner = Join-Path $dest "codegraph-$target"
if (Test-Path $inner) {
Get-ChildItem -Force $inner | Move-Item -Destination $dest -Force
Remove-Item -Recurse -Force $inner
}
Remove-Item -Recurse -Force $tmp
# 4. Put the launcher dir on the user's PATH.
$binDir = Join-Path $dest 'bin'
$userPath = [Environment]::GetEnvironmentVariable('Path', 'User')
if (($userPath -split ';') -notcontains $binDir) {
[Environment]::SetEnvironmentVariable('Path', "$binDir;$userPath", 'User')
Write-Host "Added $binDir to your PATH (restart your terminal to pick it up)."
}
Write-Host "Installed to $dest"
Write-Host "Run: codegraph --help"
Executable
+83
View File
@@ -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"
-499
View File
@@ -14,7 +14,6 @@
"fast-string-width": "^3.0.2",
"fast-wrap-ansi": "^0.2.0",
"jsonc-parser": "^3.3.1",
"node-sqlite3-wasm": "^0.8.30",
"picomatch": "^4.0.3",
"sisteransi": "^1.0.5",
"tree-sitter-wasms": "^0.1.11",
@@ -32,9 +31,6 @@
},
"engines": {
"node": ">=20.0.0 <25.0.0"
},
"optionalDependencies": {
"better-sqlite3": "^12.4.1"
}
},
"node_modules/@clack/core": {
@@ -970,89 +966,6 @@
"node": ">=12"
}
},
"node_modules/base64-js": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT",
"optional": true
},
"node_modules/better-sqlite3": {
"version": "12.10.0",
"resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-12.10.0.tgz",
"integrity": "sha512-CyzaZRQKyHkB2ZInfTTl2nvT33EbDpjkLEbE8/Zck3Ll6O0qqvuGdrJ45HgtH+HykRg88ITY3AdreBGN70aBSQ==",
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"dependencies": {
"bindings": "^1.5.0",
"prebuild-install": "^7.1.1"
},
"engines": {
"node": "20.x || 22.x || 23.x || 24.x || 25.x || 26.x"
}
},
"node_modules/bindings": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz",
"integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==",
"license": "MIT",
"optional": true,
"dependencies": {
"file-uri-to-path": "1.0.0"
}
},
"node_modules/bl": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz",
"integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==",
"license": "MIT",
"optional": true,
"dependencies": {
"buffer": "^5.5.0",
"inherits": "^2.0.4",
"readable-stream": "^3.4.0"
}
},
"node_modules/buffer": {
"version": "5.7.1",
"resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
"integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT",
"optional": true,
"dependencies": {
"base64-js": "^1.3.1",
"ieee754": "^1.1.13"
}
},
"node_modules/cac": {
"version": "6.7.14",
"resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz",
@@ -1090,13 +1003,6 @@
"node": ">= 16"
}
},
"node_modules/chownr": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz",
"integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==",
"license": "ISC",
"optional": true
},
"node_modules/commander": {
"version": "14.0.3",
"resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz",
@@ -1124,22 +1030,6 @@
}
}
},
"node_modules/decompress-response": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz",
"integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==",
"license": "MIT",
"optional": true,
"dependencies": {
"mimic-response": "^3.1.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/deep-eql": {
"version": "5.0.2",
"resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz",
@@ -1150,36 +1040,6 @@
"node": ">=6"
}
},
"node_modules/deep-extend": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz",
"integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==",
"license": "MIT",
"optional": true,
"engines": {
"node": ">=4.0.0"
}
},
"node_modules/detect-libc": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
"integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
"license": "Apache-2.0",
"optional": true,
"engines": {
"node": ">=8"
}
},
"node_modules/end-of-stream": {
"version": "1.4.5",
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz",
"integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==",
"license": "MIT",
"optional": true,
"dependencies": {
"once": "^1.4.0"
}
},
"node_modules/es-module-lexer": {
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz",
@@ -1236,16 +1096,6 @@
"@types/estree": "^1.0.0"
}
},
"node_modules/expand-template": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz",
"integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==",
"license": "(MIT OR WTFPL)",
"optional": true,
"engines": {
"node": ">=6"
}
},
"node_modules/expect-type": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz",
@@ -1280,20 +1130,6 @@
"fast-string-width": "^3.0.2"
}
},
"node_modules/file-uri-to-path": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
"integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==",
"license": "MIT",
"optional": true
},
"node_modules/fs-constants": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
"integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==",
"license": "MIT",
"optional": true
},
"node_modules/fsevents": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
@@ -1309,48 +1145,6 @@
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/github-from-package": {
"version": "0.0.0",
"resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz",
"integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==",
"license": "MIT",
"optional": true
},
"node_modules/ieee754": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
"integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "BSD-3-Clause",
"optional": true
},
"node_modules/inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
"license": "ISC",
"optional": true
},
"node_modules/ini": {
"version": "1.3.8",
"resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz",
"integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==",
"license": "ISC",
"optional": true
},
"node_modules/jsonc-parser": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz",
@@ -1374,36 +1168,6 @@
"@jridgewell/sourcemap-codec": "^1.5.5"
}
},
"node_modules/mimic-response": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz",
"integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==",
"license": "MIT",
"optional": true,
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/minimist": {
"version": "1.2.8",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
"integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
"license": "MIT",
"optional": true,
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/mkdirp-classic": {
"version": "0.5.3",
"resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz",
"integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==",
"license": "MIT",
"optional": true
},
"node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
@@ -1430,42 +1194,6 @@
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
}
},
"node_modules/napi-build-utils": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz",
"integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==",
"license": "MIT",
"optional": true
},
"node_modules/node-abi": {
"version": "3.87.0",
"resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.87.0.tgz",
"integrity": "sha512-+CGM1L1CgmtheLcBuleyYOn7NWPVu0s0EJH2C4puxgEZb9h8QpR9G2dBfZJOAUhi7VQxuBPMd0hiISWcTyiYyQ==",
"license": "MIT",
"optional": true,
"dependencies": {
"semver": "^7.3.5"
},
"engines": {
"node": ">=10"
}
},
"node_modules/node-sqlite3-wasm": {
"version": "0.8.53",
"resolved": "https://registry.npmjs.org/node-sqlite3-wasm/-/node-sqlite3-wasm-0.8.53.tgz",
"integrity": "sha512-HPuGOPj3L+h3WSf0XikIXTDpsRxlVmzBC3RMgqi3yDg9CEbm/4Hw3rrDodeITqITjm07X4atWLlDMMI8KERMiQ==",
"license": "MIT"
},
"node_modules/once": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
"license": "ISC",
"optional": true,
"dependencies": {
"wrappy": "1"
}
},
"node_modules/pathe": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz",
@@ -1531,75 +1259,6 @@
"node": "^10 || ^12 || >=14"
}
},
"node_modules/prebuild-install": {
"version": "7.1.3",
"resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz",
"integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==",
"license": "MIT",
"optional": true,
"dependencies": {
"detect-libc": "^2.0.0",
"expand-template": "^2.0.3",
"github-from-package": "0.0.0",
"minimist": "^1.2.3",
"mkdirp-classic": "^0.5.3",
"napi-build-utils": "^2.0.0",
"node-abi": "^3.3.0",
"pump": "^3.0.0",
"rc": "^1.2.7",
"simple-get": "^4.0.0",
"tar-fs": "^2.0.0",
"tunnel-agent": "^0.6.0"
},
"bin": {
"prebuild-install": "bin.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/pump": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz",
"integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==",
"license": "MIT",
"optional": true,
"dependencies": {
"end-of-stream": "^1.1.0",
"once": "^1.3.1"
}
},
"node_modules/rc": {
"version": "1.2.8",
"resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz",
"integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==",
"license": "(BSD-2-Clause OR MIT OR Apache-2.0)",
"optional": true,
"dependencies": {
"deep-extend": "^0.6.0",
"ini": "~1.3.0",
"minimist": "^1.2.0",
"strip-json-comments": "~2.0.1"
},
"bin": {
"rc": "cli.js"
}
},
"node_modules/readable-stream": {
"version": "3.6.2",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
"integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
"license": "MIT",
"optional": true,
"dependencies": {
"inherits": "^2.0.3",
"string_decoder": "^1.1.1",
"util-deprecate": "^1.0.1"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/rollup": {
"version": "4.57.1",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.57.1.tgz",
@@ -1645,40 +1304,6 @@
"fsevents": "~2.3.2"
}
},
"node_modules/safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT",
"optional": true
},
"node_modules/semver": {
"version": "7.7.4",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
"integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
"license": "ISC",
"optional": true,
"bin": {
"semver": "bin/semver.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/siginfo": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz",
@@ -1686,53 +1311,6 @@
"dev": true,
"license": "ISC"
},
"node_modules/simple-concat": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz",
"integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT",
"optional": true
},
"node_modules/simple-get": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz",
"integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT",
"optional": true,
"dependencies": {
"decompress-response": "^6.0.0",
"once": "^1.3.1",
"simple-concat": "^1.0.0"
}
},
"node_modules/sisteransi": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz",
@@ -1763,56 +1341,6 @@
"dev": true,
"license": "MIT"
},
"node_modules/string_decoder": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
"integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
"license": "MIT",
"optional": true,
"dependencies": {
"safe-buffer": "~5.2.0"
}
},
"node_modules/strip-json-comments": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz",
"integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==",
"license": "MIT",
"optional": true,
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/tar-fs": {
"version": "2.1.4",
"resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz",
"integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==",
"license": "MIT",
"optional": true,
"dependencies": {
"chownr": "^1.1.1",
"mkdirp-classic": "^0.5.2",
"pump": "^3.0.0",
"tar-stream": "^2.1.4"
}
},
"node_modules/tar-stream": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz",
"integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==",
"license": "MIT",
"optional": true,
"dependencies": {
"bl": "^4.0.3",
"end-of-stream": "^1.4.1",
"fs-constants": "^1.0.0",
"inherits": "^2.0.3",
"readable-stream": "^3.1.1"
},
"engines": {
"node": ">=6"
}
},
"node_modules/tinybench": {
"version": "2.9.0",
"resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz",
@@ -1866,19 +1394,6 @@
"tree-sitter-wasms": "^0.1.11"
}
},
"node_modules/tunnel-agent": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
"integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==",
"license": "Apache-2.0",
"optional": true,
"dependencies": {
"safe-buffer": "^5.0.1"
},
"engines": {
"node": "*"
}
},
"node_modules/typescript": {
"version": "5.9.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
@@ -1900,13 +1415,6 @@
"dev": true,
"license": "MIT"
},
"node_modules/util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
"license": "MIT",
"optional": true
},
"node_modules/vite": {
"version": "5.4.21",
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz",
@@ -2087,13 +1595,6 @@
"engines": {
"node": ">=8"
}
},
"node_modules/wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
"license": "ISC",
"optional": true
}
}
}
-4
View File
@@ -37,7 +37,6 @@
"fast-string-width": "^3.0.2",
"fast-wrap-ansi": "^0.2.0",
"jsonc-parser": "^3.3.1",
"node-sqlite3-wasm": "^0.8.30",
"picomatch": "^4.0.3",
"sisteransi": "^1.0.5",
"tree-sitter-wasms": "^0.1.11",
@@ -50,9 +49,6 @@
"typescript": "^5.0.0",
"vitest": "^2.1.9"
},
"optionalDependencies": {
"better-sqlite3": "^12.4.1"
},
"engines": {
"node": ">=20.0.0 <25.0.0"
}
+98
View File
@@ -0,0 +1,98 @@
#!/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.
#
# Because dropping better-sqlite3 left zero native addons, the recipe is pure
# file-packaging (download the target's Node, copy the app, archive) — so any
# platform's bundle can be built on any OS. No cross-compile, no native runners.
#
# Usage:
# scripts/build-bundle.sh <target> [node-version]
# target: darwin-arm64 | darwin-x64 | linux-x64 | linux-arm64
# | win32-x64 | win32-arm64
# node-version: e.g. v24.16.0 (default below; pin for reproducible builds)
#
# Output:
# unix: release/codegraph-<target>.tar.gz (launcher: bin/codegraph)
# windows: release/codegraph-<target>.zip (launcher: bin/codegraph.cmd)
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
ARCH="${TARGET##*-}" # x64 | arm64
OSFAM="${TARGET%-*}" # darwin | linux | win32
echo "[bundle] target=${TARGET} node=${NODE_VERSION}"
# 1. Download + extract the official Node runtime for the target platform.
if [ "$OSFAM" = "win32" ]; then
NODE_DIST="node-${NODE_VERSION}-win-${ARCH}"
NODE_URL="https://nodejs.org/dist/${NODE_VERSION}/${NODE_DIST}.zip"
echo "[bundle] downloading ${NODE_URL}"
curl -fsSL "$NODE_URL" -o "$WORK/node.zip"
if command -v unzip >/dev/null 2>&1; then
unzip -q "$WORK/node.zip" -d "$WORK"
else
tar -xf "$WORK/node.zip" -C "$WORK" # bsdtar can read zip
fi
NODE_BIN="$WORK/${NODE_DIST}/node.exe"
else
NODE_DIST="node-${NODE_VERSION}-${TARGET}"
NODE_URL="https://nodejs.org/dist/${NODE_VERSION}/${NODE_DIST}.tar.gz"
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"
fi
[ -f "$NODE_BIN" ] || { echo "[bundle] error: node binary not found ($NODE_BIN)" >&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: app + production-only deps (pure JS/wasm → portable across platforms).
STAGE="$WORK/codegraph-${TARGET}"
mkdir -p "$STAGE/lib" "$STAGE/bin"
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. Vendored Node + launcher (the launcher uses the bundled Node by relative
# path, so no system Node is ever needed).
if [ "$OSFAM" = "win32" ]; then
cp "$NODE_BIN" "$STAGE/node.exe"
printf '@"%%~dp0..\\node.exe" "%%~dp0..\\lib\\dist\\bin\\codegraph.js" %%*\r\n' \
> "$STAGE/bin/codegraph.cmd"
else
cp "$NODE_BIN" "$STAGE/node"
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"
fi
# 5. Archive (.zip for Windows, .tar.gz otherwise).
mkdir -p "$OUT"
if [ "$OSFAM" = "win32" ]; then
ARCHIVE="$OUT/codegraph-${TARGET}.zip"
rm -f "$ARCHIVE"
( cd "$WORK" && zip -rqX "$ARCHIVE" "codegraph-${TARGET}" )
else
ARCHIVE="$OUT/codegraph-${TARGET}.tar.gz"
# --no-xattrs: don't embed macOS xattrs that make GNU tar warn on Linux.
tar --no-xattrs -czf "$ARCHIVE" -C "$WORK" "codegraph-${TARGET}"
fi
echo "[bundle] wrote ${ARCHIVE} ($(du -h "$ARCHIVE" | cut -f1))"
+43
View File
@@ -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);
+95
View File
@@ -0,0 +1,95 @@
#!/usr/bin/env bash
#
# Assemble the npm thin-installer packages from built bundles (esbuild pattern).
#
# Produces, under release/npm/:
# codegraph-<target>/ one per built bundle — the vendored Node + app, tagged
# with os/cpu so npm installs only the matching one.
# main/ the @colbymchenry/codegraph shim package: a tiny bin
# that execs the matching platform bundle, with every
# platform package in optionalDependencies.
#
# The release pipeline then `npm publish`es each dir. This does NOT touch the
# repo's package.json — the dev/from-source path keeps working; the *published*
# main package's shape is generated here.
#
# Prereq: run build-bundle.sh for each target first (release/codegraph-*.tar.gz).
# Usage: scripts/pack-npm.sh [version] (default: version from package.json)
set -euo pipefail
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
VERSION="${1:-$(node -p "require('$ROOT/package.json').version")}"
SCOPE="@colbymchenry"
REL="$ROOT/release"
NPM="$REL/npm"
rm -rf "$NPM"
mkdir -p "$NPM/main"
shopt -s nullglob
archives=("$REL"/codegraph-*.tar.gz "$REL"/codegraph-*.zip)
[ ${#archives[@]} -gt 0 ] || { echo "[pack-npm] no bundles in $REL — run build-bundle.sh first" >&2; exit 1; }
targets=()
for archive in "${archives[@]}"; do
fname="$(basename "$archive")"
case "$fname" in
*.tar.gz) base="${fname%.tar.gz}" ;; # codegraph-<target>
*.zip) base="${fname%.zip}" ;;
esac
target="${base#codegraph-}" # <target>, e.g. darwin-arm64 / win32-x64
os="${target%-*}" # darwin | linux | win32
arch="${target##*-}" # arm64 | x64
pkgdir="$NPM/$base"
mkdir -p "$pkgdir"
case "$fname" in
*.zip)
tmpx="$(mktemp -d)"
unzip -q "$archive" -d "$tmpx"
mv "$tmpx/codegraph-${target}"/* "$pkgdir"/
rm -rf "$tmpx"
nodefile="node.exe"
;;
*)
tar -xzf "$archive" -C "$pkgdir" --strip-components=1
nodefile="node"
;;
esac
VERSION="$VERSION" SCOPE="$SCOPE" TARGET="$target" OSV="$os" ARCHV="$arch" NODEFILE="$nodefile" \
node -e '
const fs=require("fs");
fs.writeFileSync(process.argv[1], JSON.stringify({
name: `${process.env.SCOPE}/codegraph-${process.env.TARGET}`,
version: process.env.VERSION,
description: `CodeGraph self-contained bundle for ${process.env.TARGET}`,
os: [process.env.OSV], cpu: [process.env.ARCHV],
files: [process.env.NODEFILE, "lib", "bin"],
license: "MIT"
}, null, 2) + "\n");
' "$pkgdir/package.json"
targets+=("$target")
echo "[pack-npm] ${SCOPE}/codegraph-${target}@${VERSION}"
done
# Main shim package.
cp "$ROOT/scripts/npm-shim.js" "$NPM/main/npm-shim.js"
[ -f "$ROOT/README.md" ] && cp "$ROOT/README.md" "$NPM/main/README.md"
VERSION="$VERSION" SCOPE="$SCOPE" TARGETS="${targets[*]}" \
node -e '
const fs=require("fs");
const opt={};
for (const t of process.env.TARGETS.split(/\s+/).filter(Boolean))
opt[`${process.env.SCOPE}/codegraph-${t}`]=process.env.VERSION;
fs.writeFileSync(process.argv[1], JSON.stringify({
name: `${process.env.SCOPE}/codegraph`,
version: process.env.VERSION,
description: "Local-first code intelligence for AI agents (MCP). Self-contained — bundles its own runtime.",
bin: { codegraph: "npm-shim.js" },
optionalDependencies: opt,
files: ["npm-shim.js","README.md"],
license: "MIT"
}, null, 2) + "\n");
' "$NPM/main/package.json"
echo "[pack-npm] ${SCOPE}/codegraph@${VERSION} (${#targets[@]} platform packages in optionalDependencies)"
echo "[pack-npm] output: $NPM"
+24 -8
View File
@@ -25,7 +25,7 @@ import { getCodeGraphDir, isInitialized } from '../directory';
import { createShimmerProgress } from '../ui/shimmer-progress';
import { getGlyphs } from '../ui/glyphs';
import { buildNode25BlockBanner } from './node-version-check';
import { buildNode25BlockBanner, buildNodeTooOldBanner, MIN_NODE_MAJOR } from './node-version-check';
// Lazy-load heavy modules (CodeGraph, runInstaller) to keep CLI startup fast.
async function loadCodeGraph(): Promise<typeof import('../index')> {
@@ -63,6 +63,16 @@ if (nodeMajor >= 25) {
}
// Override active — banner shown for visibility, continuing.
}
// Enforce the supported Node floor. `engines` in package.json only *warns* on
// install (unless engine-strict), so hard-block here to actually keep users off
// unsupported versions. Mirrors the 25+ block above. See package.json `engines`.
if (nodeMajor < MIN_NODE_MAJOR) {
process.stderr.write(buildNodeTooOldBanner(nodeVersion) + '\n');
if (!process.env.CODEGRAPH_ALLOW_UNSAFE_NODE) {
process.exit(1);
}
// Override active — banner shown for visibility, continuing.
}
// Check if running with no arguments - run installer
if (process.argv.length === 2) {
@@ -689,6 +699,7 @@ program
const stats = cg.getStats();
const changes = cg.getChangedFiles();
const backend = cg.getBackend();
const journalMode = cg.getJournalMode();
// JSON output mode
if (options.json) {
@@ -700,6 +711,7 @@ program
edgeCount: stats.edgeCount,
dbSizeBytes: stats.dbSizeBytes,
backend,
journalMode,
nodesByKind: stats.nodesByKind,
languages: Object.entries(stats.filesByLanguage).filter(([, count]) => count > 0).map(([lang]) => lang),
pendingChanges: {
@@ -724,14 +736,18 @@ program
console.log(` Nodes: ${formatNumber(stats.nodeCount)}`);
console.log(` Edges: ${formatNumber(stats.edgeCount)}`);
console.log(` DB Size: ${(stats.dbSizeBytes / 1024 / 1024).toFixed(2)} MB`);
// Surface the active SQLite backend so users can spot the silent
// WASM fallback (5-10x slower). better-sqlite3 is in
// `optionalDependencies`, so `npm install` succeeds without it
// when the native build fails.
const backendLabel = backend === 'native'
? chalk.green('native')
: chalk.yellow(`wasm ${getGlyphs().dash} slower fallback; run \`npm rebuild better-sqlite3\``);
// Surface the active SQLite backend (node:sqlite — Node's built-in real
// SQLite, full WAL + FTS5, no native build).
const backendLabel = chalk.green(`node:sqlite ${getGlyphs().dash} built-in (full WAL)`);
console.log(` Backend: ${backendLabel}`);
// Effective journal mode: 'wal' means concurrent reads never block on a
// writer; anything else means they can ("database is locked"). node:sqlite
// supports WAL everywhere, so a non-wal mode means the filesystem can't
// (network mounts, WSL2 /mnt). See issue #238.
const journalLabel = journalMode === 'wal'
? chalk.green('wal')
: chalk.yellow(`${journalMode || 'unknown'} ${getGlyphs().dash} WAL inactive; reads can block on writes`);
console.log(` Journal: ${journalLabel}`);
console.log();
// Node breakdown
+37
View File
@@ -37,3 +37,40 @@ export function buildNode25BlockBanner(nodeVersion: string): string {
sep,
].join('\n');
}
/**
* Lowest supported Node.js major version. Matches the `engines` floor in
* package.json. Below this, CodeGraph relies on language features / native APIs
* that aren't present, and the combination is untested. `engines` alone only
* *warns* on install (unless the user set `engine-strict`), so the CLI bootstrap
* also hard-blocks here to actually enforce the floor.
*/
export const MIN_NODE_MAJOR = 20;
/**
* Build the bordered banner shown when CodeGraph detects a Node.js major below
* {@link MIN_NODE_MAJOR}. Pinned via unit test so the recovery commands and the
* override env var can't be silently stripped by future edits.
*
* Uses ASCII glyphs to stay readable on Windows OEM-codepage consoles
* (see ../ui/glyphs.ts for the rationale).
*/
export function buildNodeTooOldBanner(nodeVersion: string): string {
const sep = '-'.repeat(72);
return [
sep,
`[CodeGraph] Unsupported Node.js version: ${nodeVersion}`,
sep,
`CodeGraph requires Node.js ${MIN_NODE_MAJOR} or newer. Older versions lack`,
'language features and native APIs CodeGraph depends on, and are not',
'tested or supported.',
'',
'Fix: install Node.js 22 LTS:',
' nvm install 22 && nvm use 22 # nvm',
' brew install node@22 && brew link --overwrite --force node@22 # Homebrew',
'',
'To override (NOT recommended - unsupported):',
' CODEGRAPH_ALLOW_UNSAFE_NODE=1 codegraph ...',
sep,
].join('\n');
}
+46 -23
View File
@@ -10,7 +10,31 @@ import * as path from 'path';
import { SchemaVersion } from '../types';
import { runMigrations, getCurrentVersion, CURRENT_SCHEMA_VERSION } from './migrations';
export { SqliteDatabase, SqliteBackend, WASM_FALLBACK_FIX_RECIPE } from './sqlite-adapter';
export { SqliteDatabase, SqliteBackend } from './sqlite-adapter';
/**
* Apply connection-level PRAGMAs. Shared by `initialize` and `open` so the two
* paths can't drift.
*
* `busy_timeout` is set FIRST, before any pragma that might touch the database
* file (notably `journal_mode`). If another process holds a write lock at open
* time, the later pragmas — and the connection's first query — then wait out
* the lock instead of throwing "database is locked" immediately. See issue #238.
*
* The 5s window (was 120s) rides out a normal incremental sync; the old
* 2-minute wait presented as a frozen, hung agent. With WAL, reads never block
* on a writer, so this timeout only governs cross-process write contention
* (e.g. the git-hook `codegraph sync` running while the MCP server writes).
*/
function configureConnection(db: SqliteDatabase): void {
db.pragma('busy_timeout = 5000'); // MUST be first — see above
db.pragma('foreign_keys = ON');
db.pragma('journal_mode = WAL'); // node:sqlite supports WAL on every platform
db.pragma('synchronous = NORMAL'); // safe with WAL mode
db.pragma('cache_size = -64000'); // 64 MB page cache
db.pragma('temp_store = MEMORY'); // temp tables in memory
db.pragma('mmap_size = 268435456'); // 256 MB memory-mapped I/O
}
/**
* Database connection wrapper with lifecycle management
@@ -39,17 +63,7 @@ export class DatabaseConnection {
// Create and configure database
const { db, backend } = createDatabase(dbPath);
// Enable foreign keys and WAL mode for better performance
db.pragma('foreign_keys = ON');
db.pragma('journal_mode = WAL');
// Wait up to 2 minutes if database is locked by another process
// (indexing operations can hold locks for extended periods)
db.pragma('busy_timeout = 120000');
// Performance tuning
db.pragma('synchronous = NORMAL'); // Safe with WAL mode
db.pragma('cache_size = -64000'); // 64 MB page cache
db.pragma('temp_store = MEMORY'); // Temp tables in memory
db.pragma('mmap_size = 268435456'); // 256 MB memory-mapped I/O
configureConnection(db);
// Run schema initialization
const schemaPath = path.join(__dirname, 'schema.sql');
@@ -77,17 +91,7 @@ export class DatabaseConnection {
const { db, backend } = createDatabase(dbPath);
// Enable foreign keys and WAL mode
db.pragma('foreign_keys = ON');
db.pragma('journal_mode = WAL');
// Wait up to 2 minutes if database is locked by another process
// (indexing operations can hold locks for extended periods)
db.pragma('busy_timeout = 120000');
// Performance tuning
db.pragma('synchronous = NORMAL');
db.pragma('cache_size = -64000');
db.pragma('temp_store = MEMORY');
db.pragma('mmap_size = 268435456');
configureConnection(db);
// Check and run migrations if needed
const conn = new DatabaseConnection(db, dbPath, backend);
@@ -123,6 +127,25 @@ export class DatabaseConnection {
return this.dbPath;
}
/**
* The journal mode actually in effect (e.g. 'wal', 'delete').
*
* SQLite silently keeps the prior mode if WAL can't be enabled — e.g. on
* filesystems without shared-memory support (some network/virtualized mounts,
* WSL2 /mnt), and always on the wasm backend. So the effective mode can differ
* from what `configureConnection` requested. Surfaced in `codegraph status` so
* a "database is locked" report is triageable: 'wal' ⇒ readers never block on a
* writer; anything else ⇒ they can. See issue #238.
*/
getJournalMode(): string {
const raw = this.db.pragma('journal_mode');
const row = Array.isArray(raw) ? raw[0] : raw;
const mode = row && typeof row === 'object'
? (row as Record<string, unknown>).journal_mode
: row;
return String(mode ?? '').toLowerCase();
}
/**
* Get current schema version
*/
+56 -184
View File
@@ -1,8 +1,13 @@
/**
* SQLite Adapter
*
* Provides a unified interface over better-sqlite3 (native) and
* node-sqlite3-wasm (WASM fallback) for universal cross-platform support.
* Thin wrapper over Node's built-in `node:sqlite` (`DatabaseSync`), exposed
* through a small better-sqlite3-shaped interface so the rest of the codebase
* is storage-agnostic.
*
* CodeGraph ships with a bundled Node runtime, so `node:sqlite` (real SQLite,
* with WAL + FTS5) is always available — there is no native build step and no
* wasm fallback. When run from source instead, it requires Node >= 22.5.
*/
export interface SqliteStatement {
@@ -14,123 +19,34 @@ export interface SqliteStatement {
export interface SqliteDatabase {
prepare(sql: string): SqliteStatement;
exec(sql: string): void;
pragma(str: string): any;
pragma(str: string, options?: { simple?: boolean }): any;
transaction<T>(fn: (...args: any[]) => T): (...args: any[]) => T;
close(): void;
readonly open: boolean;
}
export type SqliteBackend = 'native' | 'wasm';
/**
* One-line summary of the recovery steps shown when WASM fallback is
* active. Single source of truth so the recipe can't drift between the
* stderr banner and the MCP status formatter.
* The active SQLite backend. Only one now (`node:sqlite`); kept as a named type
* so `codegraph status` and the per-instance reporting have a stable shape.
*/
export const WASM_FALLBACK_FIX_RECIPE =
'`xcode-select --install` (macOS) or `apt install build-essential` (Debian/Ubuntu), ' +
'then `npm rebuild better-sqlite3`, or `npm install better-sqlite3 --save` to force-include it.';
export type SqliteBackend = 'node-sqlite';
/**
* Multi-line banner shown to stderr when `createDatabase` falls back to
* WASM. Replaces a one-line `console.warn` that MCP transports (which
* take stdout for the protocol) typically swallow, leaving users on a
* 5-10x slower backend with no signal.
* Wraps Node's built-in `node:sqlite` (`DatabaseSync`) to match the
* better-sqlite3 interface the rest of the code expects.
*
* Exported for unit testing — pinning the recipe content prevents
* future edits from silently stripping the recovery commands.
* node:sqlite is real SQLite compiled into Node, so it supports WAL, FTS5,
* mmap, and `@named` params natively — the only shims needed are the
* better-sqlite3 conveniences node:sqlite omits: a `.pragma()` helper, a
* `.transaction()` helper, and `open` (node:sqlite exposes `isOpen`).
*/
export function buildWasmFallbackBanner(nativeError?: string): string {
const sep = '─'.repeat(72);
const lines = [
sep,
'[CodeGraph] WASM SQLite fallback active (better-sqlite3 unavailable)',
sep,
'Indexing and sync will be 5-10x slower than the native backend.',
'',
'Fix on macOS:',
' xcode-select --install # install C build tools',
' npm rebuild better-sqlite3 # rebuild native binding for current Node',
'',
'Fix on Linux:',
' sudo apt install build-essential python3 make # Debian/Ubuntu',
' # or: sudo yum groupinstall "Development Tools" # RHEL/Fedora',
' npm rebuild better-sqlite3',
'',
'Or force-include as a hard dependency on any platform:',
' npm install better-sqlite3 --save',
'',
'Verify after fix: `codegraph status` should show `Backend: native`.',
];
if (nativeError) {
lines.push('', `Native load error: ${nativeError}`);
}
lines.push(sep);
return lines.join('\n');
}
/**
* Translate @named parameters (better-sqlite3 style) to positional ? params
* for node-sqlite3-wasm, which only supports positional binding.
*
* Returns the rewritten SQL and an ordered list of parameter names.
* If no named params are found, returns null for paramOrder (positional mode).
*/
function translateNamedParams(sql: string): { sql: string; paramOrder: string[] | null } {
const paramOrder: string[] = [];
const rewritten = sql.replace(/@(\w+)/g, (_match, name: string) => {
paramOrder.push(name);
return '?';
});
if (paramOrder.length === 0) {
return { sql, paramOrder: null };
}
return { sql: rewritten, paramOrder };
}
/**
* Convert better-sqlite3-style params to a positional array for node-sqlite3-wasm.
*
* Handles three calling conventions:
* - Named object: run({ id: '1', name: 'a' }) → positional array via paramOrder
* - Positional args: run('a', 'b') → ['a', 'b']
* - No args: run() → undefined
*/
function resolveParams(params: any[], paramOrder: string[] | null): any {
if (params.length === 0) return undefined;
// If paramOrder exists and first arg is a plain object, do named→positional translation
if (paramOrder && params.length === 1 && params[0] !== null && typeof params[0] === 'object' && !Array.isArray(params[0]) && !(params[0] instanceof Buffer) && !(params[0] instanceof Uint8Array)) {
const obj = params[0];
return paramOrder.map(name => obj[name]);
}
// Positional: single value or already an array
if (params.length === 1) return params[0];
return params;
}
/**
* Wraps node-sqlite3-wasm to match the better-sqlite3 interface.
*
* Key differences handled:
* - better-sqlite3 uses @named params; node-sqlite3-wasm uses positional ? only
* - better-sqlite3 uses variadic args: stmt.run(a, b, c)
* - node-sqlite3-wasm uses a single array/object: stmt.run([a, b, c])
* - node-sqlite3-wasm has `isOpen` instead of `open`
* - node-sqlite3-wasm doesn't have a `pragma()` method
* - node-sqlite3-wasm doesn't have a `transaction()` method
*/
class WasmDatabaseAdapter implements SqliteDatabase {
class NodeSqliteAdapter implements SqliteDatabase {
private _db: any;
// Track raw WASM statements so we can finalize them on close.
// node-sqlite3-wasm won't release its file lock if statements are left open.
private _openStmts = new Set<any>();
constructor(dbPath: string) {
// eslint-disable-next-line @typescript-eslint/no-require-imports
const { Database } = require('node-sqlite3-wasm');
this._db = new Database(dbPath);
const { DatabaseSync } = require('node:sqlite');
this._db = new DatabaseSync(dbPath);
}
get open(): boolean {
@@ -138,25 +54,23 @@ class WasmDatabaseAdapter implements SqliteDatabase {
}
prepare(sql: string): SqliteStatement {
const { sql: rewrittenSql, paramOrder } = translateNamedParams(sql);
const stmt = this._db.prepare(rewrittenSql);
this._openStmts.add(stmt);
// node:sqlite matches better-sqlite3's calling convention (variadic
// positional args, or a single object for @named params), so params forward
// through unchanged.
const stmt = this._db.prepare(sql);
return {
run(...params: any[]) {
const resolved = resolveParams(params, paramOrder);
const result = resolved !== undefined ? stmt.run(resolved) : stmt.run();
const r = stmt.run(...params);
return {
changes: result?.changes ?? 0,
lastInsertRowid: result?.lastInsertRowid ?? 0,
changes: Number(r?.changes ?? 0),
lastInsertRowid: r?.lastInsertRowid ?? 0,
};
},
get(...params: any[]) {
const resolved = resolveParams(params, paramOrder);
return resolved !== undefined ? stmt.get(resolved) : stmt.get();
return stmt.get(...params);
},
all(...params: any[]) {
const resolved = resolveParams(params, paramOrder);
return resolved !== undefined ? stmt.all(resolved) : stmt.all();
return stmt.all(...params);
},
};
}
@@ -165,41 +79,21 @@ class WasmDatabaseAdapter implements SqliteDatabase {
this._db.exec(sql);
}
pragma(str: string): any {
pragma(str: string, options?: { simple?: boolean }): any {
const trimmed = str.trim();
// Write pragma: "key = value"
// Write pragma ("key = value"): node:sqlite is real SQLite, so every pragma
// (WAL, mmap, synchronous, …) applies as-is.
if (trimmed.includes('=')) {
const eqIdx = trimmed.indexOf('=');
const key = trimmed.substring(0, eqIdx).trim();
const value = trimmed.substring(eqIdx + 1).trim();
// WAL is not supported in WASM SQLite — use DELETE journal mode
if (key === 'journal_mode' && value.toUpperCase() === 'WAL') {
this._db.exec('PRAGMA journal_mode = DELETE');
return;
}
// mmap is not available in WASM — silently skip
if (key === 'mmap_size') {
return;
}
// synchronous = NORMAL is unsafe without WAL — use FULL
if (key === 'synchronous' && value.toUpperCase() === 'NORMAL') {
this._db.exec('PRAGMA synchronous = FULL');
return;
}
this._db.exec(`PRAGMA ${key} = ${value}`);
this._db.exec(`PRAGMA ${trimmed}`);
return;
}
// Read pragma: "key" — return the value
const stmt = this._db.prepare(`PRAGMA ${trimmed}`);
const result = stmt.get();
stmt.finalize();
return result;
// Read pragma. Default: the row object (e.g. { journal_mode: 'wal' }).
// `{ simple: true }` returns just the single column value, like better-sqlite3.
const row = this._db.prepare(`PRAGMA ${trimmed}`).get();
if (options?.simple) {
return row && typeof row === 'object' ? Object.values(row)[0] : row;
}
return row;
}
transaction<T>(fn: (...args: any[]) => T): (...args: any[]) => T {
@@ -217,51 +111,29 @@ class WasmDatabaseAdapter implements SqliteDatabase {
}
close(): void {
// Finalize all tracked statements before closing.
// node-sqlite3-wasm won't release its directory-based file lock
// if any prepared statements remain open.
for (const stmt of this._openStmts) {
try { stmt.finalize(); } catch { /* already finalized */ }
}
this._openStmts.clear();
this._db.close();
// node:sqlite's DatabaseSync.close() throws if already closed; make it
// idempotent to match better-sqlite3 (callers may close more than once).
if (this._db.isOpen) this._db.close();
}
}
/**
* Create a database connection. Tries native better-sqlite3 first,
* falls back to node-sqlite3-wasm. Returns the active backend
* alongside the db so each `DatabaseConnection` can report its own
* backend per-instance — MCP can open multiple project DBs in one
* process (`tools.ts` getCodeGraph cache), so a process-global would
* race / overwrite.
* Create a database connection backed by `node:sqlite`.
*
* Returns the active backend alongside the db so each `DatabaseConnection` can
* report it per-instance — MCP can open multiple project DBs in one process, so
* a process-global would race.
*/
export function createDatabase(dbPath: string): { db: SqliteDatabase; backend: SqliteBackend } {
let nativeError: string | undefined;
let wasmError: string | undefined;
// Try native better-sqlite3 first
try {
// eslint-disable-next-line @typescript-eslint/no-require-imports
const Database = require('better-sqlite3');
const db = new Database(dbPath);
return { db: db as SqliteDatabase, backend: 'native' };
return { db: new NodeSqliteAdapter(dbPath), backend: 'node-sqlite' };
} catch (error) {
nativeError = error instanceof Error ? error.message : String(error);
const msg = error instanceof Error ? error.message : String(error);
throw new Error(
'Failed to open SQLite via the built-in node:sqlite module.\n' +
'CodeGraph requires node:sqlite (Node.js 22.5+). Install the self-contained\n' +
'CodeGraph release (it bundles a compatible Node), or run on Node 22.5+.\n' +
`Underlying error: ${msg}`
);
}
// Fall back to WASM
try {
const db = new WasmDatabaseAdapter(dbPath);
console.warn(buildWasmFallbackBanner(nativeError));
return { db, backend: 'wasm' };
} catch (error) {
wasmError = error instanceof Error ? error.message : String(error);
}
throw new Error(
`Failed to load any SQLite backend.\n` +
` Native (better-sqlite3): ${nativeError}\n` +
` WASM (node-sqlite3-wasm): ${wasmError}`
);
}
+13 -4
View File
@@ -613,15 +613,24 @@ export class CodeGraph {
}
/**
* Active SQLite backend for this project's connection. `wasm` means
* the native better-sqlite3 install failed and the WASM fallback is
* serving requests at 5-10x the latency. Surfaced via `codegraph
* status` and the `codegraph_status` MCP tool.
* Active SQLite backend for this project's connection (`node-sqlite` — Node's
* built-in real-SQLite module). Surfaced via `codegraph status` and the
* `codegraph_status` MCP tool alongside the effective journal mode.
*/
getBackend(): import('./db').SqliteBackend {
return this.db.getBackend();
}
/**
* The journal mode actually in effect ('wal', 'delete', …). 'wal' means
* readers never block on a concurrent writer; anything else means they can,
* which is the precondition for the "database is locked" failures in issue
* #238. Surfaced via `codegraph status` and the `codegraph_status` MCP tool.
*/
getJournalMode(): string {
return this.db.getJournalMode();
}
// ===========================================================================
// Node Operations
// ===========================================================================
+24 -9
View File
@@ -11,7 +11,6 @@ import { writeFileSync, readFileSync, existsSync } from 'fs';
import { clamp, validatePathWithinRoot } from '../utils';
import { tmpdir } from 'os';
import { join } from 'path';
import { WASM_FALLBACK_FIX_RECIPE } from '../db';
/** Maximum output length to prevent context bloat (characters) */
const MAX_OUTPUT_LENGTH = 15000;
@@ -542,6 +541,17 @@ export class ToolHandler {
throw new Error(`CodeGraph not initialized in ${projectPath}. Run 'codegraph init' in that project first.`);
}
// If the path resolves to the default project, reuse the already-open
// default instance rather than opening a SECOND connection to the same DB.
// A duplicate connection serializes reads against the watcher's auto-sync
// writes; on the wasm backend (no WAL) that surfaces as intermittent
// "database is locked" on concurrent tool calls. See issue #238. Deliberately
// not cached under projectPath — the server owns and closes the default
// instance, so routing it through projectCache.closeAll() would double-close it.
if (this.cg && this.cg.getProjectRoot() === resolvedRoot) {
return this.cg;
}
// Check if we already have this resolved root cached (different path, same project)
if (this.projectCache.has(resolvedRoot)) {
const cg = this.projectCache.get(resolvedRoot)!;
@@ -1321,16 +1331,21 @@ export class ToolHandler {
`**Database size:** ${(stats.dbSizeBytes / 1024 / 1024).toFixed(2)} MB`,
];
// Surface the active SQLite backend. Without this, users on the
// silent WASM fallback (better-sqlite3 install failed) see "slow"
// indexing and DB-lock errors with no signal of why.
const backend = cg.getBackend();
if (backend === 'native') {
lines.push(`**Backend:** native (better-sqlite3)`);
// Surface the active SQLite backend (node:sqlite, Node's built-in real
// SQLite — full WAL + FTS5, no native build).
lines.push(`**Backend:** node:sqlite (Node built-in) — full WAL + FTS5`);
// Effective journal mode. 'wal' ⇒ concurrent reads never block on a writer;
// anything else ⇒ they can ("database is locked"). node:sqlite supports WAL
// everywhere, so a non-wal mode means the filesystem can't (network/
// virtualized mounts, WSL2 /mnt). See issue #238.
const journalMode = cg.getJournalMode();
if (journalMode === 'wal') {
lines.push(`**Journal mode:** wal (concurrent reads safe)`);
} else {
lines.push(
`**Backend:** ⚠ wasm (better-sqlite3 unavailable) — ` +
`5-10x slower than native. Fix: ${WASM_FALLBACK_FIX_RECIPE}`
`**Journal mode:** ⚠ ${journalMode || 'unknown'} — WAL not active, so reads ` +
`can block on a concurrent write (WAL appears unsupported on this filesystem)`
);
}