fix: drop better-sqlite3 to remove deprecated prebuild-install (#75)

npm was warning on every install that prebuild-install@7.1.3 is no
longer maintained. prebuild-install ships as a transitive dependency
of better-sqlite3 and upstream PR #1446 to replace it is still open,
so we switch to Node's built-in node:sqlite module (stable in Node 24,
experimental in Node 22/23) and remove the better-sqlite3 dep entirely.

- src/sqlite.ts: uses DatabaseSync from node:sqlite. The one-shot
  ExperimentalWarning about SQLite on Node 22/23 is silenced for that
  specific warning; other warnings pass through unchanged.
- package.json: engines.node bumped to >=22 (Node 20 EOL 2026-04-30),
  better-sqlite3 and @types/better-sqlite3 removed, @types/node added
  (it was coming in transitively via @types/better-sqlite3).
- tests/providers/opencode.test.ts: fixture DB creation switched to
  node:sqlite (API parity for the CREATE TABLE + INSERT + prepare
  path we use).

End-user install footprint shrinks from 167 to 40 packages and prints
zero deprecation warnings.

Credit: @primeminister for the report.
This commit is contained in:
AgentSeal 2026-04-18 01:26:23 -07:00
parent 2b15256189
commit 7aefd674fc
5 changed files with 83 additions and 540 deletions

View file

@ -1,6 +1,7 @@
import { createRequire } from 'node:module'
const require = createRequire(import.meta.url)
/// Thin SQLite read-only wrapper over Node's built-in `node:sqlite` module (stable in
/// Node 24, experimental in Node 22 / 23). Replaces the earlier `better-sqlite3` binding
/// so the dependency graph no longer pulls in the deprecated `prebuild-install` package
/// (issue #75). Works across Cursor and OpenCode session DBs, both of which we only read.
type Row = Record<string, unknown>
@ -9,21 +10,67 @@ export type SqliteDatabase = {
close(): void
}
let BetterSqlite3: unknown = null
type DatabaseSyncCtor = new (path: string, options?: { readOnly?: boolean }) => {
prepare(sql: string): { all(...params: unknown[]): Row[] }
close(): void
}
let DatabaseSync: DatabaseSyncCtor | null = null
let loadAttempted = false
let loadError: string | null = null
/// Lazily imports `node:sqlite`. On Node 22/23 it emits an ExperimentalWarning the first
/// time the module is loaded; we silence that specific warning once so dashboards aren't
/// preceded by a scary stderr line every run. Any other warnings (including future
/// non-SQLite ones) are left untouched.
function loadDriver(): boolean {
if (loadAttempted) return BetterSqlite3 !== null
if (loadAttempted) return DatabaseSync !== null
loadAttempted = true
const origEmit = process.emit.bind(process)
let restored = false
const restore = () => {
if (restored) return
restored = true
process.emit = origEmit
}
// Node's `process.emit` signature is overloaded; we intercept the 'warning' channel
// only and proxy everything else through unchanged. The `any` cast avoids chasing the
// overload union which isn't worth its verbosity for a single-purpose shim.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
process.emit = function patchedEmit(this: NodeJS.Process, event: string, ...args: any[]): boolean {
if (event === 'warning') {
const warning = args[0] as { name?: string; message?: string } | undefined
if (
warning?.name === 'ExperimentalWarning' &&
typeof warning.message === 'string' &&
/SQLite/i.test(warning.message)
) {
return false
}
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return (origEmit as any).call(this, event, ...args)
} as typeof process.emit
try {
BetterSqlite3 = require('better-sqlite3')
// Dynamic require via createRequire avoids TypeScript chasing types we don't need at
// build time (node:sqlite landed in @types/node much later than in Node itself).
// eslint-disable-next-line @typescript-eslint/no-require-imports
const mod = eval('require')('node:sqlite') as { DatabaseSync: DatabaseSyncCtor }
DatabaseSync = mod.DatabaseSync
return true
} catch {
loadError = 'SQLite-based providers (Cursor, OpenCode) require the better-sqlite3 package.\n' +
'Install it with: npm install -g better-sqlite3\n' +
'Then run codeburn again.'
} catch (err) {
const message = err instanceof Error ? err.message : String(err)
loadError =
'SQLite-based providers (Cursor, OpenCode) need Node 22+ with the node:sqlite module.\n' +
`Current Node: ${process.version}.\n` +
'Upgrade Node (https://nodejs.org) and run codeburn again.\n' +
`(underlying error: ${message})`
return false
} finally {
restore()
}
}
@ -36,16 +83,11 @@ export function getSqliteLoadError(): string {
}
export function openDatabase(path: string): SqliteDatabase {
if (!loadDriver()) {
if (!loadDriver() || DatabaseSync === null) {
throw new Error(getSqliteLoadError())
}
const Database = BetterSqlite3 as new (path: string, options?: Record<string, unknown>) => {
prepare(sql: string): { all(...params: unknown[]): Row[] }
close(): void
}
const db = new Database(path, { readonly: true, fileMustExist: true })
const db = new DatabaseSync(path, { readOnly: true })
return {
query<T extends Row = Row>(sql: string, params: unknown[] = []): T[] {