diff --git a/plugin-sdk/src/cli/dev.ts b/plugin-sdk/src/cli/dev.ts index 545062573..87208a9cb 100644 --- a/plugin-sdk/src/cli/dev.ts +++ b/plugin-sdk/src/cli/dev.ts @@ -26,8 +26,17 @@ interface PluginLike { onLoad?: (ctx: unknown) => unknown; routes?: PluginRouteL class PermissionDenied extends Error {} +/** + * Accept both bind shapes the real host accepts: spread positional args AND a + * single array of them (better-sqlite3 binds an array; node:sqlite does not, + * so a plugin using the array form would fail only in dev). + */ +function flatBind(args: unknown[]): unknown[] { + return args.length === 1 && Array.isArray(args[0]) ? (args[0] as unknown[]) : args; +} + /** A dev db backed by node:sqlite if available, else an in-memory recorder that returns []. */ -function createDevDb(dbFile: string): { db: PluginContextDb; note: string; close: () => void } { +export function createDevDb(dbFile: string): { db: PluginContextDb; note: string; close: () => void } { try { // node:sqlite is available on Node 22.5+ (experimental). Fall back gracefully if not. const { DatabaseSync } = createRequire(import.meta.url)('node:sqlite') as { DatabaseSync: new (p: string) => SqliteDb }; @@ -38,9 +47,9 @@ function createDevDb(dbFile: string): { db: PluginContextDb; note: string; close note: `db:own → real SQLite at ${dbFile}`, close: () => { try { sq.close(); } catch { /* ignore */ } }, db: { - async query(sql: string, ...args: unknown[]) { return sq.prepare(sql).all(...args) as unknown[]; }, + async query(sql: string, ...args: unknown[]) { return sq.prepare(sql).all(...flatBind(args)) as unknown[]; }, async exec(sql: string, ...args: unknown[]) { - if (args.length) { const r = sq.prepare(sql).run(...args); return { changes: Number(r.changes ?? 0) }; } + if (args.length) { const r = sq.prepare(sql).run(...flatBind(args)); return { changes: Number(r.changes ?? 0) }; } sq.exec(sql); return { changes: 0 }; }, async migrate(id: string, sql: string) { if (applied.has(id)) return { applied: false }; sq.exec(sql); applied.add(id); return { applied: true }; }, @@ -167,7 +176,12 @@ export async function runDev(dir: string, opts: { port?: number } = {}): Promise await plugin.onLoad?.(ctx); version++; console.log(` ↻ loaded ${plugin.routes?.length ?? 0} route(s)`); - } catch (e) { console.error(' ✗ plugin failed to load:', e instanceof Error ? e.message : e); } + } catch (e) { + // A plugin whose load/onLoad throws would fail activation in TREK — don't + // keep serving its routes here, or dev would hide exactly that failure. + plugin = {}; + console.error(' ✗ plugin failed to load:', e instanceof Error ? e.message : e); + } }; await load(); diff --git a/plugin-sdk/test/sdk.test.ts b/plugin-sdk/test/sdk.test.ts index 78b12fe04..dc8bdb5a2 100644 --- a/plugin-sdk/test/sdk.test.ts +++ b/plugin-sdk/test/sdk.test.ts @@ -164,6 +164,26 @@ describe('dev-server SDK injection', () => { }); }); +describe('dev db bind shapes', () => { + let tmp: string; + beforeEach(() => { tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'devdb-')); }); + afterEach(() => { fs.rmSync(tmp, { recursive: true, force: true }); }); + + it('accepts spread args AND a single array of them, like the real host', async () => { + const { createDevDb } = await import('../src/cli/dev.js'); + const { db, close } = createDevDb(path.join(tmp, 'db.sqlite')); + try { + await db.migrate('001', 'CREATE TABLE kv (k TEXT PRIMARY KEY, v TEXT)'); + await db.exec('INSERT INTO kv (k, v) VALUES (?, ?)', ['a', '1']); // array form (better-sqlite3 style) + await db.exec('INSERT INTO kv (k, v) VALUES (?, ?)', 'b', '2'); // spread form + expect(await db.query('SELECT v FROM kv WHERE k = ?', ['a'])).toEqual([{ v: '1' }]); + expect(await db.query('SELECT v FROM kv WHERE k = ?', 'b')).toEqual([{ v: '2' }]); + } finally { + close(); + } + }); +}); + describe('reference plugin (examples/koffi)', () => { const dir = path.resolve(import.meta.dirname, '..', 'examples', 'koffi');