zooid 0.11.2 → 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 +8 -3
- package/dist/bin.js +503 -124
- package/dist/bin.js.map +1 -1
- package/dist/{chunk-KGYQ5YNP.js → chunk-YZ4IO5MR.js} +78 -11
- package/dist/chunk-YZ4IO5MR.js.map +1 -0
- package/dist/index.js +1 -1
- package/package.json +11 -9
- package/src/bin.test.ts +63 -0
- package/src/bin.ts +55 -6
- package/src/bootstrap/configs.test.ts +7 -0
- package/src/bootstrap/configs.ts +14 -0
- package/src/build-registry.ts +1 -1
- package/src/build-registry.zod044.test.ts +23 -0
- package/src/commands/dev.ts +37 -7
- package/src/commands/init/generators.test.ts +43 -0
- package/src/commands/init/generators.ts +24 -5
- package/src/commands/init/pi-scaffold.test.ts +151 -0
- package/src/commands/init/prompts.ts +53 -2
- package/src/commands/init/registry.test.ts +60 -0
- package/src/commands/init/registry.ts +58 -0
- package/src/commands/init/sniff.test.ts +61 -2
- package/src/commands/init/sniff.ts +43 -12
- package/src/commands/init.ts +91 -5
- 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/chunk-KGYQ5YNP.js.map +0 -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,12 +1671,13 @@ function loadEnvFiles(cwd) {
|
|
|
1494
1671
|
}
|
|
1495
1672
|
|
|
1496
1673
|
// src/commands/init.ts
|
|
1497
|
-
import { existsSync as existsSync7, mkdirSync as
|
|
1498
|
-
import {
|
|
1674
|
+
import { existsSync as existsSync7, mkdirSync as mkdirSync6, readdirSync as readdirSync2, symlinkSync, writeFileSync as writeFileSync6 } from "fs";
|
|
1675
|
+
import { homedir as homedir2 } from "os";
|
|
1676
|
+
import { dirname as dirname7, join as join15, resolve as resolve4 } from "path";
|
|
1499
1677
|
|
|
1500
1678
|
// src/commands/init/generators.ts
|
|
1501
1679
|
function generateZooidYaml(opts) {
|
|
1502
|
-
const acpBlock = opts.preset === "opencode" || !opts.model ? ` acp: { preset: ${opts.preset} }` : ` acp:
|
|
1680
|
+
const acpBlock = opts.preset === "opencode" || opts.preset === "pi" || !opts.model ? ` acp: { preset: ${opts.preset} }` : ` acp:
|
|
1503
1681
|
preset: ${opts.preset}
|
|
1504
1682
|
model: ${opts.model}`;
|
|
1505
1683
|
return `runtime: local
|
|
@@ -1586,7 +1764,10 @@ function generateEnv(opts) {
|
|
|
1586
1764
|
`;
|
|
1587
1765
|
}
|
|
1588
1766
|
function generateGitignore() {
|
|
1589
|
-
return [".env", "node_modules/", "data/", ""].join("\n");
|
|
1767
|
+
return [".env", ".pi-agent/", "node_modules/", "data/", ""].join("\n");
|
|
1768
|
+
}
|
|
1769
|
+
function generatePiSettings(opts) {
|
|
1770
|
+
return JSON.stringify({ defaultProvider: opts.provider, defaultModel: opts.model }, null, 2) + "\n";
|
|
1590
1771
|
}
|
|
1591
1772
|
function generateOpencodeReadme() {
|
|
1592
1773
|
return `See https://opencode.ai/config for the full opencode.json schema, including
|
|
@@ -1643,18 +1824,59 @@ function findSimplePreset(name) {
|
|
|
1643
1824
|
function findOpencodeProvider(id) {
|
|
1644
1825
|
return OPENCODE_PROVIDERS.find((p) => p.id === id);
|
|
1645
1826
|
}
|
|
1827
|
+
var PI_PROVIDERS = [
|
|
1828
|
+
{
|
|
1829
|
+
id: "openrouter",
|
|
1830
|
+
label: "OpenRouter",
|
|
1831
|
+
description: "many providers via OpenRouter credits",
|
|
1832
|
+
apiKeyEnvVar: "OPENROUTER_API_KEY"
|
|
1833
|
+
},
|
|
1834
|
+
{
|
|
1835
|
+
id: "anthropic",
|
|
1836
|
+
label: "Anthropic",
|
|
1837
|
+
description: "Claude via direct Anthropic API key",
|
|
1838
|
+
apiKeyEnvVar: "ANTHROPIC_API_KEY"
|
|
1839
|
+
},
|
|
1840
|
+
{
|
|
1841
|
+
id: "openai",
|
|
1842
|
+
label: "OpenAI",
|
|
1843
|
+
description: "GPT via direct OpenAI API key",
|
|
1844
|
+
apiKeyEnvVar: "OPENAI_API_KEY"
|
|
1845
|
+
}
|
|
1846
|
+
];
|
|
1847
|
+
var PI_DEFAULT_PROVIDER = "openrouter";
|
|
1848
|
+
var PI_DEFAULT_MODEL = "deepseek/deepseek-v4-pro";
|
|
1849
|
+
var PI_AGENT_DIR = ".pi-agent";
|
|
1850
|
+
var PI_AUTH_FILE = ".pi/agent/auth.json";
|
|
1851
|
+
var PI_SETTINGS_FILE = ".pi/agent/settings.json";
|
|
1852
|
+
function findPiProvider(id) {
|
|
1853
|
+
return PI_PROVIDERS.find((p) => p.id === id);
|
|
1854
|
+
}
|
|
1646
1855
|
|
|
1647
1856
|
// src/commands/init/sniff.ts
|
|
1648
|
-
import { existsSync as existsSync6 } from "fs";
|
|
1857
|
+
import { existsSync as existsSync6, readFileSync as readFileSync8 } from "fs";
|
|
1649
1858
|
import { homedir } from "os";
|
|
1650
|
-
import { join as
|
|
1859
|
+
import { join as join14 } from "path";
|
|
1651
1860
|
function sniffCredentials(preset, home = homedir()) {
|
|
1652
|
-
const
|
|
1653
|
-
if (!
|
|
1654
|
-
const full =
|
|
1861
|
+
const rel = preset === "pi" ? PI_AUTH_FILE : findSimplePreset(preset)?.credentialDir;
|
|
1862
|
+
if (!rel) return { found: false };
|
|
1863
|
+
const full = join14(home, rel);
|
|
1655
1864
|
if (existsSync6(full)) return { found: true, path: full };
|
|
1656
1865
|
return { found: false };
|
|
1657
1866
|
}
|
|
1867
|
+
function sniffPiDefaults(home = homedir()) {
|
|
1868
|
+
const source = join14(home, PI_SETTINGS_FILE);
|
|
1869
|
+
try {
|
|
1870
|
+
const raw = JSON.parse(readFileSync8(source, "utf8"));
|
|
1871
|
+
const provider = raw.defaultProvider;
|
|
1872
|
+
const model = raw.defaultModel;
|
|
1873
|
+
if (typeof provider !== "string" || !provider) return void 0;
|
|
1874
|
+
if (typeof model !== "string" || !model) return void 0;
|
|
1875
|
+
return { provider, model, source };
|
|
1876
|
+
} catch {
|
|
1877
|
+
return void 0;
|
|
1878
|
+
}
|
|
1879
|
+
}
|
|
1658
1880
|
|
|
1659
1881
|
// src/commands/init.ts
|
|
1660
1882
|
var IGNORED_PREEXISTING = /* @__PURE__ */ new Set([
|
|
@@ -1670,7 +1892,7 @@ var IGNORED_PREEXISTING = /* @__PURE__ */ new Set([
|
|
|
1670
1892
|
]);
|
|
1671
1893
|
async function runInit(opts) {
|
|
1672
1894
|
const dir = resolve4(opts.dir);
|
|
1673
|
-
|
|
1895
|
+
mkdirSync6(dir, { recursive: true });
|
|
1674
1896
|
if (!opts.force) {
|
|
1675
1897
|
const blocking = readdirSync2(dir).filter((n) => !IGNORED_PREEXISTING.has(n));
|
|
1676
1898
|
if (blocking.length > 0) {
|
|
@@ -1680,6 +1902,7 @@ async function runInit(opts) {
|
|
|
1680
1902
|
}
|
|
1681
1903
|
}
|
|
1682
1904
|
const writes = [];
|
|
1905
|
+
let piInherited;
|
|
1683
1906
|
if (opts.preset === "claude" || opts.preset === "codex") {
|
|
1684
1907
|
if (!opts.auth) throw new Error("--auth (subscription|api-key) is required for claude/codex");
|
|
1685
1908
|
const meta = findSimplePreset(opts.preset);
|
|
@@ -1732,19 +1955,47 @@ async function runInit(opts) {
|
|
|
1732
1955
|
content: generateEnv({ envVar: providerMeta.apiKeyEnvVar, value: opts.apiKey })
|
|
1733
1956
|
});
|
|
1734
1957
|
}
|
|
1958
|
+
} else if (opts.preset === "pi") {
|
|
1959
|
+
if (!opts.auth) throw new Error("--auth (subscription|api-key) is required for pi");
|
|
1960
|
+
if (opts.auth === "subscription" && !sniffCredentials("pi", opts.home).found) {
|
|
1961
|
+
throw new Error(
|
|
1962
|
+
"no pi login found \u2014 run `pi` and /login first, or use --auth api-key"
|
|
1963
|
+
);
|
|
1964
|
+
}
|
|
1965
|
+
if (opts.auth === "api-key" && !opts.provider) {
|
|
1966
|
+
throw new Error("--provider is required for pi --auth api-key");
|
|
1967
|
+
}
|
|
1968
|
+
const providerMeta = opts.provider ? findPiProvider(opts.provider) : void 0;
|
|
1969
|
+
if (opts.provider && !providerMeta) throw new Error(`unknown pi provider: ${opts.provider}`);
|
|
1970
|
+
const inherited = sniffPiDefaults(opts.home);
|
|
1971
|
+
const provider = opts.provider ?? inherited?.provider ?? PI_DEFAULT_PROVIDER;
|
|
1972
|
+
const model = opts.model ?? inherited?.model ?? PI_DEFAULT_MODEL;
|
|
1973
|
+
const settingsProvider = opts.model ? provider : inherited?.provider ?? provider;
|
|
1974
|
+
writes.push({ path: "zooid.yaml", content: generateZooidYaml({ preset: "pi" }) });
|
|
1975
|
+
writes.push({ path: "agents/zooid-assistant/AGENTS.md", content: generateAgentsMd() });
|
|
1976
|
+
writes.push({
|
|
1977
|
+
path: `agents/zooid-assistant/${PI_AGENT_DIR}/settings.json`,
|
|
1978
|
+
content: generatePiSettings({ provider: settingsProvider, model })
|
|
1979
|
+
});
|
|
1980
|
+
const envLines = [`PI_CODING_AGENT_DIR=${PI_AGENT_DIR}`];
|
|
1981
|
+
if (opts.auth === "api-key" && opts.apiKey) {
|
|
1982
|
+
envLines.push(`${providerMeta.apiKeyEnvVar}=${opts.apiKey}`);
|
|
1983
|
+
}
|
|
1984
|
+
writes.push({ path: ".env", content: envLines.join("\n") + "\n" });
|
|
1985
|
+
piInherited = inherited;
|
|
1735
1986
|
} else {
|
|
1736
1987
|
throw new Error(`unknown preset: ${String(opts.preset)}`);
|
|
1737
1988
|
}
|
|
1738
1989
|
writes.push({ path: ".gitignore", content: generateGitignore() });
|
|
1739
1990
|
for (const w of writes) {
|
|
1740
|
-
const full =
|
|
1991
|
+
const full = join15(dir, w.path);
|
|
1741
1992
|
const exists = existsSync7(full);
|
|
1742
1993
|
if (exists && !opts.overwrite) {
|
|
1743
1994
|
console.warn(`\u26A0 ${w.path} exists; left as-is (use --force --overwrite to replace)`);
|
|
1744
1995
|
continue;
|
|
1745
1996
|
}
|
|
1746
|
-
|
|
1747
|
-
|
|
1997
|
+
mkdirSync6(dirname7(full), { recursive: true });
|
|
1998
|
+
writeFileSync6(full, w.content);
|
|
1748
1999
|
console.log(`\u2713 Created ${w.path}`);
|
|
1749
2000
|
}
|
|
1750
2001
|
if ((opts.preset === "claude" || opts.preset === "codex") && opts.auth === "subscription") {
|
|
@@ -1757,6 +2008,35 @@ async function runInit(opts) {
|
|
|
1757
2008
|
);
|
|
1758
2009
|
}
|
|
1759
2010
|
}
|
|
2011
|
+
if (opts.preset === "pi") {
|
|
2012
|
+
if (piInherited) {
|
|
2013
|
+
console.log(
|
|
2014
|
+
`\u2713 Inherited defaultProvider=${piInherited.provider} defaultModel=${piInherited.model} from ${piInherited.source}`
|
|
2015
|
+
);
|
|
2016
|
+
} else {
|
|
2017
|
+
console.log(
|
|
2018
|
+
`\u2713 Pinned defaultProvider=${PI_DEFAULT_PROVIDER} defaultModel=${PI_DEFAULT_MODEL} (pi's own default returns an empty turn)`
|
|
2019
|
+
);
|
|
2020
|
+
}
|
|
2021
|
+
const s = sniffCredentials("pi", opts.home);
|
|
2022
|
+
if (opts.auth === "subscription" && s.found) {
|
|
2023
|
+
const authSource = join15(opts.home ?? homedir2(), PI_AUTH_FILE);
|
|
2024
|
+
const linkPath = join15(dir, `agents/zooid-assistant/${PI_AGENT_DIR}/auth.json`);
|
|
2025
|
+
if (!existsSync7(linkPath)) {
|
|
2026
|
+
mkdirSync6(dirname7(linkPath), { recursive: true });
|
|
2027
|
+
symlinkSync(authSource, linkPath);
|
|
2028
|
+
}
|
|
2029
|
+
console.log(
|
|
2030
|
+
`\u2713 Shared pi login from ${s.path} \u2014 the agent uses your subscription via a symlink at ${PI_AGENT_DIR}/auth.json`
|
|
2031
|
+
);
|
|
2032
|
+
} else if (s.found) {
|
|
2033
|
+
console.log(
|
|
2034
|
+
`\u2713 Found pi login at ${s.path} \u2014 the agent gets its own config at ${PI_AGENT_DIR}/ and will not touch it`
|
|
2035
|
+
);
|
|
2036
|
+
} else {
|
|
2037
|
+
console.warn(`\u26A0 No pi login detected \u2014 the agent uses its own API key from .env`);
|
|
2038
|
+
}
|
|
2039
|
+
}
|
|
1760
2040
|
console.log("\nNext: zooid dev");
|
|
1761
2041
|
}
|
|
1762
2042
|
|
|
@@ -1774,10 +2054,11 @@ async function resolveOptions(flags) {
|
|
|
1774
2054
|
choices: [
|
|
1775
2055
|
{ name: "claude (Claude Code)", value: "claude" },
|
|
1776
2056
|
{ name: "codex (OpenAI Codex)", value: "codex" },
|
|
1777
|
-
{ name: "opencode", value: "opencode" }
|
|
2057
|
+
{ name: "opencode", value: "opencode" },
|
|
2058
|
+
{ name: "pi", value: "pi" }
|
|
1778
2059
|
]
|
|
1779
2060
|
}),
|
|
1780
|
-
() => "--preset is required (claude | codex | opencode)"
|
|
2061
|
+
() => "--preset is required (claude | codex | opencode | pi)"
|
|
1781
2062
|
);
|
|
1782
2063
|
if (preset === "opencode") {
|
|
1783
2064
|
const provider = flags.provider ?? "opencode-go";
|
|
@@ -1806,6 +2087,45 @@ async function resolveOptions(flags) {
|
|
|
1806
2087
|
overwrite: flags.overwrite
|
|
1807
2088
|
};
|
|
1808
2089
|
}
|
|
2090
|
+
if (preset === "pi") {
|
|
2091
|
+
const loginFound = sniffCredentials("pi").found;
|
|
2092
|
+
const auth2 = flags.auth ?? (loginFound ? await ask(
|
|
2093
|
+
() => select({
|
|
2094
|
+
message: "A pi login was found. How should the agent authenticate?",
|
|
2095
|
+
choices: [
|
|
2096
|
+
{ name: "Share my pi login (agent uses my existing subscription)", value: "subscription" },
|
|
2097
|
+
{ name: "Give the agent its own key (billed and revoked separately)", value: "api-key" }
|
|
2098
|
+
]
|
|
2099
|
+
}),
|
|
2100
|
+
() => "--auth is required (subscription | api-key)"
|
|
2101
|
+
) : "api-key");
|
|
2102
|
+
if (auth2 === "subscription") {
|
|
2103
|
+
return {
|
|
2104
|
+
dir: flags.dir,
|
|
2105
|
+
preset,
|
|
2106
|
+
auth: auth2,
|
|
2107
|
+
force: flags.force,
|
|
2108
|
+
overwrite: flags.overwrite
|
|
2109
|
+
};
|
|
2110
|
+
}
|
|
2111
|
+
const provider = flags.provider ?? PI_DEFAULT_PROVIDER;
|
|
2112
|
+
const providerMeta = findPiProvider(provider);
|
|
2113
|
+
if (!providerMeta) throw new Error(`unknown pi provider: ${provider}`);
|
|
2114
|
+
const apiKey2 = flags.apiKey ?? await ask(
|
|
2115
|
+
() => password({ message: `${providerMeta.label} API key:` }),
|
|
2116
|
+
() => "--api-key is required"
|
|
2117
|
+
);
|
|
2118
|
+
return {
|
|
2119
|
+
dir: flags.dir,
|
|
2120
|
+
preset,
|
|
2121
|
+
auth: auth2,
|
|
2122
|
+
provider,
|
|
2123
|
+
model: flags.model,
|
|
2124
|
+
apiKey: apiKey2,
|
|
2125
|
+
force: flags.force,
|
|
2126
|
+
overwrite: flags.overwrite
|
|
2127
|
+
};
|
|
2128
|
+
}
|
|
1809
2129
|
const meta = findSimplePreset(preset);
|
|
1810
2130
|
if (!meta) throw new Error(`unknown preset: ${preset}`);
|
|
1811
2131
|
const auth = flags.auth ?? await ask(
|
|
@@ -1836,7 +2156,7 @@ async function resolveOptions(flags) {
|
|
|
1836
2156
|
// src/commands/logs.ts
|
|
1837
2157
|
import { readFile, readdir as readdir2, readlink } from "fs/promises";
|
|
1838
2158
|
import { existsSync as existsSync8 } from "fs";
|
|
1839
|
-
import { join as
|
|
2159
|
+
import { join as join16 } from "path";
|
|
1840
2160
|
var KNOWN_SOURCES = ["tuwunel", "daemon", "dev"];
|
|
1841
2161
|
async function runLogs(flags) {
|
|
1842
2162
|
const writer = flags.writer ?? ((s) => process.stdout.write(s));
|
|
@@ -1855,7 +2175,7 @@ async function runLogs(flags) {
|
|
|
1855
2175
|
writer("no logs yet\n");
|
|
1856
2176
|
return;
|
|
1857
2177
|
}
|
|
1858
|
-
const dayDir =
|
|
2178
|
+
const dayDir = join16(flags.dataDir, "logs", day);
|
|
1859
2179
|
if (!existsSync8(dayDir)) {
|
|
1860
2180
|
writer(`no logs for ${day}
|
|
1861
2181
|
`);
|
|
@@ -1884,7 +2204,7 @@ async function runLogs(flags) {
|
|
|
1884
2204
|
writer(await readFile(path, "utf8"));
|
|
1885
2205
|
}
|
|
1886
2206
|
async function resolveTodaySlug(dataDir) {
|
|
1887
|
-
const link =
|
|
2207
|
+
const link = join16(dataDir, "logs", "today");
|
|
1888
2208
|
try {
|
|
1889
2209
|
return await readlink(link);
|
|
1890
2210
|
} catch {
|
|
@@ -1893,18 +2213,18 @@ async function resolveTodaySlug(dataDir) {
|
|
|
1893
2213
|
}
|
|
1894
2214
|
function resolveSourcePath(dayDir, source) {
|
|
1895
2215
|
if (source.startsWith("agent-")) {
|
|
1896
|
-
if (source.endsWith(".acp")) return
|
|
1897
|
-
return
|
|
2216
|
+
if (source.endsWith(".acp")) return join16(dayDir, `${source.slice(0, -4)}.acp.jsonl`);
|
|
2217
|
+
return join16(dayDir, `${source}.log`);
|
|
1898
2218
|
}
|
|
1899
2219
|
if (KNOWN_SOURCES.includes(source))
|
|
1900
|
-
return
|
|
1901
|
-
return
|
|
2220
|
+
return join16(dayDir, `${source}.log`);
|
|
2221
|
+
return join16(dayDir, source);
|
|
1902
2222
|
}
|
|
1903
2223
|
async function dumpByTurn(dayDir, turnId, writer) {
|
|
1904
2224
|
const entries = await readdir2(dayDir);
|
|
1905
2225
|
const taps = entries.filter((e) => e.endsWith(".acp.jsonl")).sort();
|
|
1906
2226
|
for (const f of taps) {
|
|
1907
|
-
const text = await readFile(
|
|
2227
|
+
const text = await readFile(join16(dayDir, f), "utf8");
|
|
1908
2228
|
for (const line of text.split("\n")) {
|
|
1909
2229
|
if (!line) continue;
|
|
1910
2230
|
try {
|
|
@@ -1938,9 +2258,17 @@ async function runStart(flags) {
|
|
|
1938
2258
|
}
|
|
1939
2259
|
|
|
1940
2260
|
// src/commands/status.ts
|
|
1941
|
-
import { readFileSync as
|
|
1942
|
-
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";
|
|
1943
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
|
+
}
|
|
1944
2272
|
async function probe(url, timeoutMs = 2e3) {
|
|
1945
2273
|
try {
|
|
1946
2274
|
const r = await fetch(url, { signal: AbortSignal.timeout(timeoutMs) });
|
|
@@ -1955,15 +2283,17 @@ async function collectStatus(opts) {
|
|
|
1955
2283
|
status: tuwunelUp ? "up" : "down",
|
|
1956
2284
|
url: opts.tuwunelUrl
|
|
1957
2285
|
};
|
|
2286
|
+
const vapidPublicKey = opts.dataDir ? readVapidPublicKey(opts.dataDir) : void 0;
|
|
1958
2287
|
const found = findConfigFile(opts.cwd);
|
|
1959
2288
|
if (!found) {
|
|
1960
2289
|
return {
|
|
1961
2290
|
tuwunel,
|
|
1962
2291
|
daemon: { status: "unknown", reason: "no zooid.yaml" },
|
|
1963
|
-
agents: []
|
|
2292
|
+
agents: [],
|
|
2293
|
+
...vapidPublicKey ? { vapidPublicKey } : {}
|
|
1964
2294
|
};
|
|
1965
2295
|
}
|
|
1966
|
-
const cfg = loadZooidConfig(
|
|
2296
|
+
const cfg = loadZooidConfig(readFileSync9(found.path, "utf8"), {
|
|
1967
2297
|
configDir: dirname8(found.path)
|
|
1968
2298
|
});
|
|
1969
2299
|
const matrixEntry = Object.entries(cfg.transports).find(
|
|
@@ -1985,7 +2315,8 @@ async function collectStatus(opts) {
|
|
|
1985
2315
|
return {
|
|
1986
2316
|
tuwunel,
|
|
1987
2317
|
daemon: { status: daemonUp ? "up" : "down", url: daemonUrl },
|
|
1988
|
-
agents
|
|
2318
|
+
agents,
|
|
2319
|
+
...vapidPublicKey ? { vapidPublicKey } : {}
|
|
1989
2320
|
};
|
|
1990
2321
|
}
|
|
1991
2322
|
async function runStatus(flags) {
|
|
@@ -1994,7 +2325,7 @@ async function runStatus(flags) {
|
|
|
1994
2325
|
if (port === void 0) {
|
|
1995
2326
|
const found = findConfigFile(cwd);
|
|
1996
2327
|
if (found) {
|
|
1997
|
-
const cfg = loadZooidConfig(
|
|
2328
|
+
const cfg = loadZooidConfig(readFileSync9(found.path, "utf8"), {
|
|
1998
2329
|
configDir: dirname8(found.path)
|
|
1999
2330
|
});
|
|
2000
2331
|
const matrix = findMatrixTransport(cfg);
|
|
@@ -2005,7 +2336,8 @@ async function runStatus(flags) {
|
|
|
2005
2336
|
}
|
|
2006
2337
|
}
|
|
2007
2338
|
const tuwunelUrl = `http://localhost:${port ?? 8448}`;
|
|
2008
|
-
const
|
|
2339
|
+
const dataDir = resolve6(cwd, flags.dataDir ?? "./data");
|
|
2340
|
+
const s = await collectStatus({ cwd, tuwunelUrl, dataDir });
|
|
2009
2341
|
const fmt = (st) => st === "up" ? chalk2.green("up") : st === "down" ? chalk2.red("down") : chalk2.yellow("unknown");
|
|
2010
2342
|
process.stdout.write(
|
|
2011
2343
|
[
|
|
@@ -2014,14 +2346,29 @@ async function runStatus(flags) {
|
|
|
2014
2346
|
...s.agents.map(
|
|
2015
2347
|
(a) => ` agent: ${a.name} (${a.userId}, trigger: ${a.trigger})`
|
|
2016
2348
|
),
|
|
2349
|
+
...s.vapidPublicKey ? [`vapid public key: ${s.vapidPublicKey}`] : [],
|
|
2017
2350
|
""
|
|
2018
2351
|
].join("\n")
|
|
2019
2352
|
);
|
|
2020
2353
|
}
|
|
2021
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
|
+
|
|
2022
2369
|
// src/bin.ts
|
|
2023
2370
|
var cli = cac("zooid");
|
|
2024
|
-
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) => {
|
|
2025
2372
|
await runStart({
|
|
2026
2373
|
dataDir: flags.data,
|
|
2027
2374
|
runtime: flags.runtime,
|
|
@@ -2032,7 +2379,7 @@ cli.command("start", "Run the daemon (production entry-point)").option("--data <
|
|
|
2032
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(
|
|
2033
2380
|
"--watch-web [path]",
|
|
2034
2381
|
"Run vite build --watch on @zooid/web. Path defaults to sibling ../zooid-clients/packages/web."
|
|
2035
|
-
).action(async (flags) => {
|
|
2382
|
+
).example("$ zooid dev").example("$ zooid dev --engine podman --ui-port 5174").action(async (flags) => {
|
|
2036
2383
|
await runDev({
|
|
2037
2384
|
dataDir: flags.data,
|
|
2038
2385
|
engine: flags.engine,
|
|
@@ -2045,30 +2392,30 @@ cli.command("dev", "Tuwunel + daemon + UI for local development").option("--data
|
|
|
2045
2392
|
cli.command(
|
|
2046
2393
|
"logs [source]",
|
|
2047
2394
|
'Read captured logs. source=tuwunel|daemon|dev|agent-<name>[.acp], or "prune" to delete old days'
|
|
2048
|
-
).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) => {
|
|
2049
2396
|
if (source === "prune") {
|
|
2050
2397
|
await runLogs({
|
|
2051
|
-
dataDir:
|
|
2398
|
+
dataDir: resolve7(process.cwd(), flags.data),
|
|
2052
2399
|
subcommand: "prune",
|
|
2053
2400
|
keep: Number(flags.keep)
|
|
2054
2401
|
});
|
|
2055
2402
|
return;
|
|
2056
2403
|
}
|
|
2057
2404
|
await runLogs({
|
|
2058
|
-
dataDir:
|
|
2405
|
+
dataDir: resolve7(process.cwd(), flags.data),
|
|
2059
2406
|
source,
|
|
2060
2407
|
day: flags.day,
|
|
2061
2408
|
turn: flags.turn,
|
|
2062
2409
|
follow: Boolean(flags.follow)
|
|
2063
2410
|
});
|
|
2064
2411
|
});
|
|
2065
|
-
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) => {
|
|
2066
2413
|
await runStatus({
|
|
2067
2414
|
dataDir: flags.data,
|
|
2068
2415
|
port: flags.port !== void 0 ? Number(flags.port) : void 0
|
|
2069
2416
|
});
|
|
2070
2417
|
});
|
|
2071
|
-
cli.command("init [dir]", "Scaffold a new zooid workforce in the current (or named) directory").option("--preset <name>", "claude | codex | opencode").option("--auth <mode>", "subscription | api-key (claude/codex only)").option("--model <id>", "Model identifier").option("--provider <id>", "opencode provider: opencode-go | opencode | anthropic | openrouter | custom").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) => {
|
|
2072
2419
|
const resolved = await resolveOptions({
|
|
2073
2420
|
dir: dir ?? process.cwd(),
|
|
2074
2421
|
preset: flags.preset,
|
|
@@ -2082,7 +2429,39 @@ cli.command("init [dir]", "Scaffold a new zooid workforce in the current (or nam
|
|
|
2082
2429
|
});
|
|
2083
2430
|
await runInit(resolved);
|
|
2084
2431
|
});
|
|
2085
|
-
cli.help()
|
|
2086
|
-
|
|
2087
|
-
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
|
+
}
|
|
2088
2467
|
//# sourceMappingURL=bin.js.map
|