codeburn/src/sqlite.ts
AgentSeal 11b3de89e4
fix(sqlite): load node:sqlite in ESM runtime
Replace eval-based require with createRequire(import.meta.url) so the SQLite driver loads correctly when the CLI runs as ESM.

This restores OpenCode and Cursor session discovery instead of returning empty results when require is unavailable.
2026-04-19 05:27:05 +00:00

101 lines
3.4 KiB
TypeScript

import { createRequire } from 'node:module'
/// 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.
const requireForSqlite = createRequire(import.meta.url)
type Row = Record<string, unknown>
export type SqliteDatabase = {
query<T extends Row = Row>(sql: string, params?: unknown[]): T[]
close(): void
}
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 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 {
const mod = requireForSqlite('node:sqlite') as { DatabaseSync: DatabaseSyncCtor }
DatabaseSync = mod.DatabaseSync
return true
} 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()
}
}
export function isSqliteAvailable(): boolean {
return loadDriver()
}
export function getSqliteLoadError(): string {
return loadError ?? 'SQLite driver not available'
}
export function openDatabase(path: string): SqliteDatabase {
if (!loadDriver() || DatabaseSync === null) {
throw new Error(getSqliteLoadError())
}
const db = new DatabaseSync(path, { readOnly: true })
return {
query<T extends Row = Row>(sql: string, params: unknown[] = []): T[] {
return db.prepare(sql).all(...params) as T[]
},
close() {
db.close()
},
}
}