bazilion 0.10.0 → 0.11.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/cli.js +1249 -136
- package/dist/cli.js.map +1 -1
- package/dist/daemon.js +3027 -396
- package/dist/daemon.js.map +1 -1
- package/dist/migrations/0001_init.sql +20 -0
- package/dist/web/client/assets/{ProductionTeamPolicyEditor-CiQ-VVDo.js → ProductionTeamPolicyEditor-B6dbpuYy.js} +1 -1
- package/dist/web/client/assets/{ProfileCommunicationEditor-D84F5yfr.js → ProfileCommunicationEditor-Bf8UvmZT.js} +1 -1
- package/dist/web/client/assets/{TeamTabs-Btoe_FAE.js → TeamTabs-sQBHChtd.js} +1 -1
- package/dist/web/client/assets/{TemplatesTabs-CAYY03F5.js → TemplatesTabs-DPsuwsWB.js} +1 -1
- package/dist/web/client/assets/{_id-BqU2BodK.js → _id-C9rkCclz.js} +1 -1
- package/dist/web/client/assets/{_id-Q98Of6k9.js → _id-D65IzC8H.js} +1 -1
- package/dist/web/client/assets/{_id-BeauECmk.js → _id-DE4IY5bL.js} +1 -1
- package/dist/web/client/assets/{_id-CKtoio3t.js → _id-DYASsGQ8.js} +1 -1
- package/dist/web/client/assets/{activity-DXN_N85w.js → activity-C7o6-xHg.js} +1 -1
- package/dist/web/client/assets/{agents-C5Xsdge1.js → agents-UroO4d25.js} +1 -1
- package/dist/web/client/assets/{agents-Cgxo4H5G.js → agents-h8yWXQ2H.js} +1 -1
- package/dist/web/client/assets/{approvals-BnG7fbw-.js → approvals-DKhF0YLC.js} +1 -1
- package/dist/web/client/assets/chevron-down-BK6NPMMP.js +1 -0
- package/dist/web/client/assets/{config-xw4NyrXF.js → config-q8JDresK.js} +1 -1
- package/dist/web/client/assets/{context-D2V1Oi4z.js → context-CBucAoi6.js} +1 -1
- package/dist/web/client/assets/{inbox-DK6hWCMp.js → inbox-BwoRikj1.js} +1 -1
- package/dist/web/client/assets/{index-VdURSoM4.js → index-DUXAevpZ.js} +2 -2
- package/dist/web/client/assets/{login-CvptUoMR.js → login-CNAV9Wer.js} +1 -1
- package/dist/web/client/assets/{mcp-BDFM_sW-.js → mcp-BKdEFwHO.js} +1 -1
- package/dist/web/client/assets/{members-BJvph_TR.js → members-BDqZQh26.js} +1 -1
- package/dist/web/client/assets/{memory-DRytqZCD.js → memory-DVdEVr6L.js} +1 -1
- package/dist/web/client/assets/plus-b4yFXP0O.js +1 -0
- package/dist/web/client/assets/{policy-CGP50OtQ.js → policy-DdDctqvg.js} +1 -1
- package/dist/web/client/assets/{routes-DJnWYvC9.js → routes-BEfALSZv.js} +1 -1
- package/dist/web/client/assets/{services-DHheiv7L.js → services-DZP2ybzb.js} +1 -1
- package/dist/web/client/assets/{skills-DmsitluU.js → skills-DoeIjITs.js} +1 -1
- package/dist/web/client/assets/styles-BYM0UGdF.css +2 -0
- package/dist/web/client/assets/{teams-CbxSaorm.js → teams-DlHRnrQw.js} +1 -1
- package/dist/web/client/assets/{teams-BdC4gthq.js → teams-W3j-UoJ9.js} +1 -1
- package/dist/web/client/assets/telegram-2OdeGv4E.js +1 -0
- package/dist/web/client/assets/{tokens-CjRplDrJ.js → tokens-MuBfmgue.js} +1 -1
- package/dist/web/client/assets/triggers-CX7RMQFQ.js +1 -0
- package/dist/web/client/assets/{welcome-wBfX9dAd.js → welcome-CvNr5IMi.js} +1 -1
- package/dist/web/client/assets/wire-constants-BhVKOCQa.js +74 -0
- package/dist/web/server/assets/{ChatPane-eUY_dga3.js → ChatPane-Ba5wTsVk.js} +325 -36
- package/dist/web/server/assets/{_id-ClN9hY2x.js → _id-Cg7x6q3g.js} +2 -2
- package/dist/web/server/assets/{_id-DkYodSkG.js → _id-DqON-59C.js} +1 -1
- package/dist/web/server/assets/{_tanstack-start-manifest_v-D09Xu_6u.js → _tanstack-start-manifest_v-BXauUXg6.js} +51 -51
- package/dist/web/server/assets/{router-DIdp8ZNt.js → router-C5_NjTsP.js} +5 -5
- package/dist/web/server/assets/{routes-Jb0zjKav.js → routes-B6cMLIPk.js} +3 -3
- package/dist/web/server/assets/{routes-CXz4hTAr.js → routes-BrA-U_Hm.js} +1 -1
- package/dist/web/server/assets/{routes-lIclH9Br.js → routes-C19CMLZt.js} +1 -1
- package/dist/web/server/assets/{telegram-7ub--6xz.js → telegram-CBF8SJzB.js} +1 -24
- package/dist/web/server/assets/{telegram-Bazhmpr4.js → telegram-XwyKSEtj.js} +1 -1
- package/dist/web/server/assets/{triggers-Ca57_qym.js → triggers-CBl4yMyw.js} +1 -1
- package/dist/web/server/assets/{triggers-C_0y6QV9.js → triggers-D79NoSXk.js} +3 -2
- package/dist/web/server/assets/{triggers-BMpHsGIr.js → triggers-_3rdhwSN.js} +27 -3
- package/dist/web/server/server.js +4 -4
- package/dist/worker.js +1609 -135
- package/dist/worker.js.map +1 -1
- package/package.json +4 -3
- package/dist/web/client/assets/chevron-down-BhcBspw3.js +0 -1
- package/dist/web/client/assets/plus-C9ee3SIO.js +0 -1
- package/dist/web/client/assets/styles-B5eQAwLP.css +0 -2
- package/dist/web/client/assets/telegram-D61G_alO.js +0 -1
- package/dist/web/client/assets/triggers-MLzTm_DM.js +0 -1
- package/dist/web/client/assets/wire-constants-BxfyUMXB.js +0 -74
- package/dist/web/server/assets/{Sidebar-O_CH1-JD.js → Sidebar-DsRVCszF.js} +1 -1
package/dist/cli.js
CHANGED
|
@@ -6,7 +6,7 @@ import { defineCommand as defineCommand25, runCommand, showUsage } from "citty";
|
|
|
6
6
|
// package.json
|
|
7
7
|
var package_default = {
|
|
8
8
|
name: "bazilion",
|
|
9
|
-
version: "0.
|
|
9
|
+
version: "0.11.0",
|
|
10
10
|
description: "Multi-agent runtime CLI \u2014 spawn LLM agents, manage profiles/teams/skills, and run the local daemon.",
|
|
11
11
|
license: "MIT",
|
|
12
12
|
repository: {
|
|
@@ -68,6 +68,7 @@ var package_default = {
|
|
|
68
68
|
"qrcode-terminal": "^0.12.0",
|
|
69
69
|
react: "^19.2.7",
|
|
70
70
|
"react-dom": "^19.2.7",
|
|
71
|
+
tar: "^7.5.16",
|
|
71
72
|
typebox: "^1.3.0",
|
|
72
73
|
undici: "^8.5.0",
|
|
73
74
|
yaml: "^2.9.0"
|
|
@@ -372,12 +373,12 @@ var listCmd = defineCommand({
|
|
|
372
373
|
async run({ args }) {
|
|
373
374
|
const client = createClient2();
|
|
374
375
|
const q = args.all ? "?includeArchived=true" : "";
|
|
375
|
-
const
|
|
376
|
-
if (
|
|
376
|
+
const list4 = await client.get(`/api/agents${q}`);
|
|
377
|
+
if (list4.length === 0) {
|
|
377
378
|
console.log("(no agents)");
|
|
378
379
|
return;
|
|
379
380
|
}
|
|
380
|
-
const rows =
|
|
381
|
+
const rows = list4.map(
|
|
381
382
|
(a) => args.long ? [a.id, a.status, a.name, a.profileId] : (
|
|
382
383
|
// Short UUID prefix — all CLI commands accept 4+ char prefixes now.
|
|
383
384
|
[a.id.slice(0, 8), a.status, a.name]
|
|
@@ -448,6 +449,74 @@ var deleteCmd = defineCommand({
|
|
|
448
449
|
console.log(`deleted agent ${args.id}`);
|
|
449
450
|
}
|
|
450
451
|
});
|
|
452
|
+
function bashApprovalModeForTty(inputIsTty, outputIsTty) {
|
|
453
|
+
return inputIsTty && outputIsTty ? "interactive" : "auto_deny";
|
|
454
|
+
}
|
|
455
|
+
function buildChatRequest(message, attachments, bashApprovalMode) {
|
|
456
|
+
return {
|
|
457
|
+
message,
|
|
458
|
+
bashApprovalMode,
|
|
459
|
+
...attachments && attachments.length > 0 ? { attachments } : {}
|
|
460
|
+
};
|
|
461
|
+
}
|
|
462
|
+
function terminalSafe(value) {
|
|
463
|
+
let safe = "";
|
|
464
|
+
for (const character of value) {
|
|
465
|
+
const code = character.charCodeAt(0);
|
|
466
|
+
const isControl = code <= 9 || code >= 11 && code <= 31 || code >= 127 && code <= 159;
|
|
467
|
+
safe += isControl ? `\\x${code.toString(16).padStart(2, "0")}` : character;
|
|
468
|
+
}
|
|
469
|
+
return safe;
|
|
470
|
+
}
|
|
471
|
+
async function promptForCommandApproval(approval, prompt) {
|
|
472
|
+
prompt.write(" [shell command approval required]");
|
|
473
|
+
prompt.write(" command:");
|
|
474
|
+
for (const line of approval.command.split("\n")) prompt.write(` ${terminalSafe(line)}`);
|
|
475
|
+
prompt.write(" risks:");
|
|
476
|
+
for (const risk of approval.risks) {
|
|
477
|
+
prompt.write(
|
|
478
|
+
` - ${terminalSafe(risk.code)} (${terminalSafe(risk.severity)}): ${terminalSafe(risk.message)}`
|
|
479
|
+
);
|
|
480
|
+
}
|
|
481
|
+
try {
|
|
482
|
+
const answer = (await prompt.question(" Allow this command once? [y/N] ")).trim().toLowerCase();
|
|
483
|
+
return answer === "y" || answer === "yes" ? "allow" : "deny";
|
|
484
|
+
} catch {
|
|
485
|
+
prompt.write(" [approval input unavailable; denying]");
|
|
486
|
+
return "deny";
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
async function respondToCommandApproval(client, approval, prompt) {
|
|
490
|
+
const decision2 = prompt ? await promptForCommandApproval(approval, prompt) : "deny";
|
|
491
|
+
await client.post(`/api/shell-approvals/${encodeURIComponent(approval.id)}`, { decision: decision2 });
|
|
492
|
+
return decision2;
|
|
493
|
+
}
|
|
494
|
+
function closeDeltaLine(state) {
|
|
495
|
+
if (!state.inDeltaStream) return;
|
|
496
|
+
process.stdout.write("\n");
|
|
497
|
+
state.inDeltaStream = false;
|
|
498
|
+
}
|
|
499
|
+
function printCommandApprovalStatus(approval) {
|
|
500
|
+
switch (approval.status) {
|
|
501
|
+
case "allowed":
|
|
502
|
+
console.log(" [shell command allowed]");
|
|
503
|
+
break;
|
|
504
|
+
case "denied":
|
|
505
|
+
console.log(" [shell command denied]");
|
|
506
|
+
break;
|
|
507
|
+
case "auto_denied":
|
|
508
|
+
console.log(" [shell command auto-denied: non-interactive turn]");
|
|
509
|
+
break;
|
|
510
|
+
case "expired":
|
|
511
|
+
console.log(" [shell command approval expired]");
|
|
512
|
+
break;
|
|
513
|
+
case "cancelled":
|
|
514
|
+
console.log(" [shell command approval cancelled]");
|
|
515
|
+
break;
|
|
516
|
+
case "pending":
|
|
517
|
+
break;
|
|
518
|
+
}
|
|
519
|
+
}
|
|
451
520
|
function printEvent(e, state) {
|
|
452
521
|
switch (e.type) {
|
|
453
522
|
case "assistant_delta":
|
|
@@ -494,16 +563,32 @@ function printEvent(e, state) {
|
|
|
494
563
|
break;
|
|
495
564
|
}
|
|
496
565
|
}
|
|
497
|
-
async function streamTurn(client, agentId, message, attachments) {
|
|
566
|
+
async function streamTurn(client, agentId, message, attachments, options = { bashApprovalMode: "auto_deny" }) {
|
|
498
567
|
const state = { inDeltaStream: false };
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
568
|
+
const respondedApprovalIds = /* @__PURE__ */ new Set();
|
|
569
|
+
for await (const frame of client.stream(
|
|
570
|
+
"POST",
|
|
571
|
+
`/api/agents/${agentId}/chat`,
|
|
572
|
+
buildChatRequest(message, attachments, options.bashApprovalMode)
|
|
573
|
+
)) {
|
|
503
574
|
if (frame.kind === "event") {
|
|
504
|
-
|
|
575
|
+
const event = frame.event;
|
|
576
|
+
if (event.type === "command_approval") {
|
|
577
|
+
closeDeltaLine(state);
|
|
578
|
+
const approval = event.approval;
|
|
579
|
+
if (approval.status === "pending") {
|
|
580
|
+
if (!respondedApprovalIds.has(approval.id)) {
|
|
581
|
+
respondedApprovalIds.add(approval.id);
|
|
582
|
+
await respondToCommandApproval(client, approval, options.approvalPrompt);
|
|
583
|
+
}
|
|
584
|
+
} else {
|
|
585
|
+
printCommandApprovalStatus(approval);
|
|
586
|
+
}
|
|
587
|
+
} else if (event.type !== "user_message") {
|
|
588
|
+
printEvent(event, state);
|
|
589
|
+
}
|
|
505
590
|
} else if (frame.kind === "fatal") {
|
|
506
|
-
|
|
591
|
+
closeDeltaLine(state);
|
|
507
592
|
throw new Error(frame.error);
|
|
508
593
|
}
|
|
509
594
|
}
|
|
@@ -528,34 +613,78 @@ var chatCmd = defineCommand({
|
|
|
528
613
|
async run({ args }) {
|
|
529
614
|
const client = createClient2();
|
|
530
615
|
const resolved = await client.get(`/api/agents/${args.id}`);
|
|
616
|
+
const bashApprovalMode = bashApprovalModeForTty(stdin.isTTY, stdout.isTTY);
|
|
531
617
|
const attachments = [
|
|
532
618
|
...loadImages(asPaths(args.image)),
|
|
533
619
|
...loadFiles(asPaths(args.file))
|
|
534
620
|
];
|
|
535
621
|
if (args.message || attachments.length > 0) {
|
|
536
|
-
|
|
622
|
+
const rl2 = bashApprovalMode === "interactive" ? createInterface({ input: stdin, output: stdout }) : null;
|
|
623
|
+
try {
|
|
624
|
+
await streamTurn(client, resolved.agent.id, args.message ?? "", attachments, {
|
|
625
|
+
bashApprovalMode,
|
|
626
|
+
...rl2 ? {
|
|
627
|
+
approvalPrompt: {
|
|
628
|
+
question: (question) => rl2.question(question),
|
|
629
|
+
write: (line) => console.log(line)
|
|
630
|
+
}
|
|
631
|
+
} : {}
|
|
632
|
+
});
|
|
633
|
+
} finally {
|
|
634
|
+
rl2?.close();
|
|
635
|
+
}
|
|
537
636
|
return;
|
|
538
637
|
}
|
|
539
638
|
console.log(`chatting with ${resolved.agent.name} (${resolved.model})`);
|
|
540
639
|
console.log("(type /exit to quit)");
|
|
541
640
|
const rl = createInterface({ input: stdin, output: stdout });
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
641
|
+
try {
|
|
642
|
+
if (bashApprovalMode === "interactive") {
|
|
643
|
+
while (true) {
|
|
644
|
+
let line;
|
|
645
|
+
try {
|
|
646
|
+
line = await rl.question("> ");
|
|
647
|
+
} catch {
|
|
648
|
+
break;
|
|
649
|
+
}
|
|
650
|
+
const trimmed = line.trim();
|
|
651
|
+
if (trimmed === "/exit" || trimmed === "/quit") break;
|
|
652
|
+
if (!trimmed) continue;
|
|
653
|
+
try {
|
|
654
|
+
await streamTurn(client, resolved.agent.id, trimmed, void 0, {
|
|
655
|
+
bashApprovalMode,
|
|
656
|
+
approvalPrompt: {
|
|
657
|
+
question: (question) => rl.question(question),
|
|
658
|
+
write: (output) => console.log(output)
|
|
659
|
+
}
|
|
660
|
+
});
|
|
661
|
+
} catch (err) {
|
|
662
|
+
console.error(`error: ${err.message}`);
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
} else {
|
|
666
|
+
rl.setPrompt("> ");
|
|
548
667
|
rl.prompt();
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
668
|
+
for await (const line of rl) {
|
|
669
|
+
const trimmed = line.trim();
|
|
670
|
+
if (trimmed === "/exit" || trimmed === "/quit") break;
|
|
671
|
+
if (!trimmed) {
|
|
672
|
+
rl.prompt();
|
|
673
|
+
continue;
|
|
674
|
+
}
|
|
675
|
+
try {
|
|
676
|
+
await streamTurn(client, resolved.agent.id, trimmed, void 0, {
|
|
677
|
+
bashApprovalMode
|
|
678
|
+
});
|
|
679
|
+
} catch (err) {
|
|
680
|
+
console.error(`error: ${err.message}`);
|
|
681
|
+
}
|
|
682
|
+
rl.prompt();
|
|
683
|
+
}
|
|
555
684
|
}
|
|
556
|
-
|
|
685
|
+
} finally {
|
|
686
|
+
rl.close();
|
|
557
687
|
}
|
|
558
|
-
rl.close();
|
|
559
688
|
}
|
|
560
689
|
});
|
|
561
690
|
var chatResetCmd = defineCommand({
|
|
@@ -567,8 +696,8 @@ var chatResetCmd = defineCommand({
|
|
|
567
696
|
async run({ args }) {
|
|
568
697
|
if (!args.force) {
|
|
569
698
|
process.stdout.write(`reset all chat history for ${args.id}? [y/N] `);
|
|
570
|
-
const answer = await new Promise((
|
|
571
|
-
process.stdin.once("data", (d) =>
|
|
699
|
+
const answer = await new Promise((resolve4) => {
|
|
700
|
+
process.stdin.once("data", (d) => resolve4(String(d).trim().toLowerCase()));
|
|
572
701
|
});
|
|
573
702
|
if (answer !== "y" && answer !== "yes") {
|
|
574
703
|
console.log("aborted");
|
|
@@ -716,8 +845,8 @@ var chatCompactCmd = defineCommand({
|
|
|
716
845
|
process.stdout.write(
|
|
717
846
|
`compact chat history for ${args.id}? head turns become a summary; tail is preserved. [y/N] `
|
|
718
847
|
);
|
|
719
|
-
const answer = await new Promise((
|
|
720
|
-
process.stdin.once("data", (d) =>
|
|
848
|
+
const answer = await new Promise((resolve4) => {
|
|
849
|
+
process.stdin.once("data", (d) => resolve4(String(d).trim().toLowerCase()));
|
|
721
850
|
});
|
|
722
851
|
if (answer !== "y" && answer !== "yes") {
|
|
723
852
|
console.log("aborted");
|
|
@@ -990,8 +1119,8 @@ var openaiLoginCmd = defineCommand3({
|
|
|
990
1119
|
},
|
|
991
1120
|
onPrompt: async (prompt) => {
|
|
992
1121
|
process.stdout.write(`${prompt.message}: `);
|
|
993
|
-
return new Promise((
|
|
994
|
-
process.stdin.once("data", (d) =>
|
|
1122
|
+
return new Promise((resolve4) => {
|
|
1123
|
+
process.stdin.once("data", (d) => resolve4(String(d).trim()));
|
|
995
1124
|
});
|
|
996
1125
|
},
|
|
997
1126
|
onProgress: (msg) => {
|
|
@@ -1065,12 +1194,435 @@ var authCommand = defineCommand3({
|
|
|
1065
1194
|
});
|
|
1066
1195
|
|
|
1067
1196
|
// src/commands/backup.ts
|
|
1068
|
-
import {
|
|
1069
|
-
import {
|
|
1070
|
-
|
|
1197
|
+
import { createHash as createHash3, randomUUID as randomUUID2 } from "crypto";
|
|
1198
|
+
import {
|
|
1199
|
+
chmodSync as chmodSync2,
|
|
1200
|
+
closeSync as closeSync2,
|
|
1201
|
+
copyFileSync,
|
|
1202
|
+
createWriteStream,
|
|
1203
|
+
existsSync as existsSync2,
|
|
1204
|
+
fsyncSync as fsyncSync2,
|
|
1205
|
+
lstatSync as lstatSync2,
|
|
1206
|
+
mkdirSync as mkdirSync2,
|
|
1207
|
+
mkdtempSync,
|
|
1208
|
+
openSync as openSync2,
|
|
1209
|
+
readdirSync,
|
|
1210
|
+
readFileSync as readFileSync4,
|
|
1211
|
+
renameSync as renameSync2,
|
|
1212
|
+
rmSync as rmSync2
|
|
1213
|
+
} from "fs";
|
|
1214
|
+
import { tmpdir } from "os";
|
|
1215
|
+
import { basename as basename3, dirname as dirname2, isAbsolute, parse, posix, relative, resolve as resolve2 } from "path";
|
|
1216
|
+
import { DatabaseSync } from "node:sqlite";
|
|
1071
1217
|
import { Readable } from "stream";
|
|
1072
1218
|
import { pipeline } from "stream/promises";
|
|
1073
1219
|
import { defineCommand as defineCommand4 } from "citty";
|
|
1220
|
+
import { extract, list as list2 } from "tar";
|
|
1221
|
+
|
|
1222
|
+
// src/backup-schema.ts
|
|
1223
|
+
import { createHash } from "crypto";
|
|
1224
|
+
var CANONICAL_MIGRATION = "0001_init";
|
|
1225
|
+
var CANONICAL_SCHEMA_HASH = "f2da105d21bdea7bedff3ba601243c22d0a7b2d023b954c714940642152b227f";
|
|
1226
|
+
var CANONICAL_OBJECTS = [
|
|
1227
|
+
["index", "agent_triggers_agent"],
|
|
1228
|
+
["index", "agent_triggers_enabled"],
|
|
1229
|
+
["index", "communication_approval_events_attempt"],
|
|
1230
|
+
["index", "communication_approvals_queue"],
|
|
1231
|
+
["index", "communication_approvals_teams"],
|
|
1232
|
+
["index", "idx_agents_telegram_topic_id"],
|
|
1233
|
+
["index", "idx_provider_models_provider"],
|
|
1234
|
+
["index", "messages_policy_delivery_queue"],
|
|
1235
|
+
["index", "messages_to_unread"],
|
|
1236
|
+
["index", "team_policy_baseline_owner"],
|
|
1237
|
+
["index", "team_policy_blocks_team_time"],
|
|
1238
|
+
["index", "team_template_active_position"],
|
|
1239
|
+
["index", "trigger_dispatches_claimable"],
|
|
1240
|
+
["index", "trigger_dispatches_trigger_time"],
|
|
1241
|
+
["index", "web_tokens_active"],
|
|
1242
|
+
["table", "agent_skills"],
|
|
1243
|
+
["table", "agent_triggers"],
|
|
1244
|
+
["table", "agents"],
|
|
1245
|
+
["table", "communication_approval_events"],
|
|
1246
|
+
["table", "communication_approval_message_grants"],
|
|
1247
|
+
["table", "communication_approvals"],
|
|
1248
|
+
["table", "config"],
|
|
1249
|
+
["table", "mcp_servers"],
|
|
1250
|
+
["table", "messages"],
|
|
1251
|
+
["table", "profile_communication_defaults"],
|
|
1252
|
+
["table", "profile_default_skills"],
|
|
1253
|
+
["table", "profiles"],
|
|
1254
|
+
["table", "provider_models"],
|
|
1255
|
+
["table", "provider_state"],
|
|
1256
|
+
["table", "schema_migrations"],
|
|
1257
|
+
["table", "secrets"],
|
|
1258
|
+
["table", "skill_meta"],
|
|
1259
|
+
["table", "source_slot_bindings"],
|
|
1260
|
+
["table", "team_agent_state"],
|
|
1261
|
+
["table", "team_policies"],
|
|
1262
|
+
["table", "team_policy_block_events"],
|
|
1263
|
+
["table", "team_policy_edges"],
|
|
1264
|
+
["table", "team_template_edges"],
|
|
1265
|
+
["table", "team_template_revision_edges"],
|
|
1266
|
+
["table", "team_template_revision_slots"],
|
|
1267
|
+
["table", "team_template_revisions"],
|
|
1268
|
+
["table", "team_template_slots"],
|
|
1269
|
+
["table", "team_templates"],
|
|
1270
|
+
["table", "teams"],
|
|
1271
|
+
["table", "telegram_allowed_users"],
|
|
1272
|
+
["table", "template_instantiations"],
|
|
1273
|
+
["table", "trigger_dispatches"],
|
|
1274
|
+
["table", "web_tokens"],
|
|
1275
|
+
["trigger", "create_team_policy"],
|
|
1276
|
+
["trigger", "prevent_detached_team_policy_delete"],
|
|
1277
|
+
["trigger", "validate_source_binding_insert"],
|
|
1278
|
+
["trigger", "validate_team_agent_state_insert"],
|
|
1279
|
+
["trigger", "validate_team_policy_baseline_update"],
|
|
1280
|
+
["trigger", "validate_team_policy_edge_insert"],
|
|
1281
|
+
["trigger", "validate_team_template_edge_insert"]
|
|
1282
|
+
];
|
|
1283
|
+
function normalizeSql(sql) {
|
|
1284
|
+
return sql.replace(/\s+/g, " ").trim();
|
|
1285
|
+
}
|
|
1286
|
+
function assertCanonicalBackupSchema(db) {
|
|
1287
|
+
const migrations = db.prepare("SELECT version FROM schema_migrations ORDER BY version").all();
|
|
1288
|
+
if (migrations.length !== 1 || migrations[0]?.version !== CANONICAL_MIGRATION) {
|
|
1289
|
+
throw new Error(
|
|
1290
|
+
`schema_migrations must contain exactly ${CANONICAL_MIGRATION}; found ` + (migrations.map((row) => row.version).join(", ") || "none")
|
|
1291
|
+
);
|
|
1292
|
+
}
|
|
1293
|
+
const rows = db.prepare(
|
|
1294
|
+
`SELECT type, name, tbl_name, sql
|
|
1295
|
+
FROM sqlite_schema
|
|
1296
|
+
WHERE sql IS NOT NULL AND name NOT LIKE 'sqlite_%'`
|
|
1297
|
+
).all();
|
|
1298
|
+
const byKey = new Map(rows.map((row) => [`${row.type}\0${row.name}`, row]));
|
|
1299
|
+
const canonicalKeys = new Set(CANONICAL_OBJECTS.map(([type, name]) => `${type}\0${name}`));
|
|
1300
|
+
for (const row of rows) {
|
|
1301
|
+
if (!canonicalKeys.has(`${row.type}\0${row.name}`)) {
|
|
1302
|
+
throw new Error(`unexpected schema ${row.type} is not canonical: ${row.name}`);
|
|
1303
|
+
}
|
|
1304
|
+
}
|
|
1305
|
+
const canonicalRows = [];
|
|
1306
|
+
for (const [type, name] of CANONICAL_OBJECTS) {
|
|
1307
|
+
const row = byKey.get(`${type}\0${name}`);
|
|
1308
|
+
if (!row) throw new Error(`required canonical schema ${type} is missing: ${name}`);
|
|
1309
|
+
canonicalRows.push(row);
|
|
1310
|
+
}
|
|
1311
|
+
const payload = canonicalRows.map((row) => [row.type, row.name, row.tbl_name, normalizeSql(row.sql)].join("\0")).join("\n");
|
|
1312
|
+
const actualHash = createHash("sha256").update(payload).digest("hex");
|
|
1313
|
+
if (actualHash !== CANONICAL_SCHEMA_HASH) {
|
|
1314
|
+
throw new Error(
|
|
1315
|
+
"canonical schema fingerprint does not match this Bazilion release; restore a backup created from the current clean-install schema"
|
|
1316
|
+
);
|
|
1317
|
+
}
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1320
|
+
// src/daemon-liveness.ts
|
|
1321
|
+
import { createHash as createHash2, randomUUID } from "crypto";
|
|
1322
|
+
import {
|
|
1323
|
+
chmodSync,
|
|
1324
|
+
closeSync,
|
|
1325
|
+
fstatSync,
|
|
1326
|
+
fsyncSync,
|
|
1327
|
+
ftruncateSync,
|
|
1328
|
+
futimesSync,
|
|
1329
|
+
lstatSync,
|
|
1330
|
+
mkdirSync,
|
|
1331
|
+
openSync,
|
|
1332
|
+
readFileSync as readFileSync3,
|
|
1333
|
+
realpathSync,
|
|
1334
|
+
renameSync,
|
|
1335
|
+
rmSync,
|
|
1336
|
+
writeSync
|
|
1337
|
+
} from "fs";
|
|
1338
|
+
import { basename as basename2, dirname, join as join2, resolve } from "path";
|
|
1339
|
+
var DAEMON_LIVENESS_FILENAME = "daemon-runtime.json";
|
|
1340
|
+
var STALE_AFTER_MS = 6e4;
|
|
1341
|
+
var PROBE_TIMEOUT_MS = 750;
|
|
1342
|
+
function canonicalHomeIdentity(home) {
|
|
1343
|
+
const absolute = resolve(home);
|
|
1344
|
+
try {
|
|
1345
|
+
return realpathSync.native(absolute);
|
|
1346
|
+
} catch (error) {
|
|
1347
|
+
if (errorCode(error) !== "ENOENT") throw error;
|
|
1348
|
+
const parent = dirname(absolute);
|
|
1349
|
+
if (parent === absolute) throw error;
|
|
1350
|
+
return join2(canonicalHomeIdentity(parent), basename2(absolute));
|
|
1351
|
+
}
|
|
1352
|
+
}
|
|
1353
|
+
function daemonLivenessPath(home) {
|
|
1354
|
+
const canonicalHome = canonicalHomeIdentity(home);
|
|
1355
|
+
const identity = createHash2("sha256").update(canonicalHome).digest("hex").slice(0, 24);
|
|
1356
|
+
return join2(dirname(canonicalHome), `.bazilion-runtime-${identity}.json`);
|
|
1357
|
+
}
|
|
1358
|
+
function errorCode(error) {
|
|
1359
|
+
return error instanceof Error && "code" in error && typeof error.code === "string" ? error.code : void 0;
|
|
1360
|
+
}
|
|
1361
|
+
function parseRecord(raw) {
|
|
1362
|
+
try {
|
|
1363
|
+
const value = JSON.parse(raw);
|
|
1364
|
+
if (value.version !== 1 || typeof value.instanceId !== "string" || !value.instanceId || !Number.isSafeInteger(value.pid) || (value.pid ?? 0) <= 0 || !["daemon", "restore"].includes(value.owner ?? "") || ![
|
|
1365
|
+
"starting",
|
|
1366
|
+
"listening",
|
|
1367
|
+
"restoring",
|
|
1368
|
+
"swapping",
|
|
1369
|
+
"installed",
|
|
1370
|
+
"recovery-required"
|
|
1371
|
+
].includes(value.state ?? "") || !Number.isFinite(value.startedAt)) {
|
|
1372
|
+
return null;
|
|
1373
|
+
}
|
|
1374
|
+
if (value.owner === "daemon" && !["starting", "listening"].includes(value.state ?? "") || value.owner === "restore" && !["restoring", "swapping", "installed", "recovery-required"].includes(value.state ?? "")) {
|
|
1375
|
+
return null;
|
|
1376
|
+
}
|
|
1377
|
+
if (value.owner === "restore" && ["swapping", "recovery-required"].includes(value.state ?? "") && (typeof value.recoveryPath !== "string" || !value.recoveryPath || typeof value.hadPrevious !== "boolean")) {
|
|
1378
|
+
return null;
|
|
1379
|
+
}
|
|
1380
|
+
if (value.owner === "daemon" && value.state === "listening" && (typeof value.host !== "string" || !value.host || !Number.isSafeInteger(value.port) || (value.port ?? 0) < 1 || (value.port ?? 0) > 65535)) {
|
|
1381
|
+
return null;
|
|
1382
|
+
}
|
|
1383
|
+
return value;
|
|
1384
|
+
} catch {
|
|
1385
|
+
return null;
|
|
1386
|
+
}
|
|
1387
|
+
}
|
|
1388
|
+
function readRecord(path) {
|
|
1389
|
+
try {
|
|
1390
|
+
const stat = lstatSync(path);
|
|
1391
|
+
const raw = stat.isFile() ? readFileSync3(path, "utf8") : "";
|
|
1392
|
+
return { raw, stat, record: stat.isFile() ? parseRecord(raw) : null };
|
|
1393
|
+
} catch (error) {
|
|
1394
|
+
if (errorCode(error) === "ENOENT") return null;
|
|
1395
|
+
throw error;
|
|
1396
|
+
}
|
|
1397
|
+
}
|
|
1398
|
+
function pidIsAlive(pid) {
|
|
1399
|
+
try {
|
|
1400
|
+
process.kill(pid, 0);
|
|
1401
|
+
return true;
|
|
1402
|
+
} catch (error) {
|
|
1403
|
+
return errorCode(error) !== "ESRCH";
|
|
1404
|
+
}
|
|
1405
|
+
}
|
|
1406
|
+
function endpointFor(record) {
|
|
1407
|
+
if (record.state !== "listening" || !record.host || !record.port) return null;
|
|
1408
|
+
const normalized = record.host.toLowerCase();
|
|
1409
|
+
if (normalized === "0.0.0.0") return `http://127.0.0.1:${record.port}`;
|
|
1410
|
+
if (normalized === "::" || normalized === "0:0:0:0:0:0:0:0") {
|
|
1411
|
+
return `http://[::1]:${record.port}`;
|
|
1412
|
+
}
|
|
1413
|
+
if (normalized === "localhost" || normalized === "127.0.0.1") {
|
|
1414
|
+
return `http://${normalized}:${record.port}`;
|
|
1415
|
+
}
|
|
1416
|
+
if (normalized === "::1" || normalized === "[::1]") {
|
|
1417
|
+
return `http://[::1]:${record.port}`;
|
|
1418
|
+
}
|
|
1419
|
+
return null;
|
|
1420
|
+
}
|
|
1421
|
+
async function endpointHasOwner(endpoint3, instanceId) {
|
|
1422
|
+
const controller = new AbortController();
|
|
1423
|
+
const timer = setTimeout(() => controller.abort(), PROBE_TIMEOUT_MS);
|
|
1424
|
+
try {
|
|
1425
|
+
const response = await fetch(new URL("/api/health", endpoint3), {
|
|
1426
|
+
signal: controller.signal
|
|
1427
|
+
});
|
|
1428
|
+
return response.headers.get("x-bazilion-daemon-instance") === instanceId;
|
|
1429
|
+
} catch {
|
|
1430
|
+
return false;
|
|
1431
|
+
} finally {
|
|
1432
|
+
clearTimeout(timer);
|
|
1433
|
+
}
|
|
1434
|
+
}
|
|
1435
|
+
function sameFile(left, right) {
|
|
1436
|
+
return left.dev === right.dev && left.ino === right.ino;
|
|
1437
|
+
}
|
|
1438
|
+
function writeRecord(fd, record) {
|
|
1439
|
+
const body = `${JSON.stringify(record, null, 2)}
|
|
1440
|
+
`;
|
|
1441
|
+
ftruncateSync(fd, 0);
|
|
1442
|
+
writeSync(fd, body, 0, "utf8");
|
|
1443
|
+
fsyncSync(fd);
|
|
1444
|
+
}
|
|
1445
|
+
function fsyncParentDirectory(path) {
|
|
1446
|
+
let directoryFd = null;
|
|
1447
|
+
try {
|
|
1448
|
+
directoryFd = openSync(dirname(path), "r");
|
|
1449
|
+
fsyncSync(directoryFd);
|
|
1450
|
+
} catch (error) {
|
|
1451
|
+
if (process.platform !== "win32") throw error;
|
|
1452
|
+
} finally {
|
|
1453
|
+
if (directoryFd !== null) closeSync(directoryFd);
|
|
1454
|
+
}
|
|
1455
|
+
}
|
|
1456
|
+
function reclaimRecord(path, existing) {
|
|
1457
|
+
const identity = createHash2("sha256").update(existing.record?.instanceId ?? existing.raw).update(`\0${existing.stat.dev}\0${existing.stat.ino}`).digest("hex").slice(0, 24);
|
|
1458
|
+
const claim = `${path}.reclaim-${identity}`;
|
|
1459
|
+
try {
|
|
1460
|
+
mkdirSync(claim, { mode: 448 });
|
|
1461
|
+
} catch (error) {
|
|
1462
|
+
if (errorCode(error) !== "EEXIST") throw error;
|
|
1463
|
+
return null;
|
|
1464
|
+
}
|
|
1465
|
+
const current = readRecord(path);
|
|
1466
|
+
if (!current || !sameFile(current.stat, existing.stat) || current.raw !== existing.raw) {
|
|
1467
|
+
return null;
|
|
1468
|
+
}
|
|
1469
|
+
try {
|
|
1470
|
+
renameSync(path, join2(claim, "stale-record.json"));
|
|
1471
|
+
return claim;
|
|
1472
|
+
} catch (error) {
|
|
1473
|
+
if (errorCode(error) === "ENOENT") return null;
|
|
1474
|
+
throw error;
|
|
1475
|
+
}
|
|
1476
|
+
}
|
|
1477
|
+
async function acquireHomeRestoreLock(home) {
|
|
1478
|
+
const targetHome = resolve(home);
|
|
1479
|
+
mkdirSync(dirname(targetHome), { recursive: true });
|
|
1480
|
+
const path = daemonLivenessPath(targetHome);
|
|
1481
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
1482
|
+
const instanceId = randomUUID();
|
|
1483
|
+
let fd = null;
|
|
1484
|
+
let reclaimClaim = null;
|
|
1485
|
+
for (let attempt = 0; attempt < 8 && fd === null; attempt++) {
|
|
1486
|
+
try {
|
|
1487
|
+
fd = openSync(path, "wx", 384);
|
|
1488
|
+
break;
|
|
1489
|
+
} catch (error) {
|
|
1490
|
+
if (errorCode(error) !== "EEXIST") throw error;
|
|
1491
|
+
}
|
|
1492
|
+
const existing = readRecord(path);
|
|
1493
|
+
if (!existing) continue;
|
|
1494
|
+
const endpoint3 = existing.record ? endpointFor(existing.record) : null;
|
|
1495
|
+
if (endpoint3 && existing.record && await endpointHasOwner(endpoint3, existing.record.instanceId)) {
|
|
1496
|
+
throw new Error(
|
|
1497
|
+
`bazilion daemon is running at ${endpoint3} for ${targetHome} \u2014 stop it before restoring`
|
|
1498
|
+
);
|
|
1499
|
+
}
|
|
1500
|
+
if (!existing.record) {
|
|
1501
|
+
throw new Error(
|
|
1502
|
+
`the daemon ownership record for ${targetHome} is invalid; verify no daemon is running before removing it manually`
|
|
1503
|
+
);
|
|
1504
|
+
}
|
|
1505
|
+
const livePid = pidIsAlive(existing.record.pid);
|
|
1506
|
+
if (existing.record.owner === "restore" && ["swapping", "recovery-required"].includes(existing.record.state)) {
|
|
1507
|
+
if (livePid) {
|
|
1508
|
+
throw new Error(
|
|
1509
|
+
`bazilion restore process ${existing.record.pid} is ${existing.record.state} ${targetHome} \u2014 wait for it to finish`
|
|
1510
|
+
);
|
|
1511
|
+
}
|
|
1512
|
+
const phase = existing.record.state.replace("-", " ");
|
|
1513
|
+
throw new Error(
|
|
1514
|
+
`Bazilion restore ${phase} for ${targetHome}; recovery data is at ${existing.record.recoveryPath}. Recover the previous home, then remove ${path} manually.`
|
|
1515
|
+
);
|
|
1516
|
+
}
|
|
1517
|
+
if (livePid) {
|
|
1518
|
+
const ageMs = Math.max(0, Date.now() - existing.stat.mtimeMs);
|
|
1519
|
+
const staleDetail = ageMs > STALE_AFTER_MS ? " (its heartbeat is stale)" : "";
|
|
1520
|
+
const owner = existing.record.owner === "daemon" ? "daemon" : "restore";
|
|
1521
|
+
throw new Error(
|
|
1522
|
+
`bazilion ${owner} process ${existing.record.pid} owns ${targetHome}${staleDetail} \u2014 stop it before restoring`
|
|
1523
|
+
);
|
|
1524
|
+
}
|
|
1525
|
+
const claim = reclaimRecord(path, existing);
|
|
1526
|
+
if (claim) {
|
|
1527
|
+
reclaimClaim = claim;
|
|
1528
|
+
console.warn(`removed stale Bazilion daemon ownership record for ${targetHome}`);
|
|
1529
|
+
}
|
|
1530
|
+
}
|
|
1531
|
+
if (fd === null) {
|
|
1532
|
+
throw new Error(
|
|
1533
|
+
`could not safely acquire exclusive restore ownership for ${targetHome}; try again`
|
|
1534
|
+
);
|
|
1535
|
+
}
|
|
1536
|
+
const ownedStat = fstatSync(fd);
|
|
1537
|
+
const record = {
|
|
1538
|
+
version: 1,
|
|
1539
|
+
instanceId,
|
|
1540
|
+
pid: process.pid,
|
|
1541
|
+
owner: "restore",
|
|
1542
|
+
state: "restoring",
|
|
1543
|
+
startedAt: Date.now()
|
|
1544
|
+
};
|
|
1545
|
+
try {
|
|
1546
|
+
writeRecord(fd, record);
|
|
1547
|
+
fsyncParentDirectory(path);
|
|
1548
|
+
if (reclaimClaim) rmSync(reclaimClaim, { recursive: true, force: true });
|
|
1549
|
+
try {
|
|
1550
|
+
chmodSync(path, 384);
|
|
1551
|
+
} catch {
|
|
1552
|
+
}
|
|
1553
|
+
} catch (error) {
|
|
1554
|
+
closeSync(fd);
|
|
1555
|
+
fd = null;
|
|
1556
|
+
try {
|
|
1557
|
+
if (sameFile(lstatSync(path), ownedStat)) rmSync(path, { force: true });
|
|
1558
|
+
} catch {
|
|
1559
|
+
}
|
|
1560
|
+
throw error;
|
|
1561
|
+
}
|
|
1562
|
+
let released = false;
|
|
1563
|
+
const stillOwnsPath = () => {
|
|
1564
|
+
try {
|
|
1565
|
+
return sameFile(lstatSync(path), ownedStat);
|
|
1566
|
+
} catch {
|
|
1567
|
+
return false;
|
|
1568
|
+
}
|
|
1569
|
+
};
|
|
1570
|
+
const heartbeat = setInterval(() => {
|
|
1571
|
+
if (released) return;
|
|
1572
|
+
if (!stillOwnsPath()) {
|
|
1573
|
+
console.error("restore ownership was lost; exiting to protect the Bazilion database");
|
|
1574
|
+
process.exit(1);
|
|
1575
|
+
}
|
|
1576
|
+
try {
|
|
1577
|
+
const now = /* @__PURE__ */ new Date();
|
|
1578
|
+
futimesSync(fd, now, now);
|
|
1579
|
+
} catch (error) {
|
|
1580
|
+
console.error(
|
|
1581
|
+
"restore ownership heartbeat failed:",
|
|
1582
|
+
error instanceof Error ? error.message : error
|
|
1583
|
+
);
|
|
1584
|
+
process.exit(1);
|
|
1585
|
+
}
|
|
1586
|
+
}, 2e3);
|
|
1587
|
+
heartbeat.unref();
|
|
1588
|
+
return {
|
|
1589
|
+
path,
|
|
1590
|
+
markSwapping(recoveryPath, hadPrevious) {
|
|
1591
|
+
if (released || !stillOwnsPath()) throw new Error("restore ownership was lost");
|
|
1592
|
+
record.state = "swapping";
|
|
1593
|
+
record.recoveryPath = resolve(recoveryPath);
|
|
1594
|
+
record.hadPrevious = hadPrevious;
|
|
1595
|
+
writeRecord(fd, record);
|
|
1596
|
+
fsyncParentDirectory(path);
|
|
1597
|
+
},
|
|
1598
|
+
markInstalled() {
|
|
1599
|
+
if (released || !stillOwnsPath()) throw new Error("restore ownership was lost");
|
|
1600
|
+
record.state = "installed";
|
|
1601
|
+
delete record.recoveryPath;
|
|
1602
|
+
delete record.hadPrevious;
|
|
1603
|
+
writeRecord(fd, record);
|
|
1604
|
+
},
|
|
1605
|
+
markRecoveryRequired(recoveryPath, hadPrevious) {
|
|
1606
|
+
if (released || !stillOwnsPath()) throw new Error("restore ownership was lost");
|
|
1607
|
+
record.state = "recovery-required";
|
|
1608
|
+
record.recoveryPath = resolve(recoveryPath);
|
|
1609
|
+
record.hadPrevious = hadPrevious;
|
|
1610
|
+
writeRecord(fd, record);
|
|
1611
|
+
},
|
|
1612
|
+
release() {
|
|
1613
|
+
if (released) return;
|
|
1614
|
+
released = true;
|
|
1615
|
+
clearInterval(heartbeat);
|
|
1616
|
+
const ownsPath = stillOwnsPath();
|
|
1617
|
+
const retainForRecovery = ["swapping", "recovery-required"].includes(record.state);
|
|
1618
|
+
closeSync(fd);
|
|
1619
|
+
fd = null;
|
|
1620
|
+
if (ownsPath && !retainForRecovery) rmSync(path, { force: true });
|
|
1621
|
+
}
|
|
1622
|
+
};
|
|
1623
|
+
}
|
|
1624
|
+
|
|
1625
|
+
// src/commands/backup.ts
|
|
1074
1626
|
var createCmd = defineCommand4({
|
|
1075
1627
|
meta: { name: "create", description: "Download a tar.gz backup of ~/.bazilion from the server" },
|
|
1076
1628
|
args: {
|
|
@@ -1083,7 +1635,14 @@ var createCmd = defineCommand4({
|
|
|
1083
1635
|
async run({ args }) {
|
|
1084
1636
|
const cfg = loadClientConfig();
|
|
1085
1637
|
const date = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
1086
|
-
const outAbs =
|
|
1638
|
+
const outAbs = resolve2(args.output ?? `bazilion-backup-${date}.tar.gz`);
|
|
1639
|
+
const configuredHome = resolve2(resolveCliPaths().home);
|
|
1640
|
+
const outputFromHome = relative(configuredHome, outAbs);
|
|
1641
|
+
if (!outputFromHome || !outputFromHome.startsWith("..") && !isAbsolute(outputFromHome)) {
|
|
1642
|
+
throw new Error(
|
|
1643
|
+
`backup output must be outside BAZILION_HOME (${configuredHome}) to avoid nesting backups`
|
|
1644
|
+
);
|
|
1645
|
+
}
|
|
1087
1646
|
console.log(`downloading backup \u2192 ${outAbs}`);
|
|
1088
1647
|
const res = await fetch(`${cfg.serverUrl}/api/backup`, {
|
|
1089
1648
|
headers: { authorization: `Bearer ${cfg.token}`, origin: cfg.serverUrl }
|
|
@@ -1097,21 +1656,462 @@ var createCmd = defineCommand4({
|
|
|
1097
1656
|
}
|
|
1098
1657
|
throw new Error(`backup failed: ${err}`);
|
|
1099
1658
|
}
|
|
1100
|
-
|
|
1659
|
+
const downloadDir = mkdtempSync(resolve2(tmpdir(), "bazilion-download-"));
|
|
1660
|
+
const partial = resolve2(downloadDir, "backup.tar.gz");
|
|
1661
|
+
try {
|
|
1662
|
+
await pipeline(
|
|
1663
|
+
Readable.fromWeb(res.body),
|
|
1664
|
+
createWriteStream(partial, { flags: "wx", mode: 384 })
|
|
1665
|
+
);
|
|
1666
|
+
await validateArchive(partial);
|
|
1667
|
+
try {
|
|
1668
|
+
renameSync2(partial, outAbs);
|
|
1669
|
+
} catch (error) {
|
|
1670
|
+
if (!(error instanceof Error) || !("code" in error) || error.code !== "EXDEV") throw error;
|
|
1671
|
+
const installPartial = resolve2(
|
|
1672
|
+
dirname2(outAbs),
|
|
1673
|
+
`.${basename3(outAbs)}.${randomUUID2()}.installing`
|
|
1674
|
+
);
|
|
1675
|
+
try {
|
|
1676
|
+
copyFileSync(partial, installPartial);
|
|
1677
|
+
chmodSync2(installPartial, 384);
|
|
1678
|
+
renameSync2(installPartial, outAbs);
|
|
1679
|
+
} finally {
|
|
1680
|
+
rmSync2(installPartial, { force: true });
|
|
1681
|
+
}
|
|
1682
|
+
}
|
|
1683
|
+
} finally {
|
|
1684
|
+
rmSync2(downloadDir, { recursive: true, force: true });
|
|
1685
|
+
}
|
|
1101
1686
|
console.log("done");
|
|
1102
1687
|
}
|
|
1103
1688
|
});
|
|
1104
|
-
async function probeServerRunning(
|
|
1689
|
+
async function probeServerRunning(serverUrl) {
|
|
1105
1690
|
try {
|
|
1106
1691
|
const ctrl = new AbortController();
|
|
1107
1692
|
const timer = setTimeout(() => ctrl.abort(), 500);
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1693
|
+
try {
|
|
1694
|
+
const res = await fetch(new URL("/api/health", serverUrl), { signal: ctrl.signal });
|
|
1695
|
+
return res.status >= 200 && res.status < 600;
|
|
1696
|
+
} finally {
|
|
1697
|
+
clearTimeout(timer);
|
|
1698
|
+
}
|
|
1111
1699
|
} catch {
|
|
1112
1700
|
return false;
|
|
1113
1701
|
}
|
|
1114
1702
|
}
|
|
1703
|
+
function loopbackServerUrl(raw) {
|
|
1704
|
+
if (!raw) return null;
|
|
1705
|
+
try {
|
|
1706
|
+
const url = new URL(raw);
|
|
1707
|
+
if (!["http:", "https:"].includes(url.protocol)) return null;
|
|
1708
|
+
const hostname = url.hostname.toLowerCase();
|
|
1709
|
+
if (!["127.0.0.1", "localhost", "[::1]", "::1"].includes(hostname)) return null;
|
|
1710
|
+
return url.origin;
|
|
1711
|
+
} catch {
|
|
1712
|
+
return null;
|
|
1713
|
+
}
|
|
1714
|
+
}
|
|
1715
|
+
function configuredLoopbackUrls() {
|
|
1716
|
+
const urls = /* @__PURE__ */ new Set(["http://127.0.0.1:4321"]);
|
|
1717
|
+
const envUrl = loopbackServerUrl(process.env.BAZILION_SERVER);
|
|
1718
|
+
if (envUrl) urls.add(envUrl);
|
|
1719
|
+
try {
|
|
1720
|
+
const clientUrl = loopbackServerUrl(loadClientConfig().serverUrl);
|
|
1721
|
+
if (clientUrl) urls.add(clientUrl);
|
|
1722
|
+
} catch {
|
|
1723
|
+
}
|
|
1724
|
+
return [...urls];
|
|
1725
|
+
}
|
|
1726
|
+
var EXTRACTABLE_ENTRY_TYPES = /* @__PURE__ */ new Set([
|
|
1727
|
+
"File",
|
|
1728
|
+
"OldFile",
|
|
1729
|
+
"Directory",
|
|
1730
|
+
"SymbolicLink"
|
|
1731
|
+
]);
|
|
1732
|
+
function normalizeArchivePath(rawPath) {
|
|
1733
|
+
if (!rawPath || rawPath.includes("\0")) throw new Error("backup contains an invalid empty path");
|
|
1734
|
+
if (rawPath.includes("\\")) {
|
|
1735
|
+
throw new Error(`backup contains a non-portable path: ${JSON.stringify(rawPath)}`);
|
|
1736
|
+
}
|
|
1737
|
+
if (rawPath.startsWith("/") || /^[A-Za-z]:/.test(rawPath)) {
|
|
1738
|
+
throw new Error(`backup contains an absolute path: ${JSON.stringify(rawPath)}`);
|
|
1739
|
+
}
|
|
1740
|
+
const segments = [];
|
|
1741
|
+
for (const segment of rawPath.split("/")) {
|
|
1742
|
+
if (!segment || segment === ".") continue;
|
|
1743
|
+
if (segment === "..") {
|
|
1744
|
+
throw new Error(`backup path escapes the restore directory: ${JSON.stringify(rawPath)}`);
|
|
1745
|
+
}
|
|
1746
|
+
segments.push(segment);
|
|
1747
|
+
}
|
|
1748
|
+
return segments.join("/");
|
|
1749
|
+
}
|
|
1750
|
+
function validateArchiveSymbolicLink(path, rawTarget) {
|
|
1751
|
+
if (!rawTarget || rawTarget.includes("\0")) {
|
|
1752
|
+
throw new Error(`backup contains an invalid symbolic link target: ${path}`);
|
|
1753
|
+
}
|
|
1754
|
+
const segments = path.split("/");
|
|
1755
|
+
const isTeamSlot = segments.length === 2 && segments[0] === "teams" && /^[a-z0-9][a-z0-9-]*$/.test(segments[1] ?? "");
|
|
1756
|
+
const portableTarget = rawTarget.replaceAll("\\", "/");
|
|
1757
|
+
const absoluteOnAnyPlatform = isAbsolute(rawTarget) || posix.isAbsolute(portableTarget) || /^[A-Za-z]:\//.test(portableTarget);
|
|
1758
|
+
if (absoluteOnAnyPlatform) {
|
|
1759
|
+
if (!isTeamSlot) {
|
|
1760
|
+
throw new Error(`backup absolute symbolic links are only allowed for Team slots: ${path}`);
|
|
1761
|
+
}
|
|
1762
|
+
if (!isAbsolute(rawTarget)) {
|
|
1763
|
+
throw new Error(`backup contains an invalid Team link target: ${path}`);
|
|
1764
|
+
}
|
|
1765
|
+
return;
|
|
1766
|
+
}
|
|
1767
|
+
if (isTeamSlot) {
|
|
1768
|
+
throw new Error(`backup contains an invalid Team link target: ${path}`);
|
|
1769
|
+
}
|
|
1770
|
+
if (/^[A-Za-z]:/.test(portableTarget)) {
|
|
1771
|
+
throw new Error(`backup contains a non-portable symbolic link target: ${path}`);
|
|
1772
|
+
}
|
|
1773
|
+
const target = posix.normalize(posix.join(posix.dirname(path), portableTarget));
|
|
1774
|
+
if (target === ".." || target.startsWith("../") || posix.isAbsolute(target)) {
|
|
1775
|
+
throw new Error(`backup symbolic link target escapes the restore directory: ${path}`);
|
|
1776
|
+
}
|
|
1777
|
+
}
|
|
1778
|
+
async function validateArchive(file) {
|
|
1779
|
+
const entries = [];
|
|
1780
|
+
await list2({
|
|
1781
|
+
file,
|
|
1782
|
+
strict: true,
|
|
1783
|
+
onentry(entry2) {
|
|
1784
|
+
if (entry2.meta) return;
|
|
1785
|
+
const path = normalizeArchivePath(entry2.path);
|
|
1786
|
+
if (!path) return;
|
|
1787
|
+
if (!EXTRACTABLE_ENTRY_TYPES.has(entry2.type)) {
|
|
1788
|
+
throw new Error(`backup contains unsupported ${entry2.type} entry: ${path}`);
|
|
1789
|
+
}
|
|
1790
|
+
if (entry2.type === "SymbolicLink") {
|
|
1791
|
+
validateArchiveSymbolicLink(path, entry2.linkpath);
|
|
1792
|
+
}
|
|
1793
|
+
entries.push({ path, type: entry2.type, linkpath: entry2.linkpath });
|
|
1794
|
+
}
|
|
1795
|
+
});
|
|
1796
|
+
const byPath = /* @__PURE__ */ new Map();
|
|
1797
|
+
for (const entry2 of entries) {
|
|
1798
|
+
if (byPath.has(entry2.path)) throw new Error(`backup contains a duplicate path: ${entry2.path}`);
|
|
1799
|
+
byPath.set(entry2.path, entry2);
|
|
1800
|
+
}
|
|
1801
|
+
const symlinks = new Set(
|
|
1802
|
+
entries.filter((entry2) => entry2.type === "SymbolicLink").map((entry2) => entry2.path)
|
|
1803
|
+
);
|
|
1804
|
+
for (const entry2 of entries) {
|
|
1805
|
+
const segments = entry2.path.split("/");
|
|
1806
|
+
for (let i = 1; i < segments.length; i++) {
|
|
1807
|
+
const ancestor = segments.slice(0, i).join("/");
|
|
1808
|
+
if (symlinks.has(ancestor)) {
|
|
1809
|
+
throw new Error(`backup entry traverses symbolic link ${ancestor}: ${entry2.path}`);
|
|
1810
|
+
}
|
|
1811
|
+
const ancestorEntry = byPath.get(ancestor);
|
|
1812
|
+
if (ancestorEntry && ancestorEntry.type !== "Directory") {
|
|
1813
|
+
throw new Error(`backup entry traverses non-directory ${ancestor}: ${entry2.path}`);
|
|
1814
|
+
}
|
|
1815
|
+
}
|
|
1816
|
+
}
|
|
1817
|
+
for (const required of ["bazilion.db", "auth.json"]) {
|
|
1818
|
+
const entry2 = byPath.get(required);
|
|
1819
|
+
if (!entry2 || entry2.type !== "File" && entry2.type !== "OldFile") {
|
|
1820
|
+
throw new Error(`backup is missing required regular file: ${required}`);
|
|
1821
|
+
}
|
|
1822
|
+
}
|
|
1823
|
+
for (const transient of [
|
|
1824
|
+
"bazilion.db-wal",
|
|
1825
|
+
"bazilion.db-shm",
|
|
1826
|
+
"bazilion.db-journal",
|
|
1827
|
+
DAEMON_LIVENESS_FILENAME
|
|
1828
|
+
]) {
|
|
1829
|
+
if (byPath.has(transient)) {
|
|
1830
|
+
throw new Error(`backup contains unsafe transient SQLite file: ${transient}`);
|
|
1831
|
+
}
|
|
1832
|
+
}
|
|
1833
|
+
return entries;
|
|
1834
|
+
}
|
|
1835
|
+
function validateAuthFile(path) {
|
|
1836
|
+
let parsed;
|
|
1837
|
+
try {
|
|
1838
|
+
parsed = JSON.parse(readFileSync4(path, "utf8"));
|
|
1839
|
+
} catch (error) {
|
|
1840
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
1841
|
+
throw new Error(`backup auth.json is invalid: ${detail}`);
|
|
1842
|
+
}
|
|
1843
|
+
if (typeof parsed !== "object" || parsed === null || !("token" in parsed) || typeof parsed.token !== "string" || !parsed.token.trim()) {
|
|
1844
|
+
throw new Error("backup auth.json is invalid: token is missing");
|
|
1845
|
+
}
|
|
1846
|
+
return parsed.token;
|
|
1847
|
+
}
|
|
1848
|
+
function validateDatabase(path, authToken) {
|
|
1849
|
+
let db = null;
|
|
1850
|
+
try {
|
|
1851
|
+
db = new DatabaseSync(path, { readOnly: true });
|
|
1852
|
+
const integrity = db.prepare("PRAGMA integrity_check").all();
|
|
1853
|
+
if (integrity.length !== 1 || integrity[0]?.integrity_check !== "ok") {
|
|
1854
|
+
const detail = integrity.map((row) => row.integrity_check).join("; ") || "no result";
|
|
1855
|
+
throw new Error(`integrity_check failed: ${detail}`);
|
|
1856
|
+
}
|
|
1857
|
+
const foreignKeyErrors = db.prepare("PRAGMA foreign_key_check").all();
|
|
1858
|
+
if (foreignKeyErrors.length > 0) {
|
|
1859
|
+
throw new Error(`foreign_key_check failed with ${foreignKeyErrors.length} violation(s)`);
|
|
1860
|
+
}
|
|
1861
|
+
assertCanonicalBackupSchema(db);
|
|
1862
|
+
const tokenHash = createHash3("sha256").update(authToken).digest("hex");
|
|
1863
|
+
const activeBootstrap = db.prepare(
|
|
1864
|
+
`SELECT 1 FROM web_tokens
|
|
1865
|
+
WHERE token_hash = ? AND label = 'bootstrap' AND revoked_at IS NULL
|
|
1866
|
+
LIMIT 1`
|
|
1867
|
+
).get(tokenHash);
|
|
1868
|
+
if (!activeBootstrap) {
|
|
1869
|
+
throw new Error("auth.json token does not match an active web_tokens row");
|
|
1870
|
+
}
|
|
1871
|
+
} catch (error) {
|
|
1872
|
+
if (error instanceof Error && error.message.startsWith("backup database is invalid:")) {
|
|
1873
|
+
throw error;
|
|
1874
|
+
}
|
|
1875
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
1876
|
+
throw new Error(`backup database is invalid: ${detail}`);
|
|
1877
|
+
} finally {
|
|
1878
|
+
db?.close();
|
|
1879
|
+
}
|
|
1880
|
+
}
|
|
1881
|
+
var SLUG_ID = /^[a-z0-9][a-z0-9-]*$/;
|
|
1882
|
+
var AGENT_ID = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
|
|
1883
|
+
function validateEntityId(id, pattern, entity) {
|
|
1884
|
+
if (typeof id !== "string" || !pattern.test(id)) {
|
|
1885
|
+
throw new Error(`${entity} has an invalid canonical id: ${JSON.stringify(id)}`);
|
|
1886
|
+
}
|
|
1887
|
+
return id;
|
|
1888
|
+
}
|
|
1889
|
+
function containedChildDirectory(root, id, rowLabel) {
|
|
1890
|
+
const child = resolve2(root, id);
|
|
1891
|
+
const fromRoot = relative(root, child);
|
|
1892
|
+
if (!fromRoot || fromRoot.startsWith("..") || isAbsolute(fromRoot)) {
|
|
1893
|
+
throw new Error(`${rowLabel} directory escapes its canonical root: ${JSON.stringify(id)}`);
|
|
1894
|
+
}
|
|
1895
|
+
return child;
|
|
1896
|
+
}
|
|
1897
|
+
function requireExtractedDirectory(path, rowLabel) {
|
|
1898
|
+
try {
|
|
1899
|
+
if (lstatSync2(path).isDirectory()) return;
|
|
1900
|
+
} catch {
|
|
1901
|
+
}
|
|
1902
|
+
throw new Error(`${rowLabel} is missing its canonical directory in the backup`);
|
|
1903
|
+
}
|
|
1904
|
+
function requireExtractedTeamSlot(path, rowLabel) {
|
|
1905
|
+
try {
|
|
1906
|
+
const entry2 = lstatSync2(path);
|
|
1907
|
+
if (entry2.isDirectory() || entry2.isSymbolicLink()) return;
|
|
1908
|
+
} catch {
|
|
1909
|
+
}
|
|
1910
|
+
throw new Error(`${rowLabel} is missing its canonical Team slot in the backup`);
|
|
1911
|
+
}
|
|
1912
|
+
function rebaseRestoredHomeDirectories(database, payload, targetHome) {
|
|
1913
|
+
let db = null;
|
|
1914
|
+
try {
|
|
1915
|
+
db = new DatabaseSync(database);
|
|
1916
|
+
db.exec("PRAGMA foreign_keys = ON");
|
|
1917
|
+
const profiles = db.prepare("SELECT id FROM profiles ORDER BY id").all();
|
|
1918
|
+
const agents = db.prepare("SELECT id FROM agents ORDER BY id").all();
|
|
1919
|
+
const teams = db.prepare("SELECT id FROM teams ORDER BY id").all();
|
|
1920
|
+
const rewrites = [];
|
|
1921
|
+
for (const [table, rows, idPattern] of [
|
|
1922
|
+
["profiles", profiles, SLUG_ID],
|
|
1923
|
+
["agents", agents, AGENT_ID]
|
|
1924
|
+
]) {
|
|
1925
|
+
const collection = table;
|
|
1926
|
+
const stagedRoot = resolve2(payload, collection);
|
|
1927
|
+
const targetRoot = resolve2(targetHome, collection);
|
|
1928
|
+
for (const row of rows) {
|
|
1929
|
+
const id = validateEntityId(row.id, idPattern, `${table} row`);
|
|
1930
|
+
const rowLabel = `${table} row ${JSON.stringify(id)}`;
|
|
1931
|
+
const stagedDir = containedChildDirectory(stagedRoot, id, rowLabel);
|
|
1932
|
+
requireExtractedDirectory(stagedDir, rowLabel);
|
|
1933
|
+
rewrites.push({
|
|
1934
|
+
table,
|
|
1935
|
+
id,
|
|
1936
|
+
dir: containedChildDirectory(targetRoot, id, rowLabel)
|
|
1937
|
+
});
|
|
1938
|
+
}
|
|
1939
|
+
}
|
|
1940
|
+
const stagedTeamsRoot = resolve2(payload, "teams");
|
|
1941
|
+
for (const row of teams) {
|
|
1942
|
+
const id = validateEntityId(row.id, SLUG_ID, "teams row");
|
|
1943
|
+
const rowLabel = `teams row ${JSON.stringify(id)}`;
|
|
1944
|
+
requireExtractedTeamSlot(containedChildDirectory(stagedTeamsRoot, id, rowLabel), rowLabel);
|
|
1945
|
+
}
|
|
1946
|
+
db.exec("PRAGMA journal_mode = DELETE");
|
|
1947
|
+
db.exec("PRAGMA synchronous = FULL");
|
|
1948
|
+
let transactionOpen = false;
|
|
1949
|
+
try {
|
|
1950
|
+
db.exec("BEGIN IMMEDIATE");
|
|
1951
|
+
transactionOpen = true;
|
|
1952
|
+
const updateProfile = db.prepare("UPDATE profiles SET dir = ? WHERE id = ?");
|
|
1953
|
+
const updateAgent = db.prepare("UPDATE agents SET dir = ? WHERE id = ?");
|
|
1954
|
+
for (const rewrite of rewrites) {
|
|
1955
|
+
const result = rewrite.table === "profiles" ? updateProfile.run(rewrite.dir, rewrite.id) : updateAgent.run(rewrite.dir, rewrite.id);
|
|
1956
|
+
if (result.changes !== 1) {
|
|
1957
|
+
throw new Error(`could not rebase ${rewrite.table} row ${JSON.stringify(rewrite.id)}`);
|
|
1958
|
+
}
|
|
1959
|
+
}
|
|
1960
|
+
const readProfile = db.prepare("SELECT dir FROM profiles WHERE id = ?");
|
|
1961
|
+
const readAgent = db.prepare("SELECT dir FROM agents WHERE id = ?");
|
|
1962
|
+
for (const rewrite of rewrites) {
|
|
1963
|
+
const row = rewrite.table === "profiles" ? readProfile.get(rewrite.id) : readAgent.get(rewrite.id);
|
|
1964
|
+
if (row?.dir !== rewrite.dir) {
|
|
1965
|
+
throw new Error(
|
|
1966
|
+
`could not verify rebased ${rewrite.table} row ${JSON.stringify(rewrite.id)}`
|
|
1967
|
+
);
|
|
1968
|
+
}
|
|
1969
|
+
}
|
|
1970
|
+
db.exec("COMMIT");
|
|
1971
|
+
transactionOpen = false;
|
|
1972
|
+
} catch (error) {
|
|
1973
|
+
if (transactionOpen) db.exec("ROLLBACK");
|
|
1974
|
+
throw error;
|
|
1975
|
+
}
|
|
1976
|
+
} catch (error) {
|
|
1977
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
1978
|
+
throw new Error(`backup database is invalid: ${detail}`);
|
|
1979
|
+
} finally {
|
|
1980
|
+
db?.close();
|
|
1981
|
+
}
|
|
1982
|
+
for (const suffix of ["-wal", "-shm", "-journal"]) {
|
|
1983
|
+
if (existsSync2(`${database}${suffix}`)) {
|
|
1984
|
+
throw new Error(`backup database is invalid: path rebasing left transient ${suffix} state`);
|
|
1985
|
+
}
|
|
1986
|
+
}
|
|
1987
|
+
}
|
|
1988
|
+
function removeRebuildableQmdIndexes(payload) {
|
|
1989
|
+
const teamsDir = resolve2(payload, "teams");
|
|
1990
|
+
if (!existsSync2(teamsDir) || !lstatSync2(teamsDir).isDirectory()) return;
|
|
1991
|
+
for (const team of readdirSync(teamsDir, { withFileTypes: true })) {
|
|
1992
|
+
if (!team.isDirectory() || team.isSymbolicLink()) continue;
|
|
1993
|
+
const memoryDir = resolve2(teamsDir, team.name, "memory");
|
|
1994
|
+
if (!existsSync2(memoryDir) || !lstatSync2(memoryDir).isDirectory()) continue;
|
|
1995
|
+
for (const entry2 of readdirSync(memoryDir, { withFileTypes: true })) {
|
|
1996
|
+
if (entry2.isFile() && entry2.name.startsWith(".qmd-index.sqlite")) {
|
|
1997
|
+
rmSync2(resolve2(memoryDir, entry2.name), { force: true });
|
|
1998
|
+
}
|
|
1999
|
+
}
|
|
2000
|
+
}
|
|
2001
|
+
}
|
|
2002
|
+
function installValidatedPayload(payload, targetHome, staging, lifecycle, operations = { rename: renameSync2 }) {
|
|
2003
|
+
const previous = resolve2(staging, "previous-home");
|
|
2004
|
+
const hadPrevious = existsSync2(targetHome);
|
|
2005
|
+
const targetParent = dirname2(targetHome);
|
|
2006
|
+
const syncDirectory = operations.fsyncDirectory ?? fsyncDirectory;
|
|
2007
|
+
const recoveryError = (message, recoveryPath, cause) => {
|
|
2008
|
+
let markerError;
|
|
2009
|
+
try {
|
|
2010
|
+
lifecycle?.markRecoveryRequired(recoveryPath, hadPrevious);
|
|
2011
|
+
} catch (error) {
|
|
2012
|
+
markerError = error;
|
|
2013
|
+
}
|
|
2014
|
+
const causeDetail = cause instanceof Error ? cause.message : String(cause);
|
|
2015
|
+
const markerDetail = markerError ? `; recovery marker update failed: ${markerError instanceof Error ? markerError.message : markerError}` : "";
|
|
2016
|
+
return new RestoreRecoveryRequiredError(
|
|
2017
|
+
`${message} (${causeDetail}); recovery data is at ${recoveryPath}${markerDetail}`,
|
|
2018
|
+
recoveryPath
|
|
2019
|
+
);
|
|
2020
|
+
};
|
|
2021
|
+
lifecycle?.markSwapping(previous, hadPrevious);
|
|
2022
|
+
if (hadPrevious) {
|
|
2023
|
+
try {
|
|
2024
|
+
operations.rename(targetHome, previous);
|
|
2025
|
+
} catch (moveError) {
|
|
2026
|
+
lifecycle?.markInstalled();
|
|
2027
|
+
throw moveError;
|
|
2028
|
+
}
|
|
2029
|
+
try {
|
|
2030
|
+
syncDirectory(targetParent);
|
|
2031
|
+
operations.afterPreviousMoved?.();
|
|
2032
|
+
} catch (postMoveError) {
|
|
2033
|
+
throw recoveryError(
|
|
2034
|
+
"the previous home moved but the swap could not continue safely",
|
|
2035
|
+
previous,
|
|
2036
|
+
postMoveError
|
|
2037
|
+
);
|
|
2038
|
+
}
|
|
2039
|
+
}
|
|
2040
|
+
try {
|
|
2041
|
+
operations.rename(payload, targetHome);
|
|
2042
|
+
} catch (installError) {
|
|
2043
|
+
if (hadPrevious) {
|
|
2044
|
+
try {
|
|
2045
|
+
operations.rename(previous, targetHome);
|
|
2046
|
+
} catch (rollbackError) {
|
|
2047
|
+
const installDetail = installError instanceof Error ? installError.message : String(installError);
|
|
2048
|
+
throw recoveryError(
|
|
2049
|
+
`restore install failed (${installDetail}) and rollback failed`,
|
|
2050
|
+
previous,
|
|
2051
|
+
rollbackError
|
|
2052
|
+
);
|
|
2053
|
+
}
|
|
2054
|
+
try {
|
|
2055
|
+
syncDirectory(targetParent);
|
|
2056
|
+
} catch (rollbackSyncError) {
|
|
2057
|
+
throw recoveryError(
|
|
2058
|
+
"the previous home was restored but its directory entry could not be made durable",
|
|
2059
|
+
targetHome,
|
|
2060
|
+
rollbackSyncError
|
|
2061
|
+
);
|
|
2062
|
+
}
|
|
2063
|
+
try {
|
|
2064
|
+
lifecycle?.markInstalled();
|
|
2065
|
+
} catch (phaseError) {
|
|
2066
|
+
throw recoveryError(
|
|
2067
|
+
"the previous home was restored but could not be marked safe",
|
|
2068
|
+
targetHome,
|
|
2069
|
+
phaseError
|
|
2070
|
+
);
|
|
2071
|
+
}
|
|
2072
|
+
} else {
|
|
2073
|
+
lifecycle?.markInstalled();
|
|
2074
|
+
}
|
|
2075
|
+
throw installError;
|
|
2076
|
+
}
|
|
2077
|
+
try {
|
|
2078
|
+
syncDirectory(targetParent);
|
|
2079
|
+
} catch (installSyncError) {
|
|
2080
|
+
throw recoveryError(
|
|
2081
|
+
"the restored home was installed but its directory entry could not be made durable",
|
|
2082
|
+
hadPrevious ? previous : targetHome,
|
|
2083
|
+
installSyncError
|
|
2084
|
+
);
|
|
2085
|
+
}
|
|
2086
|
+
try {
|
|
2087
|
+
lifecycle?.markInstalled();
|
|
2088
|
+
} catch (phaseError) {
|
|
2089
|
+
throw recoveryError(
|
|
2090
|
+
"the restored home was installed but could not be marked safe",
|
|
2091
|
+
hadPrevious ? previous : targetHome,
|
|
2092
|
+
phaseError
|
|
2093
|
+
);
|
|
2094
|
+
}
|
|
2095
|
+
}
|
|
2096
|
+
var RestoreRecoveryRequiredError = class extends Error {
|
|
2097
|
+
recoveryPath;
|
|
2098
|
+
constructor(message, recoveryPath) {
|
|
2099
|
+
super(message);
|
|
2100
|
+
this.name = "RestoreRecoveryRequiredError";
|
|
2101
|
+
this.recoveryPath = recoveryPath;
|
|
2102
|
+
}
|
|
2103
|
+
};
|
|
2104
|
+
function fsyncDirectory(path) {
|
|
2105
|
+
let fd = null;
|
|
2106
|
+
try {
|
|
2107
|
+
fd = openSync2(path, "r");
|
|
2108
|
+
fsyncSync2(fd);
|
|
2109
|
+
} catch (error) {
|
|
2110
|
+
if (process.platform !== "win32") throw error;
|
|
2111
|
+
} finally {
|
|
2112
|
+
if (fd !== null) closeSync2(fd);
|
|
2113
|
+
}
|
|
2114
|
+
}
|
|
1115
2115
|
var restoreCmd = defineCommand4({
|
|
1116
2116
|
meta: {
|
|
1117
2117
|
name: "restore",
|
|
@@ -1130,37 +2130,91 @@ var restoreCmd = defineCommand4({
|
|
|
1130
2130
|
}
|
|
1131
2131
|
},
|
|
1132
2132
|
async run({ args }) {
|
|
1133
|
-
const file =
|
|
2133
|
+
const file = resolve2(args.file);
|
|
1134
2134
|
if (!existsSync2(file)) throw new Error(`backup file not found: ${file}`);
|
|
1135
2135
|
const paths = resolveCliPaths(args.home);
|
|
1136
|
-
const targetHome = paths.home;
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
throw new Error(
|
|
1140
|
-
"bazilion server appears to be running on :4321 \u2014 stop it first (DB corruption risk), or pass --force to override."
|
|
1141
|
-
);
|
|
2136
|
+
const targetHome = resolve2(paths.home);
|
|
2137
|
+
if (targetHome === parse(targetHome).root) {
|
|
2138
|
+
throw new Error(`refusing to restore over filesystem root: ${targetHome}`);
|
|
1142
2139
|
}
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
2140
|
+
const targetParent = dirname2(targetHome);
|
|
2141
|
+
mkdirSync2(targetParent, { recursive: true });
|
|
2142
|
+
const restoreLock = await acquireHomeRestoreLock(targetHome);
|
|
2143
|
+
try {
|
|
2144
|
+
const targetsConfiguredHome = targetHome === resolve2(resolveCliPaths().home);
|
|
2145
|
+
if (targetsConfiguredHome) {
|
|
2146
|
+
for (const serverUrl of configuredLoopbackUrls()) {
|
|
2147
|
+
if (await probeServerRunning(serverUrl)) {
|
|
2148
|
+
throw new Error(
|
|
2149
|
+
`bazilion server appears to be running at ${serverUrl} \u2014 stop it before restoring.`
|
|
2150
|
+
);
|
|
2151
|
+
}
|
|
2152
|
+
}
|
|
2153
|
+
}
|
|
2154
|
+
if (existsSync2(targetHome)) {
|
|
2155
|
+
if (!lstatSync2(targetHome).isDirectory()) {
|
|
2156
|
+
throw new Error(`${targetHome} exists and is not a directory`);
|
|
2157
|
+
}
|
|
2158
|
+
const entries = readdirSync(targetHome);
|
|
2159
|
+
if (entries.length > 0 && !args.force) {
|
|
1147
2160
|
throw new Error(
|
|
1148
2161
|
`${targetHome} is not empty. Pass --force to overwrite (destroys existing data).`
|
|
1149
2162
|
);
|
|
1150
2163
|
}
|
|
1151
|
-
rmSync(targetHome, { recursive: true, force: true });
|
|
1152
2164
|
}
|
|
2165
|
+
const staging = mkdtempSync(resolve2(targetParent, ".bazilion-restore-"));
|
|
2166
|
+
const stagedArchive = resolve2(staging, "backup.tar.gz");
|
|
2167
|
+
const payload = resolve2(staging, "payload");
|
|
2168
|
+
let preserveStaging = false;
|
|
2169
|
+
try {
|
|
2170
|
+
copyFileSync(file, stagedArchive);
|
|
2171
|
+
await validateArchive(stagedArchive);
|
|
2172
|
+
mkdirSync2(payload);
|
|
2173
|
+
await extract({
|
|
2174
|
+
file: stagedArchive,
|
|
2175
|
+
cwd: payload,
|
|
2176
|
+
strict: true,
|
|
2177
|
+
preserveOwner: false,
|
|
2178
|
+
// The private copy has already passed complete entry-path and link
|
|
2179
|
+
// validation above. Preserve paths here so an intentionally absolute
|
|
2180
|
+
// external linked-Team target is not stripped into a different,
|
|
2181
|
+
// payload-relative path by npm-tar.
|
|
2182
|
+
preservePaths: true
|
|
2183
|
+
});
|
|
2184
|
+
const database = resolve2(payload, "bazilion.db");
|
|
2185
|
+
const authFile = resolve2(payload, "auth.json");
|
|
2186
|
+
if (!lstatSync2(database).isFile() || !lstatSync2(authFile).isFile()) {
|
|
2187
|
+
throw new Error("backup required files did not extract as regular files");
|
|
2188
|
+
}
|
|
2189
|
+
const authToken = validateAuthFile(authFile);
|
|
2190
|
+
validateDatabase(database, authToken);
|
|
2191
|
+
rebaseRestoredHomeDirectories(database, payload, targetHome);
|
|
2192
|
+
validateDatabase(database, authToken);
|
|
2193
|
+
removeRebuildableQmdIndexes(payload);
|
|
2194
|
+
console.log(`installing validated backup ${file} \u2192 ${targetHome}`);
|
|
2195
|
+
try {
|
|
2196
|
+
installValidatedPayload(payload, targetHome, staging, restoreLock);
|
|
2197
|
+
} catch (error) {
|
|
2198
|
+
if (error instanceof RestoreRecoveryRequiredError) {
|
|
2199
|
+
preserveStaging = true;
|
|
2200
|
+
}
|
|
2201
|
+
throw error;
|
|
2202
|
+
}
|
|
2203
|
+
} finally {
|
|
2204
|
+
if (!preserveStaging) {
|
|
2205
|
+
try {
|
|
2206
|
+
rmSync2(staging, { recursive: true, force: true });
|
|
2207
|
+
} catch (error) {
|
|
2208
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
2209
|
+
console.warn(
|
|
2210
|
+
`warning: could not remove restore staging directory ${staging}: ${detail}`
|
|
2211
|
+
);
|
|
2212
|
+
}
|
|
2213
|
+
}
|
|
2214
|
+
}
|
|
2215
|
+
} finally {
|
|
2216
|
+
restoreLock.release();
|
|
1153
2217
|
}
|
|
1154
|
-
mkdirSync(targetHome, { recursive: true });
|
|
1155
|
-
console.log(`extracting ${file} \u2192 ${targetHome}`);
|
|
1156
|
-
await new Promise((res, rej) => {
|
|
1157
|
-
const proc = spawn("tar", ["-xzf", file, "-C", targetHome], { stdio: "inherit" });
|
|
1158
|
-
proc.on("error", rej);
|
|
1159
|
-
proc.on("exit", (code) => {
|
|
1160
|
-
if (code === 0) res();
|
|
1161
|
-
else rej(new Error(`tar exited with code ${code}`));
|
|
1162
|
-
});
|
|
1163
|
-
});
|
|
1164
2218
|
console.log(`restored to ${targetHome}`);
|
|
1165
2219
|
console.log("start the server with: bazilion serve (migrations apply automatically)");
|
|
1166
2220
|
}
|
|
@@ -1389,19 +2443,19 @@ var configCommand = defineCommand6({
|
|
|
1389
2443
|
});
|
|
1390
2444
|
|
|
1391
2445
|
// src/commands/dashboard.ts
|
|
1392
|
-
import { spawn
|
|
2446
|
+
import { spawn } from "child_process";
|
|
1393
2447
|
import { existsSync as existsSync3 } from "fs";
|
|
1394
2448
|
import { connect } from "net";
|
|
1395
|
-
import { join as
|
|
2449
|
+
import { join as join3 } from "path";
|
|
1396
2450
|
import { defineCommand as defineCommand7 } from "citty";
|
|
1397
|
-
var bundledDaemonEntry =
|
|
1398
|
-
var sourceDaemonEntry =
|
|
2451
|
+
var bundledDaemonEntry = join3(import.meta.dirname, "daemon.js");
|
|
2452
|
+
var sourceDaemonEntry = join3(import.meta.dirname, "..", "..", "..", "daemon", "src", "index.ts");
|
|
1399
2453
|
var daemonEntry = existsSync3(bundledDaemonEntry) ? bundledDaemonEntry : sourceDaemonEntry;
|
|
1400
|
-
var bundledWebServerEntry =
|
|
1401
|
-
var sourceWebServerEntry =
|
|
2454
|
+
var bundledWebServerEntry = join3(import.meta.dirname, "web-server.js");
|
|
2455
|
+
var sourceWebServerEntry = join3(import.meta.dirname, "..", "web-server.ts");
|
|
1402
2456
|
var webServerEntry = existsSync3(bundledWebServerEntry) ? bundledWebServerEntry : sourceWebServerEntry;
|
|
1403
|
-
var bundledWebDist =
|
|
1404
|
-
var sourceWebDist =
|
|
2457
|
+
var bundledWebDist = join3(import.meta.dirname, "web");
|
|
2458
|
+
var sourceWebDist = join3(import.meta.dirname, "..", "..", "..", "web", "dist");
|
|
1405
2459
|
var webDist = existsSync3(bundledWebDist) ? bundledWebDist : sourceWebDist;
|
|
1406
2460
|
function nodeArgs(entry2) {
|
|
1407
2461
|
return entry2.endsWith(".ts") ? ["--import", "tsx/esm", entry2] : [entry2];
|
|
@@ -1420,12 +2474,12 @@ function probeHost(host) {
|
|
|
1420
2474
|
return host;
|
|
1421
2475
|
}
|
|
1422
2476
|
function isTcpPortOpen(host, port) {
|
|
1423
|
-
return new Promise((
|
|
2477
|
+
return new Promise((resolve4) => {
|
|
1424
2478
|
const socket = connect({ host: probeHost(host), port: Number(port) });
|
|
1425
2479
|
const done = (open) => {
|
|
1426
2480
|
socket.removeAllListeners();
|
|
1427
2481
|
socket.destroy();
|
|
1428
|
-
|
|
2482
|
+
resolve4(open);
|
|
1429
2483
|
};
|
|
1430
2484
|
socket.setTimeout(600);
|
|
1431
2485
|
socket.once("connect", () => done(true));
|
|
@@ -1475,14 +2529,14 @@ function openBrowser(url) {
|
|
|
1475
2529
|
const platform = process.platform;
|
|
1476
2530
|
const cmd = platform === "darwin" ? "open" : platform === "win32" ? "cmd" : process.env.BROWSER || "xdg-open";
|
|
1477
2531
|
const args = platform === "win32" ? ["/c", "start", "", url] : [url];
|
|
1478
|
-
const proc =
|
|
2532
|
+
const proc = spawn(cmd, args, { detached: true, stdio: "ignore" });
|
|
1479
2533
|
proc.unref();
|
|
1480
2534
|
}
|
|
1481
2535
|
function spawnProcess(label, entry2, env) {
|
|
1482
2536
|
if (!existsSync3(entry2)) {
|
|
1483
2537
|
throw new Error(`${label} entry not found at ${entry2}`);
|
|
1484
2538
|
}
|
|
1485
|
-
return
|
|
2539
|
+
return spawn(process.execPath, nodeArgs(entry2), {
|
|
1486
2540
|
env,
|
|
1487
2541
|
stdio: "inherit"
|
|
1488
2542
|
});
|
|
@@ -1564,7 +2618,7 @@ var dashboardCommand = defineCommand7({
|
|
|
1564
2618
|
`web UI port ${webPort} is already in use. Re-run with --port <free-port>.`
|
|
1565
2619
|
);
|
|
1566
2620
|
}
|
|
1567
|
-
if (!existsSync3(
|
|
2621
|
+
if (!existsSync3(join3(webDist, "server", "server.js"))) {
|
|
1568
2622
|
throw new Error(
|
|
1569
2623
|
`bazilion web UI bundle not found at ${webDist}. Run "pnpm --filter @bazilion/web build" from a source checkout, or reinstall the published package.`
|
|
1570
2624
|
);
|
|
@@ -1591,13 +2645,13 @@ var dashboardCommand = defineCommand7({
|
|
|
1591
2645
|
if (children.length === 0) return;
|
|
1592
2646
|
process.on("SIGINT", terminateChildren);
|
|
1593
2647
|
process.on("SIGTERM", terminateChildren);
|
|
1594
|
-
await new Promise((
|
|
2648
|
+
await new Promise((resolve4) => {
|
|
1595
2649
|
let remaining = children.length;
|
|
1596
2650
|
for (const child of children) {
|
|
1597
2651
|
child.on("close", (code) => {
|
|
1598
2652
|
remaining--;
|
|
1599
2653
|
if (!signalled && code && code !== 0) process.exitCode = code;
|
|
1600
|
-
if (remaining === 0)
|
|
2654
|
+
if (remaining === 0) resolve4();
|
|
1601
2655
|
});
|
|
1602
2656
|
}
|
|
1603
2657
|
});
|
|
@@ -1688,6 +2742,36 @@ var doctorCommand = defineCommand8({
|
|
|
1688
2742
|
` ${r.scheduler.enabled ? "\u2713" : "-"} scheduler${r.scheduler.enabled ? ` (tick ${r.scheduler.tickMs}ms)` : " (BAZILION_SCHEDULER=off)"}`
|
|
1689
2743
|
);
|
|
1690
2744
|
console.log();
|
|
2745
|
+
console.log("agent shell security");
|
|
2746
|
+
if (!r.shellSecurity.ok) {
|
|
2747
|
+
check("valid shell-security configuration", false, r.shellSecurity.error);
|
|
2748
|
+
} else {
|
|
2749
|
+
if (r.shellSecurity.approvalMode === "dangerous") {
|
|
2750
|
+
console.log(" - dangerous-command approval enabled");
|
|
2751
|
+
console.log(
|
|
2752
|
+
" web and TTY CLI turns can allow once; background and non-TTY turns auto-deny"
|
|
2753
|
+
);
|
|
2754
|
+
} else {
|
|
2755
|
+
console.log(" - dangerous-command approval off");
|
|
2756
|
+
console.log(" set BAZILION_BASH_APPROVAL=dangerous to opt in");
|
|
2757
|
+
}
|
|
2758
|
+
if (r.shellSecurity.sandboxMode === "docker") {
|
|
2759
|
+
console.log(` - Docker sandbox configured (${r.shellSecurity.sandboxImage})`);
|
|
2760
|
+
console.log(
|
|
2761
|
+
" workspace-only writable mount \xB7 bounded inputs/skills/memory read-only \xB7 network disabled"
|
|
2762
|
+
);
|
|
2763
|
+
console.log(" host coding tools hidden");
|
|
2764
|
+
console.log(
|
|
2765
|
+
" policy syntax is valid; Docker and image availability are checked on execution"
|
|
2766
|
+
);
|
|
2767
|
+
console.log(" commands fail closed if either is unavailable");
|
|
2768
|
+
} else {
|
|
2769
|
+
console.log(" - sandbox off (agent shell and coding tools run on the host)");
|
|
2770
|
+
console.log(" approval is a host-execution tripwire, not a filesystem sandbox");
|
|
2771
|
+
console.log(" set BAZILION_BASH_SANDBOX=docker to opt in");
|
|
2772
|
+
}
|
|
2773
|
+
}
|
|
2774
|
+
console.log();
|
|
1691
2775
|
console.log("operational counts");
|
|
1692
2776
|
console.log(` ${r.triggers.active} active trigger(s), ${r.triggers.disabled} disabled`);
|
|
1693
2777
|
console.log(` ${r.tokens.active} active web token(s)`);
|
|
@@ -1795,7 +2879,7 @@ var inboxCommand = defineCommand9({
|
|
|
1795
2879
|
});
|
|
1796
2880
|
|
|
1797
2881
|
// src/commands/login.ts
|
|
1798
|
-
import { existsSync as existsSync4, readFileSync as
|
|
2882
|
+
import { existsSync as existsSync4, readFileSync as readFileSync5, writeFileSync as writeFileSync2 } from "fs";
|
|
1799
2883
|
import { defineCommand as defineCommand10 } from "citty";
|
|
1800
2884
|
function normalizeServer(raw) {
|
|
1801
2885
|
let url;
|
|
@@ -1834,7 +2918,7 @@ var loginCommand = defineCommand10({
|
|
|
1834
2918
|
`${paths.authFile} not found. Start the daemon with "bazilion serve" first \u2014 login stores the remote there.`
|
|
1835
2919
|
);
|
|
1836
2920
|
}
|
|
1837
|
-
const auth = JSON.parse(
|
|
2921
|
+
const auth = JSON.parse(readFileSync5(paths.authFile, "utf8"));
|
|
1838
2922
|
if (args.clear) {
|
|
1839
2923
|
if (!auth.remote) {
|
|
1840
2924
|
console.log("no remote stored \u2014 nothing to clear");
|
|
@@ -2082,11 +3166,11 @@ var memoryCommand = defineCommand12({
|
|
|
2082
3166
|
});
|
|
2083
3167
|
|
|
2084
3168
|
// src/commands/profile.ts
|
|
2085
|
-
import { spawn as
|
|
3169
|
+
import { spawn as spawn2 } from "child_process";
|
|
2086
3170
|
import { randomBytes } from "crypto";
|
|
2087
|
-
import { readFileSync as
|
|
2088
|
-
import { tmpdir } from "os";
|
|
2089
|
-
import { join as
|
|
3171
|
+
import { readFileSync as readFileSync6, rmSync as rmSync3, writeFileSync as writeFileSync3 } from "fs";
|
|
3172
|
+
import { tmpdir as tmpdir2 } from "os";
|
|
3173
|
+
import { join as join4 } from "path";
|
|
2090
3174
|
|
|
2091
3175
|
// ../../packages/api-types/src/index.ts
|
|
2092
3176
|
var PROFILE_FILES = [
|
|
@@ -2155,7 +3239,7 @@ var createCmd2 = defineCommand13({
|
|
|
2155
3239
|
},
|
|
2156
3240
|
async run({ args }) {
|
|
2157
3241
|
const skillsMode = parseSkillsMode(args["skills-mode"]);
|
|
2158
|
-
const readTemplate = (path) => path ?
|
|
3242
|
+
const readTemplate = (path) => path ? readFileSync6(path, "utf8") : void 0;
|
|
2159
3243
|
const body = {
|
|
2160
3244
|
id: args.id,
|
|
2161
3245
|
name: args.name,
|
|
@@ -2177,12 +3261,12 @@ var listCmd6 = defineCommand13({
|
|
|
2177
3261
|
meta: { name: "list", description: "List profiles" },
|
|
2178
3262
|
async run() {
|
|
2179
3263
|
const client = createClient2();
|
|
2180
|
-
const
|
|
2181
|
-
if (
|
|
3264
|
+
const list4 = await client.get("/api/profiles");
|
|
3265
|
+
if (list4.length === 0) {
|
|
2182
3266
|
console.log("(no profiles)");
|
|
2183
3267
|
return;
|
|
2184
3268
|
}
|
|
2185
|
-
const rows =
|
|
3269
|
+
const rows = list4.map((p) => [p.id, p.defaultModel, p.name]);
|
|
2186
3270
|
for (const line of columnize(rows)) console.log(line);
|
|
2187
3271
|
}
|
|
2188
3272
|
});
|
|
@@ -2230,17 +3314,17 @@ var editCmd2 = defineCommand13({
|
|
|
2230
3314
|
const { content } = await client.get(
|
|
2231
3315
|
`/api/profiles/${args.id}/files/${file}`
|
|
2232
3316
|
);
|
|
2233
|
-
const tmpPath =
|
|
3317
|
+
const tmpPath = join4(tmpdir2(), `bazilion-${args.id}-${file}-${randomBytes(4).toString("hex")}`);
|
|
2234
3318
|
writeFileSync3(tmpPath, content);
|
|
2235
3319
|
try {
|
|
2236
3320
|
const editor = process.env.EDITOR ?? process.env.VISUAL ?? "vi";
|
|
2237
|
-
const code = await new Promise((
|
|
2238
|
-
const child =
|
|
3321
|
+
const code = await new Promise((resolve4, reject) => {
|
|
3322
|
+
const child = spawn2(editor, [tmpPath], { stdio: "inherit" });
|
|
2239
3323
|
child.on("error", reject);
|
|
2240
|
-
child.on("close", (c) =>
|
|
3324
|
+
child.on("close", (c) => resolve4(c ?? 0));
|
|
2241
3325
|
});
|
|
2242
3326
|
if (code !== 0) throw new Error(`editor exited with code ${code}`);
|
|
2243
|
-
const updated =
|
|
3327
|
+
const updated = readFileSync6(tmpPath, "utf8");
|
|
2244
3328
|
if (updated === content) {
|
|
2245
3329
|
console.log("(no changes)");
|
|
2246
3330
|
return;
|
|
@@ -2250,7 +3334,7 @@ var editCmd2 = defineCommand13({
|
|
|
2250
3334
|
console.log(`saved ${file}`);
|
|
2251
3335
|
} finally {
|
|
2252
3336
|
try {
|
|
2253
|
-
|
|
3337
|
+
rmSync3(tmpPath, { force: true });
|
|
2254
3338
|
} catch {
|
|
2255
3339
|
}
|
|
2256
3340
|
}
|
|
@@ -2495,12 +3579,12 @@ var sendCommand = defineCommand15({
|
|
|
2495
3579
|
});
|
|
2496
3580
|
|
|
2497
3581
|
// src/commands/serve.ts
|
|
2498
|
-
import { spawn as
|
|
3582
|
+
import { spawn as spawn3 } from "child_process";
|
|
2499
3583
|
import { existsSync as existsSync5 } from "fs";
|
|
2500
|
-
import { join as
|
|
3584
|
+
import { join as join5 } from "path";
|
|
2501
3585
|
import { defineCommand as defineCommand16 } from "citty";
|
|
2502
|
-
var bundledDaemonEntry2 =
|
|
2503
|
-
var sourceDaemonEntry2 =
|
|
3586
|
+
var bundledDaemonEntry2 = join5(import.meta.dirname, "daemon.js");
|
|
3587
|
+
var sourceDaemonEntry2 = join5(import.meta.dirname, "..", "..", "..", "daemon", "src", "index.ts");
|
|
2504
3588
|
var daemonEntry2 = existsSync5(bundledDaemonEntry2) ? bundledDaemonEntry2 : sourceDaemonEntry2;
|
|
2505
3589
|
var serveCommand = defineCommand16({
|
|
2506
3590
|
meta: {
|
|
@@ -2535,7 +3619,7 @@ var serveCommand = defineCommand16({
|
|
|
2535
3619
|
console.log(`starting bazilion daemon at http://${host}:${port}`);
|
|
2536
3620
|
console.log("(web UI runs separately: cd apps/web && pnpm dev)");
|
|
2537
3621
|
const nodeArgs2 = daemonEntry2.endsWith(".ts") ? ["--import", "tsx/esm", daemonEntry2] : [daemonEntry2];
|
|
2538
|
-
const proc =
|
|
3622
|
+
const proc = spawn3("node", nodeArgs2, {
|
|
2539
3623
|
env,
|
|
2540
3624
|
stdio: "inherit"
|
|
2541
3625
|
});
|
|
@@ -2551,17 +3635,17 @@ var serveCommand = defineCommand16({
|
|
|
2551
3635
|
signalled = true;
|
|
2552
3636
|
});
|
|
2553
3637
|
process.on("SIGTERM", onSigterm);
|
|
2554
|
-
await new Promise((
|
|
3638
|
+
await new Promise((resolve4, reject) => {
|
|
2555
3639
|
proc.on("error", (err) => reject(err));
|
|
2556
3640
|
proc.on("close", (code) => {
|
|
2557
3641
|
if (signalled) {
|
|
2558
3642
|
process.exitCode = 0;
|
|
2559
|
-
|
|
3643
|
+
resolve4();
|
|
2560
3644
|
} else if (code === 0 || code === null) {
|
|
2561
|
-
|
|
3645
|
+
resolve4();
|
|
2562
3646
|
} else {
|
|
2563
3647
|
process.exitCode = code;
|
|
2564
|
-
|
|
3648
|
+
resolve4();
|
|
2565
3649
|
}
|
|
2566
3650
|
});
|
|
2567
3651
|
});
|
|
@@ -2569,8 +3653,8 @@ var serveCommand = defineCommand16({
|
|
|
2569
3653
|
});
|
|
2570
3654
|
|
|
2571
3655
|
// src/commands/skill.ts
|
|
2572
|
-
import { existsSync as existsSync6, readFileSync as
|
|
2573
|
-
import { basename as
|
|
3656
|
+
import { existsSync as existsSync6, readFileSync as readFileSync7, statSync } from "fs";
|
|
3657
|
+
import { basename as basename4, resolve as resolve3 } from "path";
|
|
2574
3658
|
import { defineCommand as defineCommand17 } from "citty";
|
|
2575
3659
|
function printFindings(findings, prefix = " ") {
|
|
2576
3660
|
for (const f of findings) {
|
|
@@ -2634,13 +3718,13 @@ var importCmd = defineCommand17({
|
|
|
2634
3718
|
},
|
|
2635
3719
|
async run({ args }) {
|
|
2636
3720
|
const client = createClient2();
|
|
2637
|
-
const absFrom =
|
|
3721
|
+
const absFrom = resolve3(args.from);
|
|
2638
3722
|
const isLocalZip = args.from.toLowerCase().endsWith(".zip") && existsSync6(absFrom) && statSync(absFrom).isFile();
|
|
2639
3723
|
let result;
|
|
2640
3724
|
try {
|
|
2641
3725
|
if (isLocalZip) {
|
|
2642
|
-
const bytes =
|
|
2643
|
-
const file = new File([bytes],
|
|
3726
|
+
const bytes = readFileSync7(absFrom);
|
|
3727
|
+
const file = new File([bytes], basename4(absFrom), { type: "application/zip" });
|
|
2644
3728
|
const fd = new FormData();
|
|
2645
3729
|
fd.set("file", file);
|
|
2646
3730
|
if (args.force) fd.set("force", "true");
|
|
@@ -2709,12 +3793,12 @@ var skillCommand = defineCommand17({
|
|
|
2709
3793
|
});
|
|
2710
3794
|
|
|
2711
3795
|
// src/commands/team.ts
|
|
2712
|
-
import { readFileSync as
|
|
3796
|
+
import { readFileSync as readFileSync9 } from "fs";
|
|
2713
3797
|
import { stdin as stdin2 } from "process";
|
|
2714
3798
|
import { defineCommand as defineCommand19 } from "citty";
|
|
2715
3799
|
|
|
2716
3800
|
// src/commands/team-policy.ts
|
|
2717
|
-
import { readFileSync as
|
|
3801
|
+
import { readFileSync as readFileSync8 } from "fs";
|
|
2718
3802
|
import { defineCommand as defineCommand18 } from "citty";
|
|
2719
3803
|
|
|
2720
3804
|
// src/team-interchange.ts
|
|
@@ -2931,7 +4015,7 @@ function edgeDiff(before, after) {
|
|
|
2931
4015
|
// src/commands/team-policy.ts
|
|
2932
4016
|
function readJson(path) {
|
|
2933
4017
|
try {
|
|
2934
|
-
return JSON.parse(
|
|
4018
|
+
return JSON.parse(readFileSync8(path, "utf8"));
|
|
2935
4019
|
} catch (error) {
|
|
2936
4020
|
throw new Error(`invalid JSON: ${error.message}`);
|
|
2937
4021
|
}
|
|
@@ -3201,12 +4285,12 @@ var listCmd9 = defineCommand19({
|
|
|
3201
4285
|
meta: { name: "list", description: "List teams" },
|
|
3202
4286
|
async run() {
|
|
3203
4287
|
const client = createClient2();
|
|
3204
|
-
const
|
|
3205
|
-
if (
|
|
4288
|
+
const list4 = await client.get("/api/teams");
|
|
4289
|
+
if (list4.length === 0) {
|
|
3206
4290
|
console.log("(no teams)");
|
|
3207
4291
|
return;
|
|
3208
4292
|
}
|
|
3209
|
-
const rows =
|
|
4293
|
+
const rows = list4.map((g) => [g.id, g.path]);
|
|
3210
4294
|
for (const line of columnize(rows)) console.log(line);
|
|
3211
4295
|
}
|
|
3212
4296
|
});
|
|
@@ -3255,7 +4339,7 @@ var userMdSetCmd = defineCommand19({
|
|
|
3255
4339
|
async run({ args }) {
|
|
3256
4340
|
let content;
|
|
3257
4341
|
if (args["from-stdin"]) content = await readStdin();
|
|
3258
|
-
else if (args.file) content =
|
|
4342
|
+
else if (args.file) content = readFileSync9(args.file, "utf8");
|
|
3259
4343
|
else if (args.text !== void 0) content = args.text;
|
|
3260
4344
|
else {
|
|
3261
4345
|
console.error("team user-md set: provide --file, --from-stdin, or --text");
|
|
@@ -3348,11 +4432,11 @@ var teamCommand = defineCommand19({
|
|
|
3348
4432
|
});
|
|
3349
4433
|
|
|
3350
4434
|
// src/commands/team-template.ts
|
|
3351
|
-
import { readFileSync as
|
|
4435
|
+
import { readFileSync as readFileSync10 } from "fs";
|
|
3352
4436
|
import { defineCommand as defineCommand20 } from "citty";
|
|
3353
4437
|
function jsonFile(path) {
|
|
3354
4438
|
try {
|
|
3355
|
-
return JSON.parse(
|
|
4439
|
+
return JSON.parse(readFileSync10(path, "utf8"));
|
|
3356
4440
|
} catch (error) {
|
|
3357
4441
|
throw new Error(`invalid JSON: ${error.message}`);
|
|
3358
4442
|
}
|
|
@@ -3377,7 +4461,7 @@ function printTeam(detail) {
|
|
|
3377
4461
|
console.log(line);
|
|
3378
4462
|
}
|
|
3379
4463
|
}
|
|
3380
|
-
var
|
|
4464
|
+
var list3 = defineCommand20({
|
|
3381
4465
|
meta: { name: "list", description: "List canonical Team templates" },
|
|
3382
4466
|
args: { json: { type: "boolean", description: "Emit stable JSON" } },
|
|
3383
4467
|
async run({ args }) {
|
|
@@ -3531,7 +4615,7 @@ var teamTemplateCommand = defineCommand20({
|
|
|
3531
4615
|
name: "team-template",
|
|
3532
4616
|
description: "Inspect and exchange canonical Team templates"
|
|
3533
4617
|
},
|
|
3534
|
-
subCommands: { list:
|
|
4618
|
+
subCommands: { list: list3, show: show3, export: exportCommand2, import: importCommand2 }
|
|
3535
4619
|
});
|
|
3536
4620
|
|
|
3537
4621
|
// src/commands/telegram.ts
|
|
@@ -4024,6 +5108,34 @@ var disableCmd3 = defineCommand23({
|
|
|
4024
5108
|
console.log(`disabled trigger ${args.id}`);
|
|
4025
5109
|
}
|
|
4026
5110
|
});
|
|
5111
|
+
var historyCmd = defineCommand23({
|
|
5112
|
+
meta: { name: "history", description: "Show recent dispatches for a trigger" },
|
|
5113
|
+
args: {
|
|
5114
|
+
id: { type: "positional", required: true },
|
|
5115
|
+
limit: { type: "string", description: "Maximum rows (default 20)" }
|
|
5116
|
+
},
|
|
5117
|
+
async run({ args }) {
|
|
5118
|
+
const limit = args.limit ? Number(args.limit) : 20;
|
|
5119
|
+
if (!Number.isInteger(limit) || limit < 1 || limit > 100) {
|
|
5120
|
+
throw new Error("--limit must be an integer from 1 to 100");
|
|
5121
|
+
}
|
|
5122
|
+
const client = createClient2();
|
|
5123
|
+
const { dispatches } = await client.get(
|
|
5124
|
+
`/api/triggers/${args.id}/dispatches?limit=${limit}`
|
|
5125
|
+
);
|
|
5126
|
+
if (dispatches.length === 0) {
|
|
5127
|
+
console.log("(no dispatches)");
|
|
5128
|
+
return;
|
|
5129
|
+
}
|
|
5130
|
+
const rows = dispatches.map((dispatch) => [
|
|
5131
|
+
new Date(dispatch.scheduledAt).toISOString(),
|
|
5132
|
+
dispatch.status,
|
|
5133
|
+
`attempts: ${dispatch.attemptCount}`,
|
|
5134
|
+
dispatch.lastError ?? ""
|
|
5135
|
+
]);
|
|
5136
|
+
for (const line of columnize(rows)) console.log(line);
|
|
5137
|
+
}
|
|
5138
|
+
});
|
|
4027
5139
|
var triggerCommand = defineCommand23({
|
|
4028
5140
|
meta: { name: "trigger", description: "Manage scheduled agent triggers" },
|
|
4029
5141
|
subCommands: {
|
|
@@ -4031,13 +5143,14 @@ var triggerCommand = defineCommand23({
|
|
|
4031
5143
|
list: listCmd11,
|
|
4032
5144
|
rm: rmCmd6,
|
|
4033
5145
|
enable: enableCmd3,
|
|
4034
|
-
disable: disableCmd3
|
|
5146
|
+
disable: disableCmd3,
|
|
5147
|
+
history: historyCmd
|
|
4035
5148
|
}
|
|
4036
5149
|
});
|
|
4037
5150
|
|
|
4038
5151
|
// src/commands/uninstall.ts
|
|
4039
|
-
import { existsSync as existsSync7, readdirSync as readdirSync2, rmSync as
|
|
4040
|
-
import { join as
|
|
5152
|
+
import { existsSync as existsSync7, readdirSync as readdirSync2, rmSync as rmSync4 } from "fs";
|
|
5153
|
+
import { join as join6 } from "path";
|
|
4041
5154
|
import { defineCommand as defineCommand24 } from "citty";
|
|
4042
5155
|
function makeLineReader() {
|
|
4043
5156
|
let buffer = "";
|
|
@@ -4048,8 +5161,8 @@ function makeLineReader() {
|
|
|
4048
5161
|
const nlIdx = buffer.indexOf("\n");
|
|
4049
5162
|
if (nlIdx === -1) {
|
|
4050
5163
|
if (ended) {
|
|
4051
|
-
const
|
|
4052
|
-
|
|
5164
|
+
const resolve4 = queue.shift();
|
|
5165
|
+
resolve4?.(buffer);
|
|
4053
5166
|
buffer = "";
|
|
4054
5167
|
} else {
|
|
4055
5168
|
return;
|
|
@@ -4057,8 +5170,8 @@ function makeLineReader() {
|
|
|
4057
5170
|
} else {
|
|
4058
5171
|
const line = buffer.slice(0, nlIdx);
|
|
4059
5172
|
buffer = buffer.slice(nlIdx + 1);
|
|
4060
|
-
const
|
|
4061
|
-
|
|
5173
|
+
const resolve4 = queue.shift();
|
|
5174
|
+
resolve4?.(line);
|
|
4062
5175
|
}
|
|
4063
5176
|
}
|
|
4064
5177
|
};
|
|
@@ -4071,8 +5184,8 @@ function makeLineReader() {
|
|
|
4071
5184
|
ended = true;
|
|
4072
5185
|
tryDeliver();
|
|
4073
5186
|
});
|
|
4074
|
-
return () => new Promise((
|
|
4075
|
-
queue.push(
|
|
5187
|
+
return () => new Promise((resolve4) => {
|
|
5188
|
+
queue.push(resolve4);
|
|
4076
5189
|
tryDeliver();
|
|
4077
5190
|
});
|
|
4078
5191
|
}
|
|
@@ -4083,7 +5196,7 @@ async function askYes(readLine, prompt) {
|
|
|
4083
5196
|
}
|
|
4084
5197
|
function removePath(p) {
|
|
4085
5198
|
if (!existsSync7(p)) return false;
|
|
4086
|
-
|
|
5199
|
+
rmSync4(p, { recursive: true, force: true });
|
|
4087
5200
|
return true;
|
|
4088
5201
|
}
|
|
4089
5202
|
var uninstallCommand = defineCommand24({
|
|
@@ -4104,12 +5217,12 @@ var uninstallCommand = defineCommand24({
|
|
|
4104
5217
|
},
|
|
4105
5218
|
async run({ args }) {
|
|
4106
5219
|
const paths = resolveCliPaths(args.home);
|
|
4107
|
-
const dbFile =
|
|
4108
|
-
const profilesDir =
|
|
4109
|
-
const agentsDir =
|
|
4110
|
-
const teamsDir =
|
|
4111
|
-
const skillsDir =
|
|
4112
|
-
const logsDir =
|
|
5220
|
+
const dbFile = join6(paths.home, "bazilion.db");
|
|
5221
|
+
const profilesDir = join6(paths.home, "profiles");
|
|
5222
|
+
const agentsDir = join6(paths.home, "agents");
|
|
5223
|
+
const teamsDir = join6(paths.home, "teams");
|
|
5224
|
+
const skillsDir = join6(paths.home, "skills");
|
|
5225
|
+
const logsDir = join6(paths.home, "logs");
|
|
4113
5226
|
if (!existsSync7(paths.home)) {
|
|
4114
5227
|
console.log(`nothing to remove: ${paths.home} does not exist`);
|
|
4115
5228
|
return;
|
|
@@ -4142,7 +5255,7 @@ var uninstallCommand = defineCommand24({
|
|
|
4142
5255
|
if (removePath(p)) console.log(`removed ${p}`);
|
|
4143
5256
|
}
|
|
4144
5257
|
if (readdirSync2(paths.home).length === 0) {
|
|
4145
|
-
|
|
5258
|
+
rmSync4(paths.home, { recursive: true, force: true });
|
|
4146
5259
|
console.log(`removed ${paths.home}`);
|
|
4147
5260
|
} else {
|
|
4148
5261
|
console.log(`${paths.home} still has unmanaged files; left in place`);
|