UNPKG

akm-cli

Version:

akm (Agent Knowledge Manager) — a portable, local-first capability library for AI agents. Discover, load, share, and improve reusable skills, scripts, workflows, and knowledge across any shell-capable coding agent, including Claude Code, OpenCode, and Cur

118 lines (117 loc) • 5.24 kB
// This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. /** * Managed-database seam — the single home for the SQLite open/lifecycle recipe. * * Two lifecycle steps are shared across the current managed databases * (`state.db`, `logs.db`, and `index.db`) and their consumers: * * 1. The open recipe: `mkdir(dir) → openDatabase(path) → applyStandardPragmas * → migrate`. * 2. The borrow-or-own lifecycle: `const db = ctx?.db ?? open(); const owns = * !ctx?.db; try { … } finally { if (owns) db.close(); }`. * * {@link openManagedDatabase} owns (1); {@link withManagedDb} owns (2). Each DB * module supplies only a path + initializer and gets a `withXDb` loan helper * (see `withIndexDb`, `withStateDb`, etc.) so callers never hand-roll the * ownership flag or the finally/close again. This is also the one place to add * busy-timeout tuning, integrity checks, or test-isolation injection. */ import fs from "node:fs"; import path from "node:path"; import { openDatabase } from "./database.js"; import { applyStandardPragmas } from "./sqlite-pragmas.js"; /** * Open a managed SQLite database: ensure the parent dir exists, open the handle, * apply standard pragmas, then run the schema initializer. The single home for * the open→pragmas→migrate recipe. * * ── On file permissions (reverted, issue #791) ── * * This function briefly chmodded the database, its `-wal`/`-shm` sidecars, and * THE CONTAINING DIRECTORY to owner-only on every open (#756). That was a * mistake and is deliberately not coming back: * * - It mutated state akm did not create. The data directory belongs to the * operator; a read of the index is not consent to re-permission their disk. * - It ran on the most-traveled path in the CLI, including `create: false` * (read-only) opens, so any command at all silently converted a legacy * `0755` directory to `0700` with no prompt, warning, or migration note. * - It therefore broke installs that share `$XDG_DATA_HOME` across uids — * agent sandboxes, containers, service accounts — which worked in 0.9.0. * Worse, the read path answers an unreadable index with a false * "No search index available" at exit 0 rather than an error (#791). * * Files akm creates here get the process umask, which is the operator's lever * for this and always was. akm neither sets these modes nor reports on them. */ export function openManagedDatabase(spec) { const dir = path.dirname(spec.path); if (spec.create !== false && !fs.existsSync(dir)) { fs.mkdirSync(dir, { recursive: true }); } const db = spec.create === false ? openDatabase(spec.path, { create: false }) : openDatabase(spec.path); try { applyStandardPragmas(db, spec.pragmas ?? { dataDir: dir }); spec.init?.(db); return db; } catch (error) { // Initializers may open a transaction (source update does so before index // schema work). Never strand that transaction/handle when later setup // fails; closing rolls it back and releases its writer lock. // // `db.inTransaction` is INSIDE the try: reading it on an already-closed // handle throws "Database has closed", and it used to sit outside, so that // throw escaped and replaced `error` — masking the real initializer failure // this whole block exists to preserve. Every other step here was already // guarded for exactly that reason; this one was not, and a concurrent // close (WAL contention between two writers) reached it. try { if (db.inTransaction) db.exec("ROLLBACK"); } catch { // Closing remains the final rollback backstop. } try { db.close(); } catch { // Preserve the initializer failure, which identifies the real boundary. } throw error; } } /** * Run `fn` against a managed database, owning its lifecycle. * * When `opts.borrowed` is supplied the caller already owns an open handle: it is * passed straight through and NOT closed (borrow). Otherwise a fresh handle is * opened via `open` and closed in a `finally` (own). This replaces the * hand-rolled `ctx?.db ?? open()` + `ownsDb` flag + `finally`/close idiom — the * ownership decision and the close live here, once. * * Synchronous by design: the DB consumers (telemetry writers, planners) finish * all work within the tick, matching the inline blocks this replaces. */ export function withManagedDb(open, fn, opts) { if (opts?.borrowed) { return fn(opts.borrowed); } const db = open(); try { return fn(db); } finally { try { db.close(); } catch { // A close() throw here would REPLACE fn's in-flight exception — the same // masking openManagedDatabase above was hardened against. The handle is // being discarded either way; fn's error is the one that matters. } } }