diff options
| author | Adam Malczewski <[email protected]> | 2026-05-21 17:30:08 +0900 |
|---|---|---|
| committer | Adam Malczewski <[email protected]> | 2026-05-21 17:30:08 +0900 |
| commit | d6b208342edf97bafa5b1dcc986b782f9879d141 (patch) | |
| tree | c6d8f9bffa86f78e3b1369b811bef9642df788d0 /packages/core/src/db | |
| parent | 1f309ccca20aabbd0ee3fb8fbb3c8192124edd95 (diff) | |
| download | dispatch-d6b208342edf97bafa5b1dcc986b782f9879d141.tar.gz dispatch-d6b208342edf97bafa5b1dcc986b782f9879d141.zip | |
feat: SQLite database for all credentials, keys, wake schedule, and usage cache
- Add SQLite database at ~/.local/share/dispatch/dispatch.db with tables: credentials, api_keys, wake_schedule, usage_cache
- Store Claude OAuth credentials in DB with import button in Model Status UI
- Store OpenCode/Copilot API keys in DB with paste-to-import modal
- Store OpenCode cookie and workspace IDs in DB
- Migrate wake schedule from .wake-schedule.json to DB
- Migrate usage cache from in-memory Map + localStorage to DB
- Remove all env var and file fallbacks — DB is the single source of truth
- Add seed scripts: bin/import-credentials.ts, bin/seed-opencode-keys.ts
- Docker: container runs as host UID/GID with matching home directory
- Clean up dispatch.toml: remove env fields, update comments
- Progress bar time markers for usage cycle tracking
Diffstat (limited to 'packages/core/src/db')
| -rw-r--r-- | packages/core/src/db/index.ts | 93 |
1 files changed, 93 insertions, 0 deletions
diff --git a/packages/core/src/db/index.ts b/packages/core/src/db/index.ts new file mode 100644 index 0000000..818aff0 --- /dev/null +++ b/packages/core/src/db/index.ts @@ -0,0 +1,93 @@ +import { Database } from "bun:sqlite"; +import { existsSync, mkdirSync } from "node:fs"; +import { isAbsolute, join } from "node:path"; +import { homedir } from "node:os"; + +/** + * Returns the directory for persistent Dispatch data, following XDG Base + * Directory spec on Linux: `$XDG_DATA_HOME/dispatch` (defaults to + * `~/.local/share/dispatch`). + */ +function getDataDir(): string { + const xdg = process.env.XDG_DATA_HOME; + const base = xdg && isAbsolute(xdg) ? xdg : join(homedir(), ".local", "share"); + return join(base, "dispatch"); +} + +let _db: Database | null = null; + +/** + * Get (or create) the singleton SQLite database. + * + * - Creates the data directory if it doesn't exist. + * - Creates `dispatch.db` if it doesn't exist. + * - Enables WAL journal mode for concurrent read performance. + */ +export function getDatabase(): Database { + if (_db) return _db; + + const dir = getDataDir(); + if (!existsSync(dir)) { + mkdirSync(dir, { recursive: true }); + } + + const dbPath = join(dir, "dispatch.db"); + _db = new Database(dbPath, { create: true }); + + // WAL mode: better concurrent read performance, safe for single-writer + _db.run("PRAGMA journal_mode = WAL;"); + // Recommended for WAL: normal synchronous is safe and faster + _db.run("PRAGMA synchronous = NORMAL;"); + // Enable foreign keys + _db.run("PRAGMA foreign_keys = ON;"); + + // Create tables + _db.run(`CREATE TABLE IF NOT EXISTS credentials ( + key_id TEXT PRIMARY KEY, + provider TEXT NOT NULL, + access_token TEXT NOT NULL, + refresh_token TEXT NOT NULL, + expires_at INTEGER NOT NULL, + subscription_type TEXT, + source_file TEXT, + imported_at INTEGER NOT NULL, + updated_at INTEGER NOT NULL + )`); + + _db.run(`CREATE TABLE IF NOT EXISTS wake_schedule ( + hour INTEGER PRIMARY KEY CHECK (hour BETWEEN 0 AND 23), + next_wake_at INTEGER NOT NULL + )`); + + _db.run(`CREATE TABLE IF NOT EXISTS usage_cache ( + key_id TEXT PRIMARY KEY, + provider TEXT NOT NULL, + cached_at INTEGER NOT NULL, + report_json TEXT NOT NULL + )`); + + _db.run(`CREATE TABLE IF NOT EXISTS api_keys ( + key_id TEXT PRIMARY KEY, + provider TEXT NOT NULL, + api_key TEXT NOT NULL, + imported_at INTEGER NOT NULL, + updated_at INTEGER NOT NULL + )`); + + return _db; +} + +/** Close the database connection (e.g. on shutdown). */ +export function closeDatabase(): void { + if (_db) { + _db.close(); + _db = null; + } +} + +/** Returns the path where the database file lives (or will live). */ +export function getDatabasePath(): string { + if (_db) return _db.filename; + const dir = getDataDir(); + return join(dir, "dispatch.db"); +} |
