From 8346440592678feeafc662f20770df48bbb5f873 Mon Sep 17 00:00:00 2001 From: Colby McHenry Date: Sat, 14 Feb 2026 00:56:15 -0600 Subject: [PATCH] Add WASM fallbacks for tree-sitter and SQLite, fix installer Replace native tree-sitter with web-tree-sitter + tree-sitter-wasms for universal cross-platform support. Add node-sqlite3-wasm as a fallback when better-sqlite3 native bindings aren't available. Move better-sqlite3 and sqlite-vss to optionalDependencies so installs never fail. Fix installer to use npx fallback when global npm install fails, so MCP config, hooks, and quick-start instructions all work without the bare codegraph command in PATH. Fix tests: update schema version expectation, fix db test paths and method names, extract MAX_OUTPUT_LENGTH as module constant, normalize Windows path separators in import resolver. --- __tests__/extraction.test.ts | 8 +- __tests__/foundation.test.ts | 2 +- __tests__/pr19-improvements.test.ts | 48 +-- package-lock.json | 565 ++-------------------------- package.json | 25 +- scripts/postinstall.js | 3 - src/bin/codegraph.ts | 57 ++- src/db/index.ts | 14 +- src/db/migrations.ts | 16 +- src/db/queries.ts | 48 +-- src/db/sqlite-adapter.ts | 227 +++++++++++ src/extraction/grammars.ts | 172 ++++----- src/extraction/index.ts | 6 +- src/extraction/tree-sitter.ts | 17 +- src/index.ts | 5 +- src/installer/banner.ts | 5 +- src/installer/config-writer.ts | 19 +- src/installer/index.ts | 29 +- src/mcp/tools.ts | 14 +- src/resolution/import-resolver.ts | 2 +- src/vectors/manager.ts | 6 +- src/vectors/search.ts | 13 +- src/web-tree-sitter.d.ts | 182 +++++++++ tsconfig.json | 5 +- 24 files changed, 707 insertions(+), 781 deletions(-) create mode 100644 src/db/sqlite-adapter.ts create mode 100644 src/web-tree-sitter.d.ts diff --git a/__tests__/extraction.test.ts b/__tests__/extraction.test.ts index 0a5c46b..9f666f6 100644 --- a/__tests__/extraction.test.ts +++ b/__tests__/extraction.test.ts @@ -4,16 +4,20 @@ * Tests for the tree-sitter extraction system. */ -import { describe, it, expect, beforeEach, afterEach } from 'vitest'; +import { describe, it, expect, beforeAll, beforeEach, afterEach } from 'vitest'; import * as fs from 'fs'; import * as path from 'path'; import * as os from 'os'; import { CodeGraph } from '../src'; import { extractFromSource, scanDirectory, shouldIncludeFile } from '../src/extraction'; -import { detectLanguage, isLanguageSupported, getSupportedLanguages } from '../src/extraction/grammars'; +import { detectLanguage, isLanguageSupported, getSupportedLanguages, initGrammars } from '../src/extraction/grammars'; import { normalizePath } from '../src/utils'; import { DEFAULT_CONFIG } from '../src/types'; +beforeAll(async () => { + await initGrammars(); +}); + // Create a temporary directory for each test function createTempDir(): string { return fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-test-')); diff --git a/__tests__/foundation.test.ts b/__tests__/foundation.test.ts index f7729cd..6aa01ad 100644 --- a/__tests__/foundation.test.ts +++ b/__tests__/foundation.test.ts @@ -317,7 +317,7 @@ describe('Database Connection', () => { const version = db.getSchemaVersion(); expect(version).not.toBeNull(); - expect(version?.version).toBe(1); + expect(version?.version).toBe(2); db.close(); }); diff --git a/__tests__/pr19-improvements.test.ts b/__tests__/pr19-improvements.test.ts index dcaa564..1d0889b 100644 --- a/__tests__/pr19-improvements.test.ts +++ b/__tests__/pr19-improvements.test.ts @@ -13,7 +13,7 @@ * - CLI uninit command */ -import { describe, it, expect, beforeEach, afterEach } from 'vitest'; +import { describe, it, expect, beforeAll, beforeEach, afterEach } from 'vitest'; import * as fs from 'fs'; import * as path from 'path'; import * as os from 'os'; @@ -24,8 +24,13 @@ import { getSupportedLanguages, clearParserCache, getUnavailableGrammarErrors, + initGrammars, } from '../src/extraction/grammars'; +beforeAll(async () => { + await initGrammars(); +}); + // Create a temporary directory for each test function createTempDir(): string { return fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-pr19-test-')); @@ -320,8 +325,9 @@ describe('Database Layer Improvements', () => { const { DatabaseConnection } = await import('../src/db'); const { QueryBuilder } = await import('../src/db/queries'); - const db = DatabaseConnection.initialize(testDir); - const queries = new QueryBuilder(db.getDatabase()); + const dbPath = path.join(testDir, 'codegraph.db'); + const db = DatabaseConnection.initialize(dbPath); + const queries = new QueryBuilder(db.getDb()); // Insert a node first (needed as foreign key) queries.insertNode({ @@ -375,8 +381,9 @@ describe('Database Layer Improvements', () => { const { DatabaseConnection } = await import('../src/db'); const { QueryBuilder } = await import('../src/db/queries'); - const db = DatabaseConnection.initialize(testDir); - const queries = new QueryBuilder(db.getDatabase()); + const dbPath = path.join(testDir, 'codegraph.db'); + const db = DatabaseConnection.initialize(dbPath); + const queries = new QueryBuilder(db.getDb()); // Insert some nodes for (let i = 0; i < 3; i++) { @@ -405,8 +412,9 @@ describe('Database Layer Improvements', () => { it.skipIf(!HAS_SQLITE)('should set performance pragmas on initialization', async () => { const { DatabaseConnection } = await import('../src/db'); - const db = DatabaseConnection.initialize(testDir); - const rawDb = db.getDatabase(); + const dbPath = path.join(testDir, 'codegraph.db'); + const db = DatabaseConnection.initialize(dbPath); + const rawDb = db.getDb(); // Check pragmas were set const synchronous = rawDb.pragma('synchronous', { simple: true }); @@ -428,8 +436,9 @@ describe('Database Layer Improvements', () => { const { DatabaseConnection } = await import('../src/db'); const { QueryBuilder } = await import('../src/db/queries'); - const db = DatabaseConnection.initialize(testDir); - const queries = new QueryBuilder(db.getDatabase()); + const dbPath = path.join(testDir, 'codegraph.db'); + const db = DatabaseConnection.initialize(dbPath); + const queries = new QueryBuilder(db.getDb()); // Should not throw on empty array expect(() => queries.insertUnresolvedRefsBatch([])).not.toThrow(); @@ -665,28 +674,21 @@ describe('CLI uninit', () => { // Tree-sitter Version Pinning // ============================================================================= -describe('Tree-sitter Version Pinning', () => { - it('should have exact versions (no caret) in package.json', () => { +describe('Tree-sitter WASM Setup', () => { + it('should use web-tree-sitter and tree-sitter-wasms in dependencies', () => { const pkgPath = path.join(__dirname, '..', 'package.json'); const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8')); - const treeSitterDeps = Object.entries(pkg.dependencies as Record) - .filter(([name]) => name.startsWith('tree-sitter') || name.includes('tree-sitter')); - - for (const [name, version] of treeSitterDeps) { - // Skip github: references - if (version.startsWith('github:')) continue; - expect(version, `${name} should not use caret range`).not.toMatch(/^\^/); - } + expect(pkg.dependencies['web-tree-sitter']).toBeDefined(); + expect(pkg.dependencies['tree-sitter-wasms']).toBeDefined(); }); - it('should have tree-sitter override pinned', () => { + it('should not have native tree-sitter in dependencies', () => { const pkgPath = path.join(__dirname, '..', 'package.json'); const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8')); - expect(pkg.overrides).toBeDefined(); - expect(pkg.overrides['tree-sitter']).toBeDefined(); - expect(pkg.overrides['tree-sitter']).not.toMatch(/^\^/); + expect(pkg.dependencies['tree-sitter']).toBeUndefined(); + expect(pkg.overrides).toBeUndefined(); }); }); diff --git a/package-lock.json b/package-lock.json index 0843d38..1766a8e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,11 +11,12 @@ "license": "MIT", "dependencies": { "@xenova/transformers": "^2.17.0", - "better-sqlite3": "^11.0.0", "commander": "^14.0.2", "figlet": "^1.8.0", + "node-sqlite3-wasm": "^0.8.30", "picomatch": "^4.0.3", - "tree-sitter": "0.22.4" + "tree-sitter-wasms": "^0.1.11", + "web-tree-sitter": "^0.25.3" }, "bin": { "codegraph": "dist/bin/codegraph.js" @@ -32,21 +33,8 @@ "node": ">=18.0.0" }, "optionalDependencies": { - "@sengac/tree-sitter-dart": "1.1.6", - "sqlite-vss": "^0.1.2", - "tree-sitter-c": "0.23.2", - "tree-sitter-c-sharp": "0.23.1", - "tree-sitter-cpp": "0.23.4", - "tree-sitter-go": "0.23.4", - "tree-sitter-java": "0.23.5", - "tree-sitter-javascript": "0.23.1", - "tree-sitter-kotlin": "0.3.8", - "tree-sitter-php": "0.23.11", - "tree-sitter-python": "0.23.4", - "tree-sitter-ruby": "0.23.1", - "tree-sitter-rust": "0.23.1", - "tree-sitter-swift": "0.6.0", - "tree-sitter-typescript": "0.23.2" + "better-sqlite3": "^11.0.0", + "sqlite-vss": "^0.1.2" } }, "node_modules/@esbuild/aix-ppc64": { @@ -870,50 +858,6 @@ "win32" ] }, - "node_modules/@sengac/tree-sitter": { - "version": "0.25.15", - "resolved": "https://registry.npmjs.org/@sengac/tree-sitter/-/tree-sitter-0.25.15.tgz", - "integrity": "sha512-FQlxMNWYYp/tw03qoN9gpUZ3Lrhp1ti/MoG5Gcc4h98PFa6tbvN3qMkPRt4mWhmyKrL3QrOiLxEab8Gj6ZTHbw==", - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "node-addon-api": "^8.3.0", - "node-gyp-build": "^4.8.4" - } - }, - "node_modules/@sengac/tree-sitter-dart": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/@sengac/tree-sitter-dart/-/tree-sitter-dart-1.1.6.tgz", - "integrity": "sha512-lLsF6pVmsC8+JkCnSvRzqa1jJYs+129EOn93MZCsvNnmDrZ2gcEaiqhTj69ttsjQZ2sR+LNxumdphHsw/Ln0Ew==", - "hasInstallScript": true, - "license": "ISC", - "optional": true, - "dependencies": { - "node-addon-api": "^7.1.0", - "node-gyp-build": "^4.8.0" - }, - "peerDependencies": { - "@sengac/tree-sitter": "^0.25.10" - }, - "peerDependenciesMeta": { - "tree_sitter": { - "optional": true - } - } - }, - "node_modules/@sengac/tree-sitter/node_modules/node-addon-api": { - "version": "8.5.0", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.5.0.tgz", - "integrity": "sha512-/bRZty2mXUIFY/xU5HLvveNHlswNJej+RnxBjOMkidWfwZzgTbPG1E3K5TOxRLOR+5hX7bSofy8yf1hZevMS8A==", - "license": "MIT", - "optional": true, - "peer": true, - "engines": { - "node": "^18 || ^20 || >= 21" - } - }, "node_modules/@types/better-sqlite3": { "version": "7.6.13", "resolved": "https://registry.npmjs.org/@types/better-sqlite3/-/better-sqlite3-7.6.13.tgz", @@ -1228,6 +1172,7 @@ "integrity": "sha512-EwhOpyXiOEL/lKzHz9AW1msWFNzGc/z+LzeB3/jnFJpxu+th2yqvzsSWas1v9jgs9+xiXJcD5A8CJxAG2TaghQ==", "hasInstallScript": true, "license": "MIT", + "optional": true, "dependencies": { "bindings": "^1.5.0", "prebuild-install": "^7.1.1" @@ -1238,6 +1183,7 @@ "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" } @@ -1549,7 +1495,8 @@ "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" + "license": "MIT", + "optional": true }, "node_modules/flatbuffers": { "version": "1.12.0", @@ -1628,13 +1575,6 @@ "integrity": "sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA==", "license": "MIT" }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "license": "ISC", - "optional": true - }, "node_modules/long": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz", @@ -1729,23 +1669,11 @@ "node": ">=10" } }, - "node_modules/node-addon-api": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", - "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", - "license": "MIT", - "optional": true - }, - "node_modules/node-gyp-build": { - "version": "4.8.4", - "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz", - "integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==", - "license": "MIT", - "bin": { - "node-gyp-build": "bin.js", - "node-gyp-build-optional": "optional.js", - "node-gyp-build-test": "build-test.js" - } + "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", @@ -2336,442 +2264,13 @@ "node": ">=14.0.0" } }, - "node_modules/tree-sitter": { - "version": "0.22.4", - "resolved": "https://registry.npmjs.org/tree-sitter/-/tree-sitter-0.22.4.tgz", - "integrity": "sha512-usbHZP9/oxNsUY65MQUsduGRqDHQOou1cagUSwjhoSYAmSahjQDAVsh9s+SlZkn8X8+O1FULRGwHu7AFP3kjzg==", - "hasInstallScript": true, - "license": "MIT", + "node_modules/tree-sitter-wasms": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/tree-sitter-wasms/-/tree-sitter-wasms-0.1.13.tgz", + "integrity": "sha512-wT+cR6DwaIz80/vho3AvSF0N4txuNx/5bcRKoXouOfClpxh/qqrF4URNLQXbbt8MaAxeksZcZd1j8gcGjc+QxQ==", + "license": "Unlicense", "dependencies": { - "node-addon-api": "^8.3.0", - "node-gyp-build": "^4.8.4" - } - }, - "node_modules/tree-sitter-c": { - "version": "0.23.2", - "resolved": "https://registry.npmjs.org/tree-sitter-c/-/tree-sitter-c-0.23.2.tgz", - "integrity": "sha512-9kADOx31AF94DHcrsMGW0zM/2LS6v7wFkPHPVm7RQU+vYVVZMKZ2FJ9e99pm5feqsAcjUzB9CarqDLgRT1Fe/w==", - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "dependencies": { - "node-addon-api": "^8.2.2", - "node-gyp-build": "^4.8.2" - }, - "peerDependencies": { - "tree-sitter": "^0.21.1" - }, - "peerDependenciesMeta": { - "tree-sitter": { - "optional": true - } - } - }, - "node_modules/tree-sitter-c-sharp": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/tree-sitter-c-sharp/-/tree-sitter-c-sharp-0.23.1.tgz", - "integrity": "sha512-9zZ4FlcTRWWfRf6f4PgGhG8saPls6qOOt75tDfX7un9vQZJmARjPrAC6yBNCX2T/VKcCjIDbgq0evFaB3iGhQw==", - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "dependencies": { - "node-addon-api": "^8.2.2", - "node-gyp-build": "^4.8.2" - }, - "peerDependencies": { - "tree-sitter": "^0.21.1" - }, - "peerDependenciesMeta": { - "tree-sitter": { - "optional": true - } - } - }, - "node_modules/tree-sitter-c-sharp/node_modules/node-addon-api": { - "version": "8.5.0", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.5.0.tgz", - "integrity": "sha512-/bRZty2mXUIFY/xU5HLvveNHlswNJej+RnxBjOMkidWfwZzgTbPG1E3K5TOxRLOR+5hX7bSofy8yf1hZevMS8A==", - "license": "MIT", - "optional": true, - "engines": { - "node": "^18 || ^20 || >= 21" - } - }, - "node_modules/tree-sitter-c/node_modules/node-addon-api": { - "version": "8.5.0", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.5.0.tgz", - "integrity": "sha512-/bRZty2mXUIFY/xU5HLvveNHlswNJej+RnxBjOMkidWfwZzgTbPG1E3K5TOxRLOR+5hX7bSofy8yf1hZevMS8A==", - "license": "MIT", - "optional": true, - "engines": { - "node": "^18 || ^20 || >= 21" - } - }, - "node_modules/tree-sitter-cli": { - "version": "0.23.2", - "resolved": "https://registry.npmjs.org/tree-sitter-cli/-/tree-sitter-cli-0.23.2.tgz", - "integrity": "sha512-kPPXprOqREX+C/FgUp2Qpt9jd0vSwn+hOgjzVv/7hapdoWpa+VeWId53rf4oNNd29ikheF12BYtGD/W90feMbA==", - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "bin": { - "tree-sitter": "cli.js" - }, - "engines": { - "node": ">=12.0.0" - } - }, - "node_modules/tree-sitter-cpp": { - "version": "0.23.4", - "resolved": "https://registry.npmjs.org/tree-sitter-cpp/-/tree-sitter-cpp-0.23.4.tgz", - "integrity": "sha512-qR5qUDyhZ5jJ6V8/umiBxokRbe89bCGmcq/dk94wI4kN86qfdV8k0GHIUEKaqWgcu42wKal5E97LKpLeVW8sKw==", - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "dependencies": { - "node-addon-api": "^8.2.1", - "node-gyp-build": "^4.8.2", - "tree-sitter-c": "^0.23.1" - }, - "peerDependencies": { - "tree-sitter": "^0.21.1" - }, - "peerDependenciesMeta": { - "tree-sitter": { - "optional": true - } - } - }, - "node_modules/tree-sitter-cpp/node_modules/node-addon-api": { - "version": "8.5.0", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.5.0.tgz", - "integrity": "sha512-/bRZty2mXUIFY/xU5HLvveNHlswNJej+RnxBjOMkidWfwZzgTbPG1E3K5TOxRLOR+5hX7bSofy8yf1hZevMS8A==", - "license": "MIT", - "optional": true, - "engines": { - "node": "^18 || ^20 || >= 21" - } - }, - "node_modules/tree-sitter-cpp/node_modules/tree-sitter-c": { - "version": "0.23.6", - "resolved": "https://registry.npmjs.org/tree-sitter-c/-/tree-sitter-c-0.23.6.tgz", - "integrity": "sha512-0dxXKznVyUA0s6PjNolJNs2yF87O5aL538A/eR6njA5oqX3C3vH4vnx3QdOKwuUdpKEcFdHuiDpRKLLCA/tjvQ==", - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "dependencies": { - "node-addon-api": "^8.3.0", - "node-gyp-build": "^4.8.4" - }, - "peerDependencies": { - "tree-sitter": "^0.22.1" - }, - "peerDependenciesMeta": { - "tree-sitter": { - "optional": true - } - } - }, - "node_modules/tree-sitter-go": { - "version": "0.23.4", - "resolved": "https://registry.npmjs.org/tree-sitter-go/-/tree-sitter-go-0.23.4.tgz", - "integrity": "sha512-iQaHEs4yMa/hMo/ZCGqLfG61F0miinULU1fFh+GZreCRtKylFLtvn798ocCZjO2r/ungNZgAY1s1hPFyAwkc7w==", - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "dependencies": { - "node-addon-api": "^8.2.1", - "node-gyp-build": "^4.8.2" - }, - "peerDependencies": { - "tree-sitter": "^0.21.1" - }, - "peerDependenciesMeta": { - "tree-sitter": { - "optional": true - } - } - }, - "node_modules/tree-sitter-go/node_modules/node-addon-api": { - "version": "8.5.0", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.5.0.tgz", - "integrity": "sha512-/bRZty2mXUIFY/xU5HLvveNHlswNJej+RnxBjOMkidWfwZzgTbPG1E3K5TOxRLOR+5hX7bSofy8yf1hZevMS8A==", - "license": "MIT", - "optional": true, - "engines": { - "node": "^18 || ^20 || >= 21" - } - }, - "node_modules/tree-sitter-java": { - "version": "0.23.5", - "resolved": "https://registry.npmjs.org/tree-sitter-java/-/tree-sitter-java-0.23.5.tgz", - "integrity": "sha512-Yju7oQ0Xx7GcUT01mUglPP+bYfvqjNCGdxqigTnew9nLGoII42PNVP3bHrYeMxswiCRM0yubWmN5qk+zsg0zMA==", - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "dependencies": { - "node-addon-api": "^8.2.2", - "node-gyp-build": "^4.8.2" - }, - "peerDependencies": { - "tree-sitter": "^0.21.1" - }, - "peerDependenciesMeta": { - "tree-sitter": { - "optional": true - } - } - }, - "node_modules/tree-sitter-java/node_modules/node-addon-api": { - "version": "8.5.0", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.5.0.tgz", - "integrity": "sha512-/bRZty2mXUIFY/xU5HLvveNHlswNJej+RnxBjOMkidWfwZzgTbPG1E3K5TOxRLOR+5hX7bSofy8yf1hZevMS8A==", - "license": "MIT", - "optional": true, - "engines": { - "node": "^18 || ^20 || >= 21" - } - }, - "node_modules/tree-sitter-javascript": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/tree-sitter-javascript/-/tree-sitter-javascript-0.23.1.tgz", - "integrity": "sha512-/bnhbrTD9frUYHQTiYnPcxyHORIw157ERBa6dqzaKxvR/x3PC4Yzd+D1pZIMS6zNg2v3a8BZ0oK7jHqsQo9fWA==", - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "dependencies": { - "node-addon-api": "^8.2.2", - "node-gyp-build": "^4.8.2" - }, - "peerDependencies": { - "tree-sitter": "^0.21.1" - }, - "peerDependenciesMeta": { - "tree-sitter": { - "optional": true - } - } - }, - "node_modules/tree-sitter-javascript/node_modules/node-addon-api": { - "version": "8.5.0", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.5.0.tgz", - "integrity": "sha512-/bRZty2mXUIFY/xU5HLvveNHlswNJej+RnxBjOMkidWfwZzgTbPG1E3K5TOxRLOR+5hX7bSofy8yf1hZevMS8A==", - "license": "MIT", - "optional": true, - "engines": { - "node": "^18 || ^20 || >= 21" - } - }, - "node_modules/tree-sitter-kotlin": { - "version": "0.3.8", - "resolved": "https://registry.npmjs.org/tree-sitter-kotlin/-/tree-sitter-kotlin-0.3.8.tgz", - "integrity": "sha512-A4obq6bjzmYrA+F0JLLoheFPcofFkctNaZSpnDd+GPn1SfVZLY4/GG4C0cYVBTOShuPBGGAOPLM1JWLZQV4m1g==", - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "dependencies": { - "node-addon-api": "^7.1.0", - "node-gyp-build": "^4.8.0" - }, - "peerDependencies": { - "tree-sitter": "^0.21.0" - }, - "peerDependenciesMeta": { - "tree_sitter": { - "optional": true - } - } - }, - "node_modules/tree-sitter-php": { - "version": "0.23.11", - "resolved": "https://registry.npmjs.org/tree-sitter-php/-/tree-sitter-php-0.23.11.tgz", - "integrity": "sha512-n+YHSKmYKCyPXsg72rqoUtXyCmNRsG/xe7ExrF2g6bXDERcQ/NPOKIzNfRIcI3f3TtbD6PooA0gMW0EpuuUjVA==", - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "dependencies": { - "node-addon-api": "^8.2.2", - "node-gyp-build": "^4.8.2" - }, - "peerDependencies": { - "tree-sitter": "^0.21.1" - }, - "peerDependenciesMeta": { - "tree-sitter": { - "optional": true - } - } - }, - "node_modules/tree-sitter-php/node_modules/node-addon-api": { - "version": "8.5.0", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.5.0.tgz", - "integrity": "sha512-/bRZty2mXUIFY/xU5HLvveNHlswNJej+RnxBjOMkidWfwZzgTbPG1E3K5TOxRLOR+5hX7bSofy8yf1hZevMS8A==", - "license": "MIT", - "optional": true, - "engines": { - "node": "^18 || ^20 || >= 21" - } - }, - "node_modules/tree-sitter-python": { - "version": "0.23.4", - "resolved": "https://registry.npmjs.org/tree-sitter-python/-/tree-sitter-python-0.23.4.tgz", - "integrity": "sha512-MbmUAl7y5UCUWqHscHke7DdRDwQnVNMNKQYQc4Gq2p09j+fgPxaU8JVsuOI/0HD3BSEEe5k9j3xmdtIWbDtDgw==", - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "dependencies": { - "node-addon-api": "^8.2.1", - "node-gyp-build": "^4.8.2" - }, - "peerDependencies": { - "tree-sitter": "^0.21.1" - }, - "peerDependenciesMeta": { - "tree-sitter": { - "optional": true - } - } - }, - "node_modules/tree-sitter-python/node_modules/node-addon-api": { - "version": "8.5.0", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.5.0.tgz", - "integrity": "sha512-/bRZty2mXUIFY/xU5HLvveNHlswNJej+RnxBjOMkidWfwZzgTbPG1E3K5TOxRLOR+5hX7bSofy8yf1hZevMS8A==", - "license": "MIT", - "optional": true, - "engines": { - "node": "^18 || ^20 || >= 21" - } - }, - "node_modules/tree-sitter-ruby": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/tree-sitter-ruby/-/tree-sitter-ruby-0.23.1.tgz", - "integrity": "sha512-d9/RXgWjR6HanN7wTYhS5bpBQLz1VkH048Vm3CodPGyJVnamXMGb8oEhDypVCBq4QnHui9sTXuJBBP3WtCw5RA==", - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "dependencies": { - "node-addon-api": "^8.2.2", - "node-gyp-build": "^4.8.2" - }, - "peerDependencies": { - "tree-sitter": "^0.21.1" - }, - "peerDependenciesMeta": { - "tree-sitter": { - "optional": true - } - } - }, - "node_modules/tree-sitter-ruby/node_modules/node-addon-api": { - "version": "8.5.0", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.5.0.tgz", - "integrity": "sha512-/bRZty2mXUIFY/xU5HLvveNHlswNJej+RnxBjOMkidWfwZzgTbPG1E3K5TOxRLOR+5hX7bSofy8yf1hZevMS8A==", - "license": "MIT", - "optional": true, - "engines": { - "node": "^18 || ^20 || >= 21" - } - }, - "node_modules/tree-sitter-rust": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/tree-sitter-rust/-/tree-sitter-rust-0.23.1.tgz", - "integrity": "sha512-wrMptzUAfbl3DbNrldZveyNM2CWmRw2VvEo2j/855qQbMMz4dlCF+TBwRN/1FL1S6cYvAEAJaCMesGqhocFJhQ==", - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "dependencies": { - "node-addon-api": "^8.2.2", - "node-gyp-build": "^4.8.2" - }, - "peerDependencies": { - "tree-sitter": "^0.21.1" - }, - "peerDependenciesMeta": { - "tree-sitter": { - "optional": true - } - } - }, - "node_modules/tree-sitter-rust/node_modules/node-addon-api": { - "version": "8.5.0", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.5.0.tgz", - "integrity": "sha512-/bRZty2mXUIFY/xU5HLvveNHlswNJej+RnxBjOMkidWfwZzgTbPG1E3K5TOxRLOR+5hX7bSofy8yf1hZevMS8A==", - "license": "MIT", - "optional": true, - "engines": { - "node": "^18 || ^20 || >= 21" - } - }, - "node_modules/tree-sitter-swift": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/tree-sitter-swift/-/tree-sitter-swift-0.6.0.tgz", - "integrity": "sha512-9vOJZes4/UFjBr4COHtp6ZHVuZYwfChSQbpneXQog04dAstfx5px3ybVX2cN+ylvLqsvVpmXLpidxxgF2rDQ7A==", - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "dependencies": { - "node-addon-api": "^8.0.0", - "node-gyp-build": "^4.8.0", - "tree-sitter-cli": "^0.23", - "which": "2.0.2" - }, - "peerDependencies": { - "tree-sitter": "^0.21.1" - }, - "peerDependenciesMeta": { - "tree_sitter": { - "optional": true - } - } - }, - "node_modules/tree-sitter-swift/node_modules/node-addon-api": { - "version": "8.5.0", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.5.0.tgz", - "integrity": "sha512-/bRZty2mXUIFY/xU5HLvveNHlswNJej+RnxBjOMkidWfwZzgTbPG1E3K5TOxRLOR+5hX7bSofy8yf1hZevMS8A==", - "license": "MIT", - "optional": true, - "engines": { - "node": "^18 || ^20 || >= 21" - } - }, - "node_modules/tree-sitter-typescript": { - "version": "0.23.2", - "resolved": "https://registry.npmjs.org/tree-sitter-typescript/-/tree-sitter-typescript-0.23.2.tgz", - "integrity": "sha512-e04JUUKxTT53/x3Uq1zIL45DoYKVfHH4CZqwgZhPg5qYROl5nQjV+85ruFzFGZxu+QeFVbRTPDRnqL9UbU4VeA==", - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "dependencies": { - "node-addon-api": "^8.2.2", - "node-gyp-build": "^4.8.2", - "tree-sitter-javascript": "^0.23.1" - }, - "peerDependencies": { - "tree-sitter": "^0.21.0" - }, - "peerDependenciesMeta": { - "tree-sitter": { - "optional": true - } - } - }, - "node_modules/tree-sitter-typescript/node_modules/node-addon-api": { - "version": "8.5.0", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.5.0.tgz", - "integrity": "sha512-/bRZty2mXUIFY/xU5HLvveNHlswNJej+RnxBjOMkidWfwZzgTbPG1E3K5TOxRLOR+5hX7bSofy8yf1hZevMS8A==", - "license": "MIT", - "optional": true, - "engines": { - "node": "^18 || ^20 || >= 21" - } - }, - "node_modules/tree-sitter/node_modules/node-addon-api": { - "version": "8.5.0", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.5.0.tgz", - "integrity": "sha512-/bRZty2mXUIFY/xU5HLvveNHlswNJej+RnxBjOMkidWfwZzgTbPG1E3K5TOxRLOR+5hX7bSofy8yf1hZevMS8A==", - "license": "MIT", - "engines": { - "node": "^18 || ^20 || >= 21" + "tree-sitter-wasms": "^0.1.11" } }, "node_modules/tunnel-agent": { @@ -2961,20 +2460,18 @@ } } }, - "node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "license": "ISC", - "optional": true, - "dependencies": { - "isexe": "^2.0.0" + "node_modules/web-tree-sitter": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/web-tree-sitter/-/web-tree-sitter-0.25.10.tgz", + "integrity": "sha512-Y09sF44/13XvgVKgO2cNDw5rGk6s26MgoZPXLESvMXeefBf7i6/73eFurre0IsTW6E14Y0ArIzhUMmjoc7xyzA==", + "license": "MIT", + "peerDependencies": { + "@types/emscripten": "^1.40.0" }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" + "peerDependenciesMeta": { + "@types/emscripten": { + "optional": true + } } }, "node_modules/why-is-node-running": { diff --git a/package.json b/package.json index 7c164ca..ea4bdde 100644 --- a/package.json +++ b/package.json @@ -34,11 +34,12 @@ "license": "MIT", "dependencies": { "@xenova/transformers": "^2.17.0", - "better-sqlite3": "^11.0.0", "commander": "^14.0.2", "figlet": "^1.8.0", + "node-sqlite3-wasm": "^0.8.30", "picomatch": "^4.0.3", - "tree-sitter": "0.22.4" + "tree-sitter-wasms": "^0.1.11", + "web-tree-sitter": "^0.25.3" }, "devDependencies": { "@types/better-sqlite3": "^7.6.0", @@ -49,26 +50,10 @@ "vitest": "^2.1.9" }, "optionalDependencies": { - "@sengac/tree-sitter-dart": "1.1.6", - "sqlite-vss": "^0.1.2", - "tree-sitter-c": "0.23.2", - "tree-sitter-c-sharp": "0.23.1", - "tree-sitter-cpp": "0.23.4", - "tree-sitter-go": "0.23.4", - "tree-sitter-java": "0.23.5", - "tree-sitter-javascript": "0.23.1", - "tree-sitter-kotlin": "0.3.8", - "tree-sitter-php": "0.23.11", - "tree-sitter-python": "0.23.4", - "tree-sitter-ruby": "0.23.1", - "tree-sitter-rust": "0.23.1", - "tree-sitter-swift": "0.6.0", - "tree-sitter-typescript": "0.23.2" + "better-sqlite3": "^11.0.0", + "sqlite-vss": "^0.1.2" }, "engines": { "node": ">=18.0.0" - }, - "overrides": { - "tree-sitter": "0.22.4" } } diff --git a/scripts/postinstall.js b/scripts/postinstall.js index 7a89b2e..3725a5d 100644 --- a/scripts/postinstall.js +++ b/scripts/postinstall.js @@ -62,9 +62,6 @@ async function downloadModel() { } } -// @sengac/tree-sitter-dart ships with NAPI prebuilds for all platforms -// No patching needed (replaced old tree-sitter-dart v1.0.0 which used NAN bindings) - downloadModel().catch(() => { // Silent exit - don't break npm install process.exit(0); diff --git a/src/bin/codegraph.ts b/src/bin/codegraph.ts index 6679ac4..75a78dd 100644 --- a/src/bin/codegraph.ts +++ b/src/bin/codegraph.ts @@ -26,11 +26,25 @@ import { Command } from 'commander'; import * as path from 'path'; import * as fs from 'fs'; import { spawn } from 'child_process'; -import CodeGraph, { getCodeGraphDir, findNearestCodeGraphRoot } from '../index'; -import type { IndexProgress } from '../index'; -import { runInstaller } from '../installer'; +import { getCodeGraphDir, findNearestCodeGraphRoot, isInitialized } from '../directory'; import { initSentry, captureException } from '../sentry'; +// Lazy-load heavy modules (CodeGraph, runInstaller) to keep CLI startup fast. +async function loadCodeGraph(): Promise { + try { + return await import('../index'); + } catch (err) { + const msg = err instanceof Error ? err.message : String(err); + console.error('\x1b[31m✗\x1b[0m Failed to load CodeGraph modules.'); + console.error(`\n Node: ${process.version} Platform: ${process.platform} ${process.arch}`); + console.error(`\n Error: ${msg}`); + console.error('\n Try reinstalling with: npm install -g @colbymchenry/codegraph\n'); + process.exit(1); + } +} + +type IndexProgress = import('../index').IndexProgress; + // Check if running with no arguments - run installer // Read version for Sentry release tag const pkgVersion = (() => { @@ -41,9 +55,11 @@ const pkgVersion = (() => { initSentry({ processName: 'codegraph-cli', version: pkgVersion }); if (process.argv.length === 2) { - runInstaller().catch((err) => { + import('../installer').then(({ runInstaller }) => + runInstaller() + ).catch((err) => { captureException(err); - console.error('Installation failed:', err.message); + console.error('Installation failed:', err instanceof Error ? err.message : String(err)); process.exit(1); }); } else { @@ -117,7 +133,7 @@ function resolveProjectPath(pathArg?: string): string { const absolutePath = path.resolve(pathArg || process.cwd()); // If exact path is initialized (has codegraph.db), use it - if (CodeGraph.isInitialized(absolutePath)) { + if (isInitialized(absolutePath)) { return absolutePath; } @@ -131,7 +147,7 @@ function resolveProjectPath(pathArg?: string): string { if (parent === current) break; current = parent; - if (CodeGraph.isInitialized(current)) { + if (isInitialized(current)) { return current; } } @@ -240,13 +256,14 @@ program try { // Check if already initialized - if (CodeGraph.isInitialized(projectPath)) { + if (isInitialized(projectPath)) { warn(`CodeGraph already initialized in ${projectPath}`); info('Use "codegraph index" to re-index or "codegraph sync" to update'); return; } // Initialize + const { default: CodeGraph } = await loadCodeGraph(); const cg = await CodeGraph.init(projectPath, { index: false, // We'll handle indexing ourselves for progress }); @@ -295,7 +312,7 @@ program const projectPath = resolveProjectPath(pathArg); try { - if (!CodeGraph.isInitialized(projectPath)) { + if (!isInitialized(projectPath)) { warn(`CodeGraph is not initialized in ${projectPath}`); return; } @@ -318,6 +335,7 @@ program } } + const { default: CodeGraph } = await loadCodeGraph(); const cg = CodeGraph.openSync(projectPath); cg.uninitialize(); @@ -341,12 +359,13 @@ program const projectPath = resolveProjectPath(pathArg); try { - if (!CodeGraph.isInitialized(projectPath)) { + if (!isInitialized(projectPath)) { error(`CodeGraph not initialized in ${projectPath}`); info('Run "codegraph init" first'); process.exit(1); } + const { default: CodeGraph } = await loadCodeGraph(); const cg = await CodeGraph.open(projectPath); if (!options.quiet) { @@ -408,13 +427,14 @@ program const projectPath = resolveProjectPath(pathArg); try { - if (!CodeGraph.isInitialized(projectPath)) { + if (!isInitialized(projectPath)) { if (!options.quiet) { error(`CodeGraph not initialized in ${projectPath}`); } process.exit(1); } + const { default: CodeGraph } = await loadCodeGraph(); const cg = await CodeGraph.open(projectPath); const result = await cg.sync({ @@ -467,7 +487,7 @@ program const projectPath = resolveProjectPath(pathArg); try { - if (!CodeGraph.isInitialized(projectPath)) { + if (!isInitialized(projectPath)) { if (options.json) { console.log(JSON.stringify({ initialized: false, projectPath })); return; @@ -479,6 +499,7 @@ program return; } + const { default: CodeGraph } = await loadCodeGraph(); const cg = await CodeGraph.open(projectPath); const stats = cg.getStats(); const changes = cg.getChangedFiles(); @@ -579,11 +600,12 @@ program const projectPath = resolveProjectPath(options.path); try { - if (!CodeGraph.isInitialized(projectPath)) { + if (!isInitialized(projectPath)) { error(`CodeGraph not initialized in ${projectPath}`); process.exit(1); } + const { default: CodeGraph } = await loadCodeGraph(); const cg = await CodeGraph.open(projectPath); const limit = parseInt(options.limit || '10', 10); @@ -652,11 +674,12 @@ program const projectPath = resolveProjectPath(options.path); try { - if (!CodeGraph.isInitialized(projectPath)) { + if (!isInitialized(projectPath)) { error(`CodeGraph not initialized in ${projectPath}`); process.exit(1); } + const { default: CodeGraph } = await loadCodeGraph(); const cg = await CodeGraph.open(projectPath); let files = cg.getFiles(); @@ -854,11 +877,12 @@ program const projectPath = resolveProjectPath(options.path); try { - if (!CodeGraph.isInitialized(projectPath)) { + if (!isInitialized(projectPath)) { error(`CodeGraph not initialized in ${projectPath}`); process.exit(1); } + const { default: CodeGraph } = await loadCodeGraph(); const cg = await CodeGraph.open(projectPath); const context = await cg.buildContext(task, { @@ -987,7 +1011,7 @@ program try { fs.unlinkSync(dirtyPath); } catch { /* ignore */ } // If not fully initialized (no DB), exit - if (!CodeGraph.isInitialized(projectRoot!)) { + if (!isInitialized(projectRoot!)) { process.exit(0); } @@ -1018,6 +1042,7 @@ program .command('install') .description('Run interactive installer for Claude Code integration') .action(async () => { + const { runInstaller } = await import('../installer'); await runInstaller(); }); diff --git a/src/db/index.ts b/src/db/index.ts index 66892d6..34e9933 100644 --- a/src/db/index.ts +++ b/src/db/index.ts @@ -4,20 +4,22 @@ * Handles SQLite database initialization and connection management. */ -import Database from 'better-sqlite3'; +import { SqliteDatabase, createDatabase } from './sqlite-adapter'; import * as fs from 'fs'; import * as path from 'path'; import { SchemaVersion } from '../types'; import { runMigrations, getCurrentVersion, CURRENT_SCHEMA_VERSION } from './migrations'; +export { SqliteDatabase, getActiveBackend } from './sqlite-adapter'; + /** * Database connection wrapper with lifecycle management */ export class DatabaseConnection { - private db: Database.Database; + private db: SqliteDatabase; private dbPath: string; - private constructor(db: Database.Database, dbPath: string) { + private constructor(db: SqliteDatabase, dbPath: string) { this.db = db; this.dbPath = dbPath; } @@ -33,7 +35,7 @@ export class DatabaseConnection { } // Create and configure database - const db = new Database(dbPath); + const db = createDatabase(dbPath); // Enable foreign keys and WAL mode for better performance db.pragma('foreign_keys = ON'); @@ -71,7 +73,7 @@ export class DatabaseConnection { throw new Error(`Database not found: ${dbPath}`); } - const db = new Database(dbPath); + const db = createDatabase(dbPath); // Enable foreign keys and WAL mode db.pragma('foreign_keys = ON'); @@ -99,7 +101,7 @@ export class DatabaseConnection { /** * Get the underlying database instance */ - getDb(): Database.Database { + getDb(): SqliteDatabase { return this.db; } diff --git a/src/db/migrations.ts b/src/db/migrations.ts index 08510b3..307cc38 100644 --- a/src/db/migrations.ts +++ b/src/db/migrations.ts @@ -4,7 +4,7 @@ * Schema versioning and migration support. */ -import Database from 'better-sqlite3'; +import { SqliteDatabase } from './sqlite-adapter'; /** * Current schema version @@ -17,7 +17,7 @@ export const CURRENT_SCHEMA_VERSION = 2; interface Migration { version: number; description: string; - up: (db: Database.Database) => void; + up: (db: SqliteDatabase) => void; } /** @@ -50,7 +50,7 @@ const migrations: Migration[] = [ /** * Get the current schema version from the database */ -export function getCurrentVersion(db: Database.Database): number { +export function getCurrentVersion(db: SqliteDatabase): number { try { const row = db .prepare('SELECT MAX(version) as version FROM schema_versions') @@ -65,7 +65,7 @@ export function getCurrentVersion(db: Database.Database): number { /** * Record a migration as applied */ -function recordMigration(db: Database.Database, version: number, description: string): void { +function recordMigration(db: SqliteDatabase, version: number, description: string): void { db.prepare( 'INSERT INTO schema_versions (version, applied_at, description) VALUES (?, ?, ?)' ).run(version, Date.now(), description); @@ -74,7 +74,7 @@ function recordMigration(db: Database.Database, version: number, description: st /** * Run all pending migrations */ -export function runMigrations(db: Database.Database, fromVersion: number): void { +export function runMigrations(db: SqliteDatabase, fromVersion: number): void { const pending = migrations.filter((m) => m.version > fromVersion); if (pending.length === 0) { @@ -96,7 +96,7 @@ export function runMigrations(db: Database.Database, fromVersion: number): void /** * Check if the database needs migration */ -export function needsMigration(db: Database.Database): boolean { +export function needsMigration(db: SqliteDatabase): boolean { const current = getCurrentVersion(db); return current < CURRENT_SCHEMA_VERSION; } @@ -104,7 +104,7 @@ export function needsMigration(db: Database.Database): boolean { /** * Get list of pending migrations */ -export function getPendingMigrations(db: Database.Database): Migration[] { +export function getPendingMigrations(db: SqliteDatabase): Migration[] { const current = getCurrentVersion(db); return migrations .filter((m) => m.version > current) @@ -115,7 +115,7 @@ export function getPendingMigrations(db: Database.Database): Migration[] { * Get migration history from database */ export function getMigrationHistory( - db: Database.Database + db: SqliteDatabase ): Array<{ version: number; appliedAt: number; description: string | null }> { const rows = db .prepare('SELECT version, applied_at, description FROM schema_versions ORDER BY version') diff --git a/src/db/queries.ts b/src/db/queries.ts index 83702de..e3cea84 100644 --- a/src/db/queries.ts +++ b/src/db/queries.ts @@ -4,7 +4,7 @@ * Prepared statements for CRUD operations on the knowledge graph. */ -import Database from 'better-sqlite3'; +import { SqliteDatabase, SqliteStatement } from './sqlite-adapter'; import { Node, Edge, @@ -143,7 +143,7 @@ function rowToFileRecord(row: FileRow): FileRecord { * Query builder for the knowledge graph database */ export class QueryBuilder { - private db: Database.Database; + private db: SqliteDatabase; // Node cache for frequently accessed nodes (LRU-style, max 1000 entries) private nodeCache: Map = new Map(); @@ -151,30 +151,30 @@ export class QueryBuilder { // Prepared statements (lazily initialized) private stmts: { - insertNode?: Database.Statement; - updateNode?: Database.Statement; - deleteNode?: Database.Statement; - deleteNodesByFile?: Database.Statement; - getNodeById?: Database.Statement; - getNodesByFile?: Database.Statement; - getNodesByKind?: Database.Statement; - insertEdge?: Database.Statement; - upsertFile?: Database.Statement; - deleteEdgesBySource?: Database.Statement; - deleteEdgesByTarget?: Database.Statement; - getEdgesBySource?: Database.Statement; - getEdgesByTarget?: Database.Statement; - insertFile?: Database.Statement; - updateFile?: Database.Statement; - deleteFile?: Database.Statement; - getFileByPath?: Database.Statement; - getAllFiles?: Database.Statement; - insertUnresolved?: Database.Statement; - deleteUnresolvedByNode?: Database.Statement; - getUnresolvedByName?: Database.Statement; + insertNode?: SqliteStatement; + updateNode?: SqliteStatement; + deleteNode?: SqliteStatement; + deleteNodesByFile?: SqliteStatement; + getNodeById?: SqliteStatement; + getNodesByFile?: SqliteStatement; + getNodesByKind?: SqliteStatement; + insertEdge?: SqliteStatement; + upsertFile?: SqliteStatement; + deleteEdgesBySource?: SqliteStatement; + deleteEdgesByTarget?: SqliteStatement; + getEdgesBySource?: SqliteStatement; + getEdgesByTarget?: SqliteStatement; + insertFile?: SqliteStatement; + updateFile?: SqliteStatement; + deleteFile?: SqliteStatement; + getFileByPath?: SqliteStatement; + getAllFiles?: SqliteStatement; + insertUnresolved?: SqliteStatement; + deleteUnresolvedByNode?: SqliteStatement; + getUnresolvedByName?: SqliteStatement; } = {}; - constructor(db: Database.Database) { + constructor(db: SqliteDatabase) { this.db = db; } diff --git a/src/db/sqlite-adapter.ts b/src/db/sqlite-adapter.ts new file mode 100644 index 0000000..01155fc --- /dev/null +++ b/src/db/sqlite-adapter.ts @@ -0,0 +1,227 @@ +/** + * SQLite Adapter + * + * Provides a unified interface over better-sqlite3 (native) and + * node-sqlite3-wasm (WASM fallback) for universal cross-platform support. + */ + +export interface SqliteStatement { + run(...params: any[]): { changes: number; lastInsertRowid: number | bigint }; + get(...params: any[]): any; + all(...params: any[]): any[]; +} + +export interface SqliteDatabase { + prepare(sql: string): SqliteStatement; + exec(sql: string): void; + pragma(str: string): any; + transaction(fn: (...args: any[]) => T): (...args: any[]) => T; + close(): void; + readonly open: boolean; +} + +export type SqliteBackend = 'native' | 'wasm'; + +let activeBackend: SqliteBackend | null = null; + +/** + * Get the currently active SQLite backend. + */ +export function getActiveBackend(): SqliteBackend | null { + return activeBackend; +} + +/** + * 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 { + 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(); + + constructor(dbPath: string) { + // eslint-disable-next-line @typescript-eslint/no-require-imports + const { Database } = require('node-sqlite3-wasm'); + this._db = new Database(dbPath); + } + + get open(): boolean { + return this._db.isOpen; + } + + prepare(sql: string): SqliteStatement { + const { sql: rewrittenSql, paramOrder } = translateNamedParams(sql); + const stmt = this._db.prepare(rewrittenSql); + this._openStmts.add(stmt); + return { + run(...params: any[]) { + const resolved = resolveParams(params, paramOrder); + const result = resolved !== undefined ? stmt.run(resolved) : stmt.run(); + return { + changes: result?.changes ?? 0, + lastInsertRowid: result?.lastInsertRowid ?? 0, + }; + }, + get(...params: any[]) { + const resolved = resolveParams(params, paramOrder); + return resolved !== undefined ? stmt.get(resolved) : stmt.get(); + }, + all(...params: any[]) { + const resolved = resolveParams(params, paramOrder); + return resolved !== undefined ? stmt.all(resolved) : stmt.all(); + }, + }; + } + + exec(sql: string): void { + this._db.exec(sql); + } + + pragma(str: string): any { + const trimmed = str.trim(); + + // Write pragma: "key = value" + 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}`); + return; + } + + // Read pragma: "key" — return the value + const stmt = this._db.prepare(`PRAGMA ${trimmed}`); + const result = stmt.get(); + stmt.finalize(); + return result; + } + + transaction(fn: (...args: any[]) => T): (...args: any[]) => T { + return (...args: any[]) => { + this._db.exec('BEGIN'); + try { + const result = fn(...args); + this._db.exec('COMMIT'); + return result; + } catch (error) { + this._db.exec('ROLLBACK'); + throw error; + } + }; + } + + 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(); + } +} + +/** + * Create a database connection. Tries native better-sqlite3 first, + * falls back to node-sqlite3-wasm. + */ +export function createDatabase(dbPath: string): SqliteDatabase { + 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); + activeBackend = 'native'; + return db as SqliteDatabase; + } catch (error) { + nativeError = error instanceof Error ? error.message : String(error); + } + + // Fall back to WASM + try { + const db = new WasmDatabaseAdapter(dbPath); + activeBackend = 'wasm'; + console.warn('[CodeGraph] Using WASM SQLite backend (native better-sqlite3 unavailable)'); + return db; + } 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}` + ); +} diff --git a/src/extraction/grammars.ts b/src/extraction/grammars.ts index 5ef676b..44a4d20 100644 --- a/src/extraction/grammars.ts +++ b/src/extraction/grammars.ts @@ -1,87 +1,37 @@ /** * Grammar Loading and Caching * - * Uses lazy per-language loading so one missing native grammar does not - * break extraction for all other languages. + * Uses web-tree-sitter (WASM) for universal cross-platform support. + * All grammars are pre-loaded asynchronously via initGrammars(), then + * getParser() returns synchronously from cache. */ -import Parser from 'tree-sitter'; +import { Parser, Language as WasmLanguage } from 'web-tree-sitter'; import { Language } from '../types'; -type GrammarLoader = () => unknown; type GrammarLanguage = Exclude; /** - * Lazy grammar loaders — each language's native binding is only loaded - * on first use, so a failure in one grammar doesn't affect others. + * WASM filename map — maps each language to its .wasm grammar file + * in the tree-sitter-wasms package. */ -const grammarLoaders: Record = { - typescript: () => { - // eslint-disable-next-line @typescript-eslint/no-require-imports - return require('tree-sitter-typescript').typescript; - }, - tsx: () => { - // eslint-disable-next-line @typescript-eslint/no-require-imports - return require('tree-sitter-typescript').tsx; - }, - javascript: () => { - // eslint-disable-next-line @typescript-eslint/no-require-imports - return require('tree-sitter-javascript'); - }, - jsx: () => { - // eslint-disable-next-line @typescript-eslint/no-require-imports - return require('tree-sitter-javascript'); - }, - python: () => { - // eslint-disable-next-line @typescript-eslint/no-require-imports - return require('tree-sitter-python'); - }, - go: () => { - // eslint-disable-next-line @typescript-eslint/no-require-imports - return require('tree-sitter-go'); - }, - rust: () => { - // eslint-disable-next-line @typescript-eslint/no-require-imports - return require('tree-sitter-rust'); - }, - java: () => { - // eslint-disable-next-line @typescript-eslint/no-require-imports - return require('tree-sitter-java'); - }, - c: () => { - // eslint-disable-next-line @typescript-eslint/no-require-imports - return require('tree-sitter-c'); - }, - cpp: () => { - // eslint-disable-next-line @typescript-eslint/no-require-imports - return require('tree-sitter-cpp'); - }, - csharp: () => { - // eslint-disable-next-line @typescript-eslint/no-require-imports - return require('tree-sitter-c-sharp'); - }, - php: () => { - // eslint-disable-next-line @typescript-eslint/no-require-imports - return require('tree-sitter-php').php; - }, - ruby: () => { - // eslint-disable-next-line @typescript-eslint/no-require-imports - return require('tree-sitter-ruby'); - }, - swift: () => { - // eslint-disable-next-line @typescript-eslint/no-require-imports - return require('tree-sitter-swift'); - }, - kotlin: () => { - // eslint-disable-next-line @typescript-eslint/no-require-imports - return require('tree-sitter-kotlin'); - }, - dart: () => { - // eslint-disable-next-line @typescript-eslint/no-require-imports - return require('@sengac/tree-sitter-dart'); - }, - // Note: tree-sitter-liquid has ABI compatibility issues with tree-sitter 0.22+ - // Liquid extraction is handled separately via regex in tree-sitter.ts +const WASM_GRAMMAR_FILES: Record = { + typescript: 'tree-sitter-typescript.wasm', + tsx: 'tree-sitter-tsx.wasm', + javascript: 'tree-sitter-javascript.wasm', + jsx: 'tree-sitter-javascript.wasm', + python: 'tree-sitter-python.wasm', + go: 'tree-sitter-go.wasm', + rust: 'tree-sitter-rust.wasm', + java: 'tree-sitter-java.wasm', + c: 'tree-sitter-c.wasm', + cpp: 'tree-sitter-cpp.wasm', + csharp: 'tree-sitter-c_sharp.wasm', + php: 'tree-sitter-php.wasm', + ruby: 'tree-sitter-ruby.wasm', + swift: 'tree-sitter-swift.wasm', + kotlin: 'tree-sitter-kotlin.wasm', + dart: 'tree-sitter-dart.wasm', }; /** @@ -122,55 +72,62 @@ export const EXTENSION_MAP: Record = { * Caches for loaded grammars and parsers */ const parserCache = new Map(); -const grammarCache = new Map(); +const languageCache = new Map(); const unavailableGrammarErrors = new Map(); +let grammarsInitialized = false; + /** - * Load a grammar on demand, caching the result. - * Returns null if the grammar is not available on this platform. + * Initialize all WASM grammars. Must be called before any parsing. + * Idempotent — safe to call multiple times. */ -function loadGrammar(language: Language): unknown | null { - if (grammarCache.has(language)) { - return grammarCache.get(language) ?? null; - } +export async function initGrammars(): Promise { + if (grammarsInitialized) return; - const loader = grammarLoaders[language as GrammarLanguage]; - if (!loader) { - grammarCache.set(language, null); - return null; - } + await Parser.init(); - try { - const grammar = loader(); - if (!grammar) { - throw new Error(`Grammar loader returned empty value for ${language}`); - } - grammarCache.set(language, grammar); - return grammar; - } catch (error) { - const message = error instanceof Error ? error.message : String(error); - console.warn(`[CodeGraph] Failed to load ${language} grammar — parsing will be unavailable: ${message}`); - unavailableGrammarErrors.set(language, message); - grammarCache.set(language, null); - return null; - } + // Load all grammars in parallel + const entries = Object.entries(WASM_GRAMMAR_FILES) as [GrammarLanguage, string][]; + await Promise.allSettled( + entries.map(async ([lang, wasmFile]) => { + try { + const wasmPath = require.resolve(`tree-sitter-wasms/out/${wasmFile}`); + const language = await WasmLanguage.load(wasmPath); + languageCache.set(lang, language); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + console.warn(`[CodeGraph] Failed to load ${lang} grammar — parsing will be unavailable: ${message}`); + unavailableGrammarErrors.set(lang, message); + } + }) + ); + + grammarsInitialized = true; } /** - * Get a parser for the specified language + * Check if grammars have been initialized + */ +export function isGrammarsInitialized(): boolean { + return grammarsInitialized; +} + +/** + * Get a parser for the specified language. + * Returns synchronously from pre-loaded cache. */ export function getParser(language: Language): Parser | null { if (parserCache.has(language)) { return parserCache.get(language)!; } - const grammar = loadGrammar(language); - if (!grammar) { + const lang = languageCache.get(language); + if (!lang) { return null; } const parser = new Parser(); - parser.setLanguage(grammar as Parameters[0]); + parser.setLanguage(lang); parserCache.set(language, parser); return parser; } @@ -190,15 +147,15 @@ export function isLanguageSupported(language: Language): boolean { if (language === 'svelte') return true; // custom extractor (script block delegation) if (language === 'liquid') return true; // custom regex extractor if (language === 'unknown') return false; - return loadGrammar(language) !== null; + return languageCache.has(language); } /** * Get all currently supported languages. */ export function getSupportedLanguages(): Language[] { - const available = (Object.keys(grammarLoaders) as GrammarLanguage[]) - .filter((language) => loadGrammar(language) !== null); + const available = (Object.keys(WASM_GRAMMAR_FILES) as GrammarLanguage[]) + .filter((language) => languageCache.has(language)); return [...available, 'svelte', 'liquid']; } @@ -207,7 +164,8 @@ export function getSupportedLanguages(): Language[] { */ export function clearParserCache(): void { parserCache.clear(); - grammarCache.clear(); + // Note: languageCache is NOT cleared — WASM languages persist. + // To fully re-init, set grammarsInitialized = false and call initGrammars() again. unavailableGrammarErrors.clear(); } diff --git a/src/extraction/index.ts b/src/extraction/index.ts index 4573bb6..1abb0aa 100644 --- a/src/extraction/index.ts +++ b/src/extraction/index.ts @@ -18,7 +18,7 @@ import { } from '../types'; import { QueryBuilder } from '../db/queries'; import { extractFromSource } from './tree-sitter'; -import { detectLanguage, isLanguageSupported } from './grammars'; +import { detectLanguage, isLanguageSupported, initGrammars } from './grammars'; import { logDebug, logWarn } from '../errors'; import { captureException } from '../sentry'; import { validatePathWithinRoot, normalizePath } from '../utils'; @@ -342,6 +342,7 @@ export class ExtractionOrchestrator { onProgress?: (progress: IndexProgress) => void, signal?: AbortSignal ): Promise { + await initGrammars(); const startTime = Date.now(); const errors: ExtractionError[] = []; let filesIndexed = 0; @@ -682,6 +683,7 @@ export class ExtractionOrchestrator { * Uses git status as a fast path when available, falling back to full scan. */ async sync(onProgress?: (progress: IndexProgress) => void): Promise { + await initGrammars(); const startTime = Date.now(); let filesChecked = 0; let filesAdded = 0; @@ -918,4 +920,4 @@ export class ExtractionOrchestrator { // Re-export useful types and functions export { extractFromSource } from './tree-sitter'; -export { detectLanguage, isLanguageSupported, getSupportedLanguages } from './grammars'; +export { detectLanguage, isLanguageSupported, getSupportedLanguages, initGrammars } from './grammars'; diff --git a/src/extraction/tree-sitter.ts b/src/extraction/tree-sitter.ts index b19b7a7..ee8d6e4 100644 --- a/src/extraction/tree-sitter.ts +++ b/src/extraction/tree-sitter.ts @@ -4,7 +4,7 @@ * Handles parsing source code and extracting structural information. */ -import { SyntaxNode, Tree } from 'tree-sitter'; +import { Node as SyntaxNode, Tree } from 'web-tree-sitter'; import * as crypto from 'crypto'; import * as path from 'path'; import { @@ -875,7 +875,10 @@ export class TreeSitterExtractor { } try { - this.tree = parser.parse(this.source); + this.tree = parser.parse(this.source) ?? null; + if (!this.tree) { + throw new Error('Parser returned null tree'); + } // Create file node representing the source file const fileNode: Node = { @@ -1710,9 +1713,15 @@ export class TreeSitterExtractor { if (namespacePrefix && useGroup) { // Grouped import - create one import per item const prefix = getNodeText(namespacePrefix, this.source); - const useClauses = useGroup.namedChildren.filter((c: SyntaxNode) => c.type === 'namespace_use_clause'); + const useClauses = useGroup.namedChildren.filter((c: SyntaxNode) => + c.type === 'namespace_use_group_clause' || c.type === 'namespace_use_clause' + ); for (const clause of useClauses) { - const name = clause.namedChildren.find((c: SyntaxNode) => c.type === 'name'); + // WASM grammar wraps names in namespace_name; native uses name directly + const nsName = clause.namedChildren.find((c: SyntaxNode) => c.type === 'namespace_name'); + const name = nsName + ? nsName.namedChildren.find((c: SyntaxNode) => c.type === 'name') + : clause.namedChildren.find((c: SyntaxNode) => c.type === 'name'); if (name) { const fullPath = `${prefix}\\${getNodeText(name, this.source)}`; this.createNode('import', fullPath, node, { diff --git a/src/index.ts b/src/index.ts index 4758e91..ccc22ed 100644 --- a/src/index.ts +++ b/src/index.ts @@ -38,6 +38,7 @@ import { IndexResult, SyncResult, extractFromSource, + initGrammars, } from './extraction'; import { ReferenceResolver, @@ -60,7 +61,7 @@ export { CODEGRAPH_DIR, } from './directory'; export { IndexProgress, IndexResult, SyncResult } from './extraction'; -export { detectLanguage, isLanguageSupported, getSupportedLanguages } from './extraction'; +export { detectLanguage, isLanguageSupported, getSupportedLanguages, initGrammars } from './extraction'; export { ResolutionResult } from './resolution'; export { EmbeddingProgress } from './vectors'; export { @@ -184,6 +185,7 @@ export class CodeGraph { * @returns A new CodeGraph instance */ static async init(projectRoot: string, options: InitOptions = {}): Promise { + await initGrammars(); const resolvedRoot = path.resolve(projectRoot); // Check if already initialized @@ -253,6 +255,7 @@ export class CodeGraph { * @returns A CodeGraph instance */ static async open(projectRoot: string, options: OpenOptions = {}): Promise { + await initGrammars(); const resolvedRoot = path.resolve(projectRoot); // Check if initialized diff --git a/src/installer/banner.ts b/src/installer/banner.ts index 952f6e0..1f3f811 100644 --- a/src/installer/banner.ts +++ b/src/installer/banner.ts @@ -113,15 +113,16 @@ export function warn(message: string): void { /** * Show the "next steps" section after installation */ -export function showNextSteps(location: 'global' | 'local'): void { +export function showNextSteps(location: 'global' | 'local', useNpx?: boolean): void { console.log(); console.log(chalk.bold(' Done!') + ' Restart Claude Code to use CodeGraph.'); console.log(); if (location === 'global') { + const cmd = useNpx ? 'npx @colbymchenry/codegraph' : 'codegraph'; console.log(chalk.dim(' Quick start:')); console.log(chalk.dim(' cd your-project')); - console.log(chalk.cyan(' codegraph init -i')); + console.log(chalk.cyan(` ${cmd} init -i`)); } else { console.log(chalk.dim(' CodeGraph is ready to use in this project!')); } diff --git a/src/installer/config-writer.ts b/src/installer/config-writer.ts index 1d9b5cd..d37edd1 100644 --- a/src/installer/config-writer.ts +++ b/src/installer/config-writer.ts @@ -97,19 +97,29 @@ function writeJsonFile(filePath: string, data: Record): void { atomicWriteFileSync(filePath, JSON.stringify(data, null, 2) + '\n'); } +/** + * When true, all configs use `npx @colbymchenry/codegraph` instead of the + * bare `codegraph` command. Set by the installer when global install fails. + */ +let useNpxFallback = false; + +export function setUseNpxFallback(value: boolean): void { + useNpxFallback = value; +} + /** * Get the MCP server configuration for the given location */ function getMcpServerConfig(location: InstallLocation): Record { - if (location === 'global') { - // Global: use 'codegraph' command directly (assumes globally installed) + if (location === 'global' && !useNpxFallback) { + // Global: use 'codegraph' command directly (globally installed and in PATH) return { type: 'stdio', command: 'codegraph', args: ['serve', '--mcp'], }; } - // Local: use npx to run the package + // Local or npx fallback: use npx to run the package return { type: 'stdio', command: 'npx', @@ -212,7 +222,7 @@ export function hasPermissions(location: InstallLocation): boolean { * Stop → sync-if-dirty (sync, ensures fresh index before next user turn) */ function getHooksConfig(location: InstallLocation): Record { - const command = location === 'global' ? 'codegraph' : 'npx @colbymchenry/codegraph'; + const command = (location === 'global' && !useNpxFallback) ? 'codegraph' : 'npx @colbymchenry/codegraph'; return { PostToolUse: [ @@ -229,6 +239,7 @@ function getHooksConfig(location: InstallLocation): Record { ], Stop: [ { + matcher: '.*', hooks: [ { type: 'command', diff --git a/src/installer/index.ts b/src/installer/index.ts index e35f084..1cc6f9d 100644 --- a/src/installer/index.ts +++ b/src/installer/index.ts @@ -8,8 +8,7 @@ import { execSync } from 'child_process'; import { showBanner, showNextSteps, success, error, info, chalk } from './banner'; import { promptInstallLocation, promptAutoAllow, InstallLocation } from './prompts'; -import { writeMcpConfig, writePermissions, writeClaudeMd, writeHooks, hasMcpConfig, hasPermissions, hasHooks } from './config-writer'; -import CodeGraph from '../index'; +import { writeMcpConfig, writePermissions, writeClaudeMd, writeHooks, hasMcpConfig, hasPermissions, hasHooks, setUseNpxFallback } from './config-writer'; /** * Format a number with commas @@ -29,7 +28,8 @@ export async function runInstaller(): Promise { // Step 1: Check if codegraph is available (skip install if already there) let codegraphAvailable = false; try { - execSync('which codegraph', { stdio: 'pipe' }); + const checkCmd = process.platform === 'win32' ? 'where codegraph' : 'command -v codegraph'; + execSync(checkCmd, { stdio: 'pipe' }); codegraphAvailable = true; } catch { // Not installed globally yet @@ -40,13 +40,20 @@ export async function runInstaller(): Promise { try { execSync('npm install -g @colbymchenry/codegraph', { stdio: 'pipe' }); success('Installed codegraph command globally'); + codegraphAvailable = true; } catch { // May fail if no permissions, but that's ok - npx still works - info('Could not install globally (try with sudo if needed)'); + info('Could not install globally — will use npx instead'); + info('(MCP server and hooks will use npx @colbymchenry/codegraph)'); } console.log(); } + // If codegraph binary isn't in PATH, tell config-writer to use npx for everything + if (!codegraphAvailable) { + setUseNpxFallback(true); + } + // Step 2: Ask for installation location const location = await promptInstallLocation(); console.log(); @@ -104,7 +111,7 @@ export async function runInstaller(): Promise { } // Show next steps - showNextSteps(location); + showNextSteps(location, !codegraphAvailable); } catch (err) { console.log(); if (err instanceof Error && err.message.includes('readline was closed')) { @@ -123,6 +130,18 @@ export async function runInstaller(): Promise { async function initializeLocalProject(): Promise { const projectPath = process.cwd(); + // Lazy-load CodeGraph (requires native modules) + let CodeGraph: typeof import('../index').default; + try { + CodeGraph = (await import('../index')).default; + } catch (err) { + const msg = err instanceof Error ? err.message : String(err); + error(`Could not load native modules: ${msg}`); + info('Skipping project initialization. You can run "codegraph init -i" later.'); + info('If this persists, try a Node.js LTS version (20 or 22).'); + return; + } + // Check if already initialized if (CodeGraph.isInitialized(projectPath)) { info('CodeGraph already initialized in this project'); diff --git a/src/mcp/tools.ts b/src/mcp/tools.ts index ad7fea4..7d3e46c 100644 --- a/src/mcp/tools.ts +++ b/src/mcp/tools.ts @@ -12,6 +12,9 @@ import { clamp } from '../utils'; import { tmpdir } from 'os'; import { join } from 'path'; +/** Maximum output length to prevent context bloat (characters) */ +const MAX_OUTPUT_LENGTH = 15000; + /** * Mark a Claude session as having consulted MCP tools. * This enables Grep/Glob/Bash commands that would otherwise be blocked. @@ -820,19 +823,14 @@ export class ToolHandler { return { node: results[0]!.node, note: '' }; } - /** - * Maximum output length to prevent context bloat (characters) - */ - private readonly MAX_OUTPUT_LENGTH = 15000; - /** * Truncate output if it exceeds the maximum length */ private truncateOutput(text: string): string { - if (text.length <= this.MAX_OUTPUT_LENGTH) return text; - const truncated = text.slice(0, this.MAX_OUTPUT_LENGTH); + if (text.length <= MAX_OUTPUT_LENGTH) return text; + const truncated = text.slice(0, MAX_OUTPUT_LENGTH); const lastNewline = truncated.lastIndexOf('\n'); - const cutPoint = lastNewline > this.MAX_OUTPUT_LENGTH * 0.8 ? lastNewline : this.MAX_OUTPUT_LENGTH; + const cutPoint = lastNewline > MAX_OUTPUT_LENGTH * 0.8 ? lastNewline : MAX_OUTPUT_LENGTH; return truncated.slice(0, cutPoint) + '\n\n... (output truncated)'; } diff --git a/src/resolution/import-resolver.ts b/src/resolution/import-resolver.ts index 9418415..16307d1 100644 --- a/src/resolution/import-resolver.ts +++ b/src/resolution/import-resolver.ts @@ -105,7 +105,7 @@ function resolveRelativeImport( // Try the path as-is first const basePath = path.resolve(fromDir, importPath); - const relativePath = path.relative(projectRoot, basePath); + const relativePath = path.relative(projectRoot, basePath).replace(/\\/g, '/'); // Try each extension for (const ext of extensions) { diff --git a/src/vectors/manager.ts b/src/vectors/manager.ts index 6be1b29..3f8ac3d 100644 --- a/src/vectors/manager.ts +++ b/src/vectors/manager.ts @@ -4,7 +4,7 @@ * High-level manager that coordinates embedding generation and vector search. */ -import Database from 'better-sqlite3'; +import { SqliteDatabase } from '../db/sqlite-adapter'; import { Node, SearchResult, SearchOptions } from '../types'; import { TextEmbedder, createEmbedder, EmbedderOptions, EMBEDDING_DIMENSION } from './embedder'; import { VectorSearchManager, createVectorSearch } from './search'; @@ -68,7 +68,7 @@ export class VectorManager { private initialized = false; constructor( - db: Database.Database, + db: SqliteDatabase, queries: QueryBuilder, options: VectorManagerOptions = {} ) { @@ -355,7 +355,7 @@ export class VectorManager { * Create a vector manager */ export function createVectorManager( - db: Database.Database, + db: SqliteDatabase, queries: QueryBuilder, options?: VectorManagerOptions ): VectorManager { diff --git a/src/vectors/search.ts b/src/vectors/search.ts index d9d87e9..bdde94d 100644 --- a/src/vectors/search.ts +++ b/src/vectors/search.ts @@ -5,7 +5,7 @@ * Falls back to brute-force cosine similarity if sqlite-vss is not available. */ -import Database from 'better-sqlite3'; +import { SqliteDatabase } from '../db/sqlite-adapter'; import { Node } from '../types'; import { TextEmbedder, EMBEDDING_DIMENSION } from './embedder'; @@ -29,11 +29,11 @@ export interface VectorSearchOptions { * Handles vector storage and similarity search for semantic code search. */ export class VectorSearchManager { - private db: Database.Database; + private db: SqliteDatabase; private vssEnabled = false; private embeddingDimension: number; - constructor(db: Database.Database, dimension: number = EMBEDDING_DIMENSION) { + constructor(db: SqliteDatabase, dimension: number = EMBEDDING_DIMENSION) { this.db = db; this.embeddingDimension = dimension; } @@ -75,10 +75,11 @@ export class VectorSearchManager { const vss = await import('sqlite-vss'); // Use the load function which loads both vector0 and vss0 + // VSS extension expects the raw better-sqlite3 Database instance if (typeof vss.load === 'function') { - vss.load(this.db); + vss.load(this.db as any); } else if (typeof vss.default?.load === 'function') { - vss.default.load(this.db); + vss.default.load(this.db as any); } else { throw new Error('sqlite-vss load function not found'); } @@ -464,7 +465,7 @@ export class VectorSearchManager { * Create a vector search manager */ export function createVectorSearch( - db: Database.Database, + db: SqliteDatabase, dimension?: number ): VectorSearchManager { return new VectorSearchManager(db, dimension); diff --git a/src/web-tree-sitter.d.ts b/src/web-tree-sitter.d.ts new file mode 100644 index 0000000..aaafd5a --- /dev/null +++ b/src/web-tree-sitter.d.ts @@ -0,0 +1,182 @@ +/** + * Local type override for web-tree-sitter. + * + * The upstream types declare children/namedChildren as (Node | null)[], + * but in practice they never contain null entries. This override uses + * non-nullable arrays to match native tree-sitter's API and avoid + * pervasive null-check changes across the extraction pipeline. + * + * This file takes precedence over node_modules/web-tree-sitter/web-tree-sitter.d.ts + * because TypeScript resolves local declarations first. + */ +declare module 'web-tree-sitter' { + export interface Point { + row: number; + column: number; + } + + export interface Range { + startPosition: Point; + endPosition: Point; + startIndex: number; + endIndex: number; + } + + export interface Edit { + startPosition: Point; + oldEndPosition: Point; + newEndPosition: Point; + startIndex: number; + oldEndIndex: number; + newEndIndex: number; + } + + export type ParseCallback = (index: number, position: Point) => string | undefined; + + export interface ParseOptions { + includedRanges?: Range[]; + progressCallback?: (state: { currentOffset: number; hasError: boolean }) => void; + } + + export interface EmscriptenModule { + [key: string]: any; + } + + export class Parser { + language: Language | null; + static init(moduleOptions?: EmscriptenModule): Promise; + constructor(); + delete(): void; + setLanguage(language: Language | null): this; + parse(callback: string | ParseCallback, oldTree?: Tree | null, options?: ParseOptions): Tree | null; + reset(): void; + getIncludedRanges(): Range[]; + getTimeoutMicros(): number; + setTimeoutMicros(timeout: number): void; + setLogger(callback: ((message: string, isLex: boolean) => void) | boolean | null): this; + getLogger(): ((message: string, isLex: boolean) => void) | null; + } + + export class Language { + types: string[]; + fields: (string | null)[]; + get name(): string | null; + get version(): number; + get abiVersion(): number; + get fieldCount(): number; + get stateCount(): number; + fieldIdForName(fieldName: string): number | null; + fieldNameForId(fieldId: number): string | null; + idForNodeType(type: string, named: boolean): number | null; + get nodeTypeCount(): number; + nodeTypeForId(typeId: number): string | null; + nodeTypeIsNamed(typeId: number): boolean; + nodeTypeIsVisible(typeId: number): boolean; + get supertypes(): number[]; + subtypes(supertype: number): number[]; + nextState(stateId: number, typeId: number): number; + lookaheadIterator(stateId: number): any; + query(source: string): any; + static load(input: string | Uint8Array): Promise; + } + + export class Tree { + language: Language; + copy(): Tree; + delete(): void; + get rootNode(): Node; + rootNodeWithOffset(offsetBytes: number, offsetExtent: Point): Node; + edit(edit: Edit): void; + walk(): TreeCursor; + getChangedRanges(other: Tree): Range[]; + getIncludedRanges(): Range[]; + } + + export class Node { + id: number; + startIndex: number; + startPosition: Point; + tree: Tree; + get typeId(): number; + get grammarId(): number; + get type(): string; + get grammarType(): string; + get isNamed(): boolean; + get isExtra(): boolean; + get isError(): boolean; + get isMissing(): boolean; + get hasChanges(): boolean; + get hasError(): boolean; + get endIndex(): number; + get endPosition(): Point; + get text(): string; + get parseState(): number; + get nextParseState(): number; + equals(other: Node): boolean; + child(index: number): Node | null; + namedChild(index: number): Node | null; + childForFieldId(fieldId: number): Node | null; + childForFieldName(fieldName: string): Node | null; + fieldNameForChild(index: number): string | null; + fieldNameForNamedChild(index: number): string | null; + childrenForFieldName(fieldName: string): Node[]; + childrenForFieldId(fieldId: number): Node[]; + firstChildForIndex(index: number): Node | null; + firstNamedChildForIndex(index: number): Node | null; + get childCount(): number; + get namedChildCount(): number; + get firstChild(): Node | null; + get firstNamedChild(): Node | null; + get lastChild(): Node | null; + get lastNamedChild(): Node | null; + // Override: non-nullable arrays (tree-sitter never returns null in these) + get children(): Node[]; + get namedChildren(): Node[]; + descendantsOfType(types: string | string[], startPosition?: Point, endPosition?: Point): Node[]; + get nextSibling(): Node | null; + get previousSibling(): Node | null; + get nextNamedSibling(): Node | null; + get previousNamedSibling(): Node | null; + get descendantCount(): number; + get parent(): Node | null; + childWithDescendant(descendant: Node): Node | null; + descendantForIndex(start: number, end?: number): Node | null; + namedDescendantForIndex(start: number, end?: number): Node | null; + descendantForPosition(start: Point, end?: Point): Node | null; + namedDescendantForPosition(start: Point, end?: Point): Node | null; + walk(): TreeCursor; + edit(edit: Edit): void; + toString(): string; + } + + export class TreeCursor { + copy(): TreeCursor; + delete(): void; + get currentNode(): Node; + get currentFieldId(): number; + get currentFieldName(): string | null; + get currentDepth(): number; + get currentDescendantIndex(): number; + get nodeType(): string; + get nodeTypeId(): number; + get nodeStateId(): number; + get nodeId(): number; + get nodeIsNamed(): boolean; + get nodeIsMissing(): boolean; + get nodeText(): string; + get startPosition(): Point; + get endPosition(): Point; + get startIndex(): number; + get endIndex(): number; + gotoFirstChild(): boolean; + gotoLastChild(): boolean; + gotoParent(): boolean; + gotoNextSibling(): boolean; + gotoPreviousSibling(): boolean; + gotoDescendant(goalDescendantIndex: number): void; + gotoFirstChildForIndex(goalIndex: number): boolean; + gotoFirstChildForPosition(goalPosition: Point): boolean; + reset(node: Node): void; + resetTo(cursor: TreeCursor): void; + } +} diff --git a/tsconfig.json b/tsconfig.json index 2cc280e..4969d06 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -24,7 +24,10 @@ "esModuleInterop": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true, - "resolveJsonModule": true + "resolveJsonModule": true, + "paths": { + "web-tree-sitter": ["./src/web-tree-sitter.d.ts"] + } }, "include": ["src/**/*"], "exclude": ["node_modules", "dist", "__tests__"]