zooid 0.12.0 → 0.13.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/README.md +2 -0
- package/dist/bin.js +359 -123
- package/dist/bin.js.map +1 -1
- package/dist/{chunk-3Q4BPAZD.js → chunk-YZ4IO5MR.js} +56 -10
- package/dist/{chunk-3Q4BPAZD.js.map → chunk-YZ4IO5MR.js.map} +1 -1
- package/dist/index.js +1 -1
- package/package.json +11 -9
- package/src/bin.test.ts +63 -0
- package/src/bin.ts +52 -3
- package/src/bootstrap/configs.test.ts +7 -0
- package/src/bootstrap/configs.ts +14 -0
- package/src/commands/dev.ts +37 -7
- package/src/commands/status.test.ts +15 -0
- package/src/commands/status.ts +27 -2
- package/src/daemon/start-daemon.ts +15 -1
- package/src/push-gateway/gateway.test.ts +150 -0
- package/src/push-gateway/gateway.ts +78 -0
- package/src/push-gateway/index.ts +17 -0
- package/src/push-gateway/payload.test.ts +88 -0
- package/src/push-gateway/payload.ts +39 -0
- package/src/push-gateway/types.ts +37 -0
- package/src/push-gateway/vapid.test.ts +45 -0
- package/src/push-gateway/vapid.ts +34 -0
- package/src/services/tuwunel.ts +21 -2
- package/src/version.test.ts +67 -0
- package/src/version.ts +30 -0
- package/src/web/static.test.ts +22 -0
- package/src/web/static.ts +9 -1
package/dist/bin.js
CHANGED
|
@@ -16,17 +16,17 @@ import {
|
|
|
16
16
|
renderRegistration,
|
|
17
17
|
startDaemonSocketServer,
|
|
18
18
|
startWorkforcePublisher
|
|
19
|
-
} from "./chunk-
|
|
19
|
+
} from "./chunk-YZ4IO5MR.js";
|
|
20
20
|
|
|
21
21
|
// src/bin.ts
|
|
22
|
-
import { resolve as
|
|
22
|
+
import { resolve as resolve7 } from "path";
|
|
23
23
|
import { cac } from "cac";
|
|
24
24
|
|
|
25
25
|
// src/commands/dev.ts
|
|
26
26
|
import chalk from "chalk";
|
|
27
27
|
import { Listr } from "listr2";
|
|
28
|
-
import { existsSync as existsSync5, readFileSync as
|
|
29
|
-
import { dirname as dirname6, join as
|
|
28
|
+
import { existsSync as existsSync5, readFileSync as readFileSync7 } from "fs";
|
|
29
|
+
import { dirname as dirname6, join as join13, resolve as resolve3 } from "path";
|
|
30
30
|
import { fileURLToPath } from "url";
|
|
31
31
|
import { serve as serve2 } from "@hono/node-server";
|
|
32
32
|
|
|
@@ -83,6 +83,20 @@ function renderTuwunelToml(opts) {
|
|
|
83
83
|
"allow_local_presence = true",
|
|
84
84
|
'address = ["0.0.0.0"]',
|
|
85
85
|
`port = [${TUWUNEL_INTERNAL_PORT}]`,
|
|
86
|
+
// NOT `suppress_push_when_active` ([[ZNC025]]). It reads Matrix presence,
|
|
87
|
+
// which is both too coarse and too slow for this: coarse because it is
|
|
88
|
+
// per-user, so reading room A kills the push for room B; slow because
|
|
89
|
+
// `currently_active` lingers for minutes after the last sync, so closing
|
|
90
|
+
// the tab and waiting for an agent to finish still delivers nothing —
|
|
91
|
+
// exactly the case this feature exists for. `public/sw.js` already does
|
|
92
|
+
// the suppression we actually want, precisely: it drops a push only when
|
|
93
|
+
// a *visible* window is on *that* room.
|
|
94
|
+
// DEV ONLY — disables an SSRF guard. Tuwunel is in Podman and the push
|
|
95
|
+
// gateway runs on the host, so the default ip_range_denylist (127/8,
|
|
96
|
+
// 10/8, 172.16/12, 192.168/16, ::1) silently drops every pusher delivery.
|
|
97
|
+
// Never set on a box: there the gateway is reached at the public
|
|
98
|
+
// hostname through Caddy.
|
|
99
|
+
"ip_range_denylist = []",
|
|
86
100
|
""
|
|
87
101
|
].join("\n");
|
|
88
102
|
}
|
|
@@ -201,10 +215,10 @@ MATRIX_HS_TOKEN=${tokens.hsToken}
|
|
|
201
215
|
}
|
|
202
216
|
|
|
203
217
|
// src/daemon/start-daemon.ts
|
|
204
|
-
import { readFileSync as
|
|
218
|
+
import { readFileSync as readFileSync4 } from "fs";
|
|
205
219
|
import { mkdir, unlink } from "fs/promises";
|
|
206
220
|
import { tmpdir } from "os";
|
|
207
|
-
import { dirname as dirname2, isAbsolute, join as
|
|
221
|
+
import { dirname as dirname2, isAbsolute, join as join6, resolve } from "path";
|
|
208
222
|
import { serve } from "@hono/node-server";
|
|
209
223
|
|
|
210
224
|
// ../transport-http/src/server.ts
|
|
@@ -479,22 +493,131 @@ var defaultExec = async (cmd, args) => {
|
|
|
479
493
|
}
|
|
480
494
|
};
|
|
481
495
|
|
|
482
|
-
// src/
|
|
496
|
+
// src/push-gateway/gateway.ts
|
|
497
|
+
import { Hono as Hono2 } from "hono";
|
|
498
|
+
import webpush from "web-push";
|
|
499
|
+
|
|
500
|
+
// src/push-gateway/payload.ts
|
|
501
|
+
var MAX_BODY = 140;
|
|
502
|
+
var ZOOID_APP_ID = "dev.zooid.web";
|
|
503
|
+
function truncate(s, max) {
|
|
504
|
+
return s.length > max ? s.slice(0, max - 1) + "\u2026" : s;
|
|
505
|
+
}
|
|
506
|
+
function buildPushPayload(n, device) {
|
|
507
|
+
const body = typeof n.content?.body === "string" ? truncate(n.content.body, MAX_BODY) : void 0;
|
|
508
|
+
const preview = typeof n.content?.last_message === "string" ? truncate(n.content.last_message, MAX_BODY) : void 0;
|
|
509
|
+
return {
|
|
510
|
+
event_id: n.event_id,
|
|
511
|
+
room_id: n.room_id,
|
|
512
|
+
room_name: n.room_name ?? n.room_id,
|
|
513
|
+
...n.sender_display_name !== void 0 ? { sender_display_name: n.sender_display_name } : {},
|
|
514
|
+
type: n.type,
|
|
515
|
+
...body !== void 0 ? { body } : {},
|
|
516
|
+
...preview !== void 0 ? { preview } : {},
|
|
517
|
+
unread: n.counts?.unread ?? 0,
|
|
518
|
+
// Whether this makes a noise is a push-rule property the server evaluated,
|
|
519
|
+
// not a second decision made here (spec §12).
|
|
520
|
+
sound: device?.tweaks?.sound !== void 0
|
|
521
|
+
};
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
// src/push-gateway/gateway.ts
|
|
525
|
+
function parseNotifyBody(raw) {
|
|
526
|
+
if (!raw || typeof raw !== "object") return null;
|
|
527
|
+
const notification = raw.notification;
|
|
528
|
+
if (!notification || typeof notification !== "object") return null;
|
|
529
|
+
const n = notification;
|
|
530
|
+
if (typeof n.event_id !== "string" || typeof n.room_id !== "string" || typeof n.type !== "string")
|
|
531
|
+
return null;
|
|
532
|
+
if (!Array.isArray(n.devices)) return null;
|
|
533
|
+
return n;
|
|
534
|
+
}
|
|
535
|
+
function pushGateway(opts) {
|
|
536
|
+
const app = new Hono2();
|
|
537
|
+
app.post("/_matrix/push/v1/notify", async (c) => {
|
|
538
|
+
const parsed = parseNotifyBody(await c.req.json().catch(() => null));
|
|
539
|
+
if (!parsed) {
|
|
540
|
+
console.warn("[push] notify: malformed body");
|
|
541
|
+
return c.json({ error: "malformed notification" }, 400);
|
|
542
|
+
}
|
|
543
|
+
console.log(
|
|
544
|
+
`[push] notify room=${parsed.room_id} type=${parsed.type} devices=${parsed.devices.length}`
|
|
545
|
+
);
|
|
546
|
+
let delivered = 0;
|
|
547
|
+
const rejected = [];
|
|
548
|
+
await Promise.all(
|
|
549
|
+
parsed.devices.map(async (device) => {
|
|
550
|
+
if (device.app_id !== ZOOID_APP_ID) return;
|
|
551
|
+
const endpoint = device.data?.endpoint;
|
|
552
|
+
const auth = device.data?.auth;
|
|
553
|
+
if (typeof endpoint !== "string" || typeof auth !== "string") return;
|
|
554
|
+
try {
|
|
555
|
+
await webpush.sendNotification(
|
|
556
|
+
{ endpoint, keys: { p256dh: device.pushkey, auth } },
|
|
557
|
+
JSON.stringify(buildPushPayload(parsed, device)),
|
|
558
|
+
{
|
|
559
|
+
vapidDetails: { subject: opts.subject, ...opts.keys },
|
|
560
|
+
TTL: 60 * 60 * 12,
|
|
561
|
+
urgency: device.tweaks?.sound !== void 0 ? "high" : "normal"
|
|
562
|
+
}
|
|
563
|
+
);
|
|
564
|
+
delivered++;
|
|
565
|
+
} catch (err) {
|
|
566
|
+
const status = err.statusCode;
|
|
567
|
+
if (status === 404 || status === 410) rejected.push(device.pushkey);
|
|
568
|
+
else console.warn(`[push] delivery to ${device.pushkey} failed (${status ?? "?"}):`, err);
|
|
569
|
+
}
|
|
570
|
+
})
|
|
571
|
+
);
|
|
572
|
+
console.log(`[push] notify done: delivered=${delivered} rejected=${rejected.length}`);
|
|
573
|
+
return c.json({ rejected });
|
|
574
|
+
});
|
|
575
|
+
return app;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
// src/push-gateway/vapid.ts
|
|
483
579
|
import { mkdirSync as mkdirSync3, readFileSync as readFileSync2, writeFileSync as writeFileSync3 } from "fs";
|
|
484
580
|
import { join as join4 } from "path";
|
|
581
|
+
import webpush2 from "web-push";
|
|
582
|
+
var VAPID_FILENAME = "vapid.json";
|
|
583
|
+
function loadOrCreateVapidKeys(dataDir) {
|
|
584
|
+
const path = join4(dataDir, VAPID_FILENAME);
|
|
585
|
+
try {
|
|
586
|
+
const parsed = JSON.parse(readFileSync2(path, "utf8"));
|
|
587
|
+
if (typeof parsed.publicKey === "string" && typeof parsed.privateKey === "string")
|
|
588
|
+
return { publicKey: parsed.publicKey, privateKey: parsed.privateKey };
|
|
589
|
+
console.warn(`[push] ${path} is malformed; generating a new VAPID keypair.`);
|
|
590
|
+
} catch {
|
|
591
|
+
}
|
|
592
|
+
const keys = webpush2.generateVAPIDKeys();
|
|
593
|
+
mkdirSync3(dataDir, { recursive: true });
|
|
594
|
+
writeFileSync3(path, JSON.stringify(keys, null, 2), { mode: 384 });
|
|
595
|
+
return keys;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
// src/push-gateway/index.ts
|
|
599
|
+
function mountPushGateway(app, opts) {
|
|
600
|
+
const keys = loadOrCreateVapidKeys(opts.dataDir);
|
|
601
|
+
app.route("/", pushGateway({ keys, subject: opts.subject }));
|
|
602
|
+
return { publicKey: keys.publicKey };
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
// src/daemon/sync-cursors.ts
|
|
606
|
+
import { mkdirSync as mkdirSync4, readFileSync as readFileSync3, writeFileSync as writeFileSync4 } from "fs";
|
|
607
|
+
import { join as join5 } from "path";
|
|
485
608
|
function makeSyncCursorStore(agentsDir) {
|
|
486
|
-
const fileFor = (agentName) =>
|
|
609
|
+
const fileFor = (agentName) => join5(agentsDir, agentName, "sync-since");
|
|
487
610
|
return {
|
|
488
611
|
loadSince(agentName) {
|
|
489
612
|
try {
|
|
490
|
-
return
|
|
613
|
+
return readFileSync3(fileFor(agentName), "utf8").trim() || null;
|
|
491
614
|
} catch {
|
|
492
615
|
return null;
|
|
493
616
|
}
|
|
494
617
|
},
|
|
495
618
|
saveSince(agentName, since) {
|
|
496
|
-
|
|
497
|
-
|
|
619
|
+
mkdirSync4(join5(agentsDir, agentName), { recursive: true });
|
|
620
|
+
writeFileSync4(fileFor(agentName), since, "utf8");
|
|
498
621
|
}
|
|
499
622
|
};
|
|
500
623
|
}
|
|
@@ -506,18 +629,18 @@ function shouldBindHttpListener(mode) {
|
|
|
506
629
|
|
|
507
630
|
// src/daemon/start-daemon.ts
|
|
508
631
|
function listenAsync(server) {
|
|
509
|
-
return new Promise((
|
|
632
|
+
return new Promise((resolve8) => {
|
|
510
633
|
const check = () => {
|
|
511
634
|
const addr = server.address();
|
|
512
|
-
if (addr && typeof addr === "object")
|
|
635
|
+
if (addr && typeof addr === "object") resolve8(addr.port);
|
|
513
636
|
else setImmediate(check);
|
|
514
637
|
};
|
|
515
638
|
check();
|
|
516
639
|
});
|
|
517
640
|
}
|
|
518
641
|
function closeAsync(server) {
|
|
519
|
-
return new Promise((
|
|
520
|
-
server.close(() =>
|
|
642
|
+
return new Promise((resolve8) => {
|
|
643
|
+
server.close(() => resolve8());
|
|
521
644
|
});
|
|
522
645
|
}
|
|
523
646
|
async function startDaemon(opts = {}) {
|
|
@@ -525,10 +648,10 @@ async function startDaemon(opts = {}) {
|
|
|
525
648
|
const found = opts.configPath ? { path: opts.configPath } : findConfigFile(cwd);
|
|
526
649
|
if (!found) throw new Error("zooid.yaml is required");
|
|
527
650
|
const configDir = dirname2(found.path);
|
|
528
|
-
const base = loadZooidConfig(
|
|
651
|
+
const base = loadZooidConfig(readFileSync4(found.path, "utf8"), { configDir });
|
|
529
652
|
const config = mergeCliFlags(base, opts.cliFlags ?? {});
|
|
530
653
|
const approvals = new ApprovalCorrelator();
|
|
531
|
-
const daemonSockPath = opts.agentsDir ?
|
|
654
|
+
const daemonSockPath = opts.agentsDir ? join6(opts.agentsDir, "..", "run", "context.sock") : join6(tmpdir(), `zooid-context-${process.pid}.sock`);
|
|
532
655
|
await mkdir(dirname2(daemonSockPath), { recursive: true }).catch(() => {
|
|
533
656
|
});
|
|
534
657
|
const contextSpawnRegistry = new SpawnRegistry();
|
|
@@ -574,6 +697,7 @@ async function startDaemon(opts = {}) {
|
|
|
574
697
|
});
|
|
575
698
|
const matrix = findMatrixTransport(config);
|
|
576
699
|
let port;
|
|
700
|
+
let vapidPublicKey;
|
|
577
701
|
if (matrix) {
|
|
578
702
|
const mode = matrix.transport.mode ?? "appservice";
|
|
579
703
|
if (mode === "client" && !opts.agentsDir) {
|
|
@@ -629,6 +753,12 @@ async function startDaemon(opts = {}) {
|
|
|
629
753
|
});
|
|
630
754
|
if (shouldBindHttpListener(mode)) {
|
|
631
755
|
const requestedPort = matrix.transport.port ?? 9e3;
|
|
756
|
+
if (dataDir) {
|
|
757
|
+
vapidPublicKey = mountPushGateway(transport.app, {
|
|
758
|
+
dataDir,
|
|
759
|
+
subject: `https://${serverName}`
|
|
760
|
+
}).publicKey;
|
|
761
|
+
}
|
|
632
762
|
server = serve({ fetch: transport.app.fetch, port: requestedPort, hostname: "0.0.0.0" });
|
|
633
763
|
port = await listenAsync(server);
|
|
634
764
|
} else {
|
|
@@ -724,7 +854,7 @@ async function startDaemon(opts = {}) {
|
|
|
724
854
|
process.on("SIGINT", () => handler("SIGINT"));
|
|
725
855
|
process.on("SIGTERM", () => handler("SIGTERM"));
|
|
726
856
|
}
|
|
727
|
-
return { port, agentNames, stop, whenStopped };
|
|
857
|
+
return { port, agentNames, vapidPublicKey, stop, whenStopped };
|
|
728
858
|
}
|
|
729
859
|
|
|
730
860
|
// src/services/tuwunel.ts
|
|
@@ -769,9 +899,24 @@ var TuwunelService = class {
|
|
|
769
899
|
}
|
|
770
900
|
async stop() {
|
|
771
901
|
if (this.child && this.child.exitCode === null) {
|
|
772
|
-
this.child
|
|
773
|
-
|
|
774
|
-
|
|
902
|
+
const child = this.child;
|
|
903
|
+
child.kill("SIGTERM");
|
|
904
|
+
await new Promise((resolve8) => {
|
|
905
|
+
let done = false;
|
|
906
|
+
const finish = () => {
|
|
907
|
+
if (done) return;
|
|
908
|
+
done = true;
|
|
909
|
+
clearTimeout(timer);
|
|
910
|
+
resolve8();
|
|
911
|
+
};
|
|
912
|
+
const timer = setTimeout(() => {
|
|
913
|
+
try {
|
|
914
|
+
child.kill("SIGKILL");
|
|
915
|
+
} catch {
|
|
916
|
+
}
|
|
917
|
+
finish();
|
|
918
|
+
}, 5e3);
|
|
919
|
+
child.once("exit", finish);
|
|
775
920
|
});
|
|
776
921
|
}
|
|
777
922
|
this.child = null;
|
|
@@ -788,7 +933,7 @@ var TuwunelService = class {
|
|
|
788
933
|
`Tuwunel container exited before serving HTTP (exit=${state.exitCode}${state.error ? `, error=${state.error}` : ""}). Check the engine logs (\`${this.opts.engine} logs ${this.opts.name}\`).`
|
|
789
934
|
);
|
|
790
935
|
}
|
|
791
|
-
await new Promise((
|
|
936
|
+
await new Promise((resolve8) => setTimeout(resolve8, 500));
|
|
792
937
|
}
|
|
793
938
|
while (Date.now() < deadline) {
|
|
794
939
|
try {
|
|
@@ -796,7 +941,7 @@ var TuwunelService = class {
|
|
|
796
941
|
if (r.ok) return;
|
|
797
942
|
} catch {
|
|
798
943
|
}
|
|
799
|
-
await new Promise((
|
|
944
|
+
await new Promise((resolve8) => setTimeout(resolve8, 500));
|
|
800
945
|
}
|
|
801
946
|
const finalState = await this.inspectState().catch(() => null);
|
|
802
947
|
const detail = finalState ? ` (container status=${finalState.status}${finalState.exitCode !== void 0 ? `, exit=${finalState.exitCode}` : ""})` : "";
|
|
@@ -829,12 +974,12 @@ var TuwunelService = class {
|
|
|
829
974
|
}
|
|
830
975
|
};
|
|
831
976
|
function execEngine(engine, args) {
|
|
832
|
-
return new Promise((
|
|
977
|
+
return new Promise((resolve8, reject) => {
|
|
833
978
|
const child = spawn(engine, args, { stdio: "pipe" });
|
|
834
979
|
let stderr = "";
|
|
835
980
|
child.stderr.on("data", (b) => stderr += String(b));
|
|
836
981
|
child.on("exit", (code) => {
|
|
837
|
-
if (code === 0)
|
|
982
|
+
if (code === 0) resolve8();
|
|
838
983
|
else reject(new Error(`${engine} ${args.join(" ")} failed: ${stderr.trim()}`));
|
|
839
984
|
});
|
|
840
985
|
child.on("error", reject);
|
|
@@ -843,18 +988,18 @@ function execEngine(engine, args) {
|
|
|
843
988
|
|
|
844
989
|
// src/web/resolve.ts
|
|
845
990
|
import { existsSync as existsSync3 } from "fs";
|
|
846
|
-
import { dirname as dirname3, join as
|
|
991
|
+
import { dirname as dirname3, join as join8, resolve as resolve2 } from "path";
|
|
847
992
|
|
|
848
993
|
// src/web/fetch.ts
|
|
849
|
-
import { mkdirSync as
|
|
850
|
-
import { join as
|
|
994
|
+
import { mkdirSync as mkdirSync5, renameSync, rmSync, existsSync as existsSync2, readdirSync, writeFileSync as writeFileSync5 } from "fs";
|
|
995
|
+
import { join as join7 } from "path";
|
|
851
996
|
import { createHash, randomUUID as randomUUID2 } from "crypto";
|
|
852
997
|
import * as tar from "tar";
|
|
853
998
|
var PKG = "@zooid/web";
|
|
854
999
|
var DEFAULT_REGISTRY = "https://registry.npmjs.org";
|
|
855
1000
|
async function fetchWebBundle(opts) {
|
|
856
|
-
const target =
|
|
857
|
-
if (existsSync2(
|
|
1001
|
+
const target = join7(opts.cacheDir, opts.version);
|
|
1002
|
+
if (existsSync2(join7(target, "index.html"))) return target;
|
|
858
1003
|
if (existsSync2(target)) rmSync(target, { recursive: true, force: true });
|
|
859
1004
|
const f = opts.fetch ?? globalThis.fetch;
|
|
860
1005
|
const registry = (opts.registryUrl ?? DEFAULT_REGISTRY).replace(/\/$/, "");
|
|
@@ -882,11 +1027,11 @@ async function fetchWebBundle(opts) {
|
|
|
882
1027
|
Cause: ${err instanceof Error ? err.message : String(err)}`
|
|
883
1028
|
);
|
|
884
1029
|
}
|
|
885
|
-
const tmp =
|
|
886
|
-
|
|
887
|
-
const tgzPath =
|
|
1030
|
+
const tmp = join7(opts.cacheDir, `.tmp-${randomUUID2().slice(0, 8)}`);
|
|
1031
|
+
mkdirSync5(tmp, { recursive: true });
|
|
1032
|
+
const tgzPath = join7(tmp, ".bundle.tgz");
|
|
888
1033
|
try {
|
|
889
|
-
|
|
1034
|
+
writeFileSync5(tgzPath, tgz);
|
|
890
1035
|
await tar.extract({
|
|
891
1036
|
file: tgzPath,
|
|
892
1037
|
cwd: tmp,
|
|
@@ -895,7 +1040,7 @@ async function fetchWebBundle(opts) {
|
|
|
895
1040
|
filter: (p) => p === "package/dist" || p.startsWith("package/dist/")
|
|
896
1041
|
});
|
|
897
1042
|
rmSync(tgzPath);
|
|
898
|
-
if (!existsSync2(
|
|
1043
|
+
if (!existsSync2(join7(tmp, "index.html"))) {
|
|
899
1044
|
throw new Error(`${PKG}@${opts.version} tarball has no dist/index.html`);
|
|
900
1045
|
}
|
|
901
1046
|
renameSync(tmp, target);
|
|
@@ -905,7 +1050,7 @@ async function fetchWebBundle(opts) {
|
|
|
905
1050
|
}
|
|
906
1051
|
for (const entry of readdirSync(opts.cacheDir)) {
|
|
907
1052
|
if (entry !== opts.version) {
|
|
908
|
-
rmSync(
|
|
1053
|
+
rmSync(join7(opts.cacheDir, entry), { recursive: true, force: true });
|
|
909
1054
|
}
|
|
910
1055
|
}
|
|
911
1056
|
return target;
|
|
@@ -915,10 +1060,10 @@ async function fetchWebBundle(opts) {
|
|
|
915
1060
|
var ENV_OVERRIDE = "ZOOID_DEV_WEB_ROOT_OVERRIDE";
|
|
916
1061
|
async function ensureWebRoot(opts) {
|
|
917
1062
|
const override = process.env[ENV_OVERRIDE];
|
|
918
|
-
if (override && existsSync3(
|
|
1063
|
+
if (override && existsSync3(join8(override, "index.html"))) return resolve2(override);
|
|
919
1064
|
const fromSource = webSourcePackage(opts.cliRoot);
|
|
920
|
-
if (fromSource && existsSync3(
|
|
921
|
-
return
|
|
1065
|
+
if (fromSource && existsSync3(join8(fromSource, "dist", "index.html"))) {
|
|
1066
|
+
return join8(fromSource, "dist");
|
|
922
1067
|
}
|
|
923
1068
|
if (!opts.version) {
|
|
924
1069
|
throw new Error(
|
|
@@ -932,16 +1077,16 @@ Set ${ENV_OVERRIDE} to a built dist, or run from the monorepo.`
|
|
|
932
1077
|
}
|
|
933
1078
|
function webSourcePackage(cliRoot) {
|
|
934
1079
|
const workspaceRoot = dirname3(dirname3(dirname3(cliRoot)));
|
|
935
|
-
const candidate =
|
|
936
|
-
return existsSync3(
|
|
1080
|
+
const candidate = join8(workspaceRoot, "zooid-clients", "packages", "web");
|
|
1081
|
+
return existsSync3(join8(candidate, "package.json")) ? candidate : null;
|
|
937
1082
|
}
|
|
938
1083
|
|
|
939
1084
|
// src/web/pin.ts
|
|
940
|
-
import { readFileSync as
|
|
941
|
-
import { join as
|
|
1085
|
+
import { readFileSync as readFileSync5 } from "fs";
|
|
1086
|
+
import { join as join9 } from "path";
|
|
942
1087
|
function readZoonWebPin(cliRoot) {
|
|
943
1088
|
try {
|
|
944
|
-
const pkg = JSON.parse(
|
|
1089
|
+
const pkg = JSON.parse(readFileSync5(join9(cliRoot, "package.json"), "utf8"));
|
|
945
1090
|
return pkg.zooid?.webVersion;
|
|
946
1091
|
} catch {
|
|
947
1092
|
return void 0;
|
|
@@ -949,9 +1094,9 @@ function readZoonWebPin(cliRoot) {
|
|
|
949
1094
|
}
|
|
950
1095
|
|
|
951
1096
|
// src/web/static.ts
|
|
952
|
-
import { readFileSync as
|
|
953
|
-
import { extname, join as
|
|
954
|
-
import { Hono as
|
|
1097
|
+
import { readFileSync as readFileSync6, statSync } from "fs";
|
|
1098
|
+
import { extname, join as join10, normalize } from "path";
|
|
1099
|
+
import { Hono as Hono3 } from "hono";
|
|
955
1100
|
var MIME = {
|
|
956
1101
|
".html": "text/html; charset=utf-8",
|
|
957
1102
|
".js": "application/javascript; charset=utf-8",
|
|
@@ -969,26 +1114,33 @@ function isAssetPath(p) {
|
|
|
969
1114
|
return p.startsWith("/assets/") || /\.[a-z0-9]+$/i.test(p);
|
|
970
1115
|
}
|
|
971
1116
|
function webStatic(opts) {
|
|
972
|
-
const app = new
|
|
973
|
-
app.get(
|
|
1117
|
+
const app = new Hono3();
|
|
1118
|
+
app.get(
|
|
1119
|
+
"/config.json",
|
|
1120
|
+
(c) => c.json({
|
|
1121
|
+
homeserver_url: opts.homeserverUrl,
|
|
1122
|
+
...opts.pushGatewayUrl ? { push_gateway_url: opts.pushGatewayUrl } : {},
|
|
1123
|
+
...opts.vapidPublicKey ? { vapid_public_key: opts.vapidPublicKey } : {}
|
|
1124
|
+
})
|
|
1125
|
+
);
|
|
974
1126
|
app.get("*", (c) => {
|
|
975
1127
|
const url = new URL(c.req.url);
|
|
976
1128
|
const requested = decodeURIComponent(url.pathname);
|
|
977
1129
|
const wantFile = requested === "/" ? "/index.html" : requested;
|
|
978
1130
|
const safe = normalize(wantFile).replace(/^(\.\.[/\\])+/g, "");
|
|
979
|
-
const filePath =
|
|
1131
|
+
const filePath = join10(opts.webRoot, safe);
|
|
980
1132
|
if (!filePath.startsWith(opts.webRoot)) return c.notFound();
|
|
981
1133
|
try {
|
|
982
1134
|
const stat = statSync(filePath);
|
|
983
1135
|
if (stat.isFile()) {
|
|
984
|
-
const body =
|
|
1136
|
+
const body = readFileSync6(filePath);
|
|
985
1137
|
const ct = MIME[extname(filePath)] ?? "application/octet-stream";
|
|
986
1138
|
return c.body(body, 200, { "content-type": ct });
|
|
987
1139
|
}
|
|
988
1140
|
} catch {
|
|
989
1141
|
}
|
|
990
1142
|
if (isAssetPath(requested)) return c.notFound();
|
|
991
|
-
const indexBytes =
|
|
1143
|
+
const indexBytes = readFileSync6(join10(opts.webRoot, "index.html"));
|
|
992
1144
|
return c.body(indexBytes, 200, {
|
|
993
1145
|
"content-type": MIME[".html"]
|
|
994
1146
|
});
|
|
@@ -999,10 +1151,10 @@ function webStatic(opts) {
|
|
|
999
1151
|
// src/web/watch.ts
|
|
1000
1152
|
import { spawn as spawn2 } from "child_process";
|
|
1001
1153
|
import { existsSync as existsSync4, statSync as statSync2 } from "fs";
|
|
1002
|
-
import { join as
|
|
1154
|
+
import { join as join11 } from "path";
|
|
1003
1155
|
async function startWebWatch(opts) {
|
|
1004
|
-
const distPath =
|
|
1005
|
-
const indexPath =
|
|
1156
|
+
const distPath = join11(opts.webPackageDir, "dist");
|
|
1157
|
+
const indexPath = join11(distPath, "index.html");
|
|
1006
1158
|
const timeoutMs = opts.firstBuildTimeoutMs ?? 6e4;
|
|
1007
1159
|
const spawnTime = Date.now();
|
|
1008
1160
|
const child = spawn2(
|
|
@@ -1056,12 +1208,12 @@ async function startWebWatch(opts) {
|
|
|
1056
1208
|
async function stopChild(child) {
|
|
1057
1209
|
if (child.exitCode !== null) return;
|
|
1058
1210
|
child.kill("SIGTERM");
|
|
1059
|
-
await new Promise((
|
|
1211
|
+
await new Promise((resolve8) => {
|
|
1060
1212
|
let done = false;
|
|
1061
1213
|
const finish = () => {
|
|
1062
1214
|
if (done) return;
|
|
1063
1215
|
done = true;
|
|
1064
|
-
|
|
1216
|
+
resolve8();
|
|
1065
1217
|
};
|
|
1066
1218
|
child.once("exit", finish);
|
|
1067
1219
|
setTimeout(() => {
|
|
@@ -1076,7 +1228,7 @@ async function stopChild(child) {
|
|
|
1076
1228
|
|
|
1077
1229
|
// src/observability/paths.ts
|
|
1078
1230
|
import { mkdir as mkdir2, readdir, rm, symlink, unlink as unlink2 } from "fs/promises";
|
|
1079
|
-
import { join as
|
|
1231
|
+
import { join as join12 } from "path";
|
|
1080
1232
|
function localDateSlug(d) {
|
|
1081
1233
|
const y = d.getFullYear();
|
|
1082
1234
|
const m = String(d.getMonth() + 1).padStart(2, "0");
|
|
@@ -1086,19 +1238,19 @@ function localDateSlug(d) {
|
|
|
1086
1238
|
var DAY_RE = /^\d{4}-\d{2}-\d{2}$/;
|
|
1087
1239
|
function resolveLogPaths({ dataDir, now }) {
|
|
1088
1240
|
const slug = localDateSlug(now ?? /* @__PURE__ */ new Date());
|
|
1089
|
-
const logsDir =
|
|
1090
|
-
const dayDir =
|
|
1241
|
+
const logsDir = join12(dataDir, "logs");
|
|
1242
|
+
const dayDir = join12(logsDir, slug);
|
|
1091
1243
|
return {
|
|
1092
1244
|
dataDir,
|
|
1093
1245
|
logsDir,
|
|
1094
1246
|
dayDir,
|
|
1095
1247
|
daySlug: slug,
|
|
1096
|
-
todayLink:
|
|
1097
|
-
tuwunelLog:
|
|
1098
|
-
daemonLog:
|
|
1099
|
-
devLog:
|
|
1100
|
-
agentLog: (n) =>
|
|
1101
|
-
agentTap: (n) =>
|
|
1248
|
+
todayLink: join12(logsDir, "today"),
|
|
1249
|
+
tuwunelLog: join12(dayDir, "tuwunel.log"),
|
|
1250
|
+
daemonLog: join12(dayDir, "daemon.log"),
|
|
1251
|
+
devLog: join12(dayDir, "dev.log"),
|
|
1252
|
+
agentLog: (n) => join12(dayDir, `agent-${n}.log`),
|
|
1253
|
+
agentTap: (n) => join12(dayDir, `agent-${n}.acp.jsonl`)
|
|
1102
1254
|
};
|
|
1103
1255
|
}
|
|
1104
1256
|
async function ensureDayFolder(p) {
|
|
@@ -1111,7 +1263,7 @@ async function ensureDayFolder(p) {
|
|
|
1111
1263
|
}
|
|
1112
1264
|
async function pruneOldDays(opts) {
|
|
1113
1265
|
if (opts.retainDays <= 0) return [];
|
|
1114
|
-
const logsDir =
|
|
1266
|
+
const logsDir = join12(opts.dataDir, "logs");
|
|
1115
1267
|
let entries;
|
|
1116
1268
|
try {
|
|
1117
1269
|
entries = await readdir(logsDir);
|
|
@@ -1130,7 +1282,7 @@ async function pruneOldDays(opts) {
|
|
|
1130
1282
|
const [y, m, d] = name.split("-").map(Number);
|
|
1131
1283
|
const t = new Date(y, m - 1, d).getTime();
|
|
1132
1284
|
if (t < cutoff) {
|
|
1133
|
-
await rm(
|
|
1285
|
+
await rm(join12(logsDir, name), { recursive: true, force: true });
|
|
1134
1286
|
removed.push(name);
|
|
1135
1287
|
}
|
|
1136
1288
|
}
|
|
@@ -1159,14 +1311,14 @@ var JsonlSink = class {
|
|
|
1159
1311
|
await this.readyPromise;
|
|
1160
1312
|
const capped = capStrings(obj, this.maxStringLen);
|
|
1161
1313
|
const line = JSON.stringify(capped) + "\n";
|
|
1162
|
-
return new Promise((
|
|
1163
|
-
this.stream.write(line, (err) => err ? reject(err) :
|
|
1314
|
+
return new Promise((resolve8, reject) => {
|
|
1315
|
+
this.stream.write(line, (err) => err ? reject(err) : resolve8());
|
|
1164
1316
|
});
|
|
1165
1317
|
}
|
|
1166
1318
|
async close() {
|
|
1167
1319
|
await this.readyPromise;
|
|
1168
|
-
return new Promise((
|
|
1169
|
-
this.stream.end(() =>
|
|
1320
|
+
return new Promise((resolve8) => {
|
|
1321
|
+
this.stream.end(() => resolve8());
|
|
1170
1322
|
});
|
|
1171
1323
|
}
|
|
1172
1324
|
};
|
|
@@ -1241,8 +1393,8 @@ function captureChildToFile(child, path) {
|
|
|
1241
1393
|
const stream = createWriteStream2(path, { flags: "a" });
|
|
1242
1394
|
if (child.stdout) child.stdout.on("data", (b) => stream.write(b));
|
|
1243
1395
|
if (child.stderr) child.stderr.on("data", (b) => stream.write(b));
|
|
1244
|
-
await new Promise((
|
|
1245
|
-
child.on("exit", () => stream.end(() =>
|
|
1396
|
+
await new Promise((resolve8) => {
|
|
1397
|
+
child.on("exit", () => stream.end(() => resolve8()));
|
|
1246
1398
|
});
|
|
1247
1399
|
})();
|
|
1248
1400
|
}
|
|
@@ -1280,7 +1432,7 @@ var CLI_ROOT = (() => {
|
|
|
1280
1432
|
const pkgPath = resolve3(dir, "package.json");
|
|
1281
1433
|
if (existsSync5(pkgPath)) {
|
|
1282
1434
|
try {
|
|
1283
|
-
const pkg = JSON.parse(
|
|
1435
|
+
const pkg = JSON.parse(readFileSync7(pkgPath, "utf8"));
|
|
1284
1436
|
if (pkg.name === "zooid" || pkg.name === "@zooid/cli") return dir;
|
|
1285
1437
|
} catch {
|
|
1286
1438
|
}
|
|
@@ -1303,7 +1455,7 @@ async function runDev(flags) {
|
|
|
1303
1455
|
const tokens = ensureTokens(paths.envPath);
|
|
1304
1456
|
process.env.MATRIX_AS_TOKEN = tokens.asToken;
|
|
1305
1457
|
process.env.MATRIX_HS_TOKEN = tokens.hsToken;
|
|
1306
|
-
const rawYaml =
|
|
1458
|
+
const rawYaml = readFileSync7(found.path, "utf8");
|
|
1307
1459
|
const preview = loadZooidConfig(rawYaml, { configDir: dirname6(found.path) });
|
|
1308
1460
|
const matrix = findMatrixTransport(preview);
|
|
1309
1461
|
if (!matrix) {
|
|
@@ -1416,13 +1568,26 @@ Pass an explicit path: --watch-web=/path/to/zooid-clients/packages/web`
|
|
|
1416
1568
|
task: async (_, t) => {
|
|
1417
1569
|
const webRoot = ctx.webWatch?.distPath ?? await ensureWebRoot({
|
|
1418
1570
|
cliRoot: CLI_ROOT,
|
|
1419
|
-
cacheDir:
|
|
1571
|
+
cacheDir: join13(layout.dataRoot, "web"),
|
|
1420
1572
|
version: readZoonWebPin(CLI_ROOT),
|
|
1421
1573
|
onProgress: (msg) => {
|
|
1422
1574
|
t.output = msg;
|
|
1423
1575
|
}
|
|
1424
1576
|
});
|
|
1425
|
-
const app = webStatic({
|
|
1577
|
+
const app = webStatic({
|
|
1578
|
+
webRoot,
|
|
1579
|
+
homeserverUrl: homeserver,
|
|
1580
|
+
...ctx.daemon?.vapidPublicKey ? {
|
|
1581
|
+
// Tuwunel runs in a container; `localhost` here would
|
|
1582
|
+
// resolve to the container, not the daemon. Same shorthand
|
|
1583
|
+
// as the AS registration url
|
|
1584
|
+
// (bootstrap/registration-url.ts). This URL is stored in
|
|
1585
|
+
// the pusher and fetched by the homeserver — the browser
|
|
1586
|
+
// never requests it.
|
|
1587
|
+
pushGatewayUrl: `http://host.docker.internal:${ctx.daemon.port}/_matrix/push/v1/notify`,
|
|
1588
|
+
vapidPublicKey: ctx.daemon.vapidPublicKey
|
|
1589
|
+
} : {}
|
|
1590
|
+
});
|
|
1426
1591
|
ctx.uiServer = serve2({ fetch: app.fetch, port: flags.uiPort });
|
|
1427
1592
|
}
|
|
1428
1593
|
}
|
|
@@ -1455,13 +1620,25 @@ Pass an explicit path: --watch-web=/path/to/zooid-clients/packages/web`
|
|
|
1455
1620
|
}
|
|
1456
1621
|
});
|
|
1457
1622
|
if (flags.installSignalHandlers !== false) {
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1623
|
+
let interrupts = 0;
|
|
1624
|
+
const onSignal = () => {
|
|
1625
|
+
interrupts += 1;
|
|
1626
|
+
if (interrupts === 1) {
|
|
1627
|
+
process.stdout.write(chalk.dim("\nStopping\u2026\n"));
|
|
1628
|
+
void shutdown().then(() => process.exit(0));
|
|
1629
|
+
return;
|
|
1630
|
+
}
|
|
1631
|
+
if (interrupts === 2) {
|
|
1632
|
+
process.stdout.write(
|
|
1633
|
+
chalk.dim("Still stopping \u2014 press Ctrl-C again to force quit.\n")
|
|
1634
|
+
);
|
|
1635
|
+
return;
|
|
1636
|
+
}
|
|
1637
|
+
process.stdout.write(chalk.dim("Forced.\n"));
|
|
1638
|
+
process.exit(130);
|
|
1462
1639
|
};
|
|
1463
|
-
process.on("SIGINT",
|
|
1464
|
-
process.on("SIGTERM",
|
|
1640
|
+
process.on("SIGINT", onSignal);
|
|
1641
|
+
process.on("SIGTERM", onSignal);
|
|
1465
1642
|
}
|
|
1466
1643
|
process.stdout.write(
|
|
1467
1644
|
[
|
|
@@ -1494,9 +1671,9 @@ function loadEnvFiles(cwd) {
|
|
|
1494
1671
|
}
|
|
1495
1672
|
|
|
1496
1673
|
// src/commands/init.ts
|
|
1497
|
-
import { existsSync as existsSync7, mkdirSync as
|
|
1674
|
+
import { existsSync as existsSync7, mkdirSync as mkdirSync6, readdirSync as readdirSync2, symlinkSync, writeFileSync as writeFileSync6 } from "fs";
|
|
1498
1675
|
import { homedir as homedir2 } from "os";
|
|
1499
|
-
import { dirname as dirname7, join as
|
|
1676
|
+
import { dirname as dirname7, join as join15, resolve as resolve4 } from "path";
|
|
1500
1677
|
|
|
1501
1678
|
// src/commands/init/generators.ts
|
|
1502
1679
|
function generateZooidYaml(opts) {
|
|
@@ -1677,20 +1854,20 @@ function findPiProvider(id) {
|
|
|
1677
1854
|
}
|
|
1678
1855
|
|
|
1679
1856
|
// src/commands/init/sniff.ts
|
|
1680
|
-
import { existsSync as existsSync6, readFileSync as
|
|
1857
|
+
import { existsSync as existsSync6, readFileSync as readFileSync8 } from "fs";
|
|
1681
1858
|
import { homedir } from "os";
|
|
1682
|
-
import { join as
|
|
1859
|
+
import { join as join14 } from "path";
|
|
1683
1860
|
function sniffCredentials(preset, home = homedir()) {
|
|
1684
1861
|
const rel = preset === "pi" ? PI_AUTH_FILE : findSimplePreset(preset)?.credentialDir;
|
|
1685
1862
|
if (!rel) return { found: false };
|
|
1686
|
-
const full =
|
|
1863
|
+
const full = join14(home, rel);
|
|
1687
1864
|
if (existsSync6(full)) return { found: true, path: full };
|
|
1688
1865
|
return { found: false };
|
|
1689
1866
|
}
|
|
1690
1867
|
function sniffPiDefaults(home = homedir()) {
|
|
1691
|
-
const source =
|
|
1868
|
+
const source = join14(home, PI_SETTINGS_FILE);
|
|
1692
1869
|
try {
|
|
1693
|
-
const raw = JSON.parse(
|
|
1870
|
+
const raw = JSON.parse(readFileSync8(source, "utf8"));
|
|
1694
1871
|
const provider = raw.defaultProvider;
|
|
1695
1872
|
const model = raw.defaultModel;
|
|
1696
1873
|
if (typeof provider !== "string" || !provider) return void 0;
|
|
@@ -1715,7 +1892,7 @@ var IGNORED_PREEXISTING = /* @__PURE__ */ new Set([
|
|
|
1715
1892
|
]);
|
|
1716
1893
|
async function runInit(opts) {
|
|
1717
1894
|
const dir = resolve4(opts.dir);
|
|
1718
|
-
|
|
1895
|
+
mkdirSync6(dir, { recursive: true });
|
|
1719
1896
|
if (!opts.force) {
|
|
1720
1897
|
const blocking = readdirSync2(dir).filter((n) => !IGNORED_PREEXISTING.has(n));
|
|
1721
1898
|
if (blocking.length > 0) {
|
|
@@ -1811,14 +1988,14 @@ async function runInit(opts) {
|
|
|
1811
1988
|
}
|
|
1812
1989
|
writes.push({ path: ".gitignore", content: generateGitignore() });
|
|
1813
1990
|
for (const w of writes) {
|
|
1814
|
-
const full =
|
|
1991
|
+
const full = join15(dir, w.path);
|
|
1815
1992
|
const exists = existsSync7(full);
|
|
1816
1993
|
if (exists && !opts.overwrite) {
|
|
1817
1994
|
console.warn(`\u26A0 ${w.path} exists; left as-is (use --force --overwrite to replace)`);
|
|
1818
1995
|
continue;
|
|
1819
1996
|
}
|
|
1820
|
-
|
|
1821
|
-
|
|
1997
|
+
mkdirSync6(dirname7(full), { recursive: true });
|
|
1998
|
+
writeFileSync6(full, w.content);
|
|
1822
1999
|
console.log(`\u2713 Created ${w.path}`);
|
|
1823
2000
|
}
|
|
1824
2001
|
if ((opts.preset === "claude" || opts.preset === "codex") && opts.auth === "subscription") {
|
|
@@ -1843,10 +2020,10 @@ async function runInit(opts) {
|
|
|
1843
2020
|
}
|
|
1844
2021
|
const s = sniffCredentials("pi", opts.home);
|
|
1845
2022
|
if (opts.auth === "subscription" && s.found) {
|
|
1846
|
-
const authSource =
|
|
1847
|
-
const linkPath =
|
|
2023
|
+
const authSource = join15(opts.home ?? homedir2(), PI_AUTH_FILE);
|
|
2024
|
+
const linkPath = join15(dir, `agents/zooid-assistant/${PI_AGENT_DIR}/auth.json`);
|
|
1848
2025
|
if (!existsSync7(linkPath)) {
|
|
1849
|
-
|
|
2026
|
+
mkdirSync6(dirname7(linkPath), { recursive: true });
|
|
1850
2027
|
symlinkSync(authSource, linkPath);
|
|
1851
2028
|
}
|
|
1852
2029
|
console.log(
|
|
@@ -1979,7 +2156,7 @@ async function resolveOptions(flags) {
|
|
|
1979
2156
|
// src/commands/logs.ts
|
|
1980
2157
|
import { readFile, readdir as readdir2, readlink } from "fs/promises";
|
|
1981
2158
|
import { existsSync as existsSync8 } from "fs";
|
|
1982
|
-
import { join as
|
|
2159
|
+
import { join as join16 } from "path";
|
|
1983
2160
|
var KNOWN_SOURCES = ["tuwunel", "daemon", "dev"];
|
|
1984
2161
|
async function runLogs(flags) {
|
|
1985
2162
|
const writer = flags.writer ?? ((s) => process.stdout.write(s));
|
|
@@ -1998,7 +2175,7 @@ async function runLogs(flags) {
|
|
|
1998
2175
|
writer("no logs yet\n");
|
|
1999
2176
|
return;
|
|
2000
2177
|
}
|
|
2001
|
-
const dayDir =
|
|
2178
|
+
const dayDir = join16(flags.dataDir, "logs", day);
|
|
2002
2179
|
if (!existsSync8(dayDir)) {
|
|
2003
2180
|
writer(`no logs for ${day}
|
|
2004
2181
|
`);
|
|
@@ -2027,7 +2204,7 @@ async function runLogs(flags) {
|
|
|
2027
2204
|
writer(await readFile(path, "utf8"));
|
|
2028
2205
|
}
|
|
2029
2206
|
async function resolveTodaySlug(dataDir) {
|
|
2030
|
-
const link =
|
|
2207
|
+
const link = join16(dataDir, "logs", "today");
|
|
2031
2208
|
try {
|
|
2032
2209
|
return await readlink(link);
|
|
2033
2210
|
} catch {
|
|
@@ -2036,18 +2213,18 @@ async function resolveTodaySlug(dataDir) {
|
|
|
2036
2213
|
}
|
|
2037
2214
|
function resolveSourcePath(dayDir, source) {
|
|
2038
2215
|
if (source.startsWith("agent-")) {
|
|
2039
|
-
if (source.endsWith(".acp")) return
|
|
2040
|
-
return
|
|
2216
|
+
if (source.endsWith(".acp")) return join16(dayDir, `${source.slice(0, -4)}.acp.jsonl`);
|
|
2217
|
+
return join16(dayDir, `${source}.log`);
|
|
2041
2218
|
}
|
|
2042
2219
|
if (KNOWN_SOURCES.includes(source))
|
|
2043
|
-
return
|
|
2044
|
-
return
|
|
2220
|
+
return join16(dayDir, `${source}.log`);
|
|
2221
|
+
return join16(dayDir, source);
|
|
2045
2222
|
}
|
|
2046
2223
|
async function dumpByTurn(dayDir, turnId, writer) {
|
|
2047
2224
|
const entries = await readdir2(dayDir);
|
|
2048
2225
|
const taps = entries.filter((e) => e.endsWith(".acp.jsonl")).sort();
|
|
2049
2226
|
for (const f of taps) {
|
|
2050
|
-
const text = await readFile(
|
|
2227
|
+
const text = await readFile(join16(dayDir, f), "utf8");
|
|
2051
2228
|
for (const line of text.split("\n")) {
|
|
2052
2229
|
if (!line) continue;
|
|
2053
2230
|
try {
|
|
@@ -2081,9 +2258,17 @@ async function runStart(flags) {
|
|
|
2081
2258
|
}
|
|
2082
2259
|
|
|
2083
2260
|
// src/commands/status.ts
|
|
2084
|
-
import { readFileSync as
|
|
2085
|
-
import { dirname as dirname8 } from "path";
|
|
2261
|
+
import { readFileSync as readFileSync9 } from "fs";
|
|
2262
|
+
import { dirname as dirname8, join as join17, resolve as resolve6 } from "path";
|
|
2086
2263
|
import chalk2 from "chalk";
|
|
2264
|
+
function readVapidPublicKey(dataDir) {
|
|
2265
|
+
try {
|
|
2266
|
+
const parsed = JSON.parse(readFileSync9(join17(dataDir, VAPID_FILENAME), "utf8"));
|
|
2267
|
+
return typeof parsed.publicKey === "string" ? parsed.publicKey : void 0;
|
|
2268
|
+
} catch {
|
|
2269
|
+
return void 0;
|
|
2270
|
+
}
|
|
2271
|
+
}
|
|
2087
2272
|
async function probe(url, timeoutMs = 2e3) {
|
|
2088
2273
|
try {
|
|
2089
2274
|
const r = await fetch(url, { signal: AbortSignal.timeout(timeoutMs) });
|
|
@@ -2098,15 +2283,17 @@ async function collectStatus(opts) {
|
|
|
2098
2283
|
status: tuwunelUp ? "up" : "down",
|
|
2099
2284
|
url: opts.tuwunelUrl
|
|
2100
2285
|
};
|
|
2286
|
+
const vapidPublicKey = opts.dataDir ? readVapidPublicKey(opts.dataDir) : void 0;
|
|
2101
2287
|
const found = findConfigFile(opts.cwd);
|
|
2102
2288
|
if (!found) {
|
|
2103
2289
|
return {
|
|
2104
2290
|
tuwunel,
|
|
2105
2291
|
daemon: { status: "unknown", reason: "no zooid.yaml" },
|
|
2106
|
-
agents: []
|
|
2292
|
+
agents: [],
|
|
2293
|
+
...vapidPublicKey ? { vapidPublicKey } : {}
|
|
2107
2294
|
};
|
|
2108
2295
|
}
|
|
2109
|
-
const cfg = loadZooidConfig(
|
|
2296
|
+
const cfg = loadZooidConfig(readFileSync9(found.path, "utf8"), {
|
|
2110
2297
|
configDir: dirname8(found.path)
|
|
2111
2298
|
});
|
|
2112
2299
|
const matrixEntry = Object.entries(cfg.transports).find(
|
|
@@ -2128,7 +2315,8 @@ async function collectStatus(opts) {
|
|
|
2128
2315
|
return {
|
|
2129
2316
|
tuwunel,
|
|
2130
2317
|
daemon: { status: daemonUp ? "up" : "down", url: daemonUrl },
|
|
2131
|
-
agents
|
|
2318
|
+
agents,
|
|
2319
|
+
...vapidPublicKey ? { vapidPublicKey } : {}
|
|
2132
2320
|
};
|
|
2133
2321
|
}
|
|
2134
2322
|
async function runStatus(flags) {
|
|
@@ -2137,7 +2325,7 @@ async function runStatus(flags) {
|
|
|
2137
2325
|
if (port === void 0) {
|
|
2138
2326
|
const found = findConfigFile(cwd);
|
|
2139
2327
|
if (found) {
|
|
2140
|
-
const cfg = loadZooidConfig(
|
|
2328
|
+
const cfg = loadZooidConfig(readFileSync9(found.path, "utf8"), {
|
|
2141
2329
|
configDir: dirname8(found.path)
|
|
2142
2330
|
});
|
|
2143
2331
|
const matrix = findMatrixTransport(cfg);
|
|
@@ -2148,7 +2336,8 @@ async function runStatus(flags) {
|
|
|
2148
2336
|
}
|
|
2149
2337
|
}
|
|
2150
2338
|
const tuwunelUrl = `http://localhost:${port ?? 8448}`;
|
|
2151
|
-
const
|
|
2339
|
+
const dataDir = resolve6(cwd, flags.dataDir ?? "./data");
|
|
2340
|
+
const s = await collectStatus({ cwd, tuwunelUrl, dataDir });
|
|
2152
2341
|
const fmt = (st) => st === "up" ? chalk2.green("up") : st === "down" ? chalk2.red("down") : chalk2.yellow("unknown");
|
|
2153
2342
|
process.stdout.write(
|
|
2154
2343
|
[
|
|
@@ -2157,14 +2346,29 @@ async function runStatus(flags) {
|
|
|
2157
2346
|
...s.agents.map(
|
|
2158
2347
|
(a) => ` agent: ${a.name} (${a.userId}, trigger: ${a.trigger})`
|
|
2159
2348
|
),
|
|
2349
|
+
...s.vapidPublicKey ? [`vapid public key: ${s.vapidPublicKey}`] : [],
|
|
2160
2350
|
""
|
|
2161
2351
|
].join("\n")
|
|
2162
2352
|
);
|
|
2163
2353
|
}
|
|
2164
2354
|
|
|
2355
|
+
// src/version.ts
|
|
2356
|
+
import { readFileSync as readFileSync10 } from "fs";
|
|
2357
|
+
function readCliVersion(url = import.meta.url) {
|
|
2358
|
+
try {
|
|
2359
|
+
const manifest = new URL("../package.json", url);
|
|
2360
|
+
const raw = JSON.parse(readFileSync10(manifest, "utf8"));
|
|
2361
|
+
if (typeof raw.version === "string" && raw.version) return raw.version;
|
|
2362
|
+
return "unknown";
|
|
2363
|
+
} catch {
|
|
2364
|
+
return "unknown";
|
|
2365
|
+
}
|
|
2366
|
+
}
|
|
2367
|
+
var CLI_VERSION = readCliVersion();
|
|
2368
|
+
|
|
2165
2369
|
// src/bin.ts
|
|
2166
2370
|
var cli = cac("zooid");
|
|
2167
|
-
cli.command("start", "Run the daemon (production entry-point)").option("--data <dir>", "Persistent data root dir", { default: "./data" }).option("--runtime <local|docker|podman>", "Agent runtime").option("--image <ref>", "Agent container image").option("--print-token", "Print a 32-byte hex token and exit").action(async (flags) => {
|
|
2371
|
+
cli.command("start", "Run the daemon (production entry-point)").option("--data <dir>", "Persistent data root dir", { default: "./data" }).option("--runtime <local|docker|podman>", "Agent runtime").option("--image <ref>", "Agent container image").option("--print-token", "Print a 32-byte hex token and exit").example("$ zooid start --data ./data").example("$ zooid start --runtime docker --image ghcr.io/zooid-ai/agent:latest").action(async (flags) => {
|
|
2168
2372
|
await runStart({
|
|
2169
2373
|
dataDir: flags.data,
|
|
2170
2374
|
runtime: flags.runtime,
|
|
@@ -2175,7 +2379,7 @@ cli.command("start", "Run the daemon (production entry-point)").option("--data <
|
|
|
2175
2379
|
cli.command("dev", "Tuwunel + daemon + UI for local development").option("--data <dir>", "Persistent data root dir", { default: "./data" }).option("--engine <docker|podman>", "Container engine", { default: "docker" }).option("--ui-port <n>", "UI HTTP port", { default: 5173 }).option("--admin-user <name>", "Admin username", { default: "admin" }).option("--admin-password <pw>", "Admin password", { default: "admin" }).option(
|
|
2176
2380
|
"--watch-web [path]",
|
|
2177
2381
|
"Run vite build --watch on @zooid/web. Path defaults to sibling ../zooid-clients/packages/web."
|
|
2178
|
-
).action(async (flags) => {
|
|
2382
|
+
).example("$ zooid dev").example("$ zooid dev --engine podman --ui-port 5174").action(async (flags) => {
|
|
2179
2383
|
await runDev({
|
|
2180
2384
|
dataDir: flags.data,
|
|
2181
2385
|
engine: flags.engine,
|
|
@@ -2188,30 +2392,30 @@ cli.command("dev", "Tuwunel + daemon + UI for local development").option("--data
|
|
|
2188
2392
|
cli.command(
|
|
2189
2393
|
"logs [source]",
|
|
2190
2394
|
'Read captured logs. source=tuwunel|daemon|dev|agent-<name>[.acp], or "prune" to delete old days'
|
|
2191
|
-
).option("--data <dir>", "Persistent data root dir", { default: "./data" }).option("--day <YYYY-MM-DD>", "Day partition (defaults to today)").option("--turn <id>", "Filter ACP taps to a single turn id").option("-f, --follow", "Tail the file (not yet implemented)").option("--keep <n>", "For `logs prune`: days to retain", { default: 14 }).action(async (source, flags) => {
|
|
2395
|
+
).option("--data <dir>", "Persistent data root dir", { default: "./data" }).option("--day <YYYY-MM-DD>", "Day partition (defaults to today)").option("--turn <id>", "Filter ACP taps to a single turn id").option("-f, --follow", "Tail the file (not yet implemented)").option("--keep <n>", "For `logs prune`: days to retain", { default: 14 }).example("$ zooid logs daemon").example("$ zooid logs agent-support.acp --turn 3f9c1a --day 2026-09-06").example("$ zooid logs prune --keep 7").action(async (source, flags) => {
|
|
2192
2396
|
if (source === "prune") {
|
|
2193
2397
|
await runLogs({
|
|
2194
|
-
dataDir:
|
|
2398
|
+
dataDir: resolve7(process.cwd(), flags.data),
|
|
2195
2399
|
subcommand: "prune",
|
|
2196
2400
|
keep: Number(flags.keep)
|
|
2197
2401
|
});
|
|
2198
2402
|
return;
|
|
2199
2403
|
}
|
|
2200
2404
|
await runLogs({
|
|
2201
|
-
dataDir:
|
|
2405
|
+
dataDir: resolve7(process.cwd(), flags.data),
|
|
2202
2406
|
source,
|
|
2203
2407
|
day: flags.day,
|
|
2204
2408
|
turn: flags.turn,
|
|
2205
2409
|
follow: Boolean(flags.follow)
|
|
2206
2410
|
});
|
|
2207
2411
|
});
|
|
2208
|
-
cli.command("status", "Print Tuwunel + daemon health").option("--data <dir>", "Persistent data root dir", { default: "./data" }).option("--port <n>", "Tuwunel host port (defaults to zooid.yaml)").action(async (flags) => {
|
|
2412
|
+
cli.command("status", "Print Tuwunel + daemon health").option("--data <dir>", "Persistent data root dir", { default: "./data" }).option("--port <n>", "Tuwunel host port (defaults to zooid.yaml)").example("$ zooid status").action(async (flags) => {
|
|
2209
2413
|
await runStatus({
|
|
2210
2414
|
dataDir: flags.data,
|
|
2211
2415
|
port: flags.port !== void 0 ? Number(flags.port) : void 0
|
|
2212
2416
|
});
|
|
2213
2417
|
});
|
|
2214
|
-
cli.command("init [dir]", "Scaffold a new zooid workforce in the current (or named) directory").option("--preset <name>", "claude | codex | opencode | pi").option("--auth <mode>", "subscription | api-key (claude/codex/pi only)").option("--model <id>", "Model identifier").option("--provider <id>", "opencode provider: opencode-go | opencode | anthropic | openrouter | custom; pi provider: openrouter | anthropic | openai").option("--api-key <value>", "API key (api-key path; opencode always)").option("--force", "Allow scaffolding into a non-empty directory").option("--overwrite", "With --force, overwrite existing files").option("--no-interactive", "Disable prompts; require all flags up front").action(async (dir, flags) => {
|
|
2418
|
+
cli.command("init [dir]", "Scaffold a new zooid workforce in the current (or named) directory").option("--preset <name>", "claude | codex | opencode | pi").option("--auth <mode>", "subscription | api-key (claude/codex/pi only)").option("--model <id>", "Model identifier").option("--provider <id>", "opencode provider: opencode-go | opencode | anthropic | openrouter | custom; pi provider: openrouter | anthropic | openai").option("--api-key <value>", "API key (api-key path; opencode always)").option("--force", "Allow scaffolding into a non-empty directory").option("--overwrite", "With --force, overwrite existing files").option("--no-interactive", "Disable prompts; require all flags up front").example("$ zooid init").example("$ zooid init my-workforce --preset opencode --provider anthropic").example("$ zooid init --preset claude --auth api-key --api-key sk-... --no-interactive").action(async (dir, flags) => {
|
|
2215
2419
|
const resolved = await resolveOptions({
|
|
2216
2420
|
dir: dir ?? process.cwd(),
|
|
2217
2421
|
preset: flags.preset,
|
|
@@ -2225,7 +2429,39 @@ cli.command("init [dir]", "Scaffold a new zooid workforce in the current (or nam
|
|
|
2225
2429
|
});
|
|
2226
2430
|
await runInit(resolved);
|
|
2227
2431
|
});
|
|
2228
|
-
cli.help()
|
|
2229
|
-
|
|
2230
|
-
cli.
|
|
2432
|
+
cli.command("help [command]", "Display help for zooid, or for a specific command").example("$ zooid help").example("$ zooid help init").action((commandName) => {
|
|
2433
|
+
if (!commandName) {
|
|
2434
|
+
cli.globalCommand.outputHelp();
|
|
2435
|
+
return;
|
|
2436
|
+
}
|
|
2437
|
+
const target = cli.commands.find((c) => c.isMatched(commandName));
|
|
2438
|
+
if (!target) {
|
|
2439
|
+
console.error(`Unknown command: ${commandName}
|
|
2440
|
+
`);
|
|
2441
|
+
cli.globalCommand.outputHelp();
|
|
2442
|
+
process.exitCode = 1;
|
|
2443
|
+
return;
|
|
2444
|
+
}
|
|
2445
|
+
target.outputHelp();
|
|
2446
|
+
});
|
|
2447
|
+
cli.help((sections) => {
|
|
2448
|
+
sections.push({
|
|
2449
|
+
title: "Docs",
|
|
2450
|
+
body: " https://zooid.dev/docs"
|
|
2451
|
+
});
|
|
2452
|
+
return sections;
|
|
2453
|
+
});
|
|
2454
|
+
cli.version(CLI_VERSION);
|
|
2455
|
+
cli.on("command:*", () => {
|
|
2456
|
+
console.error(`Unknown command: ${cli.args.join(" ")}
|
|
2457
|
+
`);
|
|
2458
|
+
cli.outputHelp();
|
|
2459
|
+
process.exitCode = 1;
|
|
2460
|
+
});
|
|
2461
|
+
if (process.argv.slice(2).length === 0) {
|
|
2462
|
+
cli.outputHelp();
|
|
2463
|
+
process.exitCode = 1;
|
|
2464
|
+
} else {
|
|
2465
|
+
cli.parse();
|
|
2466
|
+
}
|
|
2231
2467
|
//# sourceMappingURL=bin.js.map
|