import { mkdirSync } from "node:fs"; import { dirname, resolve } from "node:path"; import { createClient, type Client } from "@libsql/client"; import { Effect, Schema } from "effect"; import type { StorageFailure } from "@executor-js/sdk"; import type { AppDescriptor } from "../pipeline/descriptor"; import type { AppsStore } from "../plugin/store"; // --------------------------------------------------------------------------- // SQLite-backed AppsStore (self-hosted). The apps subsystem owns its own // metadata store on disk: one small SQLite file holding the published // descriptor per scope and the content-addressed blobs (compiled ui bundles + // skill bodies). Kept separate from the executor DB so the plugin's persistence // is self-contained (matching the per-scope-file philosophy) and doesn't couple // to the executor's owner-policy layer. // --------------------------------------------------------------------------- const toUrl = (path: string): string => (path === ":memory:" ? path : `file:${resolve(path)}`); const SCHEMA = ` CREATE TABLE IF NOT EXISTS descriptors (scope TEXT PRIMARY KEY, snapshot_id TEXT NOT NULL, descriptor TEXT NOT NULL, published_at INTEGER NOT NULL); CREATE TABLE IF NOT EXISTS blobs (key TEXT PRIMARY KEY, value TEXT NOT NULL); CREATE TABLE IF NOT EXISTS scope_connections (connection_name TEXT PRIMARY KEY, scope TEXT NOT NULL); `; const decodeJsonUnknown = Schema.decodeUnknownSync(Schema.fromJsonString(Schema.Unknown)); // Not-really-failing storage errors from libSQL wrapped opaquely. const storageFail = (message: string, cause: unknown): StorageFailure => // oxlint-disable-next-line executor/no-double-cast -- boundary: AppsStore adapts libSQL failures to the SDK storage failure shape without importing its concrete class ({ _tag: "StorageError", message, cause }) as unknown as StorageFailure; export interface SqliteAppsStoreOptions { readonly path: string; } export const makeSqliteAppsStore = (options: SqliteAppsStoreOptions): AppsStore => { if (options.path !== ":memory:") mkdirSync(dirname(resolve(options.path)), { recursive: true }); const client: Client = createClient({ url: toUrl(options.path) }); let ready: Promise | undefined; const init = async () => { if (!ready) { ready = (async () => { for (const stmt of SCHEMA.split(";")) { const s = stmt.trim(); if (s) await client.execute(s); } })(); } return ready; }; return { putDescriptor: (_owner, descriptor) => Effect.tryPromise({ try: async () => { await init(); await client.execute({ sql: `INSERT INTO descriptors (scope, snapshot_id, descriptor, published_at) VALUES (?, ?, ?, ?) ON CONFLICT(scope) DO UPDATE SET snapshot_id=excluded.snapshot_id, descriptor=excluded.descriptor, published_at=excluded.published_at`, args: [descriptor.scope, descriptor.snapshotId, JSON.stringify(descriptor), Date.now()], }); }, catch: (cause) => storageFail("putDescriptor failed", cause), }), getDescriptor: (scope) => Effect.tryPromise({ try: async () => { await init(); const res = await client.execute({ sql: "SELECT descriptor FROM descriptors WHERE scope = ?", args: [scope], }); const row = res.rows[0]; return row ? (decodeJsonUnknown(String(row.descriptor)) as AppDescriptor) : null; }, catch: (cause) => storageFail("getDescriptor failed", cause), }), putScopeForConnection: (connectionName, scope) => Effect.tryPromise({ try: async () => { await init(); await client.execute({ sql: "INSERT INTO scope_connections (connection_name, scope) VALUES (?, ?) ON CONFLICT(connection_name) DO UPDATE SET scope=excluded.scope", args: [connectionName, scope], }); }, catch: (cause) => storageFail("putScopeForConnection failed", cause), }), getScopeForConnection: (connectionName) => Effect.tryPromise({ try: async () => { await init(); const res = await client.execute({ sql: "SELECT scope FROM scope_connections WHERE connection_name = ?", args: [connectionName], }); return res.rows[0] ? String(res.rows[0].scope) : null; }, catch: (cause) => storageFail("getScopeForConnection failed", cause), }), putBlob: (key, value) => Effect.tryPromise({ try: async () => { await init(); await client.execute({ sql: "INSERT INTO blobs (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value=excluded.value", args: [key, value], }); }, catch: (cause) => storageFail("putBlob failed", cause), }), getBlob: (key) => Effect.tryPromise({ try: async () => { await init(); const res = await client.execute({ sql: "SELECT value FROM blobs WHERE key = ?", args: [key], }); return res.rows[0] ? String(res.rows[0].value) : null; }, catch: (cause) => storageFail("getBlob failed", cause), }), }; };