bertrand 0.17.0 → 0.18.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/dist/bertrand.js +887 -136
- package/dist/run-screen.js +2 -1
- package/package.json +2 -1
package/dist/bertrand.js
CHANGED
|
@@ -25,12 +25,173 @@ var init_paths = __esm(() => {
|
|
|
25
25
|
root: join(homedir(), BERTRAND_DIR),
|
|
26
26
|
db: join(homedir(), BERTRAND_DIR, "bertrand.db"),
|
|
27
27
|
hooks: join(homedir(), BERTRAND_DIR, "hooks"),
|
|
28
|
-
sessions: join(homedir(), BERTRAND_DIR, "sessions")
|
|
28
|
+
sessions: join(homedir(), BERTRAND_DIR, "sessions"),
|
|
29
|
+
syncEnv: join(homedir(), BERTRAND_DIR, "sync.env")
|
|
29
30
|
};
|
|
30
31
|
});
|
|
31
32
|
|
|
33
|
+
// src/sync/config.ts
|
|
34
|
+
import {
|
|
35
|
+
readFileSync,
|
|
36
|
+
statSync,
|
|
37
|
+
writeFileSync,
|
|
38
|
+
chmodSync,
|
|
39
|
+
existsSync
|
|
40
|
+
} from "fs";
|
|
41
|
+
function parseEnv(contents) {
|
|
42
|
+
const out = {};
|
|
43
|
+
for (const rawLine of contents.split(/\r?\n/)) {
|
|
44
|
+
const line = rawLine.trim();
|
|
45
|
+
if (!line || line.startsWith("#"))
|
|
46
|
+
continue;
|
|
47
|
+
const eq = line.indexOf("=");
|
|
48
|
+
if (eq === -1)
|
|
49
|
+
continue;
|
|
50
|
+
const key = line.slice(0, eq).trim();
|
|
51
|
+
if (!KEYS.includes(key))
|
|
52
|
+
continue;
|
|
53
|
+
let value = line.slice(eq + 1).trim();
|
|
54
|
+
if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
|
|
55
|
+
value = value.slice(1, -1);
|
|
56
|
+
}
|
|
57
|
+
out[key] = value;
|
|
58
|
+
}
|
|
59
|
+
return out;
|
|
60
|
+
}
|
|
61
|
+
function hasSyncConfig() {
|
|
62
|
+
return existsSync(paths.syncEnv);
|
|
63
|
+
}
|
|
64
|
+
function loadSyncConfig() {
|
|
65
|
+
if (!existsSync(paths.syncEnv))
|
|
66
|
+
return null;
|
|
67
|
+
const mode = statSync(paths.syncEnv).mode & 511;
|
|
68
|
+
if (mode & 63) {
|
|
69
|
+
console.warn(`warning: ${paths.syncEnv} was mode 0${mode.toString(8)} (should be 0600). Tightening to 0600.`);
|
|
70
|
+
try {
|
|
71
|
+
chmodSync(paths.syncEnv, 384);
|
|
72
|
+
} catch (e) {
|
|
73
|
+
console.warn(`failed to chmod 0600: ${e instanceof Error ? e.message : String(e)}. Fix manually.`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
const env = parseEnv(readFileSync(paths.syncEnv, "utf8"));
|
|
77
|
+
if (!env.SUPABASE_URL || !env.SUPABASE_SERVICE_KEY || !env.BERTRAND_SYNC_BUCKET || !env.BERTRAND_ENCRYPTION_KEY) {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
return {
|
|
81
|
+
supabaseUrl: env.SUPABASE_URL,
|
|
82
|
+
supabaseServiceKey: env.SUPABASE_SERVICE_KEY,
|
|
83
|
+
bucket: env.BERTRAND_SYNC_BUCKET,
|
|
84
|
+
objectKey: env.BERTRAND_SYNC_OBJECT || "bertrand.db.enc",
|
|
85
|
+
encryptionKey: env.BERTRAND_ENCRYPTION_KEY,
|
|
86
|
+
clientName: env.BERTRAND_CLIENT_NAME || `bertrand-${process.platform}-${process.pid}`
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
function saveSyncConfig(cfg) {
|
|
90
|
+
const lines = [
|
|
91
|
+
"# bertrand sync configuration",
|
|
92
|
+
"# Created by `bertrand sync onboard`. chmod 600.",
|
|
93
|
+
"#",
|
|
94
|
+
"# SUPABASE_SERVICE_KEY has read+write access to your storage bucket.",
|
|
95
|
+
"# Treat it like an SSH private key. Do not commit. Do not share.",
|
|
96
|
+
"#",
|
|
97
|
+
"# BERTRAND_ENCRYPTION_KEY is the AES-256-GCM key used to encrypt the DB",
|
|
98
|
+
"# locally before upload. Without it the uploaded blob can't be decrypted.",
|
|
99
|
+
"# Use the SAME key on every machine that should be able to pull this DB.",
|
|
100
|
+
`SUPABASE_URL=${cfg.supabaseUrl}`,
|
|
101
|
+
`SUPABASE_SERVICE_KEY=${cfg.supabaseServiceKey}`,
|
|
102
|
+
`BERTRAND_SYNC_BUCKET=${cfg.bucket}`,
|
|
103
|
+
`BERTRAND_SYNC_OBJECT=${cfg.objectKey}`,
|
|
104
|
+
`BERTRAND_ENCRYPTION_KEY=${cfg.encryptionKey}`,
|
|
105
|
+
`BERTRAND_CLIENT_NAME=${cfg.clientName}`,
|
|
106
|
+
""
|
|
107
|
+
];
|
|
108
|
+
writeFileSync(paths.syncEnv, lines.join(`
|
|
109
|
+
`), { mode: 384 });
|
|
110
|
+
chmodSync(paths.syncEnv, 384);
|
|
111
|
+
}
|
|
112
|
+
var KEYS;
|
|
113
|
+
var init_config = __esm(() => {
|
|
114
|
+
init_paths();
|
|
115
|
+
KEYS = [
|
|
116
|
+
"SUPABASE_URL",
|
|
117
|
+
"SUPABASE_SERVICE_KEY",
|
|
118
|
+
"BERTRAND_SYNC_BUCKET",
|
|
119
|
+
"BERTRAND_SYNC_OBJECT",
|
|
120
|
+
"BERTRAND_ENCRYPTION_KEY",
|
|
121
|
+
"BERTRAND_CLIENT_NAME"
|
|
122
|
+
];
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
// src/lib/config.ts
|
|
126
|
+
import { readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
127
|
+
import { join as join2 } from "path";
|
|
128
|
+
function readConfig() {
|
|
129
|
+
try {
|
|
130
|
+
return JSON.parse(readFileSync2(CONFIG_PATH, "utf-8"));
|
|
131
|
+
} catch {
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
function writeConfig(config) {
|
|
136
|
+
writeFileSync2(CONFIG_PATH, JSON.stringify(config, null, 2) + `
|
|
137
|
+
`);
|
|
138
|
+
}
|
|
139
|
+
function patchConfig(patch) {
|
|
140
|
+
const current = readConfig() ?? {};
|
|
141
|
+
const next = deepMerge(current, patch);
|
|
142
|
+
writeConfig(next);
|
|
143
|
+
return next;
|
|
144
|
+
}
|
|
145
|
+
function deepMerge(base, patch) {
|
|
146
|
+
const out = { ...base };
|
|
147
|
+
for (const [k, v] of Object.entries(patch)) {
|
|
148
|
+
const existing = out[k];
|
|
149
|
+
if (isPlainObject(existing) && isPlainObject(v)) {
|
|
150
|
+
out[k] = deepMerge(existing, v);
|
|
151
|
+
} else if (v !== undefined) {
|
|
152
|
+
out[k] = v;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return out;
|
|
156
|
+
}
|
|
157
|
+
function isPlainObject(v) {
|
|
158
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
159
|
+
}
|
|
160
|
+
function isSyncEnabled() {
|
|
161
|
+
return readConfig()?.sync?.enabled === true;
|
|
162
|
+
}
|
|
163
|
+
var CONFIG_PATH;
|
|
164
|
+
var init_config2 = __esm(() => {
|
|
165
|
+
init_paths();
|
|
166
|
+
CONFIG_PATH = join2(paths.root, "config.json");
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
// src/sync/trigger.ts
|
|
170
|
+
import { spawn } from "child_process";
|
|
171
|
+
function spawnSync(op) {
|
|
172
|
+
if (!isSyncEnabled() || !hasSyncConfig())
|
|
173
|
+
return;
|
|
174
|
+
const bin = process.argv[1] && process.argv[1].endsWith(".ts") ? process.execPath : "bertrand";
|
|
175
|
+
const args = process.argv[1] && process.argv[1].endsWith(".ts") ? ["run", process.argv[1], "sync", op] : ["sync", op];
|
|
176
|
+
const child = spawn(bin, args, {
|
|
177
|
+
detached: true,
|
|
178
|
+
stdio: "ignore"
|
|
179
|
+
});
|
|
180
|
+
child.unref();
|
|
181
|
+
}
|
|
182
|
+
function triggerBackgroundPull() {
|
|
183
|
+
spawnSync("pull");
|
|
184
|
+
}
|
|
185
|
+
function triggerBackgroundPush() {
|
|
186
|
+
spawnSync("push");
|
|
187
|
+
}
|
|
188
|
+
var init_trigger = __esm(() => {
|
|
189
|
+
init_config();
|
|
190
|
+
init_config2();
|
|
191
|
+
});
|
|
192
|
+
|
|
32
193
|
// src/cli/router.ts
|
|
33
|
-
import { existsSync } from "fs";
|
|
194
|
+
import { existsSync as existsSync2 } from "fs";
|
|
34
195
|
function register(name, handler) {
|
|
35
196
|
commands.set(name, handler);
|
|
36
197
|
}
|
|
@@ -38,7 +199,7 @@ function alias(from, to) {
|
|
|
38
199
|
aliases.set(from, to);
|
|
39
200
|
}
|
|
40
201
|
async function autoInitIfFirstRun() {
|
|
41
|
-
if (
|
|
202
|
+
if (existsSync2(paths.db))
|
|
42
203
|
return;
|
|
43
204
|
const init = commands.get("init");
|
|
44
205
|
if (!init)
|
|
@@ -57,6 +218,7 @@ async function route(argv) {
|
|
|
57
218
|
const command = args[0];
|
|
58
219
|
if (!command) {
|
|
59
220
|
await autoInitIfFirstRun();
|
|
221
|
+
triggerBackgroundPull();
|
|
60
222
|
const handler2 = commands.get("launch");
|
|
61
223
|
if (!handler2)
|
|
62
224
|
throw new Error("No launch command registered");
|
|
@@ -92,11 +254,13 @@ Usage:
|
|
|
92
254
|
bertrand archive <name> Archive/unarchive a session
|
|
93
255
|
bertrand update Hook-facing state writer (internal)
|
|
94
256
|
bertrand serve Start dashboard HTTP server
|
|
257
|
+
bertrand sync <op> push|pull|status|onboard (see: bertrand sync --help)
|
|
95
258
|
`.trim());
|
|
96
259
|
}
|
|
97
260
|
var commands, aliases;
|
|
98
261
|
var init_router = __esm(() => {
|
|
99
262
|
init_paths();
|
|
263
|
+
init_trigger();
|
|
100
264
|
commands = new Map;
|
|
101
265
|
aliases = new Map;
|
|
102
266
|
});
|
|
@@ -411,6 +575,7 @@ var init_update = __esm(() => {
|
|
|
411
575
|
init_sessions();
|
|
412
576
|
init_events();
|
|
413
577
|
init_conversations();
|
|
578
|
+
init_trigger();
|
|
414
579
|
EVENT_STATUS_MAP = {
|
|
415
580
|
"session.waiting": "waiting",
|
|
416
581
|
"session.answered": "active",
|
|
@@ -480,11 +645,14 @@ var init_update = __esm(() => {
|
|
|
480
645
|
if (event === "session.waiting" && conversationId && meta?.question) {
|
|
481
646
|
updateLastQuestion(conversationId, meta.question);
|
|
482
647
|
}
|
|
648
|
+
if (event === "session.end") {
|
|
649
|
+
triggerBackgroundPush();
|
|
650
|
+
}
|
|
483
651
|
});
|
|
484
652
|
});
|
|
485
653
|
|
|
486
654
|
// src/lib/transcript.ts
|
|
487
|
-
import { existsSync as
|
|
655
|
+
import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
|
|
488
656
|
function getContextWindowSize(model) {
|
|
489
657
|
for (const [prefix, size] of Object.entries(CONTEXT_WINDOW_SIZES)) {
|
|
490
658
|
if (model.startsWith(prefix))
|
|
@@ -493,9 +661,9 @@ function getContextWindowSize(model) {
|
|
|
493
661
|
return 200000;
|
|
494
662
|
}
|
|
495
663
|
function getLatestAssistantTurn(filePath) {
|
|
496
|
-
if (!
|
|
664
|
+
if (!existsSync3(filePath))
|
|
497
665
|
return null;
|
|
498
|
-
const text2 =
|
|
666
|
+
const text2 = readFileSync3(filePath, "utf-8");
|
|
499
667
|
const lines = text2.split(`
|
|
500
668
|
`);
|
|
501
669
|
const assistantEntries = [];
|
|
@@ -553,9 +721,9 @@ function getLatestAssistantTurn(filePath) {
|
|
|
553
721
|
};
|
|
554
722
|
}
|
|
555
723
|
function getContextSnapshot(filePath) {
|
|
556
|
-
if (!
|
|
724
|
+
if (!existsSync3(filePath))
|
|
557
725
|
return null;
|
|
558
|
-
const text2 =
|
|
726
|
+
const text2 = readFileSync3(filePath, "utf-8");
|
|
559
727
|
const lines = text2.split(`
|
|
560
728
|
`);
|
|
561
729
|
for (let i = lines.length - 1;i >= 0; i--) {
|
|
@@ -822,8 +990,8 @@ class NoopAdapter {
|
|
|
822
990
|
}
|
|
823
991
|
|
|
824
992
|
// src/terminal/index.ts
|
|
825
|
-
import { readFileSync as
|
|
826
|
-
import { join as
|
|
993
|
+
import { readFileSync as readFileSync4 } from "fs";
|
|
994
|
+
import { join as join3 } from "path";
|
|
827
995
|
function getTerminalAdapter() {
|
|
828
996
|
if (cachedAdapter)
|
|
829
997
|
return cachedAdapter;
|
|
@@ -842,7 +1010,7 @@ function getTerminalAdapter() {
|
|
|
842
1010
|
}
|
|
843
1011
|
function readConfigTerminal() {
|
|
844
1012
|
try {
|
|
845
|
-
const config = JSON.parse(
|
|
1013
|
+
const config = JSON.parse(readFileSync4(join3(paths.root, "config.json"), "utf-8"));
|
|
846
1014
|
return config.terminal ?? null;
|
|
847
1015
|
} catch {
|
|
848
1016
|
return null;
|
|
@@ -1193,8 +1361,8 @@ var init_session_archive = __esm(() => {
|
|
|
1193
1361
|
|
|
1194
1362
|
// src/server/index.ts
|
|
1195
1363
|
import { execFile } from "child_process";
|
|
1196
|
-
import { existsSync as
|
|
1197
|
-
import { join as
|
|
1364
|
+
import { existsSync as existsSync4 } from "fs";
|
|
1365
|
+
import { join as join4 } from "path";
|
|
1198
1366
|
function liveStats(sessionId) {
|
|
1199
1367
|
return {
|
|
1200
1368
|
sessionId,
|
|
@@ -1247,11 +1415,11 @@ function match(pathname, url) {
|
|
|
1247
1415
|
}
|
|
1248
1416
|
function findDashboardDir() {
|
|
1249
1417
|
const candidates = [
|
|
1250
|
-
|
|
1251
|
-
|
|
1418
|
+
join4(import.meta.dir, "dashboard"),
|
|
1419
|
+
join4(import.meta.dir, "..", "dashboard")
|
|
1252
1420
|
];
|
|
1253
1421
|
for (const dir of candidates) {
|
|
1254
|
-
if (
|
|
1422
|
+
if (existsSync4(join4(dir, "index.html")))
|
|
1255
1423
|
return dir;
|
|
1256
1424
|
}
|
|
1257
1425
|
return null;
|
|
@@ -1260,13 +1428,13 @@ async function serveDashboard(pathname) {
|
|
|
1260
1428
|
if (!DASHBOARD_DIR)
|
|
1261
1429
|
return null;
|
|
1262
1430
|
const requested = pathname === "/" ? "/index.html" : pathname;
|
|
1263
|
-
const filePath =
|
|
1431
|
+
const filePath = join4(DASHBOARD_DIR, requested);
|
|
1264
1432
|
if (!filePath.startsWith(DASHBOARD_DIR))
|
|
1265
1433
|
return null;
|
|
1266
1434
|
const file = Bun.file(filePath);
|
|
1267
1435
|
if (await file.exists())
|
|
1268
1436
|
return new Response(file);
|
|
1269
|
-
return new Response(Bun.file(
|
|
1437
|
+
return new Response(Bun.file(join4(DASHBOARD_DIR, "index.html")));
|
|
1270
1438
|
}
|
|
1271
1439
|
function startServer(port = PORT) {
|
|
1272
1440
|
const server = Bun.serve({
|
|
@@ -1387,6 +1555,652 @@ var init_serve = __esm(() => {
|
|
|
1387
1555
|
});
|
|
1388
1556
|
});
|
|
1389
1557
|
|
|
1558
|
+
// src/sync/snapshot.ts
|
|
1559
|
+
import { Database as Database2 } from "bun:sqlite";
|
|
1560
|
+
import { existsSync as existsSync5, unlinkSync } from "fs";
|
|
1561
|
+
function takeSnapshot() {
|
|
1562
|
+
cleanupSnapshot();
|
|
1563
|
+
const src = new Database2(paths.db, { readonly: true });
|
|
1564
|
+
try {
|
|
1565
|
+
src.exec(`VACUUM INTO '${SNAPSHOT_PATH.replace(/'/g, "''")}'`);
|
|
1566
|
+
} finally {
|
|
1567
|
+
src.close();
|
|
1568
|
+
}
|
|
1569
|
+
return SNAPSHOT_PATH;
|
|
1570
|
+
}
|
|
1571
|
+
function cleanupSnapshot() {
|
|
1572
|
+
for (const suffix of SIDECAR_SUFFIXES) {
|
|
1573
|
+
const p = SNAPSHOT_PATH + suffix;
|
|
1574
|
+
if (existsSync5(p)) {
|
|
1575
|
+
try {
|
|
1576
|
+
unlinkSync(p);
|
|
1577
|
+
} catch {}
|
|
1578
|
+
}
|
|
1579
|
+
}
|
|
1580
|
+
}
|
|
1581
|
+
var SIDECAR_SUFFIXES, SNAPSHOT_PATH;
|
|
1582
|
+
var init_snapshot2 = __esm(() => {
|
|
1583
|
+
init_paths();
|
|
1584
|
+
SIDECAR_SUFFIXES = ["", "-wal", "-shm"];
|
|
1585
|
+
SNAPSHOT_PATH = `${paths.db}.sync-snapshot`;
|
|
1586
|
+
});
|
|
1587
|
+
|
|
1588
|
+
// src/sync/crypto.ts
|
|
1589
|
+
import { createCipheriv, createDecipheriv, randomBytes } from "crypto";
|
|
1590
|
+
function parseKey(base64Key) {
|
|
1591
|
+
const buf = Buffer.from(base64Key, "base64");
|
|
1592
|
+
if (buf.length !== 32) {
|
|
1593
|
+
throw new Error(`BERTRAND_ENCRYPTION_KEY must decode to 32 bytes (got ${buf.length}). ` + `Generate one with: openssl rand -base64 32`);
|
|
1594
|
+
}
|
|
1595
|
+
return buf;
|
|
1596
|
+
}
|
|
1597
|
+
function generateKeyBase64() {
|
|
1598
|
+
return randomBytes(32).toString("base64");
|
|
1599
|
+
}
|
|
1600
|
+
function encrypt(plaintext, base64Key) {
|
|
1601
|
+
const key = parseKey(base64Key);
|
|
1602
|
+
const iv = randomBytes(IV_LEN);
|
|
1603
|
+
const cipher = createCipheriv(ALGO, key, iv);
|
|
1604
|
+
const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]);
|
|
1605
|
+
const tag = cipher.getAuthTag();
|
|
1606
|
+
return Buffer.concat([MAGIC, iv, ciphertext, tag]);
|
|
1607
|
+
}
|
|
1608
|
+
function decrypt(blob, base64Key) {
|
|
1609
|
+
if (blob.length < MAGIC.length + IV_LEN + TAG_LEN) {
|
|
1610
|
+
throw new Error("encrypted blob too small to be valid");
|
|
1611
|
+
}
|
|
1612
|
+
if (!blob.subarray(0, MAGIC.length).equals(MAGIC)) {
|
|
1613
|
+
throw new Error("not a bertrand encrypted blob (magic prefix mismatch). " + "Did you upload a plaintext .db file by mistake?");
|
|
1614
|
+
}
|
|
1615
|
+
const key = parseKey(base64Key);
|
|
1616
|
+
const iv = blob.subarray(MAGIC.length, MAGIC.length + IV_LEN);
|
|
1617
|
+
const tag = blob.subarray(blob.length - TAG_LEN);
|
|
1618
|
+
const ciphertext = blob.subarray(MAGIC.length + IV_LEN, blob.length - TAG_LEN);
|
|
1619
|
+
const decipher = createDecipheriv(ALGO, key, iv);
|
|
1620
|
+
decipher.setAuthTag(tag);
|
|
1621
|
+
return Buffer.concat([decipher.update(ciphertext), decipher.final()]);
|
|
1622
|
+
}
|
|
1623
|
+
var ALGO = "aes-256-gcm", IV_LEN = 12, TAG_LEN = 16, MAGIC;
|
|
1624
|
+
var init_crypto = __esm(() => {
|
|
1625
|
+
MAGIC = Buffer.from("BTRD1", "ascii");
|
|
1626
|
+
});
|
|
1627
|
+
|
|
1628
|
+
// src/sync/engine.ts
|
|
1629
|
+
import { createClient } from "@supabase/supabase-js";
|
|
1630
|
+
import { readFileSync as readFileSync5, renameSync, statSync as statSync2, openSync, writeSync, fsyncSync, closeSync } from "fs";
|
|
1631
|
+
import { dirname as dirname2 } from "path";
|
|
1632
|
+
import { execFileSync } from "child_process";
|
|
1633
|
+
function client(cfg) {
|
|
1634
|
+
if (!cfg)
|
|
1635
|
+
return null;
|
|
1636
|
+
return createClient(cfg.supabaseUrl, cfg.supabaseServiceKey, {
|
|
1637
|
+
auth: { persistSession: false, autoRefreshToken: false }
|
|
1638
|
+
});
|
|
1639
|
+
}
|
|
1640
|
+
async function push() {
|
|
1641
|
+
if (!hasSyncConfig()) {
|
|
1642
|
+
return { ok: false, operation: "push", error: "no sync config \u2014 run `bertrand sync onboard`" };
|
|
1643
|
+
}
|
|
1644
|
+
const cfg = loadSyncConfig();
|
|
1645
|
+
const supabase = client(cfg);
|
|
1646
|
+
if (!cfg || !supabase) {
|
|
1647
|
+
return { ok: false, operation: "push", error: "sync config incomplete" };
|
|
1648
|
+
}
|
|
1649
|
+
const started = performance.now();
|
|
1650
|
+
try {
|
|
1651
|
+
let snapshotPath;
|
|
1652
|
+
try {
|
|
1653
|
+
snapshotPath = takeSnapshot();
|
|
1654
|
+
} catch (e) {
|
|
1655
|
+
return {
|
|
1656
|
+
ok: false,
|
|
1657
|
+
operation: "push",
|
|
1658
|
+
error: `snapshot failed: ${e instanceof Error ? e.message : String(e)}`
|
|
1659
|
+
};
|
|
1660
|
+
}
|
|
1661
|
+
const plaintext = readFileSync5(snapshotPath);
|
|
1662
|
+
const ciphertext = encrypt(plaintext, cfg.encryptionKey);
|
|
1663
|
+
const { error } = await supabase.storage.from(cfg.bucket).upload(cfg.objectKey, ciphertext, {
|
|
1664
|
+
contentType: "application/octet-stream",
|
|
1665
|
+
upsert: true
|
|
1666
|
+
});
|
|
1667
|
+
if (error) {
|
|
1668
|
+
return { ok: false, operation: "push", error: `upload failed: ${error.message}` };
|
|
1669
|
+
}
|
|
1670
|
+
return {
|
|
1671
|
+
ok: true,
|
|
1672
|
+
operation: "push",
|
|
1673
|
+
bytes: ciphertext.length,
|
|
1674
|
+
durationMs: Math.round(performance.now() - started)
|
|
1675
|
+
};
|
|
1676
|
+
} catch (e) {
|
|
1677
|
+
return { ok: false, operation: "push", error: e instanceof Error ? e.message : String(e) };
|
|
1678
|
+
} finally {
|
|
1679
|
+
cleanupSnapshot();
|
|
1680
|
+
}
|
|
1681
|
+
}
|
|
1682
|
+
async function pull(opts = {}) {
|
|
1683
|
+
if (!hasSyncConfig()) {
|
|
1684
|
+
return { ok: false, operation: "pull", error: "no sync config \u2014 run `bertrand sync onboard`" };
|
|
1685
|
+
}
|
|
1686
|
+
const cfg = loadSyncConfig();
|
|
1687
|
+
const supabase = client(cfg);
|
|
1688
|
+
if (!cfg || !supabase) {
|
|
1689
|
+
return { ok: false, operation: "pull", error: "sync config incomplete" };
|
|
1690
|
+
}
|
|
1691
|
+
const holders = findHolders(paths.db);
|
|
1692
|
+
if (holders.length > 0) {
|
|
1693
|
+
const procs = holders.map((h) => `${h.command}(${h.pid})`).join(", ");
|
|
1694
|
+
if (!opts.force) {
|
|
1695
|
+
return {
|
|
1696
|
+
ok: false,
|
|
1697
|
+
operation: "pull",
|
|
1698
|
+
error: `${paths.db} is held by ${procs} \u2014 close active bertrand sessions before pulling, ` + `or pass --force to overwrite anyway (risks corrupting the running session).`
|
|
1699
|
+
};
|
|
1700
|
+
}
|
|
1701
|
+
console.warn(`warning: --force pulling while ${procs} hold ${paths.db}. The running process may crash on next file access.`);
|
|
1702
|
+
}
|
|
1703
|
+
const started = performance.now();
|
|
1704
|
+
try {
|
|
1705
|
+
const { data, error } = await supabase.storage.from(cfg.bucket).download(cfg.objectKey);
|
|
1706
|
+
if (error || !data) {
|
|
1707
|
+
if (error?.message?.toLowerCase().includes("not found")) {
|
|
1708
|
+
return {
|
|
1709
|
+
ok: true,
|
|
1710
|
+
operation: "pull",
|
|
1711
|
+
pulled: false,
|
|
1712
|
+
bytes: 0,
|
|
1713
|
+
durationMs: Math.round(performance.now() - started)
|
|
1714
|
+
};
|
|
1715
|
+
}
|
|
1716
|
+
return { ok: false, operation: "pull", error: `download failed: ${error?.message ?? "no data"}` };
|
|
1717
|
+
}
|
|
1718
|
+
const ciphertext = Buffer.from(await data.arrayBuffer());
|
|
1719
|
+
const plaintext = decrypt(ciphertext, cfg.encryptionKey);
|
|
1720
|
+
const tmp = `${paths.db}.pull-${process.pid}`;
|
|
1721
|
+
const fd = openSync(tmp, "w");
|
|
1722
|
+
try {
|
|
1723
|
+
writeSync(fd, plaintext);
|
|
1724
|
+
fsyncSync(fd);
|
|
1725
|
+
} finally {
|
|
1726
|
+
closeSync(fd);
|
|
1727
|
+
}
|
|
1728
|
+
renameSync(tmp, paths.db);
|
|
1729
|
+
const dirFd = openSync(dirname2(paths.db), "r");
|
|
1730
|
+
try {
|
|
1731
|
+
fsyncSync(dirFd);
|
|
1732
|
+
} finally {
|
|
1733
|
+
closeSync(dirFd);
|
|
1734
|
+
}
|
|
1735
|
+
return {
|
|
1736
|
+
ok: true,
|
|
1737
|
+
operation: "pull",
|
|
1738
|
+
pulled: true,
|
|
1739
|
+
bytes: plaintext.length,
|
|
1740
|
+
durationMs: Math.round(performance.now() - started)
|
|
1741
|
+
};
|
|
1742
|
+
} catch (e) {
|
|
1743
|
+
return { ok: false, operation: "pull", error: e instanceof Error ? e.message : String(e) };
|
|
1744
|
+
}
|
|
1745
|
+
}
|
|
1746
|
+
async function status() {
|
|
1747
|
+
const cfg = loadSyncConfig();
|
|
1748
|
+
if (!cfg)
|
|
1749
|
+
return { configured: false, local: null, remote: null };
|
|
1750
|
+
let local = null;
|
|
1751
|
+
try {
|
|
1752
|
+
const s = statSync2(paths.db);
|
|
1753
|
+
local = { size: s.size, modifiedAt: new Date(s.mtimeMs) };
|
|
1754
|
+
} catch {
|
|
1755
|
+
local = null;
|
|
1756
|
+
}
|
|
1757
|
+
const supabase = client(cfg);
|
|
1758
|
+
let remote = null;
|
|
1759
|
+
if (supabase) {
|
|
1760
|
+
try {
|
|
1761
|
+
const parentPath = cfg.objectKey.includes("/") ? cfg.objectKey.slice(0, cfg.objectKey.lastIndexOf("/")) : "";
|
|
1762
|
+
const fileName = cfg.objectKey.includes("/") ? cfg.objectKey.slice(cfg.objectKey.lastIndexOf("/") + 1) : cfg.objectKey;
|
|
1763
|
+
const { data, error } = await supabase.storage.from(cfg.bucket).list(parentPath, { search: fileName });
|
|
1764
|
+
if (!error && data) {
|
|
1765
|
+
const match2 = data.find((f) => f.name === fileName);
|
|
1766
|
+
if (match2) {
|
|
1767
|
+
remote = {
|
|
1768
|
+
size: match2.metadata?.size ?? 0,
|
|
1769
|
+
modifiedAt: new Date(match2.updated_at ?? match2.created_at ?? Date.now())
|
|
1770
|
+
};
|
|
1771
|
+
}
|
|
1772
|
+
}
|
|
1773
|
+
} catch {}
|
|
1774
|
+
}
|
|
1775
|
+
return { configured: true, local, remote };
|
|
1776
|
+
}
|
|
1777
|
+
function findHolders(path) {
|
|
1778
|
+
try {
|
|
1779
|
+
const out = execFileSync("lsof", ["-F", "pcn", path], {
|
|
1780
|
+
encoding: "utf8",
|
|
1781
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
1782
|
+
});
|
|
1783
|
+
const holders = [];
|
|
1784
|
+
let pid = 0;
|
|
1785
|
+
let command = "";
|
|
1786
|
+
for (const line of out.split(`
|
|
1787
|
+
`)) {
|
|
1788
|
+
if (line.startsWith("p"))
|
|
1789
|
+
pid = Number(line.slice(1));
|
|
1790
|
+
else if (line.startsWith("c"))
|
|
1791
|
+
command = line.slice(1);
|
|
1792
|
+
else if (line.startsWith("n")) {
|
|
1793
|
+
if (pid && pid !== process.pid && command) {
|
|
1794
|
+
holders.push({ pid, command });
|
|
1795
|
+
}
|
|
1796
|
+
}
|
|
1797
|
+
}
|
|
1798
|
+
return holders;
|
|
1799
|
+
} catch {
|
|
1800
|
+
return [];
|
|
1801
|
+
}
|
|
1802
|
+
}
|
|
1803
|
+
var init_engine = __esm(() => {
|
|
1804
|
+
init_paths();
|
|
1805
|
+
init_config();
|
|
1806
|
+
init_snapshot2();
|
|
1807
|
+
init_crypto();
|
|
1808
|
+
});
|
|
1809
|
+
|
|
1810
|
+
// src/sync/invite.ts
|
|
1811
|
+
function encodeInvite(cfg) {
|
|
1812
|
+
const bundle = {
|
|
1813
|
+
v: VERSION,
|
|
1814
|
+
url: cfg.supabaseUrl,
|
|
1815
|
+
key: cfg.supabaseServiceKey,
|
|
1816
|
+
bucket: cfg.bucket,
|
|
1817
|
+
obj: cfg.objectKey,
|
|
1818
|
+
ek: cfg.encryptionKey
|
|
1819
|
+
};
|
|
1820
|
+
return SCHEME + Buffer.from(JSON.stringify(bundle), "utf8").toString("base64url");
|
|
1821
|
+
}
|
|
1822
|
+
function isInvite(value) {
|
|
1823
|
+
return typeof value === "string" && value.startsWith(SCHEME);
|
|
1824
|
+
}
|
|
1825
|
+
function decodeInvite(invite) {
|
|
1826
|
+
if (!invite.startsWith(SCHEME)) {
|
|
1827
|
+
throw new Error(`invite must start with ${SCHEME}`);
|
|
1828
|
+
}
|
|
1829
|
+
const payload = invite.slice(SCHEME.length).trim();
|
|
1830
|
+
let parsed;
|
|
1831
|
+
try {
|
|
1832
|
+
const json = Buffer.from(payload, "base64url").toString("utf8");
|
|
1833
|
+
parsed = JSON.parse(json);
|
|
1834
|
+
} catch {
|
|
1835
|
+
throw new Error("invite is malformed \u2014 could not decode base64/JSON payload");
|
|
1836
|
+
}
|
|
1837
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
1838
|
+
throw new Error("invite payload is not a JSON object");
|
|
1839
|
+
}
|
|
1840
|
+
const bundle = parsed;
|
|
1841
|
+
if (bundle.v !== VERSION) {
|
|
1842
|
+
throw new Error(`invite version ${String(bundle.v)} is not supported by this bertrand (expected v${VERSION}). ` + `Upgrade or downgrade so both machines run the same version.`);
|
|
1843
|
+
}
|
|
1844
|
+
for (const field of ["url", "key", "bucket", "obj", "ek"]) {
|
|
1845
|
+
if (!bundle[field] || typeof bundle[field] !== "string") {
|
|
1846
|
+
throw new Error(`invite is missing required field: ${field}`);
|
|
1847
|
+
}
|
|
1848
|
+
}
|
|
1849
|
+
return {
|
|
1850
|
+
supabaseUrl: bundle.url,
|
|
1851
|
+
supabaseServiceKey: bundle.key,
|
|
1852
|
+
bucket: bundle.bucket,
|
|
1853
|
+
objectKey: bundle.obj,
|
|
1854
|
+
encryptionKey: bundle.ek
|
|
1855
|
+
};
|
|
1856
|
+
}
|
|
1857
|
+
var SCHEME = "bertrand-sync://", VERSION = 1;
|
|
1858
|
+
|
|
1859
|
+
// src/lib/format.ts
|
|
1860
|
+
function formatDuration(ms) {
|
|
1861
|
+
if (ms < MINUTE)
|
|
1862
|
+
return `${Math.round(ms / SECOND)}s`;
|
|
1863
|
+
const days = Math.floor(ms / DAY);
|
|
1864
|
+
const hours = Math.floor(ms % DAY / HOUR);
|
|
1865
|
+
const minutes = Math.floor(ms % HOUR / MINUTE);
|
|
1866
|
+
if (days > 0)
|
|
1867
|
+
return hours > 0 ? `${days}d ${hours}h` : `${days}d`;
|
|
1868
|
+
if (hours > 0)
|
|
1869
|
+
return minutes > 0 ? `${hours}h ${minutes}m` : `${hours}h`;
|
|
1870
|
+
return `${minutes}m`;
|
|
1871
|
+
}
|
|
1872
|
+
function formatAgo(isoOrDate) {
|
|
1873
|
+
const date = typeof isoOrDate === "string" ? new Date(isoOrDate) : isoOrDate;
|
|
1874
|
+
const ms = Date.now() - date.getTime();
|
|
1875
|
+
if (ms < MINUTE)
|
|
1876
|
+
return "just now";
|
|
1877
|
+
if (ms < HOUR)
|
|
1878
|
+
return `${Math.floor(ms / MINUTE)}m ago`;
|
|
1879
|
+
if (ms < DAY)
|
|
1880
|
+
return `${Math.floor(ms / HOUR)}h ago`;
|
|
1881
|
+
if (ms < 2 * DAY)
|
|
1882
|
+
return "yesterday";
|
|
1883
|
+
if (ms < 7 * DAY)
|
|
1884
|
+
return `${Math.floor(ms / DAY)}d ago`;
|
|
1885
|
+
return date.toLocaleDateString("en-US", { month: "short", day: "numeric" });
|
|
1886
|
+
}
|
|
1887
|
+
function truncate(text2, maxLen) {
|
|
1888
|
+
if (text2.length <= maxLen)
|
|
1889
|
+
return text2;
|
|
1890
|
+
return text2.slice(0, maxLen - 1) + "\u2026";
|
|
1891
|
+
}
|
|
1892
|
+
function formatTime(iso, includeDate = false) {
|
|
1893
|
+
const date = new Date(iso);
|
|
1894
|
+
const time = date.toLocaleTimeString("en-US", {
|
|
1895
|
+
hour: "numeric",
|
|
1896
|
+
minute: "2-digit"
|
|
1897
|
+
});
|
|
1898
|
+
if (!includeDate)
|
|
1899
|
+
return time;
|
|
1900
|
+
const day = date.toLocaleDateString("en-US", {
|
|
1901
|
+
month: "short",
|
|
1902
|
+
day: "numeric"
|
|
1903
|
+
});
|
|
1904
|
+
return `${day} ${time}`;
|
|
1905
|
+
}
|
|
1906
|
+
var SECOND = 1000, MINUTE, HOUR, DAY;
|
|
1907
|
+
var init_format = __esm(() => {
|
|
1908
|
+
MINUTE = 60 * SECOND;
|
|
1909
|
+
HOUR = 60 * MINUTE;
|
|
1910
|
+
DAY = 24 * HOUR;
|
|
1911
|
+
});
|
|
1912
|
+
|
|
1913
|
+
// src/cli/commands/sync.ts
|
|
1914
|
+
var exports_sync = {};
|
|
1915
|
+
import { hostname } from "os";
|
|
1916
|
+
function printUsage2() {
|
|
1917
|
+
console.log(`
|
|
1918
|
+
bertrand sync \u2014 replicate your local DB to Supabase Storage (opt-in)
|
|
1919
|
+
|
|
1920
|
+
Usage:
|
|
1921
|
+
bertrand sync onboard Interactive: configure Supabase + bucket + encryption key
|
|
1922
|
+
bertrand sync invite Print a paste-able bundle for another machine
|
|
1923
|
+
bertrand sync <bundle> One-liner: decode bundle, save config, pull
|
|
1924
|
+
bertrand sync push VACUUM \u2192 encrypt \u2192 upload
|
|
1925
|
+
bertrand sync pull [--force] Download \u2192 decrypt \u2192 atomic-replace (refuses if active)
|
|
1926
|
+
bertrand sync status Local + remote object size and timestamps
|
|
1927
|
+
bertrand sync enable Turn auto-triggers on (assumes onboard already ran)
|
|
1928
|
+
bertrand sync disable Turn auto-triggers off without deleting credentials
|
|
1929
|
+
|
|
1930
|
+
Cross-machine setup:
|
|
1931
|
+
Machine A: bertrand sync invite
|
|
1932
|
+
\u2192 bertrand-sync://eyJ\u2026 (transmit via Signal/iMessage/AirDrop \u2014 sensitive)
|
|
1933
|
+
Machine B: bertrand sync bertrand-sync://eyJ\u2026
|
|
1934
|
+
\u2192 onboards, pulls, ready
|
|
1935
|
+
`.trim());
|
|
1936
|
+
}
|
|
1937
|
+
function abort(reason) {
|
|
1938
|
+
console.error(`Aborted: ${reason}`);
|
|
1939
|
+
process.exit(1);
|
|
1940
|
+
}
|
|
1941
|
+
function prompt(label, opts = {}) {
|
|
1942
|
+
const suffix = opts.default ? ` [${opts.default}]` : "";
|
|
1943
|
+
process.stdout.write(`${label}${suffix}: `);
|
|
1944
|
+
return new Promise((resolve) => {
|
|
1945
|
+
let buf = "";
|
|
1946
|
+
const onData = (chunk) => {
|
|
1947
|
+
const s = chunk.toString("utf8");
|
|
1948
|
+
for (const ch of s) {
|
|
1949
|
+
if (ch === `
|
|
1950
|
+
` || ch === "\r") {
|
|
1951
|
+
process.stdin.off("data", onData);
|
|
1952
|
+
process.stdin.pause();
|
|
1953
|
+
process.stdout.write(`
|
|
1954
|
+
`);
|
|
1955
|
+
return resolve(buf.trim() || opts.default || "");
|
|
1956
|
+
}
|
|
1957
|
+
buf += ch;
|
|
1958
|
+
}
|
|
1959
|
+
};
|
|
1960
|
+
process.stdin.resume();
|
|
1961
|
+
process.stdin.on("data", onData);
|
|
1962
|
+
});
|
|
1963
|
+
}
|
|
1964
|
+
async function runOnboard() {
|
|
1965
|
+
const existing = loadSyncConfig();
|
|
1966
|
+
if (existing) {
|
|
1967
|
+
console.log(`Existing config at ${paths.syncEnv}:`);
|
|
1968
|
+
console.log(` SUPABASE_URL: ${existing.supabaseUrl}`);
|
|
1969
|
+
console.log(` SUPABASE_SERVICE_KEY: ${existing.supabaseServiceKey.slice(0, 12)}\u2026`);
|
|
1970
|
+
console.log(` BERTRAND_SYNC_BUCKET: ${existing.bucket}`);
|
|
1971
|
+
console.log(` BERTRAND_SYNC_OBJECT: ${existing.objectKey}`);
|
|
1972
|
+
console.log(` BERTRAND_ENCRYPTION_KEY: (set, redacted)`);
|
|
1973
|
+
console.log(` BERTRAND_CLIENT_NAME: ${existing.clientName}`);
|
|
1974
|
+
console.log(`
|
|
1975
|
+
Delete ${paths.syncEnv} and re-run to start over.`);
|
|
1976
|
+
return;
|
|
1977
|
+
}
|
|
1978
|
+
console.log(`Bertrand sync setup \u2014 Supabase Storage
|
|
1979
|
+
`);
|
|
1980
|
+
console.log(`In the Supabase dashboard (app.supabase.com):`);
|
|
1981
|
+
console.log(` 1. Create or pick a project`);
|
|
1982
|
+
console.log(` 2. Settings \u2192 General: copy the Project ID (the 20-char lowercase string`);
|
|
1983
|
+
console.log(` shown under "Reference ID" \u2014 looks like "xxxxxxxxxxxxxxxxxxxx")`);
|
|
1984
|
+
console.log(` 3. Settings \u2192 API: copy the service_role key (NOT the anon key \u2014 service_role`);
|
|
1985
|
+
console.log(` is below it and warns "keep secret")`);
|
|
1986
|
+
console.log(` 4. Storage \u2192 New bucket: name it "bertrand", PRIVATE (uncheck "Public bucket")
|
|
1987
|
+
`);
|
|
1988
|
+
const projectId = await prompt("Project ID");
|
|
1989
|
+
if (!projectId)
|
|
1990
|
+
return abort("no project ID provided");
|
|
1991
|
+
if (!/^[a-z0-9]{15,30}$/i.test(projectId)) {
|
|
1992
|
+
return abort(`"${projectId}" doesn't look like a Supabase project ID. ` + `Expected the 20-ish char string from Settings \u2192 General, not the project name.`);
|
|
1993
|
+
}
|
|
1994
|
+
const supabaseUrl = `https://${projectId}.supabase.co`;
|
|
1995
|
+
console.log(` \u2192 Project URL: ${supabaseUrl}`);
|
|
1996
|
+
const supabaseServiceKey = await prompt("SUPABASE_SERVICE_KEY (eyJ\u2026)");
|
|
1997
|
+
if (!supabaseServiceKey)
|
|
1998
|
+
return abort("no service key provided");
|
|
1999
|
+
if (!supabaseServiceKey.startsWith("eyJ")) {
|
|
2000
|
+
return abort("service key must be a JWT starting with eyJ \u2014 did you paste the URL by mistake?");
|
|
2001
|
+
}
|
|
2002
|
+
const bucket = await prompt("Storage bucket name", { default: "bertrand" });
|
|
2003
|
+
const objectKey = await prompt("Object key", { default: "bertrand.db.enc" });
|
|
2004
|
+
const defaultName = `bertrand-${hostname()}`;
|
|
2005
|
+
const clientName = await prompt("Client name", { default: defaultName });
|
|
2006
|
+
console.log(`
|
|
2007
|
+
Encryption key`);
|
|
2008
|
+
console.log(` First machine? Press ENTER to generate a fresh AES-256-GCM key.`);
|
|
2009
|
+
console.log(` Additional machine? Paste the key from your other machine.
|
|
2010
|
+
`);
|
|
2011
|
+
const pasted = await prompt("BERTRAND_ENCRYPTION_KEY");
|
|
2012
|
+
let encryptionKey;
|
|
2013
|
+
if (pasted) {
|
|
2014
|
+
const decoded = Buffer.from(pasted, "base64");
|
|
2015
|
+
if (decoded.length !== 32) {
|
|
2016
|
+
return abort(`pasted key is ${decoded.length} bytes when decoded; expected 32. ` + `Copy the full key from your other machine without trimming.`);
|
|
2017
|
+
}
|
|
2018
|
+
encryptionKey = pasted;
|
|
2019
|
+
console.log(` \u2713 Using pasted key \u2014 this machine can decrypt blobs from the other one.
|
|
2020
|
+
`);
|
|
2021
|
+
} else {
|
|
2022
|
+
encryptionKey = generateKeyBase64();
|
|
2023
|
+
console.log(`
|
|
2024
|
+
Generated a new key. Save this somewhere safe \u2014 you'll need it on every`);
|
|
2025
|
+
console.log(`other machine that should be able to pull this DB:
|
|
2026
|
+
`);
|
|
2027
|
+
console.log(` ${encryptionKey}
|
|
2028
|
+
`);
|
|
2029
|
+
console.log(`Hit ENTER to confirm you've saved it.`);
|
|
2030
|
+
await prompt("");
|
|
2031
|
+
}
|
|
2032
|
+
saveSyncConfig({ supabaseUrl, supabaseServiceKey, bucket, objectKey, encryptionKey, clientName });
|
|
2033
|
+
patchConfig({ sync: { enabled: true } });
|
|
2034
|
+
console.log(`Wrote ${paths.syncEnv} (mode 0600). Sync auto-triggers enabled.`);
|
|
2035
|
+
console.log(`
|
|
2036
|
+
Next:`);
|
|
2037
|
+
console.log(` bertrand sync push Send your current local DB to Supabase`);
|
|
2038
|
+
console.log(` bertrand sync status See sizes and timestamps`);
|
|
2039
|
+
console.log(`
|
|
2040
|
+
On another machine: paste the same SUPABASE_URL, a fresh SERVICE_KEY (you can reuse),`);
|
|
2041
|
+
console.log(`the same bucket and object key, and the SAME encryption key. Then \`bertrand sync pull\`.`);
|
|
2042
|
+
}
|
|
2043
|
+
async function runPush() {
|
|
2044
|
+
if (!hasSyncConfig())
|
|
2045
|
+
return abort("no sync config \u2014 run `bertrand sync onboard`");
|
|
2046
|
+
process.stdout.write("Snapshot \u2192 encrypt \u2192 upload\u2026 ");
|
|
2047
|
+
const result = await push();
|
|
2048
|
+
if (result.ok) {
|
|
2049
|
+
console.log(`done (${formatBytes(result.bytes)} in ${result.durationMs}ms).`);
|
|
2050
|
+
} else {
|
|
2051
|
+
console.log("failed.");
|
|
2052
|
+
console.error(result.error);
|
|
2053
|
+
process.exit(1);
|
|
2054
|
+
}
|
|
2055
|
+
}
|
|
2056
|
+
async function runPull(args) {
|
|
2057
|
+
if (!hasSyncConfig())
|
|
2058
|
+
return abort("no sync config \u2014 run `bertrand sync onboard`");
|
|
2059
|
+
const force = args.includes("--force");
|
|
2060
|
+
process.stdout.write("Download \u2192 decrypt \u2192 replace\u2026 ");
|
|
2061
|
+
const result = await pull({ force });
|
|
2062
|
+
if (result.ok) {
|
|
2063
|
+
if (result.pulled) {
|
|
2064
|
+
console.log(`done (${formatBytes(result.bytes)} in ${result.durationMs}ms).`);
|
|
2065
|
+
} else {
|
|
2066
|
+
console.log("no remote object yet \u2014 nothing to pull.");
|
|
2067
|
+
}
|
|
2068
|
+
} else {
|
|
2069
|
+
console.log("failed.");
|
|
2070
|
+
console.error(result.error);
|
|
2071
|
+
process.exit(1);
|
|
2072
|
+
}
|
|
2073
|
+
}
|
|
2074
|
+
async function runStatus() {
|
|
2075
|
+
if (!hasSyncConfig()) {
|
|
2076
|
+
console.log("Sync is not configured. Run `bertrand sync onboard` to set up.");
|
|
2077
|
+
return;
|
|
2078
|
+
}
|
|
2079
|
+
console.log(`Sync status (${paths.syncEnv}):`);
|
|
2080
|
+
console.log(` Auto-triggers: ${isSyncEnabled() ? "enabled" : "disabled"}`);
|
|
2081
|
+
const s = await status();
|
|
2082
|
+
if (s.local) {
|
|
2083
|
+
console.log(` Local: ${formatBytes(s.local.size)}, modified ${formatAgo(s.local.modifiedAt)}`);
|
|
2084
|
+
} else {
|
|
2085
|
+
console.log(` Local: (no DB file yet)`);
|
|
2086
|
+
}
|
|
2087
|
+
if (s.remote) {
|
|
2088
|
+
console.log(` Remote: ${formatBytes(s.remote.size)}, modified ${formatAgo(s.remote.modifiedAt)}`);
|
|
2089
|
+
} else {
|
|
2090
|
+
console.log(` Remote: (no object \u2014 push to create)`);
|
|
2091
|
+
}
|
|
2092
|
+
}
|
|
2093
|
+
function runEnable() {
|
|
2094
|
+
if (!hasSyncConfig()) {
|
|
2095
|
+
return abort("no sync config \u2014 run `bertrand sync onboard` first");
|
|
2096
|
+
}
|
|
2097
|
+
patchConfig({ sync: { enabled: true } });
|
|
2098
|
+
console.log("Sync auto-triggers enabled. Pull-on-launch and push-on-session-end will fire.");
|
|
2099
|
+
}
|
|
2100
|
+
function runInvite() {
|
|
2101
|
+
const cfg = loadSyncConfig();
|
|
2102
|
+
if (!cfg) {
|
|
2103
|
+
return abort("no sync config \u2014 run `bertrand sync onboard` first");
|
|
2104
|
+
}
|
|
2105
|
+
const invite = encodeInvite(cfg);
|
|
2106
|
+
console.log("Sensitive: this bundle contains a Supabase service_role token AND your DB encryption key.");
|
|
2107
|
+
console.log(`Transmit only over a secure channel (Signal, iMessage, AirDrop). Treat like an SSH private key.
|
|
2108
|
+
`);
|
|
2109
|
+
console.log(invite);
|
|
2110
|
+
console.log(`
|
|
2111
|
+
On the other machine:`);
|
|
2112
|
+
console.log(` bertrand sync ${invite.slice(0, 32)}\u2026`);
|
|
2113
|
+
}
|
|
2114
|
+
async function runBootstrap(invite) {
|
|
2115
|
+
if (hasSyncConfig()) {
|
|
2116
|
+
return abort(`sync config already exists at ${paths.syncEnv}. ` + `Delete it first if you really want to overwrite: \`rm ${paths.syncEnv}\``);
|
|
2117
|
+
}
|
|
2118
|
+
let decoded;
|
|
2119
|
+
try {
|
|
2120
|
+
decoded = decodeInvite(invite);
|
|
2121
|
+
} catch (e) {
|
|
2122
|
+
return abort(e instanceof Error ? e.message : String(e));
|
|
2123
|
+
}
|
|
2124
|
+
const clientName = `bertrand-${hostname()}`;
|
|
2125
|
+
saveSyncConfig({ ...decoded, clientName });
|
|
2126
|
+
patchConfig({ sync: { enabled: true } });
|
|
2127
|
+
console.log(`Wrote ${paths.syncEnv} (mode 0600). Auto-triggers enabled.`);
|
|
2128
|
+
console.log(`Pulling\u2026`);
|
|
2129
|
+
const result = await pull();
|
|
2130
|
+
if (result.ok) {
|
|
2131
|
+
if (result.pulled) {
|
|
2132
|
+
console.log(`\u2713 Done (${formatBytes(result.bytes)} in ${result.durationMs}ms). Run \`bertrand\` to start.`);
|
|
2133
|
+
} else {
|
|
2134
|
+
console.log(`\u2713 Config saved, but no remote object yet \u2014 nothing to pull.`);
|
|
2135
|
+
console.log(` Run \`bertrand sync push\` on the source machine first.`);
|
|
2136
|
+
}
|
|
2137
|
+
} else {
|
|
2138
|
+
console.error(`Pull failed: ${result.error}`);
|
|
2139
|
+
console.error(`(Config is saved, so you can fix and re-run \`bertrand sync pull\`.)`);
|
|
2140
|
+
process.exit(1);
|
|
2141
|
+
}
|
|
2142
|
+
}
|
|
2143
|
+
function runDisable() {
|
|
2144
|
+
patchConfig({ sync: { enabled: false } });
|
|
2145
|
+
console.log("Sync auto-triggers disabled. Credentials still in place \u2014 re-enable any time with `bertrand sync enable`.");
|
|
2146
|
+
console.log("Manual `bertrand sync push` / `pull` still work while disabled.");
|
|
2147
|
+
}
|
|
2148
|
+
function formatBytes(n) {
|
|
2149
|
+
if (n < 1024)
|
|
2150
|
+
return `${n}B`;
|
|
2151
|
+
if (n < 1024 * 1024)
|
|
2152
|
+
return `${(n / 1024).toFixed(1)}KB`;
|
|
2153
|
+
return `${(n / (1024 * 1024)).toFixed(2)}MB`;
|
|
2154
|
+
}
|
|
2155
|
+
var init_sync = __esm(() => {
|
|
2156
|
+
init_router();
|
|
2157
|
+
init_config();
|
|
2158
|
+
init_engine();
|
|
2159
|
+
init_crypto();
|
|
2160
|
+
init_paths();
|
|
2161
|
+
init_format();
|
|
2162
|
+
init_config2();
|
|
2163
|
+
register("sync", async (args) => {
|
|
2164
|
+
const sub = args[0];
|
|
2165
|
+
if (sub && isInvite(sub)) {
|
|
2166
|
+
await runBootstrap(sub);
|
|
2167
|
+
return;
|
|
2168
|
+
}
|
|
2169
|
+
switch (sub) {
|
|
2170
|
+
case "push":
|
|
2171
|
+
await runPush();
|
|
2172
|
+
return;
|
|
2173
|
+
case "pull":
|
|
2174
|
+
await runPull(args.slice(1));
|
|
2175
|
+
return;
|
|
2176
|
+
case "status":
|
|
2177
|
+
await runStatus();
|
|
2178
|
+
return;
|
|
2179
|
+
case "onboard":
|
|
2180
|
+
await runOnboard();
|
|
2181
|
+
return;
|
|
2182
|
+
case "invite":
|
|
2183
|
+
runInvite();
|
|
2184
|
+
return;
|
|
2185
|
+
case "enable":
|
|
2186
|
+
runEnable();
|
|
2187
|
+
return;
|
|
2188
|
+
case "disable":
|
|
2189
|
+
runDisable();
|
|
2190
|
+
return;
|
|
2191
|
+
case undefined:
|
|
2192
|
+
case "--help":
|
|
2193
|
+
case "-h":
|
|
2194
|
+
printUsage2();
|
|
2195
|
+
return;
|
|
2196
|
+
default:
|
|
2197
|
+
console.error(`Unknown subcommand: ${sub}`);
|
|
2198
|
+
printUsage2();
|
|
2199
|
+
process.exit(1);
|
|
2200
|
+
}
|
|
2201
|
+
});
|
|
2202
|
+
});
|
|
2203
|
+
|
|
1390
2204
|
// src/db/queries/groups.ts
|
|
1391
2205
|
import { eq as eq6, like, or, isNull } from "drizzle-orm";
|
|
1392
2206
|
function createGroup(opts) {
|
|
@@ -1484,60 +2298,6 @@ var init_template2 = __esm(() => {
|
|
|
1484
2298
|
init_template();
|
|
1485
2299
|
});
|
|
1486
2300
|
|
|
1487
|
-
// src/lib/format.ts
|
|
1488
|
-
function formatDuration(ms) {
|
|
1489
|
-
if (ms < MINUTE)
|
|
1490
|
-
return `${Math.round(ms / SECOND)}s`;
|
|
1491
|
-
const days = Math.floor(ms / DAY);
|
|
1492
|
-
const hours = Math.floor(ms % DAY / HOUR);
|
|
1493
|
-
const minutes = Math.floor(ms % HOUR / MINUTE);
|
|
1494
|
-
if (days > 0)
|
|
1495
|
-
return hours > 0 ? `${days}d ${hours}h` : `${days}d`;
|
|
1496
|
-
if (hours > 0)
|
|
1497
|
-
return minutes > 0 ? `${hours}h ${minutes}m` : `${hours}h`;
|
|
1498
|
-
return `${minutes}m`;
|
|
1499
|
-
}
|
|
1500
|
-
function formatAgo(isoOrDate) {
|
|
1501
|
-
const date = typeof isoOrDate === "string" ? new Date(isoOrDate) : isoOrDate;
|
|
1502
|
-
const ms = Date.now() - date.getTime();
|
|
1503
|
-
if (ms < MINUTE)
|
|
1504
|
-
return "just now";
|
|
1505
|
-
if (ms < HOUR)
|
|
1506
|
-
return `${Math.floor(ms / MINUTE)}m ago`;
|
|
1507
|
-
if (ms < DAY)
|
|
1508
|
-
return `${Math.floor(ms / HOUR)}h ago`;
|
|
1509
|
-
if (ms < 2 * DAY)
|
|
1510
|
-
return "yesterday";
|
|
1511
|
-
if (ms < 7 * DAY)
|
|
1512
|
-
return `${Math.floor(ms / DAY)}d ago`;
|
|
1513
|
-
return date.toLocaleDateString("en-US", { month: "short", day: "numeric" });
|
|
1514
|
-
}
|
|
1515
|
-
function truncate(text2, maxLen) {
|
|
1516
|
-
if (text2.length <= maxLen)
|
|
1517
|
-
return text2;
|
|
1518
|
-
return text2.slice(0, maxLen - 1) + "\u2026";
|
|
1519
|
-
}
|
|
1520
|
-
function formatTime(iso, includeDate = false) {
|
|
1521
|
-
const date = new Date(iso);
|
|
1522
|
-
const time = date.toLocaleTimeString("en-US", {
|
|
1523
|
-
hour: "numeric",
|
|
1524
|
-
minute: "2-digit"
|
|
1525
|
-
});
|
|
1526
|
-
if (!includeDate)
|
|
1527
|
-
return time;
|
|
1528
|
-
const day = date.toLocaleDateString("en-US", {
|
|
1529
|
-
month: "short",
|
|
1530
|
-
day: "numeric"
|
|
1531
|
-
});
|
|
1532
|
-
return `${day} ${time}`;
|
|
1533
|
-
}
|
|
1534
|
-
var SECOND = 1000, MINUTE, HOUR, DAY;
|
|
1535
|
-
var init_format = __esm(() => {
|
|
1536
|
-
MINUTE = 60 * SECOND;
|
|
1537
|
-
HOUR = 60 * MINUTE;
|
|
1538
|
-
DAY = 24 * HOUR;
|
|
1539
|
-
});
|
|
1540
|
-
|
|
1541
2301
|
// src/contract/context.ts
|
|
1542
2302
|
function buildSiblingContext(groupId, groupPath, currentSessionId) {
|
|
1543
2303
|
const siblings = getSessionsByGroup(groupId).filter((s) => s.id !== currentSessionId);
|
|
@@ -1567,7 +2327,7 @@ var init_context = __esm(() => {
|
|
|
1567
2327
|
});
|
|
1568
2328
|
|
|
1569
2329
|
// src/engine/process.ts
|
|
1570
|
-
import { spawn } from "child_process";
|
|
2330
|
+
import { spawn as spawn2 } from "child_process";
|
|
1571
2331
|
function launchClaude(opts) {
|
|
1572
2332
|
const args = [];
|
|
1573
2333
|
if (opts.resume) {
|
|
@@ -1585,7 +2345,7 @@ function launchClaude(opts) {
|
|
|
1585
2345
|
BERTRAND_SESSION_SLUG: opts.sessionSlug
|
|
1586
2346
|
};
|
|
1587
2347
|
return new Promise((resolve, reject) => {
|
|
1588
|
-
const child =
|
|
2348
|
+
const child = spawn2("claude", args, {
|
|
1589
2349
|
env,
|
|
1590
2350
|
stdio: "inherit",
|
|
1591
2351
|
shell: false
|
|
@@ -1683,12 +2443,12 @@ var init_spawn_context = __esm(() => {
|
|
|
1683
2443
|
});
|
|
1684
2444
|
|
|
1685
2445
|
// src/lib/server-lifecycle.ts
|
|
1686
|
-
import { spawn as
|
|
1687
|
-
import { readFileSync as
|
|
1688
|
-
import { join as
|
|
2446
|
+
import { spawn as spawn3 } from "child_process";
|
|
2447
|
+
import { readFileSync as readFileSync6, writeFileSync as writeFileSync3, unlinkSync as unlinkSync2 } from "fs";
|
|
2448
|
+
import { join as join5 } from "path";
|
|
1689
2449
|
function readPidFile() {
|
|
1690
2450
|
try {
|
|
1691
|
-
const pid = Number(
|
|
2451
|
+
const pid = Number(readFileSync6(deps.pidFile, "utf-8").trim());
|
|
1692
2452
|
return Number.isFinite(pid) && pid > 0 ? pid : null;
|
|
1693
2453
|
} catch {
|
|
1694
2454
|
return null;
|
|
@@ -1714,7 +2474,7 @@ async function isPortListening(port) {
|
|
|
1714
2474
|
}
|
|
1715
2475
|
function removePidFile() {
|
|
1716
2476
|
try {
|
|
1717
|
-
|
|
2477
|
+
unlinkSync2(deps.pidFile);
|
|
1718
2478
|
} catch {}
|
|
1719
2479
|
}
|
|
1720
2480
|
async function ensureServerStarted() {
|
|
@@ -1728,14 +2488,14 @@ async function ensureServerStarted() {
|
|
|
1728
2488
|
const bin = deps.resolveBin();
|
|
1729
2489
|
if (!bin)
|
|
1730
2490
|
return;
|
|
1731
|
-
const child =
|
|
2491
|
+
const child = spawn3(bin, ["serve"], {
|
|
1732
2492
|
detached: true,
|
|
1733
2493
|
stdio: "ignore",
|
|
1734
2494
|
env: { ...process.env, BERTRAND_PORT: String(deps.port) }
|
|
1735
2495
|
});
|
|
1736
2496
|
child.unref();
|
|
1737
2497
|
if (child.pid)
|
|
1738
|
-
|
|
2498
|
+
writeFileSync3(deps.pidFile, String(child.pid));
|
|
1739
2499
|
}
|
|
1740
2500
|
function stopServerIfIdle() {
|
|
1741
2501
|
if (deps.getActiveCount() > 0)
|
|
@@ -1753,11 +2513,11 @@ var init_server_lifecycle = __esm(() => {
|
|
|
1753
2513
|
init_paths();
|
|
1754
2514
|
init_sessions();
|
|
1755
2515
|
defaultDeps = {
|
|
1756
|
-
pidFile:
|
|
2516
|
+
pidFile: join5(paths.root, "server.pid"),
|
|
1757
2517
|
port: Number(process.env.BERTRAND_PORT ?? 5200),
|
|
1758
2518
|
resolveBin() {
|
|
1759
2519
|
try {
|
|
1760
|
-
const config = JSON.parse(
|
|
2520
|
+
const config = JSON.parse(readFileSync6(join5(paths.root, "config.json"), "utf-8"));
|
|
1761
2521
|
return typeof config?.bin === "string" ? config.bin : null;
|
|
1762
2522
|
} catch {
|
|
1763
2523
|
return null;
|
|
@@ -1902,13 +2662,13 @@ var init_session = __esm(() => {
|
|
|
1902
2662
|
});
|
|
1903
2663
|
|
|
1904
2664
|
// src/tui/app.tsx
|
|
1905
|
-
import { spawn as
|
|
1906
|
-
import { existsSync as
|
|
1907
|
-
import { join as
|
|
2665
|
+
import { spawn as spawn4 } from "child_process";
|
|
2666
|
+
import { existsSync as existsSync6, readFileSync as readFileSync7, unlinkSync as unlinkSync3 } from "fs";
|
|
2667
|
+
import { join as join6 } from "path";
|
|
1908
2668
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
1909
2669
|
async function runScreen(screen, ...args) {
|
|
1910
2670
|
const tmpFile = `/tmp/bertrand-tui-${process.pid}-${Date.now()}.json`;
|
|
1911
|
-
const child =
|
|
2671
|
+
const child = spawn4("bun", ["run", SCREEN_ENTRY, screen, tmpFile, ...args], {
|
|
1912
2672
|
stdio: "inherit"
|
|
1913
2673
|
});
|
|
1914
2674
|
const exitCode = await new Promise((resolve) => {
|
|
@@ -1917,8 +2677,8 @@ async function runScreen(screen, ...args) {
|
|
|
1917
2677
|
if (exitCode !== 0) {
|
|
1918
2678
|
throw new Error(`TUI screen "${screen}" exited with code ${exitCode}`);
|
|
1919
2679
|
}
|
|
1920
|
-
const result = JSON.parse(
|
|
1921
|
-
|
|
2680
|
+
const result = JSON.parse(readFileSync7(tmpFile, "utf-8"));
|
|
2681
|
+
unlinkSync3(tmpFile);
|
|
1922
2682
|
return result;
|
|
1923
2683
|
}
|
|
1924
2684
|
async function startLaunchTui() {
|
|
@@ -2002,8 +2762,8 @@ var init_app = __esm(() => {
|
|
|
2002
2762
|
init_session_archive();
|
|
2003
2763
|
init_session();
|
|
2004
2764
|
SCREEN_ENTRY = (() => {
|
|
2005
|
-
const built =
|
|
2006
|
-
return
|
|
2765
|
+
const built = join6(import.meta.dir, "run-screen.js");
|
|
2766
|
+
return existsSync6(built) ? built : join6(import.meta.dir, "run-screen.tsx");
|
|
2007
2767
|
})();
|
|
2008
2768
|
});
|
|
2009
2769
|
|
|
@@ -2082,14 +2842,14 @@ var init_launch = __esm(() => {
|
|
|
2082
2842
|
});
|
|
2083
2843
|
|
|
2084
2844
|
// src/db/migrate.ts
|
|
2085
|
-
import { Database as
|
|
2845
|
+
import { Database as Database3 } from "bun:sqlite";
|
|
2086
2846
|
import { drizzle as drizzle2 } from "drizzle-orm/bun-sqlite";
|
|
2087
2847
|
import { migrate } from "drizzle-orm/bun-sqlite/migrator";
|
|
2088
2848
|
import { mkdirSync as mkdirSync2 } from "fs";
|
|
2089
|
-
import { dirname as
|
|
2849
|
+
import { dirname as dirname3 } from "path";
|
|
2090
2850
|
function runMigrations() {
|
|
2091
|
-
mkdirSync2(
|
|
2092
|
-
const sqlite = new
|
|
2851
|
+
mkdirSync2(dirname3(paths.db), { recursive: true });
|
|
2852
|
+
const sqlite = new Database3(paths.db);
|
|
2093
2853
|
sqlite.exec("PRAGMA journal_mode = WAL");
|
|
2094
2854
|
sqlite.exec("PRAGMA foreign_keys = ON");
|
|
2095
2855
|
const db = drizzle2(sqlite);
|
|
@@ -2376,14 +3136,14 @@ var init_scripts = __esm(() => {
|
|
|
2376
3136
|
});
|
|
2377
3137
|
|
|
2378
3138
|
// src/hooks/install.ts
|
|
2379
|
-
import { mkdirSync as mkdirSync3, writeFileSync as
|
|
2380
|
-
import { join as
|
|
3139
|
+
import { mkdirSync as mkdirSync3, writeFileSync as writeFileSync4, chmodSync as chmodSync2 } from "fs";
|
|
3140
|
+
import { join as join7 } from "path";
|
|
2381
3141
|
function installHookScripts(bin) {
|
|
2382
3142
|
mkdirSync3(paths.hooks, { recursive: true });
|
|
2383
3143
|
for (const [filename, scriptFn] of Object.entries(HOOK_SCRIPTS)) {
|
|
2384
|
-
const filePath =
|
|
2385
|
-
|
|
2386
|
-
|
|
3144
|
+
const filePath = join7(paths.hooks, filename);
|
|
3145
|
+
writeFileSync4(filePath, scriptFn(bin));
|
|
3146
|
+
chmodSync2(filePath, 493);
|
|
2387
3147
|
}
|
|
2388
3148
|
console.log(`Installed ${Object.keys(HOOK_SCRIPTS).length} hook scripts to ${paths.hooks}`);
|
|
2389
3149
|
}
|
|
@@ -2393,8 +3153,8 @@ var init_install = __esm(() => {
|
|
|
2393
3153
|
});
|
|
2394
3154
|
|
|
2395
3155
|
// src/hooks/settings.ts
|
|
2396
|
-
import { readFileSync as
|
|
2397
|
-
import { join as
|
|
3156
|
+
import { readFileSync as readFileSync8, writeFileSync as writeFileSync5, mkdirSync as mkdirSync4 } from "fs";
|
|
3157
|
+
import { join as join8, dirname as dirname4 } from "path";
|
|
2398
3158
|
import { homedir as homedir2 } from "os";
|
|
2399
3159
|
function isBertrandGroup(group) {
|
|
2400
3160
|
return group.hooks?.some((h) => h.command?.includes(".bertrand/hooks/")) ?? false;
|
|
@@ -2402,7 +3162,7 @@ function isBertrandGroup(group) {
|
|
|
2402
3162
|
function installHookSettings() {
|
|
2403
3163
|
let settings = {};
|
|
2404
3164
|
try {
|
|
2405
|
-
settings = JSON.parse(
|
|
3165
|
+
settings = JSON.parse(readFileSync8(SETTINGS_PATH, "utf-8"));
|
|
2406
3166
|
} catch {}
|
|
2407
3167
|
const existingHooks = settings.hooks && typeof settings.hooks === "object" && !Array.isArray(settings.hooks) ? settings.hooks : {};
|
|
2408
3168
|
const merged = { ...existingHooks };
|
|
@@ -2411,15 +3171,15 @@ function installHookSettings() {
|
|
|
2411
3171
|
merged[eventType] = [...existing, ...bertrandGroups];
|
|
2412
3172
|
}
|
|
2413
3173
|
settings.hooks = merged;
|
|
2414
|
-
mkdirSync4(
|
|
2415
|
-
|
|
3174
|
+
mkdirSync4(dirname4(SETTINGS_PATH), { recursive: true });
|
|
3175
|
+
writeFileSync5(SETTINGS_PATH, JSON.stringify(settings, null, 2) + `
|
|
2416
3176
|
`);
|
|
2417
3177
|
console.log(`Updated ${SETTINGS_PATH} with bertrand hooks`);
|
|
2418
3178
|
}
|
|
2419
3179
|
var SETTINGS_PATH, BERTRAND_HOOKS;
|
|
2420
3180
|
var init_settings = __esm(() => {
|
|
2421
3181
|
init_paths();
|
|
2422
|
-
SETTINGS_PATH =
|
|
3182
|
+
SETTINGS_PATH = join8(homedir2(), ".claude", "settings.json");
|
|
2423
3183
|
BERTRAND_HOOKS = {
|
|
2424
3184
|
PreToolUse: [
|
|
2425
3185
|
{
|
|
@@ -2463,8 +3223,8 @@ var init_settings = __esm(() => {
|
|
|
2463
3223
|
});
|
|
2464
3224
|
|
|
2465
3225
|
// src/lib/completions.ts
|
|
2466
|
-
import { writeFileSync as
|
|
2467
|
-
import { join as
|
|
3226
|
+
import { writeFileSync as writeFileSync6, mkdirSync as mkdirSync5 } from "fs";
|
|
3227
|
+
import { join as join9 } from "path";
|
|
2468
3228
|
function bashCompletion() {
|
|
2469
3229
|
return `# bertrand bash completion
|
|
2470
3230
|
_bertrand() {
|
|
@@ -2494,11 +3254,11 @@ function fishCompletion() {
|
|
|
2494
3254
|
`;
|
|
2495
3255
|
}
|
|
2496
3256
|
function generateCompletions() {
|
|
2497
|
-
const dir =
|
|
3257
|
+
const dir = join9(paths.root, "completions");
|
|
2498
3258
|
mkdirSync5(dir, { recursive: true });
|
|
2499
|
-
|
|
2500
|
-
|
|
2501
|
-
|
|
3259
|
+
writeFileSync6(join9(dir, "bertrand.bash"), bashCompletion());
|
|
3260
|
+
writeFileSync6(join9(dir, "_bertrand"), zshCompletion());
|
|
3261
|
+
writeFileSync6(join9(dir, "bertrand.fish"), fishCompletion());
|
|
2502
3262
|
console.log(`Shell completions written to ${dir}`);
|
|
2503
3263
|
console.log(" Add to your shell config:");
|
|
2504
3264
|
console.log(` bash: source ${dir}/bertrand.bash`);
|
|
@@ -2525,9 +3285,9 @@ var init_completions = __esm(() => {
|
|
|
2525
3285
|
|
|
2526
3286
|
// src/cli/commands/init.ts
|
|
2527
3287
|
var exports_init = {};
|
|
2528
|
-
import { mkdirSync as mkdirSync6,
|
|
3288
|
+
import { mkdirSync as mkdirSync6, writeFileSync as writeFileSync7, chmodSync as chmodSync3 } from "fs";
|
|
2529
3289
|
import { execSync as execSync2 } from "child_process";
|
|
2530
|
-
import { join as
|
|
3290
|
+
import { join as join10 } from "path";
|
|
2531
3291
|
function detectTerminal() {
|
|
2532
3292
|
try {
|
|
2533
3293
|
execSync2("which wsh", { stdio: "ignore" });
|
|
@@ -2536,35 +3296,24 @@ function detectTerminal() {
|
|
|
2536
3296
|
return "other";
|
|
2537
3297
|
}
|
|
2538
3298
|
}
|
|
2539
|
-
function writeConfig(config) {
|
|
2540
|
-
writeFileSync5(CONFIG_PATH, JSON.stringify(config, null, 2) + `
|
|
2541
|
-
`);
|
|
2542
|
-
}
|
|
2543
|
-
function readConfig() {
|
|
2544
|
-
try {
|
|
2545
|
-
return JSON.parse(readFileSync6(CONFIG_PATH, "utf-8"));
|
|
2546
|
-
} catch {
|
|
2547
|
-
return null;
|
|
2548
|
-
}
|
|
2549
|
-
}
|
|
2550
3299
|
function resolveBin() {
|
|
2551
3300
|
const onPath = Bun.which("bertrand");
|
|
2552
3301
|
if (onPath)
|
|
2553
3302
|
return onPath;
|
|
2554
3303
|
const entry = process.argv[1];
|
|
2555
3304
|
if (entry && SOURCE_ENTRY.test(entry)) {
|
|
2556
|
-
const launcherDir =
|
|
2557
|
-
const launcher =
|
|
3305
|
+
const launcherDir = join10(paths.root, "bin");
|
|
3306
|
+
const launcher = join10(launcherDir, "bertrand-dev");
|
|
2558
3307
|
mkdirSync6(launcherDir, { recursive: true });
|
|
2559
|
-
|
|
3308
|
+
writeFileSync7(launcher, `#!/usr/bin/env bash
|
|
2560
3309
|
exec ${process.execPath} ${JSON.stringify(entry)} "$@"
|
|
2561
3310
|
`);
|
|
2562
|
-
|
|
3311
|
+
chmodSync3(launcher, 493);
|
|
2563
3312
|
return launcher;
|
|
2564
3313
|
}
|
|
2565
3314
|
return null;
|
|
2566
3315
|
}
|
|
2567
|
-
var
|
|
3316
|
+
var SOURCE_ENTRY;
|
|
2568
3317
|
var init_init = __esm(() => {
|
|
2569
3318
|
init_router();
|
|
2570
3319
|
init_migrate();
|
|
@@ -2572,7 +3321,7 @@ var init_init = __esm(() => {
|
|
|
2572
3321
|
init_settings();
|
|
2573
3322
|
init_paths();
|
|
2574
3323
|
init_completions();
|
|
2575
|
-
|
|
3324
|
+
init_config2();
|
|
2576
3325
|
SOURCE_ENTRY = /\/src\/index\.tsx?$/;
|
|
2577
3326
|
register("init", async () => {
|
|
2578
3327
|
console.log(`bertrand init
|
|
@@ -3122,10 +3871,10 @@ var init_log = __esm(() => {
|
|
|
3122
3871
|
|
|
3123
3872
|
// src/cli/commands/stats.ts
|
|
3124
3873
|
var exports_stats = {};
|
|
3125
|
-
function getMetrics(sessionId, name,
|
|
3874
|
+
function getMetrics(sessionId, name, status2) {
|
|
3126
3875
|
const stats = getSessionStats(sessionId);
|
|
3127
3876
|
if (stats) {
|
|
3128
|
-
return { name, status, ...stats };
|
|
3877
|
+
return { name, status: status2, ...stats };
|
|
3129
3878
|
}
|
|
3130
3879
|
const timing = computeTimingsLive(sessionId);
|
|
3131
3880
|
const allEvents = getEventsBySession(sessionId);
|
|
@@ -3142,7 +3891,7 @@ function getMetrics(sessionId, name, status) {
|
|
|
3142
3891
|
}
|
|
3143
3892
|
return {
|
|
3144
3893
|
name,
|
|
3145
|
-
status,
|
|
3894
|
+
status: status2,
|
|
3146
3895
|
eventCount: allEvents.length,
|
|
3147
3896
|
conversationCount: conversations2.size,
|
|
3148
3897
|
interactionCount,
|
|
@@ -3390,7 +4139,8 @@ var hotPath = {
|
|
|
3390
4139
|
"recap-thinking": () => Promise.resolve().then(() => (init_recap_thinking(), exports_recap_thinking)),
|
|
3391
4140
|
badge: () => Promise.resolve().then(() => (init_badge(), exports_badge)),
|
|
3392
4141
|
notify: () => Promise.resolve().then(() => (init_notify(), exports_notify)),
|
|
3393
|
-
serve: () => Promise.resolve().then(() => (init_serve(), exports_serve))
|
|
4142
|
+
serve: () => Promise.resolve().then(() => (init_serve(), exports_serve)),
|
|
4143
|
+
sync: () => Promise.resolve().then(() => (init_sync(), exports_sync))
|
|
3394
4144
|
};
|
|
3395
4145
|
if (command && command in hotPath) {
|
|
3396
4146
|
await hotPath[command]();
|
|
@@ -3409,7 +4159,8 @@ if (command && command in hotPath) {
|
|
|
3409
4159
|
Promise.resolve().then(() => (init_recap_thinking(), exports_recap_thinking)),
|
|
3410
4160
|
Promise.resolve().then(() => (init_serve(), exports_serve)),
|
|
3411
4161
|
Promise.resolve().then(() => (init_badge(), exports_badge)),
|
|
3412
|
-
Promise.resolve().then(() => (init_notify(), exports_notify))
|
|
4162
|
+
Promise.resolve().then(() => (init_notify(), exports_notify)),
|
|
4163
|
+
Promise.resolve().then(() => (init_sync(), exports_sync))
|
|
3413
4164
|
]);
|
|
3414
4165
|
}
|
|
3415
4166
|
await route(process.argv);
|
package/dist/run-screen.js
CHANGED
|
@@ -577,7 +577,8 @@ var paths = {
|
|
|
577
577
|
root: join(homedir(), BERTRAND_DIR),
|
|
578
578
|
db: join(homedir(), BERTRAND_DIR, "bertrand.db"),
|
|
579
579
|
hooks: join(homedir(), BERTRAND_DIR, "hooks"),
|
|
580
|
-
sessions: join(homedir(), BERTRAND_DIR, "sessions")
|
|
580
|
+
sessions: join(homedir(), BERTRAND_DIR, "sessions"),
|
|
581
|
+
syncEnv: join(homedir(), BERTRAND_DIR, "sync.env")
|
|
581
582
|
};
|
|
582
583
|
|
|
583
584
|
// src/db/schema.ts
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bertrand",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.18.0",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public"
|
|
6
6
|
},
|
|
@@ -47,6 +47,7 @@
|
|
|
47
47
|
},
|
|
48
48
|
"dependencies": {
|
|
49
49
|
"@orchetron/storm": "^0.2.0",
|
|
50
|
+
"@supabase/supabase-js": "^2.108.1",
|
|
50
51
|
"drizzle-orm": "^0.44.0",
|
|
51
52
|
"nanoid": "^5.1.5",
|
|
52
53
|
"react": "^19.2.5"
|