From 94c707469f908676bd5ce1bdcc9108a6c2222752 Mon Sep 17 00:00:00 2001 From: inquerium Date: Wed, 5 Aug 2026 17:14:16 -0500 Subject: [PATCH] fix: use embedded schema in packaged binaries --- package-lock.json | 4 ++-- package.json | 2 +- src/db/index.ts | 21 ++++++++++++++++----- test/packaging.test.ts | 8 ++++++++ 4 files changed, 27 insertions(+), 8 deletions(-) diff --git a/package-lock.json b/package-lock.json index bf435bd..10f50db 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "primer-brain", - "version": "0.1.4", + "version": "0.1.5", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "primer-brain", - "version": "0.1.4", + "version": "0.1.5", "license": "Apache-2.0", "dependencies": { "@agentclientprotocol/sdk": "^1.3.0", diff --git a/package.json b/package.json index 48fd1f9..db71801 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "primer-brain", - "version": "0.1.4", + "version": "0.1.5", "description": "An open learner record + MCP server. The memory layer for AI tutoring. No fixed interface \u2014 the tutor builds one per child, per moment.", "license": "Apache-2.0", "type": "module", diff --git a/src/db/index.ts b/src/db/index.ts index ec4bad0..83ed540 100644 --- a/src/db/index.ts +++ b/src/db/index.ts @@ -3,6 +3,7 @@ import { readFileSync, readdirSync, mkdirSync, existsSync } from 'node:fs'; import { dirname, join, resolve } from 'node:path'; import { fileURLToPath } from 'node:url'; import { homedir } from 'node:os'; +import { createRequire } from 'node:module'; import { SCHEMA_SQL, MIGRATIONS } from '../generated/assets.ts'; const HERE = dirname(fileURLToPath(import.meta.url)); @@ -42,18 +43,28 @@ export function closeDb(): void { * Disk first so an edit in a checkout takes effect immediately; the embedded copy * otherwise, so primer still starts when there is no source tree beside it. */ +function packaged(): boolean { + try { + return createRequire(import.meta.url)('node:sea').isSea(); + } catch { + return false; + } +} + +export function diskAssetPaths(name: string, isPackaged: boolean): string[] { + if (isPackaged) return []; + return [join(HERE, name), join(HERE, '..', '..', 'src', 'db', name)]; +} + function schemaSql(): string { - for (const candidate of [join(HERE, 'schema.sql'), join(HERE, '..', '..', 'src', 'db', 'schema.sql')]) { + for (const candidate of diskAssetPaths('schema.sql', packaged())) { if (existsSync(candidate)) return readFileSync(candidate, 'utf8'); } return SCHEMA_SQL; } function migrationsDir(): string | null { - for (const candidate of [ - join(HERE, 'migrations'), - join(HERE, '..', '..', 'src', 'db', 'migrations'), - ]) { + for (const candidate of diskAssetPaths('migrations', packaged())) { if (existsSync(candidate)) return candidate; } return null; diff --git a/test/packaging.test.ts b/test/packaging.test.ts index 1510cb8..b212bce 100644 --- a/test/packaging.test.ts +++ b/test/packaging.test.ts @@ -4,6 +4,7 @@ import { readFileSync } from 'node:fs'; import { join, dirname } from 'node:path'; import { fileURLToPath } from 'node:url'; import { isPackaged } from '../src/agent/claude-code.ts'; +import { diskAssetPaths } from '../src/db/index.ts'; import { SCHEMA_SQL, RUNTIME_JS, MIGRATIONS, CURRICULUM } from '../src/generated/assets.ts'; /** @@ -23,6 +24,13 @@ test('running from source is not mistaken for a packaged binary', () => { assert.equal(isPackaged(), false); }); +test('a packaged binary never reads schema or migrations beside itself', () => { + // A downloaded binary can sit in any directory. Neighbouring files belong to + // that directory, not to the release, and must not change a record migration. + assert.deepEqual(diskAssetPaths('schema.sql', true), []); + assert.deepEqual(diskAssetPaths('migrations', true), []); +}); + test('every asset the installers download is one the release workflow builds', () => { const workflow = read('.github/workflows/release.yml'); const built = new Set([...workflow.matchAll(/asset:\s*(\S+)/g)].map((m) => m[1]!));