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.
Files changed (3) hide show
  1. package/README.md +12 -6
  2. package/dist/index.js +809 -639
  3. 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 fs6 = await import("fs");
244
- fs6.writeFileSync(getStatePath(), JSON.stringify(file, null, 2) + "\n");
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/publish.ts
250
- import fs2 from "fs";
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/share.ts
497
- async function runShare(channelIds, options = {}) {
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 path2 from "path";
710
- import { fileURLToPath } from "url";
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 fs4 from "fs";
834
- import path3 from "path";
835
- import readline2 from "readline/promises";
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 path3.join(process.cwd(), CONFIG_FILENAME);
360
+ return path2.join(process.cwd(), CONFIG_FILENAME);
839
361
  }
840
362
  function loadServerConfig() {
841
363
  const configPath = getConfigPath();
842
- if (!fs4.existsSync(configPath)) return null;
843
- const raw = fs4.readFileSync(configPath, "utf-8");
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
- fs4.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
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 = readline2.createInterface({
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
- fs4.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
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 path4.dirname(pkgJson);
450
+ return path3.dirname(pkgJson);
927
451
  }
928
- var USER_WRANGLER_TOML = path4.join(process.cwd(), "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 = fs5.readFileSync(path4.join(serverDir, "wrangler.toml"), "utf-8");
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
- fs5.writeFileSync(USER_WRANGLER_TOML, toml);
470
+ fs3.writeFileSync(USER_WRANGLER_TOML, toml);
947
471
  }
948
472
  function prepareStagingDir() {
949
473
  const serverDir = resolvePackageDir("@zooid/server");
950
- const serverRequire = createRequire(path4.join(serverDir, "package.json"));
951
- const webDir = path4.dirname(serverRequire.resolve("@zooid/web/package.json"));
952
- const webDistDir = path4.join(webDir, "dist");
953
- if (!fs5.existsSync(webDistDir)) {
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 = fs5.mkdtempSync(path4.join(os.tmpdir(), "zooid-deploy-"));
957
- copyDirSync(path4.join(serverDir, "src"), path4.join(tmpDir, "src"));
958
- copyDirSync(webDistDir, path4.join(tmpDir, "web-dist"));
959
- if (fs5.existsSync(USER_WRANGLER_TOML)) {
960
- fs5.copyFileSync(USER_WRANGLER_TOML, path4.join(tmpDir, "wrangler.toml"));
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 (!fs5.existsSync(path4.join(serverDir, "wrangler.toml"))) {
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 = fs5.readFileSync(path4.join(serverDir, "wrangler.toml"), "utf-8");
489
+ let toml = fs3.readFileSync(path3.join(serverDir, "wrangler.toml"), "utf-8");
966
490
  toml = toml.replace(/directory\s*=\s*"[^"]*"/, 'directory = "./web-dist/"');
967
- fs5.writeFileSync(path4.join(tmpDir, "wrangler.toml"), toml);
491
+ fs3.writeFileSync(path3.join(tmpDir, "wrangler.toml"), toml);
968
492
  }
969
493
  const nodeModules = findServerNodeModules(serverDir);
970
494
  if (nodeModules) {
971
- fs5.symlinkSync(nodeModules, path4.join(tmpDir, "node_modules"), "junction");
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 = path4.join(serverDir, "node_modules");
977
- if (fs5.existsSync(path4.join(local, "hono"))) return local;
978
- const storeNodeModules = path4.resolve(serverDir, "..", "..");
979
- if (fs5.existsSync(path4.join(storeNodeModules, "hono")))
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 !== path4.dirname(dir)) {
983
- dir = path4.dirname(dir);
984
- const nm = path4.join(dir, "node_modules");
985
- if (fs5.existsSync(path4.join(nm, "hono"))) return nm;
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
- fs5.mkdirSync(dest, { recursive: true });
991
- for (const entry of fs5.readdirSync(src, { withFileTypes: true })) {
992
- const srcPath = path4.join(src, entry.name);
993
- const destPath = path4.join(dest, entry.name);
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
- fs5.copyFileSync(srcPath, destPath);
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 execSync2(`npx wrangler ${cmd}`, {
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 = path4.join(process.cwd(), ".env");
1052
- if (!fs5.existsSync(envPath)) return {};
1053
- const content = fs5.readFileSync(envPath, "utf-8");
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 = readline3.createInterface({
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
- execSync2("npx wrangler --version", { cwd: stagingDir, stdio: "pipe" });
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
- fs5.copyFileSync(USER_WRANGLER_TOML, path4.join(stagingDir, "wrangler.toml"));
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 = path4.join(stagingDir, "src/db/schema.sql");
1164
- if (fs5.existsSync(schemaPath)) {
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 crypto3.subtle.generateKey("Ed25519", true, [
710
+ const keyPair = await crypto2.subtle.generateKey("Ed25519", true, [
1175
711
  "sign",
1176
712
  "verify"
1177
713
  ]);
1178
- const privateKeyRaw = await crypto3.subtle.exportKey(
714
+ const privateKeyRaw = await crypto2.subtle.exportKey(
1179
715
  "pkcs8",
1180
716
  keyPair.privateKey
1181
717
  );
1182
- const publicKeyRaw = await crypto3.subtle.exportKey(
718
+ const publicKeyRaw = await crypto2.subtle.exportKey(
1183
719
  "raw",
1184
720
  keyPair.publicKey
1185
721
  );
1186
- const privateKeyJwk = await crypto3.subtle.exportKey(
722
+ const privateKeyJwk = await crypto2.subtle.exportKey(
1187
723
  "jwk",
1188
724
  keyPair.privateKey
1189
725
  );
1190
- const publicKeyJwk = await crypto3.subtle.exportKey(
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 (!fs5.existsSync(USER_WRANGLER_TOML)) {
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
- fs5.copyFileSync(USER_WRANGLER_TOML, path4.join(stagingDir, "wrangler.toml"));
1235
- const schemaPath = path4.join(stagingDir, "src/db/schema.sql");
1236
- if (fs5.existsSync(schemaPath)) {
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 = path4.join(stagingDir, "src/db/migrations");
1246
- if (fs5.existsSync(migrationsDir)) {
1247
- const migrationFiles = fs5.readdirSync(migrationsDir).filter((f) => f.endsWith(".sql")).sort();
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 = path4.join(migrationsDir, file);
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 crypto3.subtle.generateKey("Ed25519", true, [
806
+ const keyPair = await crypto2.subtle.generateKey("Ed25519", true, [
1271
807
  "sign",
1272
808
  "verify"
1273
809
  ]);
1274
- const privateKeyRaw = await crypto3.subtle.exportKey(
810
+ const privateKeyRaw = await crypto2.subtle.exportKey(
1275
811
  "pkcs8",
1276
812
  keyPair.privateKey
1277
813
  );
1278
- const publicKeyRaw = await crypto3.subtle.exportKey(
814
+ const publicKeyRaw = await crypto2.subtle.exportKey(
1279
815
  "raw",
1280
816
  keyPair.publicKey
1281
817
  );
1282
- const privateKeyJwk = await crypto3.subtle.exportKey(
818
+ const privateKeyJwk = await crypto2.subtle.exportKey(
1283
819
  "jwk",
1284
820
  keyPair.privateKey
1285
821
  );
1286
- const publicKeyJwk = await crypto3.subtle.exportKey(
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
- if (customDomain) {
1337
- printInfo("Custom domain", customDomain);
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
- const canonicalUrl = config.url || customDomain || workerUrl;
1340
- await new Promise((r) => setTimeout(r, 2e3));
1341
- if (canonicalUrl && adminToken) {
1342
- try {
1343
- const client = new ZooidClient({
1344
- server: canonicalUrl,
1345
- token: adminToken
1346
- });
1347
- await client.updateServerMeta({
1348
- name: config.name || void 0,
1349
- description: config.description || void 0,
1350
- tags: config.tags.length > 0 ? config.tags : void 0,
1351
- owner: config.owner || void 0,
1352
- company: config.company || void 0,
1353
- email: config.email || void 0
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
- printSuccess("Server identity updated");
1356
- } catch (err) {
1357
- printError(
1358
- `Failed to push server identity: ${err instanceof Error ? err.message : err}`
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
- if (!config.url && (customDomain || workerUrl)) {
1363
- config.url = customDomain || workerUrl;
1364
- saveServerConfig(config);
1365
- printSuccess("Saved URL to zooid.json");
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 configToSave = {
1368
- worker_url: workerUrl || void 0,
1369
- admin_token: adminToken
1370
- };
1371
- if (isFirstDeploy) {
1372
- configToSave.channels = {};
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
- saveConfig(configToSave, canonicalUrl || void 0);
1375
- printSuccess("Saved connection config to ~/.zooid/state.json");
1376
- cleanup(stagingDir);
1377
- console.log("");
1378
- 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");
1379
- console.log(" \u{1FAB8} Zooid server deployed!");
1380
- 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");
1381
- printInfo("Server", canonicalUrl || "(unknown)");
1382
- if (workerUrl && workerUrl !== canonicalUrl) {
1383
- printInfo("Worker URL", workerUrl);
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
- printInfo("Name", config.name || "(not set)");
1386
- if (isFirstDeploy) {
1387
- printInfo("Admin token", adminToken.slice(0, 20) + "...");
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
- if (isFirstDeploy) {
1392
- console.log(" Next steps:");
1393
- console.log(" Edit wrangler.toml to add env vars, observability, etc.");
1394
- console.log(" npx zooid channel create my-channel");
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
- function cleanup(dir) {
1402
- try {
1403
- fs5.rmSync(dir, { recursive: true, force: true });
1404
- } catch {
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.4.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 fs6 = await import("fs");
1545
- const raw = fs6.readFileSync(opts.config, "utf-8");
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 fs6 = await import("fs");
1549
- const raw = fs6.readFileSync(opts.schema, "utf-8");
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 fs6 = await import("fs");
1587
- const raw = fs6.readFileSync(opts.config, "utf-8");
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 fs6 = await import("fs");
1591
- const raw = fs6.readFileSync(opts.schema, "utf-8");
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 readline4 = await import("readline");
1638
- const rl = readline4.createInterface({
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 readline4 = await import("readline");
1686
- const rl = readline4.createInterface({
1855
+ const readline5 = await import("readline");
1856
+ const rl = readline5.createInterface({
1687
1857
  input: process.stdin,
1688
1858
  output: process.stdout
1689
1859
  });