Phase 8c: drizzle and better-sqlite3 out, one node:sqlite connection, no native module #15
Loading…
Reference in a new issue
No description provided.
Delete branch "refactor/phase-8c-sqlite"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Phase 8, step c, of the DDD/CQRS refactor: drizzle and better-sqlite3 leave Kanbot. All database access now goes through one connection to Node's built-in
node:sqlite, and the desktop bundle no longer contains any native module. Stacked on #14.What changes
Plain SQL instead of drizzle's builder
server/database/sql.ts(all,get,run,inTransaction) is the only legacy file that touches the driver. It caches prepared statements and copies rows into plain objects.inTransactionbehaves like better-sqlite3's.transaction():BEGIN/COMMITon its own, a savepoint inside another transaction (a legacy one or an Effect command's), and a rollback before rethrowing.server/database/tables/<table>.ts, with field names, order, JSON and boolean handling exactly as drizzle mapped them.statements.tshas the insert/update/upsert/delete helpers.test/infra/tableRows.test.ts: every column the database has must be declared, and every field of every table must survive a write and a read. That closes the "a new field is stored but read back empty" trap for rows.Migrations without drizzle
runMigrations()runs only Kanbot's migrator. Its first migration,0001_legacy_handover, already applies the drizzle-era migrations from the now-frozenlegacy.generated.ts.CREATEdiffers.server/database/schema.ts,server/database/migrations/,drizzle.config.ts, the generator script,db:generate, andKANBOT_MIGRATIONS_DIR(from the server, Electron, tests and the packaged app's resources).One node:sqlite connection
DatabaseSyncwith WAL, foreign keys on, a 5 s busy timeout, plain-object rows and numbers rather than BigInts. Effect'sSqlClient(SharedSqliteClient, with its guard against yielding inside transactions) runs on the same connection.node:sqliteas external, which silences a "could not be resolved" build warning.Packages removed: drizzle-orm, drizzle-kit, better-sqlite3, @types/better-sqlite3 and @electron/rebuild.
desktop-prepareno longer trims prebuilds, and now fails if a.nodefile ever appears in the server bundle.Checks
sqliteLocks.test.tskeeps what can still be tested: two node:sqlite connections respect a held lock, and better-sqlite3 stays uninstalled.Machines.list'slastSeenAt, which is boot time.integrity_checkpassed.electron-builder --mac --diranddesktop-verifypassed./api/status,/api/tasks,/api/projectsand RPC..nodefile.Not done
🤖 Generated with Claude Code
packages/{core,contracts,infra-sqlite,plugin-sdk} and plugins/* become workspaces (@kanbot/core, @kanbot/contracts, @kanbot/infra-sqlite, @kanbot/plugin-sdk, @kanbot/plugin-<id>), each with the dependencies it imports and nothing else: effect pinned at the repo's exact version, the workspace packages as "*" (npm links a workspace whose version satisfies the range; it has no workspace: protocol). Only the SDK is not private. The plugins take effect and the SDK as peers: the host supplies the one copy of each, which is also what a prebuilt plugin bundle leaves external. zod and the Claude Agent SDK leave the root for @kanbot/plugin-claude-code, the only code that imports them. A plugin's package version is its manifest's. Each package's exports point at its TypeScript sources: the app, Nitro, vitest and Storybook compile them as they did, through the workspace symlinks. Imports still use relative paths; the next commit switches them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>Cookies are not scoped by port. The packaged app set its per-launch cookie (kanbot_local) on 127.0.0.1, and after the tray's "Open in browser" the system browser had it too: it then sent it to every server on 127.0.0.1, whatever the port, for instance a dev server an agent started from a cloned repository's code, which could log it (and drive Kanbot with it) or overwrite it with a Set-Cookie of its own. The shell now makes a name for this machine once per install, kanbot-<16 hex>.localhost (kept 0600 in the user-data folder), and the window, its cookie and the browser handoff all use it; the shell's own REST calls and boot poll stay on 127.0.0.1 with the Bearer header. The name goes to the server with the token over the message port (it is not secret, but must be known), and hostIsLoopback accepts exactly that name besides the loopback names; the server takes no name the shell could not have made (/^kanbot-[a-z0-9]{12,32}\.localhost$/). Nothing changes in how the server listens: it still binds 127.0.0.1. Browsers resolve *.localhost to this machine by themselves (Chromium, Firefox), and Safari asks macOS's resolver, which does too (getaddrinfo answers 127.0.0.1 and ::1 here), so the handoff needs no fallback to 127.0.0.1. Checked in headless Chrome against a server bound to 127.0.0.1: the name resolves, and a cookie set under it is not sent to 127.0.0.1 or localhost on another port. Remaining: the cookie still goes to the same name on another port, so a program that reads the name from the user-data folder and gets the browser to open it on its own port could still read the cookie; it is one more step than before, not a wall. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>vue-i18n reads @ as a linked message, so plugins.problem.noMetadata and undecorated failed to compile and took the dev page down with them. They are reworded rather than escaped as {'@'}, which the server's translate() would print verbatim. A test compiles every message, the plugins' included. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>Any code from Settings › Devices could pair a node, so a code offered for someone's phone could become a node the hub sends prompts to. An offer is now for a node or, by default, for a browser or a script (Devices.offerPairing takes an optional { kind }; no payload still works), and redeeming it as the other kind fails like a wrong code (it counts towards burning it; every failure is the same PairingRefused / 401). Settings › Devices' pairing card has a "For a node" switch: it asks for a node's code and shows the kanbot-node pair command to run there instead of the address and QR code. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>A code offered for a desktop (Devices.offerPairing({ kind: 'desktop' })) pairs two devices in one transaction: the window's browser device (its token is the kanbot_device cookie, Secure over HTTPS) and the node it runs (its token in the answer: { deviceId, nodeDeviceId, nodeToken }). devices.companion_id (migration 0022) points each at the other. Revoking either revokes both and cuts both off. A code for another kind fails like a wrong one. Settings › Devices offers the three kinds and shows a desktop as one entry. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>Over the utility process's parent port ({ type: 'kanbot-shell', action: 'open-connect' }), only from the desktop app's own window (a Local caller) and only when the shell started the server to talk over that port; anything else is ShellUnavailable. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>