Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
21 changes: 16 additions & 5 deletions src/db/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Expand Down Expand Up @@ -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;
Expand Down
8 changes: 8 additions & 0 deletions test/packaging.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

/**
Expand All @@ -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]!));
Expand Down
Loading