@lorekit/cli 1.19.0 → 1.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/lorekit.mjs +6 -0
- package/package.json +1 -1
- package/src/bootstrap.mjs +141 -0
- package/src/doctor.mjs +37 -0
package/bin/lorekit.mjs
CHANGED
|
@@ -18,6 +18,7 @@ import { lint } from '../src/lint.mjs';
|
|
|
18
18
|
import { dedupe } from '../src/dedupe.mjs';
|
|
19
19
|
import { hook } from '../src/hook.mjs';
|
|
20
20
|
import { migrate } from '../src/migrate.mjs';
|
|
21
|
+
import { bootstrap } from '../src/bootstrap.mjs';
|
|
21
22
|
import { mcpServer } from '../src/mcp-server.mjs';
|
|
22
23
|
import { traceCommand } from '../src/telemetry.mjs';
|
|
23
24
|
import { loadDotEnv } from '../src/dotenv.mjs';
|
|
@@ -80,6 +81,9 @@ ${c.bold('Commands')}
|
|
|
80
81
|
dedupe Find likely-duplicate memories via a zero-dep word-overlap HEURISTIC
|
|
81
82
|
(Jaccard >= threshold, not semantic), grouped into clusters per
|
|
82
83
|
store. --json, --scope <s>, --threshold <0..1>.
|
|
84
|
+
bootstrap Apply the BYOD schema to a user-supplied Supabase database.
|
|
85
|
+
Only needed when using LOREKIT_STORAGE_URL / LOREKIT_STORAGE_ANON_KEY.
|
|
86
|
+
See docs/byod.md for setup instructions.
|
|
83
87
|
migrate Relocate a LoreKit-format local store into the current layout.
|
|
84
88
|
Dry-run by default; pass --yes to apply. Idempotent.
|
|
85
89
|
hook Hook engine for Claude Code / Cursor / Codex. Reads the host's
|
|
@@ -613,6 +617,8 @@ async function main() {
|
|
|
613
617
|
return traceCommand('dedupe', args, VERSION, () => dedupe(args));
|
|
614
618
|
case 'migrate':
|
|
615
619
|
return traceCommand('migrate', args, VERSION, () => migrate(args));
|
|
620
|
+
case 'bootstrap':
|
|
621
|
+
return traceCommand('bootstrap', args, VERSION, () => bootstrap(args));
|
|
616
622
|
case 'write':
|
|
617
623
|
return traceCommand('write', args, VERSION, () => write(args));
|
|
618
624
|
default:
|
package/package.json
CHANGED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
// `lorekit bootstrap` — apply the BYOD schema to a user-supplied Supabase database.
|
|
2
|
+
//
|
|
3
|
+
// Reads LOREKIT_STORAGE_URL and LOREKIT_STORAGE_SERVICE_KEY from the environment.
|
|
4
|
+
// If neither is set, prints a helpful message and exits 0 — bootstrap is only
|
|
5
|
+
// needed for BYOD (Bring Your Own Database) setups.
|
|
6
|
+
//
|
|
7
|
+
// Because Supabase's JS client does not expose a raw SQL execution method for DDL,
|
|
8
|
+
// this command instructs the user to run the SQL file directly with psql when no
|
|
9
|
+
// direct execution path is available. It validates connectivity using the anon key
|
|
10
|
+
// and confirms the bootstrap.sql path for the user.
|
|
11
|
+
import fs from 'node:fs';
|
|
12
|
+
import path from 'node:path';
|
|
13
|
+
import { fileURLToPath } from 'node:url';
|
|
14
|
+
import process from 'node:process';
|
|
15
|
+
import { log, err, heading, status, c } from './util.mjs';
|
|
16
|
+
|
|
17
|
+
// The bootstrap.sql file is at <repo-root>/supabase/byod/bootstrap.sql.
|
|
18
|
+
// This file lives at packages/cli/src/bootstrap.mjs, so the relative path
|
|
19
|
+
// from here to the repo root is ../../../ (src → cli → packages → root).
|
|
20
|
+
const BOOTSTRAP_SQL_PATH = fileURLToPath(
|
|
21
|
+
new URL('../../../supabase/byod/bootstrap.sql', import.meta.url),
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
export async function bootstrap(_args) {
|
|
25
|
+
heading('LoreKit bootstrap');
|
|
26
|
+
|
|
27
|
+
const storageUrl = process.env['LOREKIT_STORAGE_URL'];
|
|
28
|
+
const storageServiceKey = process.env['LOREKIT_STORAGE_SERVICE_KEY'];
|
|
29
|
+
const storageAnonKey = process.env['LOREKIT_STORAGE_ANON_KEY'];
|
|
30
|
+
|
|
31
|
+
// If no BYOD env vars are set, this command is a no-op — it's only for BYOD.
|
|
32
|
+
if (!storageUrl && !storageServiceKey) {
|
|
33
|
+
log('');
|
|
34
|
+
log(
|
|
35
|
+
` ${c.cyan('•')} No BYOD storage configured — ${c.dim('bootstrap is only needed for custom databases.')}`,
|
|
36
|
+
);
|
|
37
|
+
log('');
|
|
38
|
+
log(' Set LOREKIT_STORAGE_URL and LOREKIT_STORAGE_SERVICE_KEY to use your own');
|
|
39
|
+
log(' Supabase project, then re-run this command.');
|
|
40
|
+
log('');
|
|
41
|
+
log(' See docs/byod.md for setup instructions.');
|
|
42
|
+
return 0;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (!storageUrl) {
|
|
46
|
+
err(`${c.red('Error:')} LOREKIT_STORAGE_SERVICE_KEY is set but LOREKIT_STORAGE_URL is missing.`);
|
|
47
|
+
err(' Both variables are required. Set LOREKIT_STORAGE_URL and try again.');
|
|
48
|
+
return 1;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Locate the bootstrap SQL file.
|
|
52
|
+
let sqlPath = BOOTSTRAP_SQL_PATH;
|
|
53
|
+
|
|
54
|
+
// When running from a published npm package, the SQL file is not bundled
|
|
55
|
+
// with the CLI. Fall back to looking for it relative to CWD (for dev use).
|
|
56
|
+
if (!fs.existsSync(sqlPath)) {
|
|
57
|
+
const cwdPath = path.resolve(process.cwd(), 'supabase/byod/bootstrap.sql');
|
|
58
|
+
if (fs.existsSync(cwdPath)) {
|
|
59
|
+
sqlPath = cwdPath;
|
|
60
|
+
} else {
|
|
61
|
+
err(`${c.red('Error:')} bootstrap.sql not found at expected path:`);
|
|
62
|
+
err(` ${sqlPath}`);
|
|
63
|
+
err('');
|
|
64
|
+
err(' Apply the schema manually with psql:');
|
|
65
|
+
err(' psql "$DATABASE_URL" -f supabase/byod/bootstrap.sql');
|
|
66
|
+
err('');
|
|
67
|
+
err(' Or download it from the LoreKit repository:');
|
|
68
|
+
err(' https://github.com/mthines/lorekit/blob/main/supabase/byod/bootstrap.sql');
|
|
69
|
+
return 1;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
status('pass', 'bootstrap.sql', sqlPath);
|
|
74
|
+
status('info', 'storage url', storageUrl);
|
|
75
|
+
|
|
76
|
+
// Test connectivity using the anon key if available (non-DDL path).
|
|
77
|
+
if (storageAnonKey) {
|
|
78
|
+
try {
|
|
79
|
+
const { createClient } = await import('@supabase/supabase-js');
|
|
80
|
+
const db = createClient(storageUrl, storageAnonKey, {
|
|
81
|
+
auth: { persistSession: false, autoRefreshToken: false },
|
|
82
|
+
});
|
|
83
|
+
// Simple ping: list memories (will return 0 rows or error if schema not applied yet).
|
|
84
|
+
const { error } = await db.from('memories').select('id').limit(1);
|
|
85
|
+
if (error && error.code === '42P01') {
|
|
86
|
+
// Table does not exist yet — that's expected before bootstrap.
|
|
87
|
+
status('info', 'connectivity', 'connected — schema not yet applied (run bootstrap)');
|
|
88
|
+
} else if (error) {
|
|
89
|
+
status('warn', 'connectivity', `connected but got: ${error.message}`);
|
|
90
|
+
} else {
|
|
91
|
+
status('pass', 'connectivity', 'connected and schema already present');
|
|
92
|
+
}
|
|
93
|
+
} catch (e) {
|
|
94
|
+
status('warn', 'connectivity', `could not verify: ${e && e.message ? e.message : String(e)}`);
|
|
95
|
+
}
|
|
96
|
+
} else {
|
|
97
|
+
status('info', 'connectivity', 'skipped — LOREKIT_STORAGE_ANON_KEY not set');
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Supabase JS client cannot execute raw DDL SQL directly.
|
|
101
|
+
// The correct path is psql or the Supabase dashboard SQL editor.
|
|
102
|
+
log('');
|
|
103
|
+
log(` ${c.bold('To apply the schema, run:')}`);
|
|
104
|
+
log('');
|
|
105
|
+
log(` ${c.cyan('psql "$DATABASE_URL" -f')} ${sqlPath}`);
|
|
106
|
+
log('');
|
|
107
|
+
log(` Or paste the contents of ${c.dim(sqlPath)}`);
|
|
108
|
+
log(' into the Supabase dashboard → SQL Editor.');
|
|
109
|
+
log('');
|
|
110
|
+
|
|
111
|
+
if (storageServiceKey) {
|
|
112
|
+
status(
|
|
113
|
+
'info',
|
|
114
|
+
'service key',
|
|
115
|
+
'LOREKIT_STORAGE_SERVICE_KEY is set — use it as $DATABASE_URL password with psql',
|
|
116
|
+
);
|
|
117
|
+
log('');
|
|
118
|
+
log(` ${c.dim('Example:')}`);
|
|
119
|
+
log(
|
|
120
|
+
` ${c.cyan('DATABASE_URL')}="postgresql://postgres.${parseRef(storageUrl)}:${storageServiceKey}@aws-0-us-east-1.pooler.supabase.com:6543/postgres"`,
|
|
121
|
+
);
|
|
122
|
+
log(` ${c.cyan('psql "$DATABASE_URL"')} -f ${sqlPath}`);
|
|
123
|
+
log('');
|
|
124
|
+
log(
|
|
125
|
+
` ${c.dim('(Replace the host/port above with the exact connection string from your')}`,
|
|
126
|
+
);
|
|
127
|
+
log(` ${c.dim('Supabase dashboard → Settings → Database → Connection string.)')}`);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return 0;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Extract the project ref from a Supabase URL for display in the example.
|
|
134
|
+
function parseRef(url) {
|
|
135
|
+
try {
|
|
136
|
+
const host = new URL(url).hostname; // e.g. abcdefgh.supabase.co
|
|
137
|
+
return host.split('.')[0];
|
|
138
|
+
} catch {
|
|
139
|
+
return '<project-ref>';
|
|
140
|
+
}
|
|
141
|
+
}
|
package/src/doctor.mjs
CHANGED
|
@@ -98,6 +98,9 @@ export async function doctor(args) {
|
|
|
98
98
|
await checkRemote(control, root, args, record);
|
|
99
99
|
}
|
|
100
100
|
|
|
101
|
+
// 4b. BYOD storage connectivity check.
|
|
102
|
+
await checkBYODStorage(record);
|
|
103
|
+
|
|
101
104
|
// 5. Scope.
|
|
102
105
|
const scope = deriveScope(root);
|
|
103
106
|
if (scope.hasRemote) {
|
|
@@ -351,6 +354,40 @@ function hooksForEvent(hooksObj, event) {
|
|
|
351
354
|
return commands;
|
|
352
355
|
}
|
|
353
356
|
|
|
357
|
+
async function checkBYODStorage(record) {
|
|
358
|
+
const storageUrl = process.env['LOREKIT_STORAGE_URL'];
|
|
359
|
+
const storageAnonKey = process.env['LOREKIT_STORAGE_ANON_KEY'];
|
|
360
|
+
|
|
361
|
+
if (!storageUrl && !storageAnonKey) {
|
|
362
|
+
return; // No BYOD configured — skip silently.
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
if (storageUrl && !storageAnonKey) {
|
|
366
|
+
record('fail', 'byod storage', 'LOREKIT_STORAGE_URL is set but LOREKIT_STORAGE_ANON_KEY is missing');
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
if (!storageUrl && storageAnonKey) {
|
|
371
|
+
record('fail', 'byod storage', 'LOREKIT_STORAGE_ANON_KEY is set but LOREKIT_STORAGE_URL is missing');
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
try {
|
|
376
|
+
const { createClient } = await import('@supabase/supabase-js');
|
|
377
|
+
const db = createClient(storageUrl, storageAnonKey, {
|
|
378
|
+
auth: { persistSession: false, autoRefreshToken: false },
|
|
379
|
+
});
|
|
380
|
+
const { error } = await db.from('memories').select('id').limit(1);
|
|
381
|
+
if (error && error.code !== '42P01') {
|
|
382
|
+
record('fail', 'byod storage', `connectivity error: ${error.message}`);
|
|
383
|
+
} else {
|
|
384
|
+
record('pass', 'byod storage', `ok — ${storageUrl}`);
|
|
385
|
+
}
|
|
386
|
+
} catch (e) {
|
|
387
|
+
record('fail', 'byod storage', `could not connect: ${e && e.message ? e.message : String(e)}`);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
354
391
|
function gitTracked(root, dir) {
|
|
355
392
|
// Heuristic: is the store dir ignored by git? If `git check-ignore` names it,
|
|
356
393
|
// it is private; otherwise it will be committed (team-shared).
|