refactor: migrate from better-sqlite3 to node:sqlite, npm glob to node:fs

Since Node >= 24 is the minimum engine, remove the better-sqlite3 fallback
chain from sf-db.ts, unit-ownership.ts, and cli-stats.ts. Use DatabaseSync
from node:sqlite directly. Also replace the `glob` npm package with built-in
node:fs/promises.glob and node:fs.globSync in pi-coding-agent LSP utils.

- Remove createRequire boilerplate and suppressSqliteWarning helper
- Simplify loadProvider() and openRawDb()
- Net -177 lines of fallback/middleware code

💘 Generated with Crush

Assisted-by: GLM-5.1 via Crush <crush@charm.land>
This commit is contained in:
Mikael Hugo 2026-05-02 06:13:57 +02:00
parent 040bdf4eb8
commit 980772cc90
5 changed files with 15 additions and 177 deletions

View file

@ -4,7 +4,7 @@ import * as os from "node:os";
import * as path from "node:path";
import { spawnSync } from "node:child_process";
import YAML from "yaml";
import { globSync } from "glob";
import { globSync } from "node:fs";
import { CONFIG_DIR_NAME } from "../../config.js";
import { isRecord } from "./helpers.js";
import type { ServerConfig } from "./types.js";
@ -151,7 +151,7 @@ export function hasRootMarkers(cwd: string, markers: string[]): boolean {
for (const marker of markers) {
if (marker.includes("*")) {
try {
const matches = globSync(marker, { cwd, nodir: false });
const matches = globSync(marker, { cwd });
if (matches.length > 0) {
return true;
}

View file

@ -1,6 +1,6 @@
import * as fsPromises from "node:fs/promises";
import path from "node:path";
import { glob } from "glob";
import { glob } from "node:fs/promises";
import { isEnoent } from "./helpers.js";
import type {
CallHierarchyItem,
@ -533,7 +533,7 @@ export async function collectGlobMatches(
maxMatches: number,
): Promise<{ matches: string[]; truncated: boolean }> {
const normalizedLimit = Number.isFinite(maxMatches) ? Math.max(1, Math.trunc(maxMatches)) : 1;
const allMatches = await glob(pattern, { cwd });
const allMatches: string[] = []; for await (const p of glob(pattern, { cwd })) allMatches.push(p);
if (allMatches.length > normalizedLimit) {
return { matches: allMatches.slice(0, normalizedLimit), truncated: true };
}

View file

@ -1,6 +1,6 @@
import { existsSync } from "node:fs";
import { createRequire } from "node:module";
import { join } from "node:path";
import { DatabaseSync } from "node:sqlite";
export interface ModelStatsRow {
model_id: string;
@ -33,7 +33,6 @@ interface ParsedStatsArgs {
unitType?: string;
}
const require = createRequire(import.meta.url);
export function parseDurationSeconds(value: string): number {
const match = value.trim().match(/^(\d+(?:\.\d+)?)([smhdw])$/i);
@ -203,42 +202,7 @@ function usage(): string {
}
function openSqliteDb(dbPath: string): SqliteDb {
try {
const sqlite = require("node:sqlite") as {
DatabaseSync?: new (
path: string,
options?: Record<string, unknown>,
) => SqliteDb;
};
if (sqlite.DatabaseSync) {
return new sqlite.DatabaseSync(dbPath, { readOnly: true });
}
} catch {
// Try better-sqlite3 below.
}
try {
const mod = require("better-sqlite3") as
| {
default?: new (
path: string,
options?: Record<string, unknown>,
) => SqliteDb;
}
| (new (
path: string,
options?: Record<string, unknown>,
) => SqliteDb);
const Database = typeof mod === "function" ? mod : mod.default;
if (Database)
return new Database(dbPath, { readonly: true, fileMustExist: true });
} catch {
// Report a single actionable error below.
}
throw new Error(
"No SQLite provider available (tried node:sqlite, better-sqlite3)",
);
return new DatabaseSync(dbPath, { readOnly: true }) as SqliteDb;
}
export async function runStatsCli(

View file

@ -1,6 +1,5 @@
// SF Database Abstraction Layer
// Provides a SQLite database with provider fallback chain:
// node:sqlite (built-in) → better-sqlite3 (npm) → null (unavailable)
// Provides a SQLite database via node:sqlite (Node >= 24 built-in).
//
// Exposes a unified sync API for decisions and requirements storage.
// Schema is initialized on first open with WAL mode for file-backed DBs.
@ -21,8 +20,8 @@
// excluded from this invariant.
import { copyFileSync, existsSync, mkdirSync, realpathSync } from "node:fs";
import { createRequire } from "node:module";
import { dirname } from "node:path";
import { DatabaseSync } from "node:sqlite";
import { SF_STALE_STATE, SFError } from "./errors.js";
import { getGateIdsForTurn, type OwnerTurn } from "./gate-registry.js";
import type { VisionAlignmentMeetingRecord } from "./milestone-quality.js";
@ -42,7 +41,6 @@ import { logError, logWarning } from "./workflow-logger.js";
// pure structure with no runtime coupling.
import type { StateManifest } from "./workflow-manifest.js";
const _require = createRequire(import.meta.url);
interface DbStatement {
run(...params: unknown[]): unknown;
@ -56,69 +54,14 @@ interface DbAdapter {
close(): void;
}
type ProviderName = "node:sqlite" | "better-sqlite3";
let providerName: ProviderName | null = null;
let providerModule: unknown = null;
let loadAttempted = false;
function suppressSqliteWarning(): void {
const origEmit = process.emit;
// Override via loose cast: Node's overloaded emit signature is not directly assignable.
(process as any).emit = (event: string, ...args: unknown[]): boolean => {
if (
event === "warning" &&
args[0] &&
typeof args[0] === "object" &&
"name" in args[0] &&
(args[0] as { name: string }).name === "ExperimentalWarning" &&
"message" in args[0] &&
typeof (args[0] as { message: string }).message === "string" &&
(args[0] as { message: string }).message.includes("SQLite")
) {
return false;
}
return origEmit.apply(process, [event, ...args] as Parameters<
typeof process.emit
>) as unknown as boolean;
};
}
function loadProvider(): void {
if (loadAttempted) return;
loadAttempted = true;
try {
suppressSqliteWarning();
const mod = _require("node:sqlite");
if (mod.DatabaseSync) {
providerModule = mod;
providerName = "node:sqlite";
return;
}
} catch {
// unavailable
}
try {
const mod = _require("better-sqlite3");
if (typeof mod === "function" || (mod && mod.default)) {
providerModule = mod.default || mod;
providerName = "better-sqlite3";
return;
}
} catch {
// unavailable
}
const nodeMajor = parseInt(process.versions.node.split(".")[0], 10);
const versionHint =
nodeMajor < 22
? ` SF requires Node >= 22.0.0 (current: v${process.versions.node}). Upgrade Node to fix this.`
: "";
process.stderr.write(
`sf-db: No SQLite provider available (tried node:sqlite, better-sqlite3).${versionHint}\n`,
);
// node:sqlite is built-in in Node >= 24
}
function normalizeRow(row: unknown): Record<string, unknown> | undefined {
@ -184,17 +127,7 @@ function createAdapter(rawDb: unknown): DbAdapter {
function openRawDb(path: string): unknown {
loadProvider();
if (!providerModule || !providerName) return null;
if (providerName === "node:sqlite") {
const { DatabaseSync } = providerModule as {
DatabaseSync: new (path: string) => unknown;
};
return new DatabaseSync(path);
}
const Database = providerModule as new (path: string) => unknown;
return new Database(path);
return new DatabaseSync(path);
}
const SCHEMA_VERSION = 21;
@ -1479,9 +1412,9 @@ let _dbOpenAttempted = false;
/**
* Get the name of the SQLite provider currently loaded (or null if unavailable).
*/
export function getDbProvider(): ProviderName | null {
export function getDbProvider(): string | null {
loadProvider();
return providerName;
return "node:sqlite";
}
/**
@ -1830,8 +1763,6 @@ export function _getAdapter(): DbAdapter | null {
export function _resetProvider(): void {
loadAttempted = false;
providerModule = null;
providerName = null;
}
export function upsertDecision(d: Omit<Decision, "seq">): void {

View file

@ -13,10 +13,9 @@
// Copyright (c) 2026 Jeremy McSpadden <jeremy@fluxlabs.net>
import { mkdirSync } from "node:fs";
import { createRequire } from "node:module";
import { join } from "node:path";
import { DatabaseSync } from "node:sqlite";
const _require = createRequire(import.meta.url);
// ─── Types ───────────────────────────────────────────────────────────────
@ -38,60 +37,14 @@ interface DbLike {
close(): void;
}
type ProviderName = "node:sqlite" | "better-sqlite3";
let providerName: ProviderName | null = null;
let providerModule: unknown = null;
let loadAttempted = false;
function suppressSqliteWarning(): void {
const origEmit = process.emit;
// Override via loose cast: Node's overloaded emit signature is not directly assignable.
(process as any).emit = (event: string, ...args: unknown[]): boolean => {
if (
event === "warning" &&
args[0] &&
typeof args[0] === "object" &&
"name" in args[0] &&
(args[0] as { name: string }).name === "ExperimentalWarning" &&
"message" in args[0] &&
typeof (args[0] as { message: string }).message === "string" &&
(args[0] as { message: string }).message.includes("SQLite")
) {
return false;
}
return origEmit.apply(process, [event, ...args] as Parameters<
typeof process.emit
>) as unknown as boolean;
};
}
function loadProvider(): void {
if (loadAttempted) return;
loadAttempted = true;
try {
suppressSqliteWarning();
const mod = _require("node:sqlite");
if (mod.DatabaseSync) {
providerModule = mod;
providerName = "node:sqlite";
return;
}
} catch {
// unavailable
}
try {
const mod = _require("better-sqlite3");
if (typeof mod === "function" || (mod && mod.default)) {
providerModule = mod.default || mod;
providerName = "better-sqlite3";
return;
}
} catch {
// unavailable
}
// node:sqlite is built-in in Node >= 24
}
function normalizeRow(row: unknown): Record<string, unknown> | undefined {
@ -104,17 +57,7 @@ function normalizeRow(row: unknown): Record<string, unknown> | undefined {
function openRawDb(path: string): unknown {
loadProvider();
if (!providerModule || !providerName) return null;
if (providerName === "node:sqlite") {
const { DatabaseSync } = providerModule as {
DatabaseSync: new (path: string) => unknown;
};
return new DatabaseSync(path);
}
const Database = providerModule as new (path: string) => unknown;
return new Database(path);
return new DatabaseSync(path);
}
function wrapDb(rawDb: unknown): DbLike {