zooid 0.4.1 → 0.5.1
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 +12 -6
- package/dist/index.js +809 -639
- package/package.json +4 -4
package/dist/index.js
CHANGED
|
@@ -240,134 +240,14 @@ async function runChannelDelete(channelId, client) {
|
|
|
240
240
|
const serverUrl = resolveServer();
|
|
241
241
|
if (serverUrl && file.servers?.[serverUrl]?.channels?.[channelId]) {
|
|
242
242
|
delete file.servers[serverUrl].channels[channelId];
|
|
243
|
-
const
|
|
244
|
-
|
|
243
|
+
const fs7 = await import("fs");
|
|
244
|
+
fs7.writeFileSync(getStatePath(), JSON.stringify(file, null, 2) + "\n");
|
|
245
245
|
}
|
|
246
246
|
}
|
|
247
247
|
}
|
|
248
248
|
|
|
249
|
-
// src/commands/
|
|
250
|
-
import
|
|
251
|
-
async function runPublish(channelId, options, client) {
|
|
252
|
-
const c = client ?? createPublishClient(channelId);
|
|
253
|
-
let type;
|
|
254
|
-
let data;
|
|
255
|
-
if (options.file) {
|
|
256
|
-
const raw = fs2.readFileSync(options.file, "utf-8");
|
|
257
|
-
const parsed = JSON.parse(raw);
|
|
258
|
-
type = parsed.type;
|
|
259
|
-
data = parsed.data ?? parsed;
|
|
260
|
-
} else if (options.data) {
|
|
261
|
-
data = JSON.parse(options.data);
|
|
262
|
-
type = options.type;
|
|
263
|
-
} else {
|
|
264
|
-
throw new Error("Either --data or --file is required");
|
|
265
|
-
}
|
|
266
|
-
const publishOpts = { data };
|
|
267
|
-
if (type) publishOpts.type = type;
|
|
268
|
-
return c.publish(channelId, publishOpts);
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
// src/commands/subscribe.ts
|
|
272
|
-
async function runSubscribePoll(channelId, options = {}, client) {
|
|
273
|
-
const c = client ?? createSubscribeClient(channelId);
|
|
274
|
-
const callback = options.callback ?? ((event) => {
|
|
275
|
-
console.log(JSON.stringify(event));
|
|
276
|
-
});
|
|
277
|
-
return c.subscribe(channelId, callback, {
|
|
278
|
-
interval: options.interval ?? 5e3,
|
|
279
|
-
mode: options.mode,
|
|
280
|
-
type: options.type
|
|
281
|
-
});
|
|
282
|
-
}
|
|
283
|
-
async function runSubscribeWebhook(channelId, url, client) {
|
|
284
|
-
const c = client ?? createSubscribeClient(channelId);
|
|
285
|
-
return c.registerWebhook(channelId, url);
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
// src/commands/tail.ts
|
|
289
|
-
async function runTail(channelId, options = {}, client) {
|
|
290
|
-
const c = client ?? createSubscribeClient(channelId);
|
|
291
|
-
if (options.follow) {
|
|
292
|
-
return runTailFollow(c, channelId, options);
|
|
293
|
-
}
|
|
294
|
-
const pollOpts = {};
|
|
295
|
-
if (options.limit !== void 0) pollOpts.limit = options.limit;
|
|
296
|
-
if (options.type) pollOpts.type = options.type;
|
|
297
|
-
if (options.since) pollOpts.since = options.since;
|
|
298
|
-
if (options.cursor) pollOpts.cursor = options.cursor;
|
|
299
|
-
return c.tail(channelId, pollOpts);
|
|
300
|
-
}
|
|
301
|
-
async function runTailFollow(client, channelId, options) {
|
|
302
|
-
const tailOpts = {
|
|
303
|
-
follow: true,
|
|
304
|
-
mode: options.mode,
|
|
305
|
-
interval: options.interval,
|
|
306
|
-
type: options.type
|
|
307
|
-
};
|
|
308
|
-
const stream = client.tail(channelId, tailOpts);
|
|
309
|
-
for await (const event of stream) {
|
|
310
|
-
console.log(JSON.stringify(event));
|
|
311
|
-
}
|
|
312
|
-
return void 0;
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
// src/commands/status.ts
|
|
316
|
-
async function runStatus(client) {
|
|
317
|
-
const c = client ?? createClient();
|
|
318
|
-
const [discovery, identity] = await Promise.all([
|
|
319
|
-
c.getMetadata(),
|
|
320
|
-
c.getServerMeta()
|
|
321
|
-
]);
|
|
322
|
-
return { discovery, identity };
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
// src/commands/history.ts
|
|
326
|
-
function runHistory() {
|
|
327
|
-
const file = loadConfigFile();
|
|
328
|
-
const entries = [];
|
|
329
|
-
if (!file.servers) return entries;
|
|
330
|
-
const seen = /* @__PURE__ */ new Map();
|
|
331
|
-
for (const [serverUrl, serverConfig] of Object.entries(file.servers)) {
|
|
332
|
-
if (!serverConfig.channels) continue;
|
|
333
|
-
const normalizedServer = normalizeServerUrl(serverUrl);
|
|
334
|
-
for (const [channelId, channelData] of Object.entries(
|
|
335
|
-
serverConfig.channels
|
|
336
|
-
)) {
|
|
337
|
-
if (!channelData.stats) continue;
|
|
338
|
-
const key = `${normalizedServer}\0${channelId}`;
|
|
339
|
-
const existingIdx = seen.get(key);
|
|
340
|
-
if (existingIdx !== void 0) {
|
|
341
|
-
const existing = entries[existingIdx];
|
|
342
|
-
existing.num_tails += channelData.stats.num_tails;
|
|
343
|
-
if (channelData.stats.last_tailed_at > existing.last_tailed_at) {
|
|
344
|
-
existing.last_tailed_at = channelData.stats.last_tailed_at;
|
|
345
|
-
existing.name = channelData.name ?? existing.name;
|
|
346
|
-
}
|
|
347
|
-
if (channelData.stats.first_tailed_at < existing.first_tailed_at) {
|
|
348
|
-
existing.first_tailed_at = channelData.stats.first_tailed_at;
|
|
349
|
-
}
|
|
350
|
-
} else {
|
|
351
|
-
seen.set(key, entries.length);
|
|
352
|
-
entries.push({
|
|
353
|
-
server: normalizedServer,
|
|
354
|
-
channel_id: channelId,
|
|
355
|
-
name: channelData.name,
|
|
356
|
-
num_tails: channelData.stats.num_tails,
|
|
357
|
-
last_tailed_at: channelData.stats.last_tailed_at,
|
|
358
|
-
first_tailed_at: channelData.stats.first_tailed_at
|
|
359
|
-
});
|
|
360
|
-
}
|
|
361
|
-
}
|
|
362
|
-
}
|
|
363
|
-
entries.sort(
|
|
364
|
-
(a, b) => new Date(b.last_tailed_at).getTime() - new Date(a.last_tailed_at).getTime()
|
|
365
|
-
);
|
|
366
|
-
return entries;
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
// src/commands/share.ts
|
|
370
|
-
import readline from "readline/promises";
|
|
249
|
+
// src/commands/push.ts
|
|
250
|
+
import readline3 from "readline/promises";
|
|
371
251
|
|
|
372
252
|
// src/lib/output.ts
|
|
373
253
|
function printSuccess(message) {
|
|
@@ -393,99 +273,6 @@ function formatRelative(isoString) {
|
|
|
393
273
|
return `${days}d ago`;
|
|
394
274
|
}
|
|
395
275
|
|
|
396
|
-
// src/lib/directory.ts
|
|
397
|
-
var DIRECTORY_BASE_URL = "https://directory.zooid.dev";
|
|
398
|
-
var TOKEN_PREFIX = "zd_";
|
|
399
|
-
var POLL_INTERVAL_MS = 3e3;
|
|
400
|
-
var POLL_TIMEOUT_MS = 2 * 60 * 1e3;
|
|
401
|
-
function getDirectoryToken() {
|
|
402
|
-
const token = loadDirectoryToken();
|
|
403
|
-
if (token && token.startsWith(TOKEN_PREFIX)) {
|
|
404
|
-
return token;
|
|
405
|
-
}
|
|
406
|
-
return void 0;
|
|
407
|
-
}
|
|
408
|
-
async function ensureDirectoryToken() {
|
|
409
|
-
const existing = getDirectoryToken();
|
|
410
|
-
if (existing) return existing;
|
|
411
|
-
return deviceAuth();
|
|
412
|
-
}
|
|
413
|
-
async function deviceAuth() {
|
|
414
|
-
const res = await fetch(`${DIRECTORY_BASE_URL}/api/auth/device`, {
|
|
415
|
-
method: "POST",
|
|
416
|
-
headers: { "Content-Type": "application/json" }
|
|
417
|
-
});
|
|
418
|
-
if (!res.ok) {
|
|
419
|
-
throw new Error(`Directory auth failed: ${res.status} ${res.statusText}`);
|
|
420
|
-
}
|
|
421
|
-
const data = await res.json();
|
|
422
|
-
const { device_code, verification_url } = data;
|
|
423
|
-
console.log("");
|
|
424
|
-
printInfo("Authorize", verification_url);
|
|
425
|
-
console.log(" Opening browser to complete GitHub sign-in...\n");
|
|
426
|
-
try {
|
|
427
|
-
const { exec } = await import("child_process");
|
|
428
|
-
const platform = process.platform;
|
|
429
|
-
const cmd = platform === "darwin" ? "open" : platform === "win32" ? "start" : "xdg-open";
|
|
430
|
-
exec(`${cmd} "${verification_url}"`);
|
|
431
|
-
} catch {
|
|
432
|
-
console.log(
|
|
433
|
-
" Could not open browser automatically. Please visit the URL above.\n"
|
|
434
|
-
);
|
|
435
|
-
}
|
|
436
|
-
const deadline = Date.now() + POLL_TIMEOUT_MS;
|
|
437
|
-
while (Date.now() < deadline) {
|
|
438
|
-
await sleep(POLL_INTERVAL_MS);
|
|
439
|
-
const statusRes = await fetch(
|
|
440
|
-
`${DIRECTORY_BASE_URL}/api/auth/status?code=${encodeURIComponent(device_code)}`
|
|
441
|
-
);
|
|
442
|
-
if (!statusRes.ok) continue;
|
|
443
|
-
const status = await statusRes.json();
|
|
444
|
-
if (status.status === "complete" && status.token) {
|
|
445
|
-
saveDirectoryToken(status.token);
|
|
446
|
-
console.log(" Authenticated with Zooid Directory.\n");
|
|
447
|
-
return status.token;
|
|
448
|
-
}
|
|
449
|
-
if (status.status === "expired") {
|
|
450
|
-
throw new Error("Device authorization expired. Please try again.");
|
|
451
|
-
}
|
|
452
|
-
}
|
|
453
|
-
throw new Error(
|
|
454
|
-
"Authorization timed out. You need your human to authorize you. Run `npx zooid share` again to retry."
|
|
455
|
-
);
|
|
456
|
-
}
|
|
457
|
-
async function directoryFetch(path5, options = {}) {
|
|
458
|
-
let token = await ensureDirectoryToken();
|
|
459
|
-
const doFetch = (t) => {
|
|
460
|
-
const headers = new Headers(options.headers);
|
|
461
|
-
headers.set("Authorization", `Bearer ${t}`);
|
|
462
|
-
headers.set("Content-Type", "application/json");
|
|
463
|
-
return fetch(`${DIRECTORY_BASE_URL}${path5}`, { ...options, headers });
|
|
464
|
-
};
|
|
465
|
-
let res = await doFetch(token);
|
|
466
|
-
if (res.status === 401) {
|
|
467
|
-
console.log(" Directory token expired. Re-authenticating...\n");
|
|
468
|
-
token = await deviceAuth();
|
|
469
|
-
res = await doFetch(token);
|
|
470
|
-
}
|
|
471
|
-
return res;
|
|
472
|
-
}
|
|
473
|
-
function sleep(ms) {
|
|
474
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
475
|
-
}
|
|
476
|
-
async function formatDirectoryError(res) {
|
|
477
|
-
let msg = `Directory returned ${res.status}`;
|
|
478
|
-
try {
|
|
479
|
-
const body = await res.json();
|
|
480
|
-
const parts = [];
|
|
481
|
-
if (body.error) parts.push(String(body.error));
|
|
482
|
-
if (body.message) parts.push(String(body.message));
|
|
483
|
-
if (parts.length > 0) msg = parts.join(": ");
|
|
484
|
-
} catch {
|
|
485
|
-
}
|
|
486
|
-
return msg;
|
|
487
|
-
}
|
|
488
|
-
|
|
489
276
|
// src/lib/prompts.ts
|
|
490
277
|
async function ask(rl, label, defaultValue) {
|
|
491
278
|
const hint = defaultValue ? ` [${defaultValue}]` : "";
|
|
@@ -493,221 +280,15 @@ async function ask(rl, label, defaultValue) {
|
|
|
493
280
|
return answer.trim() || defaultValue;
|
|
494
281
|
}
|
|
495
282
|
|
|
496
|
-
// src/commands/
|
|
497
|
-
|
|
498
|
-
const client = createClient();
|
|
499
|
-
const config = loadConfig();
|
|
500
|
-
const serverUrl = config.server;
|
|
501
|
-
if (!serverUrl) {
|
|
502
|
-
throw new Error(
|
|
503
|
-
"No server configured. Run: npx zooid config set server <url>"
|
|
504
|
-
);
|
|
505
|
-
}
|
|
506
|
-
const allChannels = await client.listChannels();
|
|
507
|
-
const publicChannels = allChannels.filter((ch) => ch.is_public);
|
|
508
|
-
if (publicChannels.length === 0) {
|
|
509
|
-
throw new Error("No public channels found on this server.");
|
|
510
|
-
}
|
|
511
|
-
let selected;
|
|
512
|
-
if (channelIds.length > 0) {
|
|
513
|
-
const byId = new Map(allChannels.map((ch) => [ch.id, ch]));
|
|
514
|
-
const missing = [];
|
|
515
|
-
selected = [];
|
|
516
|
-
for (const id of channelIds) {
|
|
517
|
-
const ch = byId.get(id);
|
|
518
|
-
if (!ch) {
|
|
519
|
-
missing.push(id);
|
|
520
|
-
} else {
|
|
521
|
-
selected.push(ch);
|
|
522
|
-
}
|
|
523
|
-
}
|
|
524
|
-
if (missing.length > 0) {
|
|
525
|
-
throw new Error(`Channels not found: ${missing.join(", ")}`);
|
|
526
|
-
}
|
|
527
|
-
const privateChannels = selected.filter((ch) => !ch.is_public);
|
|
528
|
-
if (privateChannels.length > 0) {
|
|
529
|
-
throw new Error(
|
|
530
|
-
`Cannot share private channels: ${privateChannels.map((ch) => ch.id).join(", ")}. Only public channels can be listed in the directory.`
|
|
531
|
-
);
|
|
532
|
-
}
|
|
533
|
-
} else if (options.yes) {
|
|
534
|
-
selected = publicChannels;
|
|
535
|
-
} else {
|
|
536
|
-
selected = await pickChannels(publicChannels);
|
|
537
|
-
if (selected.length === 0) {
|
|
538
|
-
throw new Error("No channels selected.");
|
|
539
|
-
}
|
|
540
|
-
}
|
|
541
|
-
const channelDetails = await promptChannelDetails(selected, options.yes);
|
|
542
|
-
const ids = selected.map((ch) => ch.id);
|
|
543
|
-
const { claim, signature } = await client.getClaim(ids);
|
|
544
|
-
const channels = selected.map((ch) => {
|
|
545
|
-
const details = channelDetails.get(ch.id);
|
|
546
|
-
const entry = {
|
|
547
|
-
channel_id: ch.id,
|
|
548
|
-
name: ch.name
|
|
549
|
-
};
|
|
550
|
-
if (details.description) entry.description = details.description;
|
|
551
|
-
if (details.tags.length > 0) entry.tags = details.tags;
|
|
552
|
-
return entry;
|
|
553
|
-
});
|
|
554
|
-
const res = await directoryFetch("/api/servers", {
|
|
555
|
-
method: "POST",
|
|
556
|
-
body: JSON.stringify({ server_url: serverUrl, claim, signature, channels })
|
|
557
|
-
});
|
|
558
|
-
if (!res.ok) {
|
|
559
|
-
throw new Error(await formatDirectoryError(res));
|
|
560
|
-
}
|
|
561
|
-
await res.json();
|
|
562
|
-
console.log("");
|
|
563
|
-
for (const ch of selected) {
|
|
564
|
-
console.log(` ${ch.id} \u2192 ${serverUrl}/${ch.id}`);
|
|
565
|
-
}
|
|
566
|
-
console.log("");
|
|
567
|
-
console.log(` Any zooid can find your channel using:`);
|
|
568
|
-
console.log(` npx zooid discover --query ${selected[0].id}`);
|
|
569
|
-
const tags = [
|
|
570
|
-
...new Set(selected.flatMap((ch) => channelDetails.get(ch.id)?.tags ?? []))
|
|
571
|
-
];
|
|
572
|
-
if (tags.length > 0) {
|
|
573
|
-
console.log(` npx zooid discover --tag ${tags[0]}`);
|
|
574
|
-
}
|
|
575
|
-
console.log("");
|
|
576
|
-
}
|
|
577
|
-
async function pickChannels(channels) {
|
|
578
|
-
const { default: checkbox } = await import("@inquirer/checkbox");
|
|
579
|
-
const selected = await checkbox({
|
|
580
|
-
message: "Select channels to share",
|
|
581
|
-
choices: channels.map((ch) => ({
|
|
582
|
-
name: ch.description ? `${ch.id} \u2014 ${ch.description}` : ch.id,
|
|
583
|
-
value: ch.id,
|
|
584
|
-
checked: true
|
|
585
|
-
})),
|
|
586
|
-
theme: {
|
|
587
|
-
icon: { cursor: "> " },
|
|
588
|
-
style: { highlight: (text) => text }
|
|
589
|
-
}
|
|
590
|
-
});
|
|
591
|
-
const selectedSet = new Set(selected);
|
|
592
|
-
return channels.filter((ch) => selectedSet.has(ch.id));
|
|
593
|
-
}
|
|
594
|
-
async function promptChannelDetails(channels, skipPrompt) {
|
|
595
|
-
const result = /* @__PURE__ */ new Map();
|
|
596
|
-
if (skipPrompt) {
|
|
597
|
-
for (const ch of channels) {
|
|
598
|
-
result.set(ch.id, {
|
|
599
|
-
description: ch.description ?? "",
|
|
600
|
-
tags: ch.tags
|
|
601
|
-
});
|
|
602
|
-
}
|
|
603
|
-
return result;
|
|
604
|
-
}
|
|
605
|
-
const rl = readline.createInterface({
|
|
606
|
-
input: process.stdin,
|
|
607
|
-
output: process.stdout
|
|
608
|
-
});
|
|
609
|
-
try {
|
|
610
|
-
console.log("");
|
|
611
|
-
console.log(" Set description and tags for each channel.");
|
|
612
|
-
console.log(" Press Enter to accept defaults shown in [brackets].\n");
|
|
613
|
-
for (const ch of channels) {
|
|
614
|
-
console.log(` ${ch.id}:`);
|
|
615
|
-
const desc = await ask(rl, "Description", ch.description ?? "");
|
|
616
|
-
const tagsRaw = await ask(
|
|
617
|
-
rl,
|
|
618
|
-
"Tags (comma-separated)",
|
|
619
|
-
ch.tags.join(", ")
|
|
620
|
-
);
|
|
621
|
-
const tags = tagsRaw.split(",").map((t) => t.trim()).filter(Boolean);
|
|
622
|
-
result.set(ch.id, { description: desc, tags });
|
|
623
|
-
console.log("");
|
|
624
|
-
}
|
|
625
|
-
} finally {
|
|
626
|
-
rl.close();
|
|
627
|
-
}
|
|
628
|
-
return result;
|
|
629
|
-
}
|
|
630
|
-
|
|
631
|
-
// src/commands/unshare.ts
|
|
632
|
-
async function runUnshare(channelId) {
|
|
633
|
-
const client = createClient();
|
|
634
|
-
const config = loadConfig();
|
|
635
|
-
const serverUrl = config.server;
|
|
636
|
-
if (!serverUrl) {
|
|
637
|
-
throw new Error(
|
|
638
|
-
"No server configured. Run: npx zooid config set server <url>"
|
|
639
|
-
);
|
|
640
|
-
}
|
|
641
|
-
const { claim, signature } = await client.getClaim([channelId], "delete");
|
|
642
|
-
const res = await directoryFetch("/api/servers/channels", {
|
|
643
|
-
method: "DELETE",
|
|
644
|
-
body: JSON.stringify({
|
|
645
|
-
server_url: serverUrl,
|
|
646
|
-
channel_id: channelId,
|
|
647
|
-
claim,
|
|
648
|
-
signature
|
|
649
|
-
})
|
|
650
|
-
});
|
|
651
|
-
if (!res.ok) {
|
|
652
|
-
throw new Error(await formatDirectoryError(res));
|
|
653
|
-
}
|
|
654
|
-
}
|
|
655
|
-
|
|
656
|
-
// src/commands/discover.ts
|
|
657
|
-
async function runDiscover(options) {
|
|
658
|
-
const params = new URLSearchParams();
|
|
659
|
-
if (options.query) params.set("q", options.query);
|
|
660
|
-
if (options.tag) params.set("tag", options.tag);
|
|
661
|
-
if (options.limit) params.set("limit", String(options.limit));
|
|
662
|
-
const qs = params.toString();
|
|
663
|
-
const url = `${DIRECTORY_BASE_URL}/api/discover${qs ? `?${qs}` : ""}`;
|
|
664
|
-
const res = await fetch(url);
|
|
665
|
-
if (!res.ok) {
|
|
666
|
-
throw new Error(`Directory returned ${res.status}`);
|
|
667
|
-
}
|
|
668
|
-
const data = await res.json();
|
|
669
|
-
if (data.channels.length === 0) {
|
|
670
|
-
console.log("No channels found.");
|
|
671
|
-
return;
|
|
672
|
-
}
|
|
673
|
-
console.log("");
|
|
674
|
-
for (const ch of data.channels) {
|
|
675
|
-
const host = new URL(ch.server_url).host;
|
|
676
|
-
const tags = ch.tags.length > 0 ? ` [${ch.tags.join(", ")}]` : "";
|
|
677
|
-
const desc = ch.description ? ` \u2014 ${ch.description}` : "";
|
|
678
|
-
console.log(` ${host}/${ch.channel_id}${desc}${tags}`);
|
|
679
|
-
}
|
|
680
|
-
console.log(`
|
|
681
|
-
${data.total} channel${data.total === 1 ? "" : "s"} found.`);
|
|
682
|
-
console.log("");
|
|
683
|
-
}
|
|
684
|
-
|
|
685
|
-
// src/commands/server.ts
|
|
686
|
-
async function runServerGet(client) {
|
|
687
|
-
const c = client ?? createClient();
|
|
688
|
-
return c.getServerMeta();
|
|
689
|
-
}
|
|
690
|
-
async function runServerSet(fields, client) {
|
|
691
|
-
const c = client ?? createClient();
|
|
692
|
-
return c.updateServerMeta(fields);
|
|
693
|
-
}
|
|
694
|
-
|
|
695
|
-
// src/commands/token.ts
|
|
696
|
-
async function runTokenMint(scopes, options) {
|
|
697
|
-
const client = createClient();
|
|
698
|
-
const body = { scopes };
|
|
699
|
-
if (options.sub) body.sub = options.sub;
|
|
700
|
-
if (options.name) body.name = options.name;
|
|
701
|
-
if (options.expiresIn) body.expires_in = options.expiresIn;
|
|
702
|
-
return client.mintToken(body);
|
|
703
|
-
}
|
|
704
|
-
|
|
705
|
-
// src/commands/dev.ts
|
|
706
|
-
import { execSync, spawn as spawn2 } from "child_process";
|
|
283
|
+
// src/commands/deploy.ts
|
|
284
|
+
import { execSync, spawnSync } from "child_process";
|
|
707
285
|
import crypto2 from "crypto";
|
|
708
286
|
import fs3 from "fs";
|
|
709
|
-
import
|
|
710
|
-
import
|
|
287
|
+
import os from "os";
|
|
288
|
+
import path3 from "path";
|
|
289
|
+
import readline2 from "readline/promises";
|
|
290
|
+
import { createRequire } from "module";
|
|
291
|
+
import { ZooidClient } from "@zooid/sdk";
|
|
711
292
|
|
|
712
293
|
// src/lib/crypto.ts
|
|
713
294
|
function base64url(buf) {
|
|
@@ -770,82 +351,23 @@ async function createEdDSAAdminToken(privateKeyJwk, kid) {
|
|
|
770
351
|
return `${message}.${signature}`;
|
|
771
352
|
}
|
|
772
353
|
|
|
773
|
-
// src/commands/dev.ts
|
|
774
|
-
function findServerDir() {
|
|
775
|
-
const cliDir = path2.dirname(fileURLToPath(import.meta.url));
|
|
776
|
-
return path2.resolve(cliDir, "../../server");
|
|
777
|
-
}
|
|
778
|
-
async function runDev(port = 8787) {
|
|
779
|
-
const serverDir = findServerDir();
|
|
780
|
-
if (!fs3.existsSync(path2.join(serverDir, "wrangler.toml"))) {
|
|
781
|
-
throw new Error(
|
|
782
|
-
`Server directory not found at ${serverDir}. Make sure you're running from the zooid monorepo.`
|
|
783
|
-
);
|
|
784
|
-
}
|
|
785
|
-
const jwtSecret = crypto2.randomUUID();
|
|
786
|
-
const devVarsPath = path2.join(serverDir, ".dev.vars");
|
|
787
|
-
fs3.writeFileSync(devVarsPath, `ZOOID_JWT_SECRET=${jwtSecret}
|
|
788
|
-
`);
|
|
789
|
-
const adminToken = await createAdminToken(jwtSecret);
|
|
790
|
-
const serverUrl = `http://localhost:${port}`;
|
|
791
|
-
saveConfig({ admin_token: adminToken }, serverUrl);
|
|
792
|
-
const schemaPath = path2.join(serverDir, "src/db/schema.sql");
|
|
793
|
-
if (fs3.existsSync(schemaPath)) {
|
|
794
|
-
try {
|
|
795
|
-
execSync(
|
|
796
|
-
`npx wrangler d1 execute zooid-db --local --file=${schemaPath}`,
|
|
797
|
-
{
|
|
798
|
-
cwd: serverDir,
|
|
799
|
-
stdio: "pipe"
|
|
800
|
-
}
|
|
801
|
-
);
|
|
802
|
-
printSuccess("Database schema initialized");
|
|
803
|
-
} catch {
|
|
804
|
-
printSuccess("Database ready (schema already exists)");
|
|
805
|
-
}
|
|
806
|
-
}
|
|
807
|
-
printSuccess("Local server configured");
|
|
808
|
-
printInfo("Server", serverUrl);
|
|
809
|
-
printInfo("Admin token", adminToken.slice(0, 20) + "...");
|
|
810
|
-
printInfo("Config saved to", "~/.zooid/state.json");
|
|
811
|
-
console.log("");
|
|
812
|
-
console.log("Starting wrangler dev...");
|
|
813
|
-
console.log("");
|
|
814
|
-
const child = spawn2(
|
|
815
|
-
"npx",
|
|
816
|
-
["wrangler", "dev", "--local", "--port", String(port)],
|
|
817
|
-
{
|
|
818
|
-
cwd: serverDir,
|
|
819
|
-
stdio: "inherit",
|
|
820
|
-
shell: true
|
|
821
|
-
}
|
|
822
|
-
);
|
|
823
|
-
child.on("error", (err) => {
|
|
824
|
-
console.error(`Failed to start local server: ${err.message}`);
|
|
825
|
-
process.exit(1);
|
|
826
|
-
});
|
|
827
|
-
child.on("exit", (code) => {
|
|
828
|
-
process.exit(code ?? 0);
|
|
829
|
-
});
|
|
830
|
-
}
|
|
831
|
-
|
|
832
354
|
// src/commands/init.ts
|
|
833
|
-
import
|
|
834
|
-
import
|
|
835
|
-
import
|
|
355
|
+
import fs2 from "fs";
|
|
356
|
+
import path2 from "path";
|
|
357
|
+
import readline from "readline/promises";
|
|
836
358
|
var CONFIG_FILENAME = "zooid.json";
|
|
837
359
|
function getConfigPath() {
|
|
838
|
-
return
|
|
360
|
+
return path2.join(process.cwd(), CONFIG_FILENAME);
|
|
839
361
|
}
|
|
840
362
|
function loadServerConfig() {
|
|
841
363
|
const configPath = getConfigPath();
|
|
842
|
-
if (!
|
|
843
|
-
const raw =
|
|
364
|
+
if (!fs2.existsSync(configPath)) return null;
|
|
365
|
+
const raw = fs2.readFileSync(configPath, "utf-8");
|
|
844
366
|
return JSON.parse(raw);
|
|
845
367
|
}
|
|
846
368
|
function saveServerConfig(config) {
|
|
847
369
|
const configPath = getConfigPath();
|
|
848
|
-
|
|
370
|
+
fs2.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
|
|
849
371
|
}
|
|
850
372
|
async function runInit() {
|
|
851
373
|
const configPath = getConfigPath();
|
|
@@ -854,7 +376,7 @@ async function runInit() {
|
|
|
854
376
|
printInfo("Found existing", configPath);
|
|
855
377
|
console.log("");
|
|
856
378
|
}
|
|
857
|
-
const rl =
|
|
379
|
+
const rl = readline.createInterface({
|
|
858
380
|
input: process.stdin,
|
|
859
381
|
output: process.stdout
|
|
860
382
|
});
|
|
@@ -891,7 +413,15 @@ async function runInit() {
|
|
|
891
413
|
tags,
|
|
892
414
|
url
|
|
893
415
|
};
|
|
894
|
-
|
|
416
|
+
fs2.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
|
|
417
|
+
const channelsDir = path2.join(process.cwd(), "channels");
|
|
418
|
+
const rolesDir = path2.join(process.cwd(), "roles");
|
|
419
|
+
for (const dir of [channelsDir, rolesDir]) {
|
|
420
|
+
if (!fs2.existsSync(dir)) {
|
|
421
|
+
fs2.mkdirSync(dir, { recursive: true });
|
|
422
|
+
fs2.writeFileSync(path2.join(dir, ".gitkeep"), "");
|
|
423
|
+
}
|
|
424
|
+
}
|
|
895
425
|
console.log("");
|
|
896
426
|
printSuccess(`Saved ${CONFIG_FILENAME}`);
|
|
897
427
|
console.log("");
|
|
@@ -905,6 +435,8 @@ async function runInit() {
|
|
|
905
435
|
config.tags.length > 0 ? config.tags.join(", ") : "(none)"
|
|
906
436
|
);
|
|
907
437
|
printInfo("URL", config.url || "(not set)");
|
|
438
|
+
printInfo("Channels", "channels/");
|
|
439
|
+
printInfo("Roles", "roles/");
|
|
908
440
|
console.log("");
|
|
909
441
|
} finally {
|
|
910
442
|
rl.close();
|
|
@@ -912,23 +444,15 @@ async function runInit() {
|
|
|
912
444
|
}
|
|
913
445
|
|
|
914
446
|
// src/commands/deploy.ts
|
|
915
|
-
import { execSync as execSync2, spawnSync } from "child_process";
|
|
916
|
-
import crypto3 from "crypto";
|
|
917
|
-
import fs5 from "fs";
|
|
918
|
-
import os from "os";
|
|
919
|
-
import path4 from "path";
|
|
920
|
-
import readline3 from "readline/promises";
|
|
921
|
-
import { createRequire } from "module";
|
|
922
|
-
import { ZooidClient } from "@zooid/sdk";
|
|
923
447
|
var require2 = createRequire(import.meta.url);
|
|
924
448
|
function resolvePackageDir(packageName) {
|
|
925
449
|
const pkgJson = require2.resolve(`${packageName}/package.json`);
|
|
926
|
-
return
|
|
450
|
+
return path3.dirname(pkgJson);
|
|
927
451
|
}
|
|
928
|
-
var USER_WRANGLER_TOML =
|
|
452
|
+
var USER_WRANGLER_TOML = path3.join(process.cwd(), "wrangler.toml");
|
|
929
453
|
function ejectWranglerToml(opts) {
|
|
930
454
|
const serverDir = resolvePackageDir("@zooid/server");
|
|
931
|
-
let toml =
|
|
455
|
+
let toml = fs3.readFileSync(path3.join(serverDir, "wrangler.toml"), "utf-8");
|
|
932
456
|
toml = toml.replace(/directory\s*=\s*"[^"]*"/, 'directory = "./web-dist/"');
|
|
933
457
|
toml = toml.replace(/name = "[^"]*"/, `name = "${opts.workerName}"`);
|
|
934
458
|
toml = toml.replace(
|
|
@@ -943,58 +467,58 @@ function ejectWranglerToml(opts) {
|
|
|
943
467
|
/ZOOID_SERVER_ID = "[^"]*"/,
|
|
944
468
|
`ZOOID_SERVER_ID = "${opts.serverSlug}"`
|
|
945
469
|
);
|
|
946
|
-
|
|
470
|
+
fs3.writeFileSync(USER_WRANGLER_TOML, toml);
|
|
947
471
|
}
|
|
948
472
|
function prepareStagingDir() {
|
|
949
473
|
const serverDir = resolvePackageDir("@zooid/server");
|
|
950
|
-
const serverRequire = createRequire(
|
|
951
|
-
const webDir =
|
|
952
|
-
const webDistDir =
|
|
953
|
-
if (!
|
|
474
|
+
const serverRequire = createRequire(path3.join(serverDir, "package.json"));
|
|
475
|
+
const webDir = path3.dirname(serverRequire.resolve("@zooid/web/package.json"));
|
|
476
|
+
const webDistDir = path3.join(webDir, "dist");
|
|
477
|
+
if (!fs3.existsSync(webDistDir)) {
|
|
954
478
|
throw new Error(`Web dashboard not built. Missing: ${webDistDir}`);
|
|
955
479
|
}
|
|
956
|
-
const tmpDir =
|
|
957
|
-
copyDirSync(
|
|
958
|
-
copyDirSync(webDistDir,
|
|
959
|
-
if (
|
|
960
|
-
|
|
480
|
+
const tmpDir = fs3.mkdtempSync(path3.join(os.tmpdir(), "zooid-deploy-"));
|
|
481
|
+
copyDirSync(path3.join(serverDir, "src"), path3.join(tmpDir, "src"));
|
|
482
|
+
copyDirSync(webDistDir, path3.join(tmpDir, "web-dist"));
|
|
483
|
+
if (fs3.existsSync(USER_WRANGLER_TOML)) {
|
|
484
|
+
fs3.copyFileSync(USER_WRANGLER_TOML, path3.join(tmpDir, "wrangler.toml"));
|
|
961
485
|
} else {
|
|
962
|
-
if (!
|
|
486
|
+
if (!fs3.existsSync(path3.join(serverDir, "wrangler.toml"))) {
|
|
963
487
|
throw new Error(`Server package missing wrangler.toml at ${serverDir}`);
|
|
964
488
|
}
|
|
965
|
-
let toml =
|
|
489
|
+
let toml = fs3.readFileSync(path3.join(serverDir, "wrangler.toml"), "utf-8");
|
|
966
490
|
toml = toml.replace(/directory\s*=\s*"[^"]*"/, 'directory = "./web-dist/"');
|
|
967
|
-
|
|
491
|
+
fs3.writeFileSync(path3.join(tmpDir, "wrangler.toml"), toml);
|
|
968
492
|
}
|
|
969
493
|
const nodeModules = findServerNodeModules(serverDir);
|
|
970
494
|
if (nodeModules) {
|
|
971
|
-
|
|
495
|
+
fs3.symlinkSync(nodeModules, path3.join(tmpDir, "node_modules"), "junction");
|
|
972
496
|
}
|
|
973
497
|
return tmpDir;
|
|
974
498
|
}
|
|
975
499
|
function findServerNodeModules(serverDir) {
|
|
976
|
-
const local =
|
|
977
|
-
if (
|
|
978
|
-
const storeNodeModules =
|
|
979
|
-
if (
|
|
500
|
+
const local = path3.join(serverDir, "node_modules");
|
|
501
|
+
if (fs3.existsSync(path3.join(local, "hono"))) return local;
|
|
502
|
+
const storeNodeModules = path3.resolve(serverDir, "..", "..");
|
|
503
|
+
if (fs3.existsSync(path3.join(storeNodeModules, "hono")))
|
|
980
504
|
return storeNodeModules;
|
|
981
505
|
let dir = serverDir;
|
|
982
|
-
while (dir !==
|
|
983
|
-
dir =
|
|
984
|
-
const nm =
|
|
985
|
-
if (
|
|
506
|
+
while (dir !== path3.dirname(dir)) {
|
|
507
|
+
dir = path3.dirname(dir);
|
|
508
|
+
const nm = path3.join(dir, "node_modules");
|
|
509
|
+
if (fs3.existsSync(path3.join(nm, "hono"))) return nm;
|
|
986
510
|
}
|
|
987
511
|
return null;
|
|
988
512
|
}
|
|
989
513
|
function copyDirSync(src, dest) {
|
|
990
|
-
|
|
991
|
-
for (const entry of
|
|
992
|
-
const srcPath =
|
|
993
|
-
const destPath =
|
|
514
|
+
fs3.mkdirSync(dest, { recursive: true });
|
|
515
|
+
for (const entry of fs3.readdirSync(src, { withFileTypes: true })) {
|
|
516
|
+
const srcPath = path3.join(src, entry.name);
|
|
517
|
+
const destPath = path3.join(dest, entry.name);
|
|
994
518
|
if (entry.isDirectory()) {
|
|
995
519
|
copyDirSync(srcPath, destPath);
|
|
996
520
|
} else {
|
|
997
|
-
|
|
521
|
+
fs3.copyFileSync(srcPath, destPath);
|
|
998
522
|
}
|
|
999
523
|
}
|
|
1000
524
|
}
|
|
@@ -1009,7 +533,7 @@ function wranglerEnv(creds) {
|
|
|
1009
533
|
return env;
|
|
1010
534
|
}
|
|
1011
535
|
function wrangler(cmd, cwd, creds, opts) {
|
|
1012
|
-
return
|
|
536
|
+
return execSync(`npx wrangler ${cmd}`, {
|
|
1013
537
|
cwd,
|
|
1014
538
|
stdio: "pipe",
|
|
1015
539
|
encoding: "utf-8",
|
|
@@ -1048,9 +572,9 @@ function parseDeployUrls(output) {
|
|
|
1048
572
|
};
|
|
1049
573
|
}
|
|
1050
574
|
function loadDotEnv() {
|
|
1051
|
-
const envPath =
|
|
1052
|
-
if (!
|
|
1053
|
-
const content =
|
|
575
|
+
const envPath = path3.join(process.cwd(), ".env");
|
|
576
|
+
if (!fs3.existsSync(envPath)) return {};
|
|
577
|
+
const content = fs3.readFileSync(envPath, "utf-8");
|
|
1054
578
|
const tokenMatch = content.match(/^CLOUDFLARE_API_TOKEN=(.+)$/m);
|
|
1055
579
|
const accountMatch = content.match(/^CLOUDFLARE_ACCOUNT_ID=(.+)$/m);
|
|
1056
580
|
return {
|
|
@@ -1069,7 +593,7 @@ async function getCfCredentials() {
|
|
|
1069
593
|
printInfo("Using credentials from", ".env");
|
|
1070
594
|
return { apiToken: dotEnv.apiToken, accountId: dotEnv.accountId };
|
|
1071
595
|
}
|
|
1072
|
-
const rl =
|
|
596
|
+
const rl = readline2.createInterface({
|
|
1073
597
|
input: process.stdin,
|
|
1074
598
|
output: process.stdout
|
|
1075
599
|
});
|
|
@@ -1094,6 +618,18 @@ async function getCfCredentials() {
|
|
|
1094
618
|
rl.close();
|
|
1095
619
|
}
|
|
1096
620
|
}
|
|
621
|
+
function loadChannelDefs() {
|
|
622
|
+
const channelsDir = path3.join(process.cwd(), "channels");
|
|
623
|
+
if (!fs3.existsSync(channelsDir)) return /* @__PURE__ */ new Map();
|
|
624
|
+
const defs = /* @__PURE__ */ new Map();
|
|
625
|
+
for (const file of fs3.readdirSync(channelsDir)) {
|
|
626
|
+
if (!file.endsWith(".json")) continue;
|
|
627
|
+
const id = file.replace(/\.json$/, "");
|
|
628
|
+
const raw = fs3.readFileSync(path3.join(channelsDir, file), "utf-8");
|
|
629
|
+
defs.set(id, JSON.parse(raw));
|
|
630
|
+
}
|
|
631
|
+
return defs;
|
|
632
|
+
}
|
|
1097
633
|
async function runDeploy() {
|
|
1098
634
|
let config = loadServerConfig();
|
|
1099
635
|
if (!config) {
|
|
@@ -1117,7 +653,7 @@ async function runDeploy() {
|
|
|
1117
653
|
const dbName = `zooid-db-${serverSlug}`;
|
|
1118
654
|
const workerName = `zooid-${serverSlug}`;
|
|
1119
655
|
try {
|
|
1120
|
-
|
|
656
|
+
execSync("npx wrangler --version", { cwd: stagingDir, stdio: "pipe" });
|
|
1121
657
|
} catch {
|
|
1122
658
|
printError("wrangler not found. Install with: npm install -g wrangler");
|
|
1123
659
|
process.exit(1);
|
|
@@ -1156,12 +692,12 @@ async function runDeploy() {
|
|
|
1156
692
|
const databaseId = dbIdMatch[1];
|
|
1157
693
|
printSuccess(`D1 database created (${databaseId})`);
|
|
1158
694
|
ejectWranglerToml({ workerName, dbName, databaseId, serverSlug });
|
|
1159
|
-
|
|
695
|
+
fs3.copyFileSync(USER_WRANGLER_TOML, path3.join(stagingDir, "wrangler.toml"));
|
|
1160
696
|
printSuccess(
|
|
1161
697
|
"Created wrangler.toml (edit to add vars, observability, etc.)"
|
|
1162
698
|
);
|
|
1163
|
-
const schemaPath =
|
|
1164
|
-
if (
|
|
699
|
+
const schemaPath = path3.join(stagingDir, "src/db/schema.sql");
|
|
700
|
+
if (fs3.existsSync(schemaPath)) {
|
|
1165
701
|
printStep("Running database schema migration...");
|
|
1166
702
|
wrangler(
|
|
1167
703
|
`d1 execute ${dbName} --remote --file=${schemaPath}`,
|
|
@@ -1171,23 +707,23 @@ async function runDeploy() {
|
|
|
1171
707
|
printSuccess("Database schema initialized");
|
|
1172
708
|
}
|
|
1173
709
|
printStep("Generating secrets...");
|
|
1174
|
-
const keyPair = await
|
|
710
|
+
const keyPair = await crypto2.subtle.generateKey("Ed25519", true, [
|
|
1175
711
|
"sign",
|
|
1176
712
|
"verify"
|
|
1177
713
|
]);
|
|
1178
|
-
const privateKeyRaw = await
|
|
714
|
+
const privateKeyRaw = await crypto2.subtle.exportKey(
|
|
1179
715
|
"pkcs8",
|
|
1180
716
|
keyPair.privateKey
|
|
1181
717
|
);
|
|
1182
|
-
const publicKeyRaw = await
|
|
718
|
+
const publicKeyRaw = await crypto2.subtle.exportKey(
|
|
1183
719
|
"raw",
|
|
1184
720
|
keyPair.publicKey
|
|
1185
721
|
);
|
|
1186
|
-
const privateKeyJwk = await
|
|
722
|
+
const privateKeyJwk = await crypto2.subtle.exportKey(
|
|
1187
723
|
"jwk",
|
|
1188
724
|
keyPair.privateKey
|
|
1189
725
|
);
|
|
1190
|
-
const publicKeyJwk = await
|
|
726
|
+
const publicKeyJwk = await crypto2.subtle.exportKey(
|
|
1191
727
|
"jwk",
|
|
1192
728
|
keyPair.publicKey
|
|
1193
729
|
);
|
|
@@ -1216,7 +752,7 @@ async function runDeploy() {
|
|
|
1216
752
|
console.log("");
|
|
1217
753
|
printInfo("Deploy type", "Redeploying existing server");
|
|
1218
754
|
console.log("");
|
|
1219
|
-
if (!
|
|
755
|
+
if (!fs3.existsSync(USER_WRANGLER_TOML)) {
|
|
1220
756
|
printStep("Ejecting wrangler.toml...");
|
|
1221
757
|
let databaseId = "";
|
|
1222
758
|
try {
|
|
@@ -1231,9 +767,9 @@ async function runDeploy() {
|
|
|
1231
767
|
"Created wrangler.toml (edit to add vars, observability, etc.)"
|
|
1232
768
|
);
|
|
1233
769
|
}
|
|
1234
|
-
|
|
1235
|
-
const schemaPath =
|
|
1236
|
-
if (
|
|
770
|
+
fs3.copyFileSync(USER_WRANGLER_TOML, path3.join(stagingDir, "wrangler.toml"));
|
|
771
|
+
const schemaPath = path3.join(stagingDir, "src/db/schema.sql");
|
|
772
|
+
if (fs3.existsSync(schemaPath)) {
|
|
1237
773
|
printStep("Running schema migration...");
|
|
1238
774
|
wrangler(
|
|
1239
775
|
`d1 execute ${dbName} --remote --file=${schemaPath}`,
|
|
@@ -1242,11 +778,11 @@ async function runDeploy() {
|
|
|
1242
778
|
);
|
|
1243
779
|
printSuccess("Schema up to date");
|
|
1244
780
|
}
|
|
1245
|
-
const migrationsDir =
|
|
1246
|
-
if (
|
|
1247
|
-
const migrationFiles =
|
|
781
|
+
const migrationsDir = path3.join(stagingDir, "src/db/migrations");
|
|
782
|
+
if (fs3.existsSync(migrationsDir)) {
|
|
783
|
+
const migrationFiles = fs3.readdirSync(migrationsDir).filter((f) => f.endsWith(".sql")).sort();
|
|
1248
784
|
for (const file of migrationFiles) {
|
|
1249
|
-
const migrationPath =
|
|
785
|
+
const migrationPath = path3.join(migrationsDir, file);
|
|
1250
786
|
try {
|
|
1251
787
|
wrangler(
|
|
1252
788
|
`d1 execute ${dbName} --remote --file=${migrationPath}`,
|
|
@@ -1267,23 +803,23 @@ async function runDeploy() {
|
|
|
1267
803
|
const hasLocalKey = keysResult?.[0]?.results?.length > 0;
|
|
1268
804
|
if (!hasLocalKey) {
|
|
1269
805
|
printStep("Upgrading to EdDSA auth...");
|
|
1270
|
-
const keyPair = await
|
|
806
|
+
const keyPair = await crypto2.subtle.generateKey("Ed25519", true, [
|
|
1271
807
|
"sign",
|
|
1272
808
|
"verify"
|
|
1273
809
|
]);
|
|
1274
|
-
const privateKeyRaw = await
|
|
810
|
+
const privateKeyRaw = await crypto2.subtle.exportKey(
|
|
1275
811
|
"pkcs8",
|
|
1276
812
|
keyPair.privateKey
|
|
1277
813
|
);
|
|
1278
|
-
const publicKeyRaw = await
|
|
814
|
+
const publicKeyRaw = await crypto2.subtle.exportKey(
|
|
1279
815
|
"raw",
|
|
1280
816
|
keyPair.publicKey
|
|
1281
817
|
);
|
|
1282
|
-
const privateKeyJwk = await
|
|
818
|
+
const privateKeyJwk = await crypto2.subtle.exportKey(
|
|
1283
819
|
"jwk",
|
|
1284
820
|
keyPair.privateKey
|
|
1285
821
|
);
|
|
1286
|
-
const publicKeyJwk = await
|
|
822
|
+
const publicKeyJwk = await crypto2.subtle.exportKey(
|
|
1287
823
|
"jwk",
|
|
1288
824
|
keyPair.publicKey
|
|
1289
825
|
);
|
|
@@ -1326,83 +862,703 @@ async function runDeploy() {
|
|
|
1326
862
|
process.exit(1);
|
|
1327
863
|
}
|
|
1328
864
|
}
|
|
1329
|
-
printStep("Deploying worker...");
|
|
1330
|
-
const deployOutput = wranglerVerbose("deploy", stagingDir, creds);
|
|
1331
|
-
const { workerUrl, customDomain } = parseDeployUrls(deployOutput);
|
|
1332
|
-
printSuccess("Worker deployed");
|
|
1333
|
-
if (workerUrl) {
|
|
1334
|
-
printInfo("Worker URL", workerUrl);
|
|
865
|
+
printStep("Deploying worker...");
|
|
866
|
+
const deployOutput = wranglerVerbose("deploy", stagingDir, creds);
|
|
867
|
+
const { workerUrl, customDomain } = parseDeployUrls(deployOutput);
|
|
868
|
+
printSuccess("Worker deployed");
|
|
869
|
+
if (workerUrl) {
|
|
870
|
+
printInfo("Worker URL", workerUrl);
|
|
871
|
+
}
|
|
872
|
+
if (customDomain) {
|
|
873
|
+
printInfo("Custom domain", customDomain);
|
|
874
|
+
}
|
|
875
|
+
const canonicalUrl = config.url || customDomain || workerUrl;
|
|
876
|
+
await new Promise((r) => setTimeout(r, 2e3));
|
|
877
|
+
if (canonicalUrl && adminToken) {
|
|
878
|
+
try {
|
|
879
|
+
const client = new ZooidClient({
|
|
880
|
+
server: canonicalUrl,
|
|
881
|
+
token: adminToken
|
|
882
|
+
});
|
|
883
|
+
await client.updateServerMeta({
|
|
884
|
+
name: config.name || void 0,
|
|
885
|
+
description: config.description || void 0,
|
|
886
|
+
tags: config.tags.length > 0 ? config.tags : void 0,
|
|
887
|
+
owner: config.owner || void 0,
|
|
888
|
+
company: config.company || void 0,
|
|
889
|
+
email: config.email || void 0
|
|
890
|
+
});
|
|
891
|
+
printSuccess("Server identity updated");
|
|
892
|
+
} catch (err) {
|
|
893
|
+
printError(
|
|
894
|
+
`Failed to push server identity: ${err instanceof Error ? err.message : err}`
|
|
895
|
+
);
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
if (!config.url && (customDomain || workerUrl)) {
|
|
899
|
+
config.url = customDomain || workerUrl;
|
|
900
|
+
saveServerConfig(config);
|
|
901
|
+
printSuccess("Saved URL to zooid.json");
|
|
902
|
+
}
|
|
903
|
+
const configToSave = {
|
|
904
|
+
worker_url: workerUrl || void 0,
|
|
905
|
+
admin_token: adminToken
|
|
906
|
+
};
|
|
907
|
+
if (isFirstDeploy) {
|
|
908
|
+
configToSave.channels = {};
|
|
909
|
+
}
|
|
910
|
+
saveConfig(configToSave, canonicalUrl || void 0);
|
|
911
|
+
printSuccess("Saved connection config to ~/.zooid/state.json");
|
|
912
|
+
cleanup(stagingDir);
|
|
913
|
+
console.log("");
|
|
914
|
+
console.log(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
|
|
915
|
+
console.log(" \u{1FAB8} Zooid server deployed!");
|
|
916
|
+
console.log(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
|
|
917
|
+
printInfo("Server", canonicalUrl || "(unknown)");
|
|
918
|
+
if (workerUrl && workerUrl !== canonicalUrl) {
|
|
919
|
+
printInfo("Worker URL", workerUrl);
|
|
920
|
+
}
|
|
921
|
+
printInfo("Name", config.name || "(not set)");
|
|
922
|
+
if (isFirstDeploy) {
|
|
923
|
+
printInfo("Admin token", adminToken.slice(0, 20) + "...");
|
|
924
|
+
}
|
|
925
|
+
printInfo("Config", "~/.zooid/state.json");
|
|
926
|
+
console.log("");
|
|
927
|
+
const channelDefs = loadChannelDefs();
|
|
928
|
+
if (channelDefs.size > 0) {
|
|
929
|
+
console.log(
|
|
930
|
+
` Found ${channelDefs.size} channel definition(s) in channels/`
|
|
931
|
+
);
|
|
932
|
+
console.log(" Run npx zooid push to sync them to the server.");
|
|
933
|
+
console.log("");
|
|
934
|
+
} else if (isFirstDeploy) {
|
|
935
|
+
console.log(" Next steps:");
|
|
936
|
+
console.log(" Edit wrangler.toml to add env vars, observability, etc.");
|
|
937
|
+
console.log(" npx zooid channel create my-channel");
|
|
938
|
+
console.log(
|
|
939
|
+
` npx zooid publish my-channel --data='{"hello": "world"}'`
|
|
940
|
+
);
|
|
941
|
+
console.log("");
|
|
942
|
+
}
|
|
943
|
+
if (canonicalUrl) {
|
|
944
|
+
try {
|
|
945
|
+
const openCmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
|
|
946
|
+
execSync(`${openCmd} ${canonicalUrl}`, { stdio: "ignore" });
|
|
947
|
+
} catch {
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
function cleanup(dir) {
|
|
952
|
+
try {
|
|
953
|
+
fs3.rmSync(dir, { recursive: true, force: true });
|
|
954
|
+
} catch {
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
// src/commands/push.ts
|
|
959
|
+
async function defaultConfirmDelete(orphaned) {
|
|
960
|
+
console.log("");
|
|
961
|
+
printInfo(
|
|
962
|
+
"Warning",
|
|
963
|
+
`${orphaned.length} channel(s) on server not in channels/:`
|
|
964
|
+
);
|
|
965
|
+
for (const ch of orphaned) {
|
|
966
|
+
printInfo(" -", `${ch.id}${ch.name ? ` (${ch.name})` : ""}`);
|
|
967
|
+
}
|
|
968
|
+
const rl = readline3.createInterface({
|
|
969
|
+
input: process.stdin,
|
|
970
|
+
output: process.stdout
|
|
971
|
+
});
|
|
972
|
+
try {
|
|
973
|
+
const answer = await ask(
|
|
974
|
+
rl,
|
|
975
|
+
"Delete these channels from server? (y/N)",
|
|
976
|
+
"N"
|
|
977
|
+
);
|
|
978
|
+
return answer.toLowerCase() === "y";
|
|
979
|
+
} finally {
|
|
980
|
+
rl.close();
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
async function runPush(client, options = {}) {
|
|
984
|
+
const localDefs = loadChannelDefs();
|
|
985
|
+
if (localDefs.size === 0) {
|
|
986
|
+
printInfo("Nothing to push", "no channel definitions in channels/");
|
|
987
|
+
return;
|
|
988
|
+
}
|
|
989
|
+
const c = client ?? createClient();
|
|
990
|
+
printStep("Syncing channels...");
|
|
991
|
+
const remoteChannels = await c.listChannels();
|
|
992
|
+
const remoteIds = new Set(remoteChannels.map((ch) => ch.id));
|
|
993
|
+
const localIds = new Set(localDefs.keys());
|
|
994
|
+
let created = 0;
|
|
995
|
+
let updated = 0;
|
|
996
|
+
let deleted = 0;
|
|
997
|
+
for (const [id, def] of localDefs) {
|
|
998
|
+
if (!remoteIds.has(id)) {
|
|
999
|
+
await c.createChannel({
|
|
1000
|
+
id,
|
|
1001
|
+
name: def.name ?? id,
|
|
1002
|
+
description: def.description,
|
|
1003
|
+
is_public: def.visibility === "public",
|
|
1004
|
+
config: def.config
|
|
1005
|
+
});
|
|
1006
|
+
printSuccess(`Channel created: ${id}`);
|
|
1007
|
+
created++;
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
for (const [id, def] of localDefs) {
|
|
1011
|
+
if (remoteIds.has(id)) {
|
|
1012
|
+
await c.updateChannel(id, {
|
|
1013
|
+
name: def.name,
|
|
1014
|
+
description: def.description,
|
|
1015
|
+
is_public: def.visibility === "public",
|
|
1016
|
+
config: def.config
|
|
1017
|
+
});
|
|
1018
|
+
printSuccess(`Channel updated: ${id}`);
|
|
1019
|
+
updated++;
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
const orphaned = remoteChannels.filter((ch) => !localIds.has(ch.id));
|
|
1023
|
+
if (orphaned.length > 0) {
|
|
1024
|
+
const confirmFn = options.confirmDelete ?? defaultConfirmDelete;
|
|
1025
|
+
const confirmed = await confirmFn(orphaned);
|
|
1026
|
+
if (confirmed) {
|
|
1027
|
+
for (const ch of orphaned) {
|
|
1028
|
+
await c.deleteChannel(ch.id);
|
|
1029
|
+
printSuccess(`Channel deleted: ${ch.id}`);
|
|
1030
|
+
deleted++;
|
|
1031
|
+
}
|
|
1032
|
+
} else {
|
|
1033
|
+
printInfo("Skipped", "Remote-only channels left unchanged");
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
if (created || updated || deleted) {
|
|
1037
|
+
printSuccess(
|
|
1038
|
+
`Channels synced (${created} created, ${updated} updated, ${deleted} deleted)`
|
|
1039
|
+
);
|
|
1040
|
+
} else {
|
|
1041
|
+
printSuccess("Channels up to date");
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
// src/commands/pull.ts
|
|
1046
|
+
import fs4 from "fs";
|
|
1047
|
+
import path4 from "path";
|
|
1048
|
+
async function runPull(client) {
|
|
1049
|
+
const c = client ?? createClient();
|
|
1050
|
+
const channels = await c.listChannels();
|
|
1051
|
+
if (channels.length === 0) {
|
|
1052
|
+
printInfo("Nothing to pull", "no channels on server");
|
|
1053
|
+
return [];
|
|
1054
|
+
}
|
|
1055
|
+
const channelsDir = path4.join(process.cwd(), "channels");
|
|
1056
|
+
fs4.mkdirSync(channelsDir, { recursive: true });
|
|
1057
|
+
printStep("Pulling channels...");
|
|
1058
|
+
const written = [];
|
|
1059
|
+
for (const ch of channels) {
|
|
1060
|
+
const filePath = path4.join(channelsDir, `${ch.id}.json`);
|
|
1061
|
+
const def = {
|
|
1062
|
+
visibility: ch.is_public ? "public" : "private"
|
|
1063
|
+
};
|
|
1064
|
+
if (ch.name && ch.name !== ch.id) def.name = ch.name;
|
|
1065
|
+
if (ch.description) def.description = ch.description;
|
|
1066
|
+
if (ch.config) def.config = ch.config;
|
|
1067
|
+
fs4.writeFileSync(filePath, JSON.stringify(def, null, 2) + "\n");
|
|
1068
|
+
printSuccess(`channels/${ch.id}.json`);
|
|
1069
|
+
written.push(ch.id);
|
|
1070
|
+
}
|
|
1071
|
+
printSuccess(`Pulled ${written.length} channel(s) into channels/`);
|
|
1072
|
+
return written;
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
// src/commands/publish.ts
|
|
1076
|
+
import fs5 from "fs";
|
|
1077
|
+
async function runPublish(channelId, options, client) {
|
|
1078
|
+
const c = client ?? createPublishClient(channelId);
|
|
1079
|
+
let type;
|
|
1080
|
+
let data;
|
|
1081
|
+
if (options.file) {
|
|
1082
|
+
const raw = fs5.readFileSync(options.file, "utf-8");
|
|
1083
|
+
const parsed = JSON.parse(raw);
|
|
1084
|
+
type = parsed.type;
|
|
1085
|
+
data = parsed.data ?? parsed;
|
|
1086
|
+
} else if (options.data) {
|
|
1087
|
+
data = JSON.parse(options.data);
|
|
1088
|
+
type = options.type;
|
|
1089
|
+
} else {
|
|
1090
|
+
throw new Error("Either --data or --file is required");
|
|
1091
|
+
}
|
|
1092
|
+
const publishOpts = { data };
|
|
1093
|
+
if (type) publishOpts.type = type;
|
|
1094
|
+
return c.publish(channelId, publishOpts);
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
// src/commands/subscribe.ts
|
|
1098
|
+
async function runSubscribePoll(channelId, options = {}, client) {
|
|
1099
|
+
const c = client ?? createSubscribeClient(channelId);
|
|
1100
|
+
const callback = options.callback ?? ((event) => {
|
|
1101
|
+
console.log(JSON.stringify(event));
|
|
1102
|
+
});
|
|
1103
|
+
return c.subscribe(channelId, callback, {
|
|
1104
|
+
interval: options.interval ?? 5e3,
|
|
1105
|
+
mode: options.mode,
|
|
1106
|
+
type: options.type
|
|
1107
|
+
});
|
|
1108
|
+
}
|
|
1109
|
+
async function runSubscribeWebhook(channelId, url, client) {
|
|
1110
|
+
const c = client ?? createSubscribeClient(channelId);
|
|
1111
|
+
return c.registerWebhook(channelId, url);
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
// src/commands/tail.ts
|
|
1115
|
+
async function runTail(channelId, options = {}, client) {
|
|
1116
|
+
const c = client ?? createSubscribeClient(channelId);
|
|
1117
|
+
if (options.follow) {
|
|
1118
|
+
return runTailFollow(c, channelId, options);
|
|
1119
|
+
}
|
|
1120
|
+
const pollOpts = {};
|
|
1121
|
+
if (options.limit !== void 0) pollOpts.limit = options.limit;
|
|
1122
|
+
if (options.type) pollOpts.type = options.type;
|
|
1123
|
+
if (options.since) pollOpts.since = options.since;
|
|
1124
|
+
if (options.cursor) pollOpts.cursor = options.cursor;
|
|
1125
|
+
return c.tail(channelId, pollOpts);
|
|
1126
|
+
}
|
|
1127
|
+
async function runTailFollow(client, channelId, options) {
|
|
1128
|
+
const tailOpts = {
|
|
1129
|
+
follow: true,
|
|
1130
|
+
mode: options.mode,
|
|
1131
|
+
interval: options.interval,
|
|
1132
|
+
type: options.type
|
|
1133
|
+
};
|
|
1134
|
+
const stream = client.tail(channelId, tailOpts);
|
|
1135
|
+
for await (const event of stream) {
|
|
1136
|
+
console.log(JSON.stringify(event));
|
|
1137
|
+
}
|
|
1138
|
+
return void 0;
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
// src/commands/status.ts
|
|
1142
|
+
async function runStatus(client) {
|
|
1143
|
+
const c = client ?? createClient();
|
|
1144
|
+
const [discovery, identity] = await Promise.all([
|
|
1145
|
+
c.getMetadata(),
|
|
1146
|
+
c.getServerMeta()
|
|
1147
|
+
]);
|
|
1148
|
+
return { discovery, identity };
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
// src/commands/history.ts
|
|
1152
|
+
function runHistory() {
|
|
1153
|
+
const file = loadConfigFile();
|
|
1154
|
+
const entries = [];
|
|
1155
|
+
if (!file.servers) return entries;
|
|
1156
|
+
const seen = /* @__PURE__ */ new Map();
|
|
1157
|
+
for (const [serverUrl, serverConfig] of Object.entries(file.servers)) {
|
|
1158
|
+
if (!serverConfig.channels) continue;
|
|
1159
|
+
const normalizedServer = normalizeServerUrl(serverUrl);
|
|
1160
|
+
for (const [channelId, channelData] of Object.entries(
|
|
1161
|
+
serverConfig.channels
|
|
1162
|
+
)) {
|
|
1163
|
+
if (!channelData.stats) continue;
|
|
1164
|
+
const key = `${normalizedServer}\0${channelId}`;
|
|
1165
|
+
const existingIdx = seen.get(key);
|
|
1166
|
+
if (existingIdx !== void 0) {
|
|
1167
|
+
const existing = entries[existingIdx];
|
|
1168
|
+
existing.num_tails += channelData.stats.num_tails;
|
|
1169
|
+
if (channelData.stats.last_tailed_at > existing.last_tailed_at) {
|
|
1170
|
+
existing.last_tailed_at = channelData.stats.last_tailed_at;
|
|
1171
|
+
existing.name = channelData.name ?? existing.name;
|
|
1172
|
+
}
|
|
1173
|
+
if (channelData.stats.first_tailed_at < existing.first_tailed_at) {
|
|
1174
|
+
existing.first_tailed_at = channelData.stats.first_tailed_at;
|
|
1175
|
+
}
|
|
1176
|
+
} else {
|
|
1177
|
+
seen.set(key, entries.length);
|
|
1178
|
+
entries.push({
|
|
1179
|
+
server: normalizedServer,
|
|
1180
|
+
channel_id: channelId,
|
|
1181
|
+
name: channelData.name,
|
|
1182
|
+
num_tails: channelData.stats.num_tails,
|
|
1183
|
+
last_tailed_at: channelData.stats.last_tailed_at,
|
|
1184
|
+
first_tailed_at: channelData.stats.first_tailed_at
|
|
1185
|
+
});
|
|
1186
|
+
}
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
1189
|
+
entries.sort(
|
|
1190
|
+
(a, b) => new Date(b.last_tailed_at).getTime() - new Date(a.last_tailed_at).getTime()
|
|
1191
|
+
);
|
|
1192
|
+
return entries;
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
// src/commands/share.ts
|
|
1196
|
+
import readline4 from "readline/promises";
|
|
1197
|
+
|
|
1198
|
+
// src/lib/directory.ts
|
|
1199
|
+
var DIRECTORY_BASE_URL = "https://directory.zooid.dev";
|
|
1200
|
+
var TOKEN_PREFIX = "zd_";
|
|
1201
|
+
var POLL_INTERVAL_MS = 3e3;
|
|
1202
|
+
var POLL_TIMEOUT_MS = 2 * 60 * 1e3;
|
|
1203
|
+
function getDirectoryToken() {
|
|
1204
|
+
const token = loadDirectoryToken();
|
|
1205
|
+
if (token && token.startsWith(TOKEN_PREFIX)) {
|
|
1206
|
+
return token;
|
|
1207
|
+
}
|
|
1208
|
+
return void 0;
|
|
1209
|
+
}
|
|
1210
|
+
async function ensureDirectoryToken() {
|
|
1211
|
+
const existing = getDirectoryToken();
|
|
1212
|
+
if (existing) return existing;
|
|
1213
|
+
return deviceAuth();
|
|
1214
|
+
}
|
|
1215
|
+
async function deviceAuth() {
|
|
1216
|
+
const res = await fetch(`${DIRECTORY_BASE_URL}/api/auth/device`, {
|
|
1217
|
+
method: "POST",
|
|
1218
|
+
headers: { "Content-Type": "application/json" }
|
|
1219
|
+
});
|
|
1220
|
+
if (!res.ok) {
|
|
1221
|
+
throw new Error(`Directory auth failed: ${res.status} ${res.statusText}`);
|
|
1222
|
+
}
|
|
1223
|
+
const data = await res.json();
|
|
1224
|
+
const { device_code, verification_url } = data;
|
|
1225
|
+
console.log("");
|
|
1226
|
+
printInfo("Authorize", verification_url);
|
|
1227
|
+
console.log(" Opening browser to complete GitHub sign-in...\n");
|
|
1228
|
+
try {
|
|
1229
|
+
const { exec } = await import("child_process");
|
|
1230
|
+
const platform = process.platform;
|
|
1231
|
+
const cmd = platform === "darwin" ? "open" : platform === "win32" ? "start" : "xdg-open";
|
|
1232
|
+
exec(`${cmd} "${verification_url}"`);
|
|
1233
|
+
} catch {
|
|
1234
|
+
console.log(
|
|
1235
|
+
" Could not open browser automatically. Please visit the URL above.\n"
|
|
1236
|
+
);
|
|
1237
|
+
}
|
|
1238
|
+
const deadline = Date.now() + POLL_TIMEOUT_MS;
|
|
1239
|
+
while (Date.now() < deadline) {
|
|
1240
|
+
await sleep(POLL_INTERVAL_MS);
|
|
1241
|
+
const statusRes = await fetch(
|
|
1242
|
+
`${DIRECTORY_BASE_URL}/api/auth/status?code=${encodeURIComponent(device_code)}`
|
|
1243
|
+
);
|
|
1244
|
+
if (!statusRes.ok) continue;
|
|
1245
|
+
const status = await statusRes.json();
|
|
1246
|
+
if (status.status === "complete" && status.token) {
|
|
1247
|
+
saveDirectoryToken(status.token);
|
|
1248
|
+
console.log(" Authenticated with Zooid Directory.\n");
|
|
1249
|
+
return status.token;
|
|
1250
|
+
}
|
|
1251
|
+
if (status.status === "expired") {
|
|
1252
|
+
throw new Error("Device authorization expired. Please try again.");
|
|
1253
|
+
}
|
|
1254
|
+
}
|
|
1255
|
+
throw new Error(
|
|
1256
|
+
"Authorization timed out. You need your human to authorize you. Run `npx zooid share` again to retry."
|
|
1257
|
+
);
|
|
1258
|
+
}
|
|
1259
|
+
async function directoryFetch(path6, options = {}) {
|
|
1260
|
+
let token = await ensureDirectoryToken();
|
|
1261
|
+
const doFetch = (t) => {
|
|
1262
|
+
const headers = new Headers(options.headers);
|
|
1263
|
+
headers.set("Authorization", `Bearer ${t}`);
|
|
1264
|
+
headers.set("Content-Type", "application/json");
|
|
1265
|
+
return fetch(`${DIRECTORY_BASE_URL}${path6}`, { ...options, headers });
|
|
1266
|
+
};
|
|
1267
|
+
let res = await doFetch(token);
|
|
1268
|
+
if (res.status === 401) {
|
|
1269
|
+
console.log(" Directory token expired. Re-authenticating...\n");
|
|
1270
|
+
token = await deviceAuth();
|
|
1271
|
+
res = await doFetch(token);
|
|
1272
|
+
}
|
|
1273
|
+
return res;
|
|
1274
|
+
}
|
|
1275
|
+
function sleep(ms) {
|
|
1276
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1277
|
+
}
|
|
1278
|
+
async function formatDirectoryError(res) {
|
|
1279
|
+
let msg = `Directory returned ${res.status}`;
|
|
1280
|
+
try {
|
|
1281
|
+
const body = await res.json();
|
|
1282
|
+
const parts = [];
|
|
1283
|
+
if (body.error) parts.push(String(body.error));
|
|
1284
|
+
if (body.message) parts.push(String(body.message));
|
|
1285
|
+
if (parts.length > 0) msg = parts.join(": ");
|
|
1286
|
+
} catch {
|
|
1287
|
+
}
|
|
1288
|
+
return msg;
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1291
|
+
// src/commands/share.ts
|
|
1292
|
+
async function runShare(channelIds, options = {}) {
|
|
1293
|
+
const client = createClient();
|
|
1294
|
+
const config = loadConfig();
|
|
1295
|
+
const serverUrl = config.server;
|
|
1296
|
+
if (!serverUrl) {
|
|
1297
|
+
throw new Error(
|
|
1298
|
+
"No server configured. Run: npx zooid config set server <url>"
|
|
1299
|
+
);
|
|
1300
|
+
}
|
|
1301
|
+
const allChannels = await client.listChannels();
|
|
1302
|
+
const publicChannels = allChannels.filter((ch) => ch.is_public);
|
|
1303
|
+
if (publicChannels.length === 0) {
|
|
1304
|
+
throw new Error("No public channels found on this server.");
|
|
1305
|
+
}
|
|
1306
|
+
let selected;
|
|
1307
|
+
if (channelIds.length > 0) {
|
|
1308
|
+
const byId = new Map(allChannels.map((ch) => [ch.id, ch]));
|
|
1309
|
+
const missing = [];
|
|
1310
|
+
selected = [];
|
|
1311
|
+
for (const id of channelIds) {
|
|
1312
|
+
const ch = byId.get(id);
|
|
1313
|
+
if (!ch) {
|
|
1314
|
+
missing.push(id);
|
|
1315
|
+
} else {
|
|
1316
|
+
selected.push(ch);
|
|
1317
|
+
}
|
|
1318
|
+
}
|
|
1319
|
+
if (missing.length > 0) {
|
|
1320
|
+
throw new Error(`Channels not found: ${missing.join(", ")}`);
|
|
1321
|
+
}
|
|
1322
|
+
const privateChannels = selected.filter((ch) => !ch.is_public);
|
|
1323
|
+
if (privateChannels.length > 0) {
|
|
1324
|
+
throw new Error(
|
|
1325
|
+
`Cannot share private channels: ${privateChannels.map((ch) => ch.id).join(", ")}. Only public channels can be listed in the directory.`
|
|
1326
|
+
);
|
|
1327
|
+
}
|
|
1328
|
+
} else if (options.yes) {
|
|
1329
|
+
selected = publicChannels;
|
|
1330
|
+
} else {
|
|
1331
|
+
selected = await pickChannels(publicChannels);
|
|
1332
|
+
if (selected.length === 0) {
|
|
1333
|
+
throw new Error("No channels selected.");
|
|
1334
|
+
}
|
|
1335
|
+
}
|
|
1336
|
+
const channelDetails = await promptChannelDetails(selected, options.yes);
|
|
1337
|
+
const ids = selected.map((ch) => ch.id);
|
|
1338
|
+
const { claim, signature } = await client.getClaim(ids);
|
|
1339
|
+
const channels = selected.map((ch) => {
|
|
1340
|
+
const details = channelDetails.get(ch.id);
|
|
1341
|
+
const entry = {
|
|
1342
|
+
channel_id: ch.id,
|
|
1343
|
+
name: ch.name
|
|
1344
|
+
};
|
|
1345
|
+
if (details.description) entry.description = details.description;
|
|
1346
|
+
if (details.tags.length > 0) entry.tags = details.tags;
|
|
1347
|
+
return entry;
|
|
1348
|
+
});
|
|
1349
|
+
const res = await directoryFetch("/api/servers", {
|
|
1350
|
+
method: "POST",
|
|
1351
|
+
body: JSON.stringify({ server_url: serverUrl, claim, signature, channels })
|
|
1352
|
+
});
|
|
1353
|
+
if (!res.ok) {
|
|
1354
|
+
throw new Error(await formatDirectoryError(res));
|
|
1355
|
+
}
|
|
1356
|
+
await res.json();
|
|
1357
|
+
console.log("");
|
|
1358
|
+
for (const ch of selected) {
|
|
1359
|
+
console.log(` ${ch.id} \u2192 ${serverUrl}/${ch.id}`);
|
|
1335
1360
|
}
|
|
1336
|
-
|
|
1337
|
-
|
|
1361
|
+
console.log("");
|
|
1362
|
+
console.log(` Any zooid can find your channel using:`);
|
|
1363
|
+
console.log(` npx zooid discover --query ${selected[0].id}`);
|
|
1364
|
+
const tags = [
|
|
1365
|
+
...new Set(selected.flatMap((ch) => channelDetails.get(ch.id)?.tags ?? []))
|
|
1366
|
+
];
|
|
1367
|
+
if (tags.length > 0) {
|
|
1368
|
+
console.log(` npx zooid discover --tag ${tags[0]}`);
|
|
1338
1369
|
}
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
}
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1370
|
+
console.log("");
|
|
1371
|
+
}
|
|
1372
|
+
async function pickChannels(channels) {
|
|
1373
|
+
const { default: checkbox } = await import("@inquirer/checkbox");
|
|
1374
|
+
const selected = await checkbox({
|
|
1375
|
+
message: "Select channels to share",
|
|
1376
|
+
choices: channels.map((ch) => ({
|
|
1377
|
+
name: ch.description ? `${ch.id} \u2014 ${ch.description}` : ch.id,
|
|
1378
|
+
value: ch.id,
|
|
1379
|
+
checked: true
|
|
1380
|
+
})),
|
|
1381
|
+
theme: {
|
|
1382
|
+
icon: { cursor: "> " },
|
|
1383
|
+
style: { highlight: (text) => text }
|
|
1384
|
+
}
|
|
1385
|
+
});
|
|
1386
|
+
const selectedSet = new Set(selected);
|
|
1387
|
+
return channels.filter((ch) => selectedSet.has(ch.id));
|
|
1388
|
+
}
|
|
1389
|
+
async function promptChannelDetails(channels, skipPrompt) {
|
|
1390
|
+
const result = /* @__PURE__ */ new Map();
|
|
1391
|
+
if (skipPrompt) {
|
|
1392
|
+
for (const ch of channels) {
|
|
1393
|
+
result.set(ch.id, {
|
|
1394
|
+
description: ch.description ?? "",
|
|
1395
|
+
tags: ch.tags
|
|
1354
1396
|
});
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1397
|
+
}
|
|
1398
|
+
return result;
|
|
1399
|
+
}
|
|
1400
|
+
const rl = readline4.createInterface({
|
|
1401
|
+
input: process.stdin,
|
|
1402
|
+
output: process.stdout
|
|
1403
|
+
});
|
|
1404
|
+
try {
|
|
1405
|
+
console.log("");
|
|
1406
|
+
console.log(" Set description and tags for each channel.");
|
|
1407
|
+
console.log(" Press Enter to accept defaults shown in [brackets].\n");
|
|
1408
|
+
for (const ch of channels) {
|
|
1409
|
+
console.log(` ${ch.id}:`);
|
|
1410
|
+
const desc = await ask(rl, "Description", ch.description ?? "");
|
|
1411
|
+
const tagsRaw = await ask(
|
|
1412
|
+
rl,
|
|
1413
|
+
"Tags (comma-separated)",
|
|
1414
|
+
ch.tags.join(", ")
|
|
1359
1415
|
);
|
|
1416
|
+
const tags = tagsRaw.split(",").map((t) => t.trim()).filter(Boolean);
|
|
1417
|
+
result.set(ch.id, { description: desc, tags });
|
|
1418
|
+
console.log("");
|
|
1360
1419
|
}
|
|
1420
|
+
} finally {
|
|
1421
|
+
rl.close();
|
|
1361
1422
|
}
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1423
|
+
return result;
|
|
1424
|
+
}
|
|
1425
|
+
|
|
1426
|
+
// src/commands/unshare.ts
|
|
1427
|
+
async function runUnshare(channelId) {
|
|
1428
|
+
const client = createClient();
|
|
1429
|
+
const config = loadConfig();
|
|
1430
|
+
const serverUrl = config.server;
|
|
1431
|
+
if (!serverUrl) {
|
|
1432
|
+
throw new Error(
|
|
1433
|
+
"No server configured. Run: npx zooid config set server <url>"
|
|
1434
|
+
);
|
|
1366
1435
|
}
|
|
1367
|
-
const
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1436
|
+
const { claim, signature } = await client.getClaim([channelId], "delete");
|
|
1437
|
+
const res = await directoryFetch("/api/servers/channels", {
|
|
1438
|
+
method: "DELETE",
|
|
1439
|
+
body: JSON.stringify({
|
|
1440
|
+
server_url: serverUrl,
|
|
1441
|
+
channel_id: channelId,
|
|
1442
|
+
claim,
|
|
1443
|
+
signature
|
|
1444
|
+
})
|
|
1445
|
+
});
|
|
1446
|
+
if (!res.ok) {
|
|
1447
|
+
throw new Error(await formatDirectoryError(res));
|
|
1373
1448
|
}
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1449
|
+
}
|
|
1450
|
+
|
|
1451
|
+
// src/commands/discover.ts
|
|
1452
|
+
async function runDiscover(options) {
|
|
1453
|
+
const params = new URLSearchParams();
|
|
1454
|
+
if (options.query) params.set("q", options.query);
|
|
1455
|
+
if (options.tag) params.set("tag", options.tag);
|
|
1456
|
+
if (options.limit) params.set("limit", String(options.limit));
|
|
1457
|
+
const qs = params.toString();
|
|
1458
|
+
const url = `${DIRECTORY_BASE_URL}/api/discover${qs ? `?${qs}` : ""}`;
|
|
1459
|
+
const res = await fetch(url);
|
|
1460
|
+
if (!res.ok) {
|
|
1461
|
+
throw new Error(`Directory returned ${res.status}`);
|
|
1384
1462
|
}
|
|
1385
|
-
|
|
1386
|
-
if (
|
|
1387
|
-
|
|
1463
|
+
const data = await res.json();
|
|
1464
|
+
if (data.channels.length === 0) {
|
|
1465
|
+
console.log("No channels found.");
|
|
1466
|
+
return;
|
|
1388
1467
|
}
|
|
1389
|
-
printInfo("Config", "~/.zooid/state.json");
|
|
1390
1468
|
console.log("");
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
console.log(
|
|
1396
|
-
` npx zooid publish my-channel --data='{"hello": "world"}'`
|
|
1397
|
-
);
|
|
1398
|
-
console.log("");
|
|
1469
|
+
for (const ch of data.channels) {
|
|
1470
|
+
const host = new URL(ch.server_url).host;
|
|
1471
|
+
const tags = ch.tags.length > 0 ? ` [${ch.tags.join(", ")}]` : "";
|
|
1472
|
+
const desc = ch.description ? ` \u2014 ${ch.description}` : "";
|
|
1473
|
+
console.log(` ${host}/${ch.channel_id}${desc}${tags}`);
|
|
1399
1474
|
}
|
|
1475
|
+
console.log(`
|
|
1476
|
+
${data.total} channel${data.total === 1 ? "" : "s"} found.`);
|
|
1477
|
+
console.log("");
|
|
1400
1478
|
}
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1479
|
+
|
|
1480
|
+
// src/commands/server.ts
|
|
1481
|
+
async function runServerGet(client) {
|
|
1482
|
+
const c = client ?? createClient();
|
|
1483
|
+
return c.getServerMeta();
|
|
1484
|
+
}
|
|
1485
|
+
async function runServerSet(fields, client) {
|
|
1486
|
+
const c = client ?? createClient();
|
|
1487
|
+
return c.updateServerMeta(fields);
|
|
1488
|
+
}
|
|
1489
|
+
|
|
1490
|
+
// src/commands/token.ts
|
|
1491
|
+
async function runTokenMint(scopes, options) {
|
|
1492
|
+
const client = createClient();
|
|
1493
|
+
const body = { scopes };
|
|
1494
|
+
if (options.sub) body.sub = options.sub;
|
|
1495
|
+
if (options.name) body.name = options.name;
|
|
1496
|
+
if (options.expiresIn) body.expires_in = options.expiresIn;
|
|
1497
|
+
return client.mintToken(body);
|
|
1498
|
+
}
|
|
1499
|
+
|
|
1500
|
+
// src/commands/dev.ts
|
|
1501
|
+
import { execSync as execSync2, spawn as spawn2 } from "child_process";
|
|
1502
|
+
import crypto3 from "crypto";
|
|
1503
|
+
import fs6 from "fs";
|
|
1504
|
+
import path5 from "path";
|
|
1505
|
+
import { fileURLToPath } from "url";
|
|
1506
|
+
function findServerDir() {
|
|
1507
|
+
const cliDir = path5.dirname(fileURLToPath(import.meta.url));
|
|
1508
|
+
return path5.resolve(cliDir, "../../server");
|
|
1509
|
+
}
|
|
1510
|
+
async function runDev(port = 8787) {
|
|
1511
|
+
const serverDir = findServerDir();
|
|
1512
|
+
if (!fs6.existsSync(path5.join(serverDir, "wrangler.toml"))) {
|
|
1513
|
+
throw new Error(
|
|
1514
|
+
`Server directory not found at ${serverDir}. Make sure you're running from the zooid monorepo.`
|
|
1515
|
+
);
|
|
1516
|
+
}
|
|
1517
|
+
const jwtSecret = crypto3.randomUUID();
|
|
1518
|
+
const devVarsPath = path5.join(serverDir, ".dev.vars");
|
|
1519
|
+
fs6.writeFileSync(devVarsPath, `ZOOID_JWT_SECRET=${jwtSecret}
|
|
1520
|
+
`);
|
|
1521
|
+
const adminToken = await createAdminToken(jwtSecret);
|
|
1522
|
+
const serverUrl = `http://localhost:${port}`;
|
|
1523
|
+
saveConfig({ admin_token: adminToken }, serverUrl);
|
|
1524
|
+
const schemaPath = path5.join(serverDir, "src/db/schema.sql");
|
|
1525
|
+
if (fs6.existsSync(schemaPath)) {
|
|
1526
|
+
try {
|
|
1527
|
+
execSync2(
|
|
1528
|
+
`npx wrangler d1 execute zooid-db --local --file=${schemaPath}`,
|
|
1529
|
+
{
|
|
1530
|
+
cwd: serverDir,
|
|
1531
|
+
stdio: "pipe"
|
|
1532
|
+
}
|
|
1533
|
+
);
|
|
1534
|
+
printSuccess("Database schema initialized");
|
|
1535
|
+
} catch {
|
|
1536
|
+
printSuccess("Database ready (schema already exists)");
|
|
1537
|
+
}
|
|
1405
1538
|
}
|
|
1539
|
+
printSuccess("Local server configured");
|
|
1540
|
+
printInfo("Server", serverUrl);
|
|
1541
|
+
printInfo("Admin token", adminToken.slice(0, 20) + "...");
|
|
1542
|
+
printInfo("Config saved to", "~/.zooid/state.json");
|
|
1543
|
+
console.log("");
|
|
1544
|
+
console.log("Starting wrangler dev...");
|
|
1545
|
+
console.log("");
|
|
1546
|
+
const child = spawn2(
|
|
1547
|
+
"npx",
|
|
1548
|
+
["wrangler", "dev", "--local", "--port", String(port)],
|
|
1549
|
+
{
|
|
1550
|
+
cwd: serverDir,
|
|
1551
|
+
stdio: "inherit",
|
|
1552
|
+
shell: true
|
|
1553
|
+
}
|
|
1554
|
+
);
|
|
1555
|
+
child.on("error", (err) => {
|
|
1556
|
+
console.error(`Failed to start local server: ${err.message}`);
|
|
1557
|
+
process.exit(1);
|
|
1558
|
+
});
|
|
1559
|
+
child.on("exit", (code) => {
|
|
1560
|
+
process.exit(code ?? 0);
|
|
1561
|
+
});
|
|
1406
1562
|
}
|
|
1407
1563
|
|
|
1408
1564
|
// src/index.ts
|
|
@@ -1428,7 +1584,7 @@ async function resolveAndRecord(channel, opts) {
|
|
|
1428
1584
|
return result;
|
|
1429
1585
|
}
|
|
1430
1586
|
var program = new Command();
|
|
1431
|
-
program.name("zooid").description("\u{1FAB8} Pub/sub for AI agents").version("0.
|
|
1587
|
+
program.name("zooid").description("\u{1FAB8} Pub/sub for AI agents").version("0.5.1");
|
|
1432
1588
|
var telemetryCtx = { startTime: 0 };
|
|
1433
1589
|
function setTelemetryChannel(channelId) {
|
|
1434
1590
|
telemetryCtx.channelId = channelId;
|
|
@@ -1541,12 +1697,12 @@ channelCmd.command("create <id>").description("Create a new channel").option("--
|
|
|
1541
1697
|
try {
|
|
1542
1698
|
let config;
|
|
1543
1699
|
if (opts.config) {
|
|
1544
|
-
const
|
|
1545
|
-
const raw =
|
|
1700
|
+
const fs7 = await import("fs");
|
|
1701
|
+
const raw = fs7.readFileSync(opts.config, "utf-8");
|
|
1546
1702
|
config = JSON.parse(raw);
|
|
1547
1703
|
} else if (opts.schema) {
|
|
1548
|
-
const
|
|
1549
|
-
const raw =
|
|
1704
|
+
const fs7 = await import("fs");
|
|
1705
|
+
const raw = fs7.readFileSync(opts.schema, "utf-8");
|
|
1550
1706
|
const parsed = JSON.parse(raw);
|
|
1551
1707
|
const types = {};
|
|
1552
1708
|
for (const [eventType, schemaDef] of Object.entries(parsed)) {
|
|
@@ -1583,12 +1739,12 @@ channelCmd.command("update <id>").description("Update a channel").option("--name
|
|
|
1583
1739
|
if (opts.public) fields.is_public = true;
|
|
1584
1740
|
if (opts.private) fields.is_public = false;
|
|
1585
1741
|
if (opts.config) {
|
|
1586
|
-
const
|
|
1587
|
-
const raw =
|
|
1742
|
+
const fs7 = await import("fs");
|
|
1743
|
+
const raw = fs7.readFileSync(opts.config, "utf-8");
|
|
1588
1744
|
fields.config = JSON.parse(raw);
|
|
1589
1745
|
} else if (opts.schema) {
|
|
1590
|
-
const
|
|
1591
|
-
const raw =
|
|
1746
|
+
const fs7 = await import("fs");
|
|
1747
|
+
const raw = fs7.readFileSync(opts.schema, "utf-8");
|
|
1592
1748
|
const parsed = JSON.parse(raw);
|
|
1593
1749
|
const types = {};
|
|
1594
1750
|
for (const [eventType, schemaDef] of Object.entries(parsed)) {
|
|
@@ -1634,8 +1790,8 @@ channelCmd.command("list").description("List all channels").action(async () => {
|
|
|
1634
1790
|
channelCmd.command("delete <id>").description("Delete a channel and all its data").option("-y, --yes", "Skip confirmation prompt").action(async (id, opts) => {
|
|
1635
1791
|
try {
|
|
1636
1792
|
if (!opts.yes) {
|
|
1637
|
-
const
|
|
1638
|
-
const rl =
|
|
1793
|
+
const readline5 = await import("readline");
|
|
1794
|
+
const rl = readline5.createInterface({
|
|
1639
1795
|
input: process.stdin,
|
|
1640
1796
|
output: process.stdout
|
|
1641
1797
|
});
|
|
@@ -1657,6 +1813,20 @@ channelCmd.command("delete <id>").description("Delete a channel and all its data
|
|
|
1657
1813
|
handleError("channel delete", err);
|
|
1658
1814
|
}
|
|
1659
1815
|
});
|
|
1816
|
+
program.command("push").description("Push local channel definitions (channels/) to server").action(async () => {
|
|
1817
|
+
try {
|
|
1818
|
+
await runPush();
|
|
1819
|
+
} catch (err) {
|
|
1820
|
+
handleError("push", err);
|
|
1821
|
+
}
|
|
1822
|
+
});
|
|
1823
|
+
program.command("pull").description("Pull channel definitions from server into channels/").action(async () => {
|
|
1824
|
+
try {
|
|
1825
|
+
await runPull();
|
|
1826
|
+
} catch (err) {
|
|
1827
|
+
handleError("pull", err);
|
|
1828
|
+
}
|
|
1829
|
+
});
|
|
1660
1830
|
program.command("publish <channel>").description("Publish an event to a channel").option("--type <type>", "Event type").option("--data <json>", "Event data as JSON string").option("--file <path>", "Read event from JSON file").option("--token <token>", "Auth token (for remote/private channels)").action(async (channel, opts) => {
|
|
1661
1831
|
try {
|
|
1662
1832
|
const { client, channelId, tokenSaved } = resolveChannel(channel, {
|
|
@@ -1682,8 +1852,8 @@ program.command("delete-event <channel> <event-id>").description("Delete a singl
|
|
|
1682
1852
|
tokenType: "publish"
|
|
1683
1853
|
});
|
|
1684
1854
|
if (!opts.yes) {
|
|
1685
|
-
const
|
|
1686
|
-
const rl =
|
|
1855
|
+
const readline5 = await import("readline");
|
|
1856
|
+
const rl = readline5.createInterface({
|
|
1687
1857
|
input: process.stdin,
|
|
1688
1858
|
output: process.stdout
|
|
1689
1859
|
});
|