zooid 0.5.1 → 0.6.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 +195 -163
- package/dist/chunk-AR456MHY.js +29 -0
- package/dist/index.js +2425 -765
- package/dist/template-T5IB4YWC.js +92 -0
- package/package.json +5 -5
package/dist/index.js
CHANGED
|
@@ -18,6 +18,9 @@ import {
|
|
|
18
18
|
saveDirectoryToken,
|
|
19
19
|
switchServer
|
|
20
20
|
} from "./chunk-67ZRMVHO.js";
|
|
21
|
+
import {
|
|
22
|
+
parseGitHubUrl
|
|
23
|
+
} from "./chunk-AR456MHY.js";
|
|
21
24
|
|
|
22
25
|
// src/index.ts
|
|
23
26
|
import { Command } from "commander";
|
|
@@ -202,6 +205,285 @@ function runConfigGet(key) {
|
|
|
202
205
|
);
|
|
203
206
|
}
|
|
204
207
|
|
|
208
|
+
// src/lib/workforce.ts
|
|
209
|
+
import fs3 from "fs";
|
|
210
|
+
import path3 from "path";
|
|
211
|
+
|
|
212
|
+
// src/lib/project.ts
|
|
213
|
+
import fs2 from "fs";
|
|
214
|
+
import path2 from "path";
|
|
215
|
+
function findProjectRoot(from) {
|
|
216
|
+
let dir = fs2.realpathSync(from ?? process.cwd());
|
|
217
|
+
while (true) {
|
|
218
|
+
if (fs2.existsSync(path2.join(dir, "zooid.json")) || fs2.existsSync(path2.join(dir, ".zooid"))) {
|
|
219
|
+
return dir;
|
|
220
|
+
}
|
|
221
|
+
const parent = path2.dirname(dir);
|
|
222
|
+
if (parent === dir) return null;
|
|
223
|
+
dir = parent;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
function getZooidDir(from) {
|
|
227
|
+
const root = findProjectRoot(from);
|
|
228
|
+
if (!root) {
|
|
229
|
+
throw new Error(
|
|
230
|
+
"Not a Zooid project (no zooid.json or .zooid/ found). Run `npx zooid init` first."
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
return path2.join(root, ".zooid");
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// src/lib/workforce.ts
|
|
237
|
+
var WORKFORCE_FILENAME = "workforce.json";
|
|
238
|
+
var SLUG_RE = /^[a-z0-9][a-z0-9-]{1,62}[a-z0-9]$/;
|
|
239
|
+
function isValidSlug(s) {
|
|
240
|
+
return SLUG_RE.test(s);
|
|
241
|
+
}
|
|
242
|
+
function validateWorkforceFile(raw, filePath) {
|
|
243
|
+
if (raw.meta && typeof raw.meta === "object") {
|
|
244
|
+
const meta = raw.meta;
|
|
245
|
+
if (meta.slug !== void 0) {
|
|
246
|
+
if (typeof meta.slug !== "string" || !isValidSlug(meta.slug)) {
|
|
247
|
+
throw new Error(
|
|
248
|
+
`Invalid meta.slug in ${filePath}: "${meta.slug}" \u2014 must be a valid slug (lowercase alphanumeric + hyphens, 3-64 chars)`
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
if (raw.include) {
|
|
254
|
+
if (!Array.isArray(raw.include)) {
|
|
255
|
+
throw new Error(`"include" must be an array in ${filePath}`);
|
|
256
|
+
}
|
|
257
|
+
for (const p of raw.include) {
|
|
258
|
+
if (typeof p !== "string") {
|
|
259
|
+
throw new Error(`Include entries must be strings in ${filePath}`);
|
|
260
|
+
}
|
|
261
|
+
if (path3.isAbsolute(p)) {
|
|
262
|
+
throw new Error(`Include path must be relative in ${filePath}: ${p}`);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
if (raw.channels && typeof raw.channels === "object") {
|
|
267
|
+
for (const [id, ch] of Object.entries(
|
|
268
|
+
raw.channels
|
|
269
|
+
)) {
|
|
270
|
+
if (!isValidSlug(id)) {
|
|
271
|
+
throw new Error(
|
|
272
|
+
`Invalid channel slug "${id}" in ${filePath} \u2014 must be lowercase alphanumeric + hyphens, 3-64 chars`
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
if (!ch || typeof ch !== "object" || !ch.visibility) {
|
|
276
|
+
throw new Error(
|
|
277
|
+
`Channel "${id}" in ${filePath} must have a "visibility" field`
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
if (raw.roles && typeof raw.roles === "object") {
|
|
283
|
+
for (const [id, role] of Object.entries(
|
|
284
|
+
raw.roles
|
|
285
|
+
)) {
|
|
286
|
+
if (!isValidSlug(id)) {
|
|
287
|
+
throw new Error(
|
|
288
|
+
`Invalid role slug "${id}" in ${filePath} \u2014 must be lowercase alphanumeric + hyphens, 3-64 chars`
|
|
289
|
+
);
|
|
290
|
+
}
|
|
291
|
+
if (!role || typeof role !== "object" || !Array.isArray(role.scopes)) {
|
|
292
|
+
throw new Error(
|
|
293
|
+
`Role "${id}" in ${filePath} must have a "scopes" array`
|
|
294
|
+
);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
function resolveIncludes(filePath, ancestors, isRoot = false) {
|
|
300
|
+
const realPath = fs3.realpathSync(filePath);
|
|
301
|
+
if (ancestors.has(realPath)) {
|
|
302
|
+
const chain = [...ancestors, realPath].map((p) => path3.basename(p)).join(" \u2192 ");
|
|
303
|
+
throw new Error(`Circular include: ${chain}`);
|
|
304
|
+
}
|
|
305
|
+
const childAncestors = new Set(ancestors);
|
|
306
|
+
childAncestors.add(realPath);
|
|
307
|
+
const raw = JSON.parse(fs3.readFileSync(filePath, "utf-8"));
|
|
308
|
+
validateWorkforceFile(raw, filePath);
|
|
309
|
+
const wf = raw;
|
|
310
|
+
const baseDir = path3.dirname(filePath);
|
|
311
|
+
const result = {
|
|
312
|
+
channels: {},
|
|
313
|
+
roles: {},
|
|
314
|
+
agents: {},
|
|
315
|
+
provenance: { channels: {}, roles: {}, agents: {} }
|
|
316
|
+
};
|
|
317
|
+
if (wf.include) {
|
|
318
|
+
for (const includePath of wf.include) {
|
|
319
|
+
const resolved = path3.resolve(baseDir, includePath);
|
|
320
|
+
try {
|
|
321
|
+
const zooidDir = getZooidDir();
|
|
322
|
+
const realZooidDir = fs3.realpathSync(zooidDir);
|
|
323
|
+
if (!resolved.startsWith(realZooidDir + path3.sep) && resolved !== realZooidDir) {
|
|
324
|
+
throw new Error(
|
|
325
|
+
`Include path escapes .zooid/ in ${filePath}: ${includePath}`
|
|
326
|
+
);
|
|
327
|
+
}
|
|
328
|
+
} catch (e) {
|
|
329
|
+
if (e instanceof Error && e.message.includes("escapes")) throw e;
|
|
330
|
+
}
|
|
331
|
+
if (!fs3.existsSync(resolved)) {
|
|
332
|
+
throw new Error(
|
|
333
|
+
`Included file not found: ${includePath} (resolved to ${resolved})`
|
|
334
|
+
);
|
|
335
|
+
}
|
|
336
|
+
const included = resolveIncludes(resolved, childAncestors);
|
|
337
|
+
if (!isRoot) {
|
|
338
|
+
for (const id of Object.keys(included.channels)) {
|
|
339
|
+
if (id in result.channels) {
|
|
340
|
+
const prev = path3.basename(result.provenance.channels[id]);
|
|
341
|
+
const curr = path3.basename(included.provenance.channels[id]);
|
|
342
|
+
console.warn(
|
|
343
|
+
`\u26A0 Channel "${id}" defined in both ${prev} and ${curr} \u2014 using ${curr} (last wins)`
|
|
344
|
+
);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
for (const id of Object.keys(included.roles)) {
|
|
348
|
+
if (id in result.roles) {
|
|
349
|
+
const prev = path3.basename(result.provenance.roles[id]);
|
|
350
|
+
const curr = path3.basename(included.provenance.roles[id]);
|
|
351
|
+
console.warn(
|
|
352
|
+
`\u26A0 Role "${id}" defined in both ${prev} and ${curr} \u2014 using ${curr} (last wins)`
|
|
353
|
+
);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
Object.assign(result.channels, included.channels);
|
|
358
|
+
Object.assign(result.roles, included.roles);
|
|
359
|
+
Object.assign(result.agents, included.agents);
|
|
360
|
+
Object.assign(result.provenance.channels, included.provenance.channels);
|
|
361
|
+
Object.assign(result.provenance.roles, included.provenance.roles);
|
|
362
|
+
Object.assign(result.provenance.agents, included.provenance.agents);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
if (wf.channels) {
|
|
366
|
+
for (const [id, def] of Object.entries(wf.channels)) {
|
|
367
|
+
result.channels[id] = def;
|
|
368
|
+
result.provenance.channels[id] = filePath;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
if (wf.roles) {
|
|
372
|
+
for (const [id, def] of Object.entries(wf.roles)) {
|
|
373
|
+
result.roles[id] = def;
|
|
374
|
+
result.provenance.roles[id] = filePath;
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
if (wf.agents) {
|
|
378
|
+
for (const [id, def] of Object.entries(wf.agents)) {
|
|
379
|
+
result.agents[id] = def;
|
|
380
|
+
result.provenance.agents[id] = filePath;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
return result;
|
|
384
|
+
}
|
|
385
|
+
function loadWorkforce() {
|
|
386
|
+
let zooidDir;
|
|
387
|
+
try {
|
|
388
|
+
zooidDir = getZooidDir();
|
|
389
|
+
} catch {
|
|
390
|
+
return {
|
|
391
|
+
channels: {},
|
|
392
|
+
roles: {},
|
|
393
|
+
provenance: { channels: {}, roles: {} }
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
const filePath = path3.join(zooidDir, WORKFORCE_FILENAME);
|
|
397
|
+
if (!fs3.existsSync(filePath)) {
|
|
398
|
+
return {
|
|
399
|
+
channels: {},
|
|
400
|
+
roles: {},
|
|
401
|
+
provenance: { channels: {}, roles: {} }
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
const resolved = resolveIncludes(filePath, /* @__PURE__ */ new Set(), true);
|
|
405
|
+
if (Object.keys(resolved.agents).length > 0) {
|
|
406
|
+
for (const agentId of Object.keys(resolved.agents)) {
|
|
407
|
+
if (agentId in resolved.roles) {
|
|
408
|
+
throw new Error(
|
|
409
|
+
`agent "${agentId}" collides with a role of the same name. Use one or the other.`
|
|
410
|
+
);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
const derivedRoles = compileAgents(resolved.agents);
|
|
414
|
+
Object.assign(resolved.roles, derivedRoles);
|
|
415
|
+
for (const id of Object.keys(derivedRoles)) {
|
|
416
|
+
resolved.provenance.roles[id] = resolved.provenance.agents[id] ?? filePath;
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
return {
|
|
420
|
+
channels: resolved.channels,
|
|
421
|
+
roles: resolved.roles,
|
|
422
|
+
provenance: resolved.provenance
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
function saveWorkforce(data, options) {
|
|
426
|
+
let filePath;
|
|
427
|
+
if (options?.targetFile) {
|
|
428
|
+
filePath = options.targetFile;
|
|
429
|
+
} else {
|
|
430
|
+
let zooidDir;
|
|
431
|
+
try {
|
|
432
|
+
zooidDir = getZooidDir();
|
|
433
|
+
} catch {
|
|
434
|
+
zooidDir = path3.join(process.cwd(), ".zooid");
|
|
435
|
+
}
|
|
436
|
+
fs3.mkdirSync(zooidDir, { recursive: true });
|
|
437
|
+
filePath = path3.join(zooidDir, WORKFORCE_FILENAME);
|
|
438
|
+
}
|
|
439
|
+
let existing = {};
|
|
440
|
+
if (fs3.existsSync(filePath)) {
|
|
441
|
+
existing = JSON.parse(fs3.readFileSync(filePath, "utf-8"));
|
|
442
|
+
}
|
|
443
|
+
const output = {};
|
|
444
|
+
if (existing.$schema) output.$schema = existing.$schema;
|
|
445
|
+
if (existing.meta) output.meta = existing.meta;
|
|
446
|
+
if (existing.include) output.include = existing.include;
|
|
447
|
+
output.channels = data.channels;
|
|
448
|
+
output.roles = data.roles;
|
|
449
|
+
if (existing.agents) output.agents = existing.agents;
|
|
450
|
+
fs3.writeFileSync(filePath, JSON.stringify(output, null, 2) + "\n");
|
|
451
|
+
}
|
|
452
|
+
function updateInFile(filePath, section, id, def) {
|
|
453
|
+
const raw = JSON.parse(fs3.readFileSync(filePath, "utf-8"));
|
|
454
|
+
if (!raw[section]) raw[section] = {};
|
|
455
|
+
raw[section][id] = def;
|
|
456
|
+
fs3.writeFileSync(filePath, JSON.stringify(raw, null, 2) + "\n");
|
|
457
|
+
}
|
|
458
|
+
function removeFromFile(filePath, section, id) {
|
|
459
|
+
const raw = JSON.parse(fs3.readFileSync(filePath, "utf-8"));
|
|
460
|
+
if (raw[section]) {
|
|
461
|
+
delete raw[section][id];
|
|
462
|
+
}
|
|
463
|
+
fs3.writeFileSync(filePath, JSON.stringify(raw, null, 2) + "\n");
|
|
464
|
+
}
|
|
465
|
+
function compileAgents(agents) {
|
|
466
|
+
const roles = {};
|
|
467
|
+
for (const [id, agent] of Object.entries(agents)) {
|
|
468
|
+
const scopes = [];
|
|
469
|
+
if (agent.subscribes) {
|
|
470
|
+
for (const ch of agent.subscribes) {
|
|
471
|
+
scopes.push(`sub:${ch}`);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
if (agent.publishes) {
|
|
475
|
+
for (const ch of agent.publishes) {
|
|
476
|
+
scopes.push(`pub:${ch}`);
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
const role = { scopes };
|
|
480
|
+
if (agent.name) role.name = agent.name;
|
|
481
|
+
if (agent.description) role.description = agent.description;
|
|
482
|
+
roles[id] = role;
|
|
483
|
+
}
|
|
484
|
+
return roles;
|
|
485
|
+
}
|
|
486
|
+
|
|
205
487
|
// src/commands/channel.ts
|
|
206
488
|
async function runChannelCreate(id, options, client) {
|
|
207
489
|
const c = client ?? createClient();
|
|
@@ -213,7 +495,7 @@ async function runChannelCreate(id, options, client) {
|
|
|
213
495
|
id,
|
|
214
496
|
name: options.name ?? id,
|
|
215
497
|
description: options.description,
|
|
216
|
-
is_public: options.public ??
|
|
498
|
+
is_public: options.public ?? false,
|
|
217
499
|
config
|
|
218
500
|
});
|
|
219
501
|
if (!client) {
|
|
@@ -222,6 +504,19 @@ async function runChannelCreate(id, options, client) {
|
|
|
222
504
|
channels[id] = { token: result.token };
|
|
223
505
|
saveConfig({ channels });
|
|
224
506
|
}
|
|
507
|
+
if (findProjectRoot()) {
|
|
508
|
+
try {
|
|
509
|
+
const wf = loadWorkforce();
|
|
510
|
+
wf.channels[id] = {
|
|
511
|
+
visibility: options.public ? "public" : "private",
|
|
512
|
+
...options.name && { name: options.name },
|
|
513
|
+
...options.description && { description: options.description },
|
|
514
|
+
...config && { config }
|
|
515
|
+
};
|
|
516
|
+
saveWorkforce(wf);
|
|
517
|
+
} catch {
|
|
518
|
+
}
|
|
519
|
+
}
|
|
225
520
|
return result;
|
|
226
521
|
}
|
|
227
522
|
async function runChannelList(client) {
|
|
@@ -230,7 +525,27 @@ async function runChannelList(client) {
|
|
|
230
525
|
}
|
|
231
526
|
async function runChannelUpdate(channelId, options, client) {
|
|
232
527
|
const c = client ?? createClient();
|
|
233
|
-
|
|
528
|
+
const result = await c.updateChannel(channelId, options);
|
|
529
|
+
if (findProjectRoot()) {
|
|
530
|
+
try {
|
|
531
|
+
const wf = loadWorkforce();
|
|
532
|
+
const def = {
|
|
533
|
+
visibility: result.is_public ? "public" : "private",
|
|
534
|
+
...result.name && result.name !== channelId && { name: result.name },
|
|
535
|
+
...result.description && { description: result.description },
|
|
536
|
+
...result.config && { config: result.config }
|
|
537
|
+
};
|
|
538
|
+
const targetFile = wf.provenance.channels[channelId];
|
|
539
|
+
if (targetFile) {
|
|
540
|
+
updateInFile(targetFile, "channels", channelId, def);
|
|
541
|
+
} else {
|
|
542
|
+
wf.channels[channelId] = def;
|
|
543
|
+
saveWorkforce(wf);
|
|
544
|
+
}
|
|
545
|
+
} catch {
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
return result;
|
|
234
549
|
}
|
|
235
550
|
async function runChannelDelete(channelId, client) {
|
|
236
551
|
const c = client ?? createClient();
|
|
@@ -240,14 +555,131 @@ async function runChannelDelete(channelId, client) {
|
|
|
240
555
|
const serverUrl = resolveServer();
|
|
241
556
|
if (serverUrl && file.servers?.[serverUrl]?.channels?.[channelId]) {
|
|
242
557
|
delete file.servers[serverUrl].channels[channelId];
|
|
243
|
-
const
|
|
244
|
-
|
|
558
|
+
const fs13 = await import("fs");
|
|
559
|
+
fs13.writeFileSync(getStatePath(), JSON.stringify(file, null, 2) + "\n");
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
if (findProjectRoot()) {
|
|
563
|
+
try {
|
|
564
|
+
const wf = loadWorkforce();
|
|
565
|
+
const targetFile = wf.provenance.channels[channelId];
|
|
566
|
+
if (targetFile) {
|
|
567
|
+
removeFromFile(targetFile, "channels", channelId);
|
|
568
|
+
} else {
|
|
569
|
+
delete wf.channels[channelId];
|
|
570
|
+
saveWorkforce(wf);
|
|
571
|
+
}
|
|
572
|
+
} catch {
|
|
245
573
|
}
|
|
246
574
|
}
|
|
247
575
|
}
|
|
248
576
|
|
|
249
|
-
// src/
|
|
250
|
-
|
|
577
|
+
// src/lib/zoon.ts
|
|
578
|
+
var DEFAULT_PLATFORM_URL = "https://api.zooid.dev";
|
|
579
|
+
function getPlatformUrl() {
|
|
580
|
+
return process.env.ZOON_PLATFORM_URL || DEFAULT_PLATFORM_URL;
|
|
581
|
+
}
|
|
582
|
+
function extractSubdomain(serverUrl) {
|
|
583
|
+
try {
|
|
584
|
+
const url = new URL(serverUrl);
|
|
585
|
+
const match = url.hostname.match(/^([^.]+)\.zoon\.eco$/);
|
|
586
|
+
return match ? match[1] : null;
|
|
587
|
+
} catch {
|
|
588
|
+
return null;
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
function isZoonHosted(serverUrl) {
|
|
592
|
+
return extractSubdomain(serverUrl) !== null;
|
|
593
|
+
}
|
|
594
|
+
function authHeaders(token) {
|
|
595
|
+
return {
|
|
596
|
+
Authorization: `Bearer ${token}`,
|
|
597
|
+
"Content-Type": "application/json"
|
|
598
|
+
};
|
|
599
|
+
}
|
|
600
|
+
function platformUrl(subdomain, path10) {
|
|
601
|
+
return `${getPlatformUrl()}/api/v1/servers/${subdomain}${path10}`;
|
|
602
|
+
}
|
|
603
|
+
async function syncRolesToZoon(serverUrl, token, roles, options) {
|
|
604
|
+
const subdomain = extractSubdomain(serverUrl);
|
|
605
|
+
if (!subdomain) {
|
|
606
|
+
throw new Error(`${serverUrl} is not a Zoon-hosted server`);
|
|
607
|
+
}
|
|
608
|
+
const _fetch = options?.fetch ?? globalThis.fetch;
|
|
609
|
+
const res = await _fetch(platformUrl(subdomain, "/roles"), {
|
|
610
|
+
method: "PUT",
|
|
611
|
+
headers: authHeaders(token),
|
|
612
|
+
body: JSON.stringify(roles)
|
|
613
|
+
});
|
|
614
|
+
if (!res.ok) {
|
|
615
|
+
const err = await res.json().catch(() => ({}));
|
|
616
|
+
throw new Error(
|
|
617
|
+
`Role sync failed: ${err.message || res.status}`
|
|
618
|
+
);
|
|
619
|
+
}
|
|
620
|
+
return res.json();
|
|
621
|
+
}
|
|
622
|
+
async function createCredential(serverUrl, token, name, roleNames, options) {
|
|
623
|
+
const subdomain = extractSubdomain(serverUrl);
|
|
624
|
+
const _fetch = options?.fetch ?? globalThis.fetch;
|
|
625
|
+
const rolesRes = await _fetch(platformUrl(subdomain, "/roles"), {
|
|
626
|
+
headers: authHeaders(token)
|
|
627
|
+
});
|
|
628
|
+
const roles = await rolesRes.json();
|
|
629
|
+
const roleSlugs = roleNames.map((n) => roles.find((r) => r.slug === n || r.name === n)?.slug).filter(Boolean);
|
|
630
|
+
if (roleSlugs.length === 0) {
|
|
631
|
+
throw new Error(`No matching roles found for: ${roleNames.join(", ")}`);
|
|
632
|
+
}
|
|
633
|
+
const res = await _fetch(platformUrl(subdomain, "/credentials"), {
|
|
634
|
+
method: "POST",
|
|
635
|
+
headers: authHeaders(token),
|
|
636
|
+
body: JSON.stringify({ name, role_slugs: roleSlugs })
|
|
637
|
+
});
|
|
638
|
+
if (!res.ok) {
|
|
639
|
+
const err = await res.json().catch(() => ({}));
|
|
640
|
+
throw new Error(
|
|
641
|
+
`Credential creation failed: ${err.message || res.status}`
|
|
642
|
+
);
|
|
643
|
+
}
|
|
644
|
+
return res.json();
|
|
645
|
+
}
|
|
646
|
+
async function listCredentials(serverUrl, token, options) {
|
|
647
|
+
const subdomain = extractSubdomain(serverUrl);
|
|
648
|
+
const _fetch = options?.fetch ?? globalThis.fetch;
|
|
649
|
+
const res = await _fetch(platformUrl(subdomain, "/credentials"), {
|
|
650
|
+
headers: authHeaders(token)
|
|
651
|
+
});
|
|
652
|
+
if (!res.ok) throw new Error(`Failed to list credentials: ${res.status}`);
|
|
653
|
+
return await res.json();
|
|
654
|
+
}
|
|
655
|
+
async function rotateCredential(serverUrl, token, clientId, options) {
|
|
656
|
+
const subdomain = extractSubdomain(serverUrl);
|
|
657
|
+
const _fetch = options?.fetch ?? globalThis.fetch;
|
|
658
|
+
const res = await _fetch(
|
|
659
|
+
platformUrl(subdomain, `/credentials/${clientId}/rotate`),
|
|
660
|
+
{ method: "POST", headers: authHeaders(token) }
|
|
661
|
+
);
|
|
662
|
+
if (!res.ok) throw new Error(`Failed to rotate credential: ${res.status}`);
|
|
663
|
+
return res.json();
|
|
664
|
+
}
|
|
665
|
+
async function listRolesFromZoon(serverUrl, token, options) {
|
|
666
|
+
const subdomain = extractSubdomain(serverUrl);
|
|
667
|
+
const _fetch = options?.fetch ?? globalThis.fetch;
|
|
668
|
+
const res = await _fetch(platformUrl(subdomain, "/roles"), {
|
|
669
|
+
headers: authHeaders(token)
|
|
670
|
+
});
|
|
671
|
+
if (!res.ok) throw new Error(`Failed to list roles: ${res.status}`);
|
|
672
|
+
return res.json();
|
|
673
|
+
}
|
|
674
|
+
async function revokeCredential(serverUrl, token, clientId, options) {
|
|
675
|
+
const subdomain = extractSubdomain(serverUrl);
|
|
676
|
+
const _fetch = options?.fetch ?? globalThis.fetch;
|
|
677
|
+
const res = await _fetch(platformUrl(subdomain, `/credentials/${clientId}`), {
|
|
678
|
+
method: "DELETE",
|
|
679
|
+
headers: authHeaders(token)
|
|
680
|
+
});
|
|
681
|
+
if (!res.ok) throw new Error(`Failed to revoke credential: ${res.status}`);
|
|
682
|
+
}
|
|
251
683
|
|
|
252
684
|
// src/lib/output.ts
|
|
253
685
|
function printSuccess(message) {
|
|
@@ -273,110 +705,856 @@ function formatRelative(isoString) {
|
|
|
273
705
|
return `${days}d ago`;
|
|
274
706
|
}
|
|
275
707
|
|
|
276
|
-
// src/
|
|
277
|
-
async function
|
|
278
|
-
const
|
|
279
|
-
const
|
|
280
|
-
|
|
708
|
+
// src/commands/pull.ts
|
|
709
|
+
async function runPull(client) {
|
|
710
|
+
const c = client ?? createClient();
|
|
711
|
+
const wf = loadWorkforce();
|
|
712
|
+
const written = [];
|
|
713
|
+
let newChannelsAdded = false;
|
|
714
|
+
const channels = await c.listChannels();
|
|
715
|
+
if (channels.length > 0) {
|
|
716
|
+
printStep("Pulling channels...");
|
|
717
|
+
for (const ch of channels) {
|
|
718
|
+
const def = {
|
|
719
|
+
visibility: ch.is_public ? "public" : "private"
|
|
720
|
+
};
|
|
721
|
+
if (ch.name && ch.name !== ch.id) def.name = ch.name;
|
|
722
|
+
if (ch.description) def.description = ch.description;
|
|
723
|
+
if (ch.config) def.config = ch.config;
|
|
724
|
+
const targetFile = wf.provenance.channels[ch.id];
|
|
725
|
+
if (targetFile) {
|
|
726
|
+
updateInFile(targetFile, "channels", ch.id, def);
|
|
727
|
+
} else {
|
|
728
|
+
wf.channels[ch.id] = def;
|
|
729
|
+
newChannelsAdded = true;
|
|
730
|
+
}
|
|
731
|
+
written.push(ch.id);
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
let newRolesAdded = false;
|
|
735
|
+
try {
|
|
736
|
+
const server = resolveServer();
|
|
737
|
+
const file = loadConfigFile();
|
|
738
|
+
const entry = server ? file.servers?.[server] : void 0;
|
|
739
|
+
let roles;
|
|
740
|
+
if (server && isZoonHosted(server) && entry?.platform_token) {
|
|
741
|
+
roles = await listRolesFromZoon(server, entry.platform_token);
|
|
742
|
+
} else {
|
|
743
|
+
roles = await c.listRoles();
|
|
744
|
+
}
|
|
745
|
+
if (roles.length > 0) {
|
|
746
|
+
printStep("Pulling roles...");
|
|
747
|
+
for (const role of roles) {
|
|
748
|
+
const roleKey = role.slug ?? role.id;
|
|
749
|
+
const def = { scopes: role.scopes };
|
|
750
|
+
if (role.name) def.name = role.name;
|
|
751
|
+
if (role.description) def.description = role.description;
|
|
752
|
+
const targetFile = wf.provenance.roles[roleKey];
|
|
753
|
+
if (targetFile) {
|
|
754
|
+
updateInFile(targetFile, "roles", roleKey, def);
|
|
755
|
+
} else {
|
|
756
|
+
wf.roles[roleKey] = def;
|
|
757
|
+
newRolesAdded = true;
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
} catch {
|
|
762
|
+
}
|
|
763
|
+
if (newChannelsAdded || newRolesAdded) {
|
|
764
|
+
saveWorkforce(wf);
|
|
765
|
+
}
|
|
766
|
+
if (written.length > 0 || Object.keys(wf.roles).length > 0) {
|
|
767
|
+
printSuccess(
|
|
768
|
+
`Pulled ${written.length} channel(s) and ${Object.keys(wf.roles).length} role(s) into .zooid/workforce.json`
|
|
769
|
+
);
|
|
770
|
+
} else {
|
|
771
|
+
printInfo("Nothing to pull", "no channels or roles on server");
|
|
772
|
+
}
|
|
773
|
+
return written;
|
|
281
774
|
}
|
|
282
775
|
|
|
283
|
-
// src/commands/
|
|
284
|
-
import
|
|
285
|
-
import
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
// src/lib/crypto.ts
|
|
294
|
-
function base64url(buf) {
|
|
295
|
-
return buf.toString("base64url");
|
|
296
|
-
}
|
|
297
|
-
async function createAdminToken(secret) {
|
|
298
|
-
const header = base64url(
|
|
299
|
-
Buffer.from(JSON.stringify({ alg: "HS256", typ: "JWT" }))
|
|
300
|
-
);
|
|
301
|
-
const payload = base64url(
|
|
302
|
-
Buffer.from(
|
|
303
|
-
JSON.stringify({
|
|
304
|
-
scope: "admin",
|
|
305
|
-
iat: Math.floor(Date.now() / 1e3)
|
|
306
|
-
})
|
|
307
|
-
)
|
|
308
|
-
);
|
|
309
|
-
const data = `${header}.${payload}`;
|
|
310
|
-
const key = await crypto.subtle.importKey(
|
|
311
|
-
"raw",
|
|
312
|
-
new TextEncoder().encode(secret),
|
|
313
|
-
{ name: "HMAC", hash: "SHA-256" },
|
|
314
|
-
false,
|
|
315
|
-
["sign"]
|
|
316
|
-
);
|
|
317
|
-
const sig = await crypto.subtle.sign(
|
|
318
|
-
"HMAC",
|
|
319
|
-
key,
|
|
320
|
-
new TextEncoder().encode(data)
|
|
321
|
-
);
|
|
322
|
-
const signature = base64url(Buffer.from(sig));
|
|
323
|
-
return `${data}.${signature}`;
|
|
324
|
-
}
|
|
325
|
-
async function createEdDSAAdminToken(privateKeyJwk, kid) {
|
|
326
|
-
const header = base64url(
|
|
327
|
-
Buffer.from(JSON.stringify({ alg: "EdDSA", typ: "JWT", kid }))
|
|
328
|
-
);
|
|
329
|
-
const payload = base64url(
|
|
330
|
-
Buffer.from(
|
|
331
|
-
JSON.stringify({
|
|
332
|
-
scope: "admin",
|
|
333
|
-
iat: Math.floor(Date.now() / 1e3)
|
|
334
|
-
})
|
|
335
|
-
)
|
|
336
|
-
);
|
|
337
|
-
const message = `${header}.${payload}`;
|
|
338
|
-
const privateKey = await crypto.subtle.importKey(
|
|
339
|
-
"jwk",
|
|
340
|
-
privateKeyJwk,
|
|
341
|
-
{ name: "Ed25519" },
|
|
342
|
-
false,
|
|
343
|
-
["sign"]
|
|
344
|
-
);
|
|
345
|
-
const sig = await crypto.subtle.sign(
|
|
346
|
-
"Ed25519",
|
|
347
|
-
privateKey,
|
|
348
|
-
new TextEncoder().encode(message)
|
|
349
|
-
);
|
|
350
|
-
const signature = base64url(Buffer.from(sig));
|
|
351
|
-
return `${message}.${signature}`;
|
|
776
|
+
// src/commands/publish.ts
|
|
777
|
+
import fs4 from "fs";
|
|
778
|
+
import readline from "readline";
|
|
779
|
+
function parseJSON(raw, source) {
|
|
780
|
+
try {
|
|
781
|
+
return JSON.parse(raw);
|
|
782
|
+
} catch {
|
|
783
|
+
throw new Error(`Invalid JSON from ${source}: ${raw.slice(0, 100)}`);
|
|
784
|
+
}
|
|
352
785
|
}
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
786
|
+
function readStdin() {
|
|
787
|
+
return new Promise((resolve, reject) => {
|
|
788
|
+
if (process.stdin.isTTY) {
|
|
789
|
+
reject(
|
|
790
|
+
new Error(
|
|
791
|
+
"No data provided. Usage: zooid publish <channel> <json> or pipe via stdin"
|
|
792
|
+
)
|
|
793
|
+
);
|
|
794
|
+
return;
|
|
795
|
+
}
|
|
796
|
+
let data = "";
|
|
797
|
+
process.stdin.setEncoding("utf-8");
|
|
798
|
+
process.stdin.on("data", (chunk) => data += chunk);
|
|
799
|
+
process.stdin.on("end", () => resolve(data.trim()));
|
|
800
|
+
process.stdin.on("error", reject);
|
|
801
|
+
});
|
|
802
|
+
}
|
|
803
|
+
async function runPublish(channelId, options, client, dataArg) {
|
|
804
|
+
const c = client ?? createPublishClient(channelId);
|
|
805
|
+
let type;
|
|
806
|
+
let data;
|
|
807
|
+
if (options.file) {
|
|
808
|
+
const raw = fs4.readFileSync(options.file, "utf-8");
|
|
809
|
+
const parsed = parseJSON(raw, options.file);
|
|
810
|
+
if (typeof parsed === "object" && parsed !== null && "type" in parsed) {
|
|
811
|
+
const obj = parsed;
|
|
812
|
+
type = obj.type;
|
|
813
|
+
data = obj.data ?? parsed;
|
|
814
|
+
} else {
|
|
815
|
+
data = parsed;
|
|
816
|
+
}
|
|
817
|
+
} else if (options.data) {
|
|
818
|
+
data = parseJSON(options.data, "--data");
|
|
819
|
+
type = options.type;
|
|
820
|
+
} else if (dataArg) {
|
|
821
|
+
data = parseJSON(dataArg, "argument");
|
|
822
|
+
type = options.type;
|
|
823
|
+
} else {
|
|
824
|
+
const raw = await readStdin();
|
|
825
|
+
data = parseJSON(raw, "stdin");
|
|
826
|
+
type = options.type;
|
|
827
|
+
}
|
|
828
|
+
const publishOpts = { data };
|
|
829
|
+
if (type) publishOpts.type = type;
|
|
830
|
+
return c.publish(channelId, publishOpts);
|
|
831
|
+
}
|
|
832
|
+
async function runPublishStream(channelId, options, client, onEvent) {
|
|
833
|
+
if (process.stdin.isTTY) {
|
|
834
|
+
throw new Error(
|
|
835
|
+
"--stream requires piped input (e.g. cat events.jsonl | zooid publish channel --stream)"
|
|
836
|
+
);
|
|
837
|
+
}
|
|
838
|
+
const c = client ?? createPublishClient(channelId);
|
|
839
|
+
const rl = readline.createInterface({ input: process.stdin });
|
|
840
|
+
let published = 0;
|
|
841
|
+
let errors = 0;
|
|
842
|
+
let lineNum = 0;
|
|
843
|
+
for await (const line of rl) {
|
|
844
|
+
lineNum++;
|
|
845
|
+
const trimmed = line.trim();
|
|
846
|
+
if (!trimmed) continue;
|
|
847
|
+
const data = parseJSON(trimmed, `stdin line ${lineNum}`);
|
|
848
|
+
const publishOpts = { data };
|
|
849
|
+
if (options.type) publishOpts.type = options.type;
|
|
850
|
+
try {
|
|
851
|
+
const event = await c.publish(channelId, publishOpts);
|
|
852
|
+
published++;
|
|
853
|
+
onEvent?.(event, lineNum);
|
|
854
|
+
} catch (err) {
|
|
855
|
+
errors++;
|
|
856
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
857
|
+
process.stderr.write(`Line ${lineNum}: ${msg}
|
|
858
|
+
`);
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
return { published, errors };
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
// src/commands/subscribe.ts
|
|
865
|
+
async function runSubscribePoll(channelId, options = {}, client) {
|
|
866
|
+
const c = client ?? createSubscribeClient(channelId);
|
|
867
|
+
const callback = options.callback ?? ((event) => {
|
|
868
|
+
console.log(JSON.stringify(event));
|
|
869
|
+
});
|
|
870
|
+
return c.subscribe(channelId, callback, {
|
|
871
|
+
interval: options.interval ?? 5e3,
|
|
872
|
+
mode: options.mode,
|
|
873
|
+
type: options.type
|
|
874
|
+
});
|
|
875
|
+
}
|
|
876
|
+
async function runSubscribeWebhook(channelId, url, client) {
|
|
877
|
+
const c = client ?? createSubscribeClient(channelId);
|
|
878
|
+
return c.registerWebhook(channelId, url);
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
// src/commands/tail.ts
|
|
882
|
+
async function runTail(channelId, options = {}, client) {
|
|
883
|
+
const c = client ?? createSubscribeClient(channelId);
|
|
884
|
+
if (options.follow) {
|
|
885
|
+
return runTailFollow(c, channelId, options);
|
|
886
|
+
}
|
|
887
|
+
const pollOpts = {};
|
|
888
|
+
if (options.limit !== void 0) pollOpts.limit = options.limit;
|
|
889
|
+
if (options.type) pollOpts.type = options.type;
|
|
890
|
+
if (options.since) pollOpts.since = options.since;
|
|
891
|
+
if (options.cursor) pollOpts.cursor = options.cursor;
|
|
892
|
+
return c.tail(channelId, pollOpts);
|
|
893
|
+
}
|
|
894
|
+
async function runTailFollow(client, channelId, options) {
|
|
895
|
+
const tailOpts = {
|
|
896
|
+
follow: true,
|
|
897
|
+
mode: options.mode,
|
|
898
|
+
interval: options.interval,
|
|
899
|
+
type: options.type
|
|
900
|
+
};
|
|
901
|
+
const stream = client.tail(channelId, tailOpts);
|
|
902
|
+
for await (const event of stream) {
|
|
903
|
+
console.log(JSON.stringify(event));
|
|
904
|
+
}
|
|
905
|
+
return void 0;
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
// src/commands/status.ts
|
|
909
|
+
async function runStatus(client) {
|
|
910
|
+
const c = client ?? createClient();
|
|
911
|
+
const [discovery, identity] = await Promise.all([
|
|
912
|
+
c.getMetadata(),
|
|
913
|
+
c.getServerMeta()
|
|
914
|
+
]);
|
|
915
|
+
return { discovery, identity };
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
// src/commands/history.ts
|
|
919
|
+
function runHistory() {
|
|
920
|
+
const file = loadConfigFile();
|
|
921
|
+
const entries = [];
|
|
922
|
+
if (!file.servers) return entries;
|
|
923
|
+
const seen = /* @__PURE__ */ new Map();
|
|
924
|
+
for (const [serverUrl, serverConfig] of Object.entries(file.servers)) {
|
|
925
|
+
if (!serverConfig.channels) continue;
|
|
926
|
+
const normalizedServer = normalizeServerUrl(serverUrl);
|
|
927
|
+
for (const [channelId, channelData] of Object.entries(
|
|
928
|
+
serverConfig.channels
|
|
929
|
+
)) {
|
|
930
|
+
if (!channelData.stats) continue;
|
|
931
|
+
const key = `${normalizedServer}\0${channelId}`;
|
|
932
|
+
const existingIdx = seen.get(key);
|
|
933
|
+
if (existingIdx !== void 0) {
|
|
934
|
+
const existing = entries[existingIdx];
|
|
935
|
+
existing.num_tails += channelData.stats.num_tails;
|
|
936
|
+
if (channelData.stats.last_tailed_at > existing.last_tailed_at) {
|
|
937
|
+
existing.last_tailed_at = channelData.stats.last_tailed_at;
|
|
938
|
+
existing.name = channelData.name ?? existing.name;
|
|
939
|
+
}
|
|
940
|
+
if (channelData.stats.first_tailed_at < existing.first_tailed_at) {
|
|
941
|
+
existing.first_tailed_at = channelData.stats.first_tailed_at;
|
|
942
|
+
}
|
|
943
|
+
} else {
|
|
944
|
+
seen.set(key, entries.length);
|
|
945
|
+
entries.push({
|
|
946
|
+
server: normalizedServer,
|
|
947
|
+
channel_id: channelId,
|
|
948
|
+
name: channelData.name,
|
|
949
|
+
num_tails: channelData.stats.num_tails,
|
|
950
|
+
last_tailed_at: channelData.stats.last_tailed_at,
|
|
951
|
+
first_tailed_at: channelData.stats.first_tailed_at
|
|
952
|
+
});
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
entries.sort(
|
|
957
|
+
(a, b) => new Date(b.last_tailed_at).getTime() - new Date(a.last_tailed_at).getTime()
|
|
958
|
+
);
|
|
959
|
+
return entries;
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
// src/commands/share.ts
|
|
963
|
+
import readline2 from "readline/promises";
|
|
964
|
+
|
|
965
|
+
// src/lib/directory.ts
|
|
966
|
+
var DIRECTORY_BASE_URL = "https://directory.zooid.dev";
|
|
967
|
+
var TOKEN_PREFIX = "zd_";
|
|
968
|
+
var POLL_INTERVAL_MS = 3e3;
|
|
969
|
+
var POLL_TIMEOUT_MS = 2 * 60 * 1e3;
|
|
970
|
+
function getDirectoryToken() {
|
|
971
|
+
const token = loadDirectoryToken();
|
|
972
|
+
if (token && token.startsWith(TOKEN_PREFIX)) {
|
|
973
|
+
return token;
|
|
974
|
+
}
|
|
975
|
+
return void 0;
|
|
976
|
+
}
|
|
977
|
+
async function ensureDirectoryToken() {
|
|
978
|
+
const existing = getDirectoryToken();
|
|
979
|
+
if (existing) return existing;
|
|
980
|
+
return deviceAuth();
|
|
981
|
+
}
|
|
982
|
+
async function deviceAuth() {
|
|
983
|
+
const res = await fetch(`${DIRECTORY_BASE_URL}/api/auth/device`, {
|
|
984
|
+
method: "POST",
|
|
985
|
+
headers: { "Content-Type": "application/json" }
|
|
986
|
+
});
|
|
987
|
+
if (!res.ok) {
|
|
988
|
+
throw new Error(`Directory auth failed: ${res.status} ${res.statusText}`);
|
|
989
|
+
}
|
|
990
|
+
const data = await res.json();
|
|
991
|
+
const { device_code, verification_url } = data;
|
|
992
|
+
console.log("");
|
|
993
|
+
printInfo("Authorize", verification_url);
|
|
994
|
+
console.log(" Opening browser to complete GitHub sign-in...\n");
|
|
995
|
+
try {
|
|
996
|
+
const { exec: exec2 } = await import("child_process");
|
|
997
|
+
const platform = process.platform;
|
|
998
|
+
const cmd = platform === "darwin" ? "open" : platform === "win32" ? "start" : "xdg-open";
|
|
999
|
+
exec2(`${cmd} "${verification_url}"`);
|
|
1000
|
+
} catch {
|
|
1001
|
+
console.log(
|
|
1002
|
+
" Could not open browser automatically. Please visit the URL above.\n"
|
|
1003
|
+
);
|
|
1004
|
+
}
|
|
1005
|
+
const deadline = Date.now() + POLL_TIMEOUT_MS;
|
|
1006
|
+
while (Date.now() < deadline) {
|
|
1007
|
+
await sleep(POLL_INTERVAL_MS);
|
|
1008
|
+
const statusRes = await fetch(
|
|
1009
|
+
`${DIRECTORY_BASE_URL}/api/auth/status?code=${encodeURIComponent(device_code)}`
|
|
1010
|
+
);
|
|
1011
|
+
if (!statusRes.ok) continue;
|
|
1012
|
+
const status = await statusRes.json();
|
|
1013
|
+
if (status.status === "complete" && status.token) {
|
|
1014
|
+
saveDirectoryToken(status.token);
|
|
1015
|
+
console.log(" Authenticated with Zooid Directory.\n");
|
|
1016
|
+
return status.token;
|
|
1017
|
+
}
|
|
1018
|
+
if (status.status === "expired") {
|
|
1019
|
+
throw new Error("Device authorization expired. Please try again.");
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
throw new Error(
|
|
1023
|
+
"Authorization timed out. You need your human to authorize you. Run `npx zooid share` again to retry."
|
|
1024
|
+
);
|
|
1025
|
+
}
|
|
1026
|
+
async function directoryFetch(path10, options = {}) {
|
|
1027
|
+
let token = await ensureDirectoryToken();
|
|
1028
|
+
const doFetch = (t) => {
|
|
1029
|
+
const headers = new Headers(options.headers);
|
|
1030
|
+
headers.set("Authorization", `Bearer ${t}`);
|
|
1031
|
+
headers.set("Content-Type", "application/json");
|
|
1032
|
+
return fetch(`${DIRECTORY_BASE_URL}${path10}`, { ...options, headers });
|
|
1033
|
+
};
|
|
1034
|
+
let res = await doFetch(token);
|
|
1035
|
+
if (res.status === 401) {
|
|
1036
|
+
console.log(" Directory token expired. Re-authenticating...\n");
|
|
1037
|
+
token = await deviceAuth();
|
|
1038
|
+
res = await doFetch(token);
|
|
1039
|
+
}
|
|
1040
|
+
return res;
|
|
1041
|
+
}
|
|
1042
|
+
function sleep(ms) {
|
|
1043
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1044
|
+
}
|
|
1045
|
+
async function formatDirectoryError(res) {
|
|
1046
|
+
let msg = `Directory returned ${res.status}`;
|
|
1047
|
+
try {
|
|
1048
|
+
const body = await res.json();
|
|
1049
|
+
const parts = [];
|
|
1050
|
+
if (body.error) parts.push(String(body.error));
|
|
1051
|
+
if (body.message) parts.push(String(body.message));
|
|
1052
|
+
if (parts.length > 0) msg = parts.join(": ");
|
|
1053
|
+
} catch {
|
|
1054
|
+
}
|
|
1055
|
+
return msg;
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
// src/lib/prompts.ts
|
|
1059
|
+
async function ask(rl, label, defaultValue) {
|
|
1060
|
+
const hint = defaultValue ? ` [${defaultValue}]` : "";
|
|
1061
|
+
const answer = await rl.question(` ${label}${hint}: `);
|
|
1062
|
+
return answer.trim() || defaultValue;
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
// src/commands/share.ts
|
|
1066
|
+
async function runShare(channelIds, options = {}) {
|
|
1067
|
+
const client = createClient();
|
|
1068
|
+
const config = loadConfig();
|
|
1069
|
+
const serverUrl = config.server;
|
|
1070
|
+
if (!serverUrl) {
|
|
1071
|
+
throw new Error(
|
|
1072
|
+
"No server configured. Run: npx zooid config set server <url>"
|
|
1073
|
+
);
|
|
1074
|
+
}
|
|
1075
|
+
const allChannels = await client.listChannels();
|
|
1076
|
+
const publicChannels = allChannels.filter((ch) => ch.is_public);
|
|
1077
|
+
if (publicChannels.length === 0) {
|
|
1078
|
+
throw new Error("No public channels found on this server.");
|
|
1079
|
+
}
|
|
1080
|
+
let selected;
|
|
1081
|
+
if (channelIds.length > 0) {
|
|
1082
|
+
const byId = new Map(allChannels.map((ch) => [ch.id, ch]));
|
|
1083
|
+
const missing = [];
|
|
1084
|
+
selected = [];
|
|
1085
|
+
for (const id of channelIds) {
|
|
1086
|
+
const ch = byId.get(id);
|
|
1087
|
+
if (!ch) {
|
|
1088
|
+
missing.push(id);
|
|
1089
|
+
} else {
|
|
1090
|
+
selected.push(ch);
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
if (missing.length > 0) {
|
|
1094
|
+
throw new Error(`Channels not found: ${missing.join(", ")}`);
|
|
1095
|
+
}
|
|
1096
|
+
const privateChannels = selected.filter((ch) => !ch.is_public);
|
|
1097
|
+
if (privateChannels.length > 0) {
|
|
1098
|
+
throw new Error(
|
|
1099
|
+
`Cannot share private channels: ${privateChannels.map((ch) => ch.id).join(", ")}. Only public channels can be listed in the directory.`
|
|
1100
|
+
);
|
|
1101
|
+
}
|
|
1102
|
+
} else if (options.yes) {
|
|
1103
|
+
selected = publicChannels;
|
|
1104
|
+
} else {
|
|
1105
|
+
selected = await pickChannels(publicChannels);
|
|
1106
|
+
if (selected.length === 0) {
|
|
1107
|
+
throw new Error("No channels selected.");
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
const channelDetails = await promptChannelDetails(selected, options.yes);
|
|
1111
|
+
const ids = selected.map((ch) => ch.id);
|
|
1112
|
+
const { claim, signature } = await client.getClaim(ids);
|
|
1113
|
+
const channels = selected.map((ch) => {
|
|
1114
|
+
const details = channelDetails.get(ch.id);
|
|
1115
|
+
const entry = {
|
|
1116
|
+
channel_id: ch.id,
|
|
1117
|
+
name: ch.name
|
|
1118
|
+
};
|
|
1119
|
+
if (details.description) entry.description = details.description;
|
|
1120
|
+
if (details.tags.length > 0) entry.tags = details.tags;
|
|
1121
|
+
return entry;
|
|
1122
|
+
});
|
|
1123
|
+
const res = await directoryFetch("/api/servers", {
|
|
1124
|
+
method: "POST",
|
|
1125
|
+
body: JSON.stringify({ server_url: serverUrl, claim, signature, channels })
|
|
1126
|
+
});
|
|
1127
|
+
if (!res.ok) {
|
|
1128
|
+
throw new Error(await formatDirectoryError(res));
|
|
1129
|
+
}
|
|
1130
|
+
await res.json();
|
|
1131
|
+
console.log("");
|
|
1132
|
+
for (const ch of selected) {
|
|
1133
|
+
console.log(` ${ch.id} \u2192 ${serverUrl}/${ch.id}`);
|
|
1134
|
+
}
|
|
1135
|
+
console.log("");
|
|
1136
|
+
console.log(` Any zooid can find your channel using:`);
|
|
1137
|
+
console.log(` npx zooid discover --query ${selected[0].id}`);
|
|
1138
|
+
const tags = [
|
|
1139
|
+
...new Set(selected.flatMap((ch) => channelDetails.get(ch.id)?.tags ?? []))
|
|
1140
|
+
];
|
|
1141
|
+
if (tags.length > 0) {
|
|
1142
|
+
console.log(` npx zooid discover --tag ${tags[0]}`);
|
|
1143
|
+
}
|
|
1144
|
+
console.log("");
|
|
1145
|
+
}
|
|
1146
|
+
async function pickChannels(channels) {
|
|
1147
|
+
const { default: checkbox } = await import("@inquirer/checkbox");
|
|
1148
|
+
const selected = await checkbox({
|
|
1149
|
+
message: "Select channels to share",
|
|
1150
|
+
choices: channels.map((ch) => ({
|
|
1151
|
+
name: ch.description ? `${ch.id} \u2014 ${ch.description}` : ch.id,
|
|
1152
|
+
value: ch.id,
|
|
1153
|
+
checked: true
|
|
1154
|
+
})),
|
|
1155
|
+
theme: {
|
|
1156
|
+
icon: { cursor: "> " },
|
|
1157
|
+
style: { highlight: (text) => text }
|
|
1158
|
+
}
|
|
1159
|
+
});
|
|
1160
|
+
const selectedSet = new Set(selected);
|
|
1161
|
+
return channels.filter((ch) => selectedSet.has(ch.id));
|
|
1162
|
+
}
|
|
1163
|
+
async function promptChannelDetails(channels, skipPrompt) {
|
|
1164
|
+
const result = /* @__PURE__ */ new Map();
|
|
1165
|
+
if (skipPrompt) {
|
|
1166
|
+
for (const ch of channels) {
|
|
1167
|
+
result.set(ch.id, {
|
|
1168
|
+
description: ch.description ?? "",
|
|
1169
|
+
tags: ch.tags
|
|
1170
|
+
});
|
|
1171
|
+
}
|
|
1172
|
+
return result;
|
|
1173
|
+
}
|
|
1174
|
+
const rl = readline2.createInterface({
|
|
1175
|
+
input: process.stdin,
|
|
1176
|
+
output: process.stdout
|
|
1177
|
+
});
|
|
1178
|
+
try {
|
|
1179
|
+
console.log("");
|
|
1180
|
+
console.log(" Set description and tags for each channel.");
|
|
1181
|
+
console.log(" Press Enter to accept defaults shown in [brackets].\n");
|
|
1182
|
+
for (const ch of channels) {
|
|
1183
|
+
console.log(` ${ch.id}:`);
|
|
1184
|
+
const desc = await ask(rl, "Description", ch.description ?? "");
|
|
1185
|
+
const tagsRaw = await ask(
|
|
1186
|
+
rl,
|
|
1187
|
+
"Tags (comma-separated)",
|
|
1188
|
+
ch.tags.join(", ")
|
|
1189
|
+
);
|
|
1190
|
+
const tags = tagsRaw.split(",").map((t) => t.trim()).filter(Boolean);
|
|
1191
|
+
result.set(ch.id, { description: desc, tags });
|
|
1192
|
+
console.log("");
|
|
1193
|
+
}
|
|
1194
|
+
} finally {
|
|
1195
|
+
rl.close();
|
|
1196
|
+
}
|
|
1197
|
+
return result;
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
// src/commands/unshare.ts
|
|
1201
|
+
async function runUnshare(channelId) {
|
|
1202
|
+
const client = createClient();
|
|
1203
|
+
const config = loadConfig();
|
|
1204
|
+
const serverUrl = config.server;
|
|
1205
|
+
if (!serverUrl) {
|
|
1206
|
+
throw new Error(
|
|
1207
|
+
"No server configured. Run: npx zooid config set server <url>"
|
|
1208
|
+
);
|
|
1209
|
+
}
|
|
1210
|
+
const { claim, signature } = await client.getClaim([channelId], "delete");
|
|
1211
|
+
const res = await directoryFetch("/api/servers/channels", {
|
|
1212
|
+
method: "DELETE",
|
|
1213
|
+
body: JSON.stringify({
|
|
1214
|
+
server_url: serverUrl,
|
|
1215
|
+
channel_id: channelId,
|
|
1216
|
+
claim,
|
|
1217
|
+
signature
|
|
1218
|
+
})
|
|
1219
|
+
});
|
|
1220
|
+
if (!res.ok) {
|
|
1221
|
+
throw new Error(await formatDirectoryError(res));
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
// src/commands/discover.ts
|
|
1226
|
+
async function runDiscover(options) {
|
|
1227
|
+
const params = new URLSearchParams();
|
|
1228
|
+
if (options.query) params.set("q", options.query);
|
|
1229
|
+
if (options.tag) params.set("tag", options.tag);
|
|
1230
|
+
if (options.limit) params.set("limit", String(options.limit));
|
|
1231
|
+
const qs = params.toString();
|
|
1232
|
+
const url = `${DIRECTORY_BASE_URL}/api/discover${qs ? `?${qs}` : ""}`;
|
|
1233
|
+
const res = await fetch(url);
|
|
1234
|
+
if (!res.ok) {
|
|
1235
|
+
throw new Error(`Directory returned ${res.status}`);
|
|
1236
|
+
}
|
|
1237
|
+
const data = await res.json();
|
|
1238
|
+
if (data.channels.length === 0) {
|
|
1239
|
+
console.log("No channels found.");
|
|
1240
|
+
return;
|
|
1241
|
+
}
|
|
1242
|
+
console.log("");
|
|
1243
|
+
for (const ch of data.channels) {
|
|
1244
|
+
const host = new URL(ch.server_url).host;
|
|
1245
|
+
const tags = ch.tags.length > 0 ? ` [${ch.tags.join(", ")}]` : "";
|
|
1246
|
+
const desc = ch.description ? ` \u2014 ${ch.description}` : "";
|
|
1247
|
+
console.log(` ${host}/${ch.channel_id}${desc}${tags}`);
|
|
1248
|
+
}
|
|
1249
|
+
console.log(`
|
|
1250
|
+
${data.total} channel${data.total === 1 ? "" : "s"} found.`);
|
|
1251
|
+
console.log("");
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1254
|
+
// src/commands/server.ts
|
|
1255
|
+
async function runServerGet(client) {
|
|
1256
|
+
const c = client ?? createClient();
|
|
1257
|
+
return c.getServerMeta();
|
|
1258
|
+
}
|
|
1259
|
+
async function runServerSet(fields, client) {
|
|
1260
|
+
const c = client ?? createClient();
|
|
1261
|
+
return c.updateServerMeta(fields);
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
// src/lib/roles.ts
|
|
1265
|
+
function loadRoleDefs() {
|
|
1266
|
+
const wf = loadWorkforce();
|
|
1267
|
+
return new Map(Object.entries(wf.roles));
|
|
1268
|
+
}
|
|
1269
|
+
function rolesToScopeMapping(roles) {
|
|
1270
|
+
const mapping = {};
|
|
1271
|
+
for (const [id, def] of roles) {
|
|
1272
|
+
mapping[id] = def.scopes;
|
|
1273
|
+
}
|
|
1274
|
+
return JSON.stringify(mapping);
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
// src/lib/resolve-roles.ts
|
|
1278
|
+
function resolveRoleScopes(roleNames) {
|
|
1279
|
+
const roles = loadRoleDefs();
|
|
1280
|
+
const allScopes = /* @__PURE__ */ new Set();
|
|
1281
|
+
for (const name of roleNames) {
|
|
1282
|
+
const role = roles.get(name);
|
|
1283
|
+
if (!role) {
|
|
1284
|
+
throw new Error(
|
|
1285
|
+
`Role "${name}" not found in .zooid/workforce.json. Available: ${[...roles.keys()].join(", ") || "(none)"}`
|
|
1286
|
+
);
|
|
1287
|
+
}
|
|
1288
|
+
for (const scope of role.scopes) {
|
|
1289
|
+
allScopes.add(scope);
|
|
1290
|
+
}
|
|
1291
|
+
}
|
|
1292
|
+
return [...allScopes];
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
// src/commands/token.ts
|
|
1296
|
+
async function runTokenMint(scopes, options) {
|
|
1297
|
+
const client = createClient();
|
|
1298
|
+
if (options.role?.length && scopes.length) {
|
|
1299
|
+
throw new Error("Cannot combine --role with explicit scopes");
|
|
1300
|
+
}
|
|
1301
|
+
if (options.role?.length) {
|
|
1302
|
+
scopes = resolveRoleScopes(options.role);
|
|
1303
|
+
}
|
|
1304
|
+
const body = { scopes };
|
|
1305
|
+
if (options.sub) body.sub = options.sub;
|
|
1306
|
+
if (options.name) body.name = options.name;
|
|
1307
|
+
if (options.expiresIn) body.expires_in = options.expiresIn;
|
|
1308
|
+
if (options.role?.length) body.groups = options.role;
|
|
1309
|
+
return client.mintToken(body);
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
// src/commands/dev.ts
|
|
1313
|
+
import { execSync, spawn as spawn2 } from "child_process";
|
|
1314
|
+
import crypto2 from "crypto";
|
|
1315
|
+
import fs5 from "fs";
|
|
1316
|
+
import path4 from "path";
|
|
1317
|
+
import { fileURLToPath } from "url";
|
|
1318
|
+
|
|
1319
|
+
// src/lib/crypto.ts
|
|
1320
|
+
function base64url(buf) {
|
|
1321
|
+
return buf.toString("base64url");
|
|
1322
|
+
}
|
|
1323
|
+
async function createAdminToken(secret) {
|
|
1324
|
+
const header = base64url(
|
|
1325
|
+
Buffer.from(JSON.stringify({ alg: "HS256", typ: "JWT" }))
|
|
1326
|
+
);
|
|
1327
|
+
const payload = base64url(
|
|
1328
|
+
Buffer.from(
|
|
1329
|
+
JSON.stringify({
|
|
1330
|
+
scope: "admin",
|
|
1331
|
+
iat: Math.floor(Date.now() / 1e3)
|
|
1332
|
+
})
|
|
1333
|
+
)
|
|
1334
|
+
);
|
|
1335
|
+
const data = `${header}.${payload}`;
|
|
1336
|
+
const key = await crypto.subtle.importKey(
|
|
1337
|
+
"raw",
|
|
1338
|
+
new TextEncoder().encode(secret),
|
|
1339
|
+
{ name: "HMAC", hash: "SHA-256" },
|
|
1340
|
+
false,
|
|
1341
|
+
["sign"]
|
|
1342
|
+
);
|
|
1343
|
+
const sig = await crypto.subtle.sign(
|
|
1344
|
+
"HMAC",
|
|
1345
|
+
key,
|
|
1346
|
+
new TextEncoder().encode(data)
|
|
1347
|
+
);
|
|
1348
|
+
const signature = base64url(Buffer.from(sig));
|
|
1349
|
+
return `${data}.${signature}`;
|
|
1350
|
+
}
|
|
1351
|
+
async function createEdDSAAdminToken(privateKeyJwk, kid) {
|
|
1352
|
+
const header = base64url(
|
|
1353
|
+
Buffer.from(JSON.stringify({ alg: "EdDSA", typ: "JWT", kid }))
|
|
1354
|
+
);
|
|
1355
|
+
const payload = base64url(
|
|
1356
|
+
Buffer.from(
|
|
1357
|
+
JSON.stringify({
|
|
1358
|
+
scope: "admin",
|
|
1359
|
+
iat: Math.floor(Date.now() / 1e3)
|
|
1360
|
+
})
|
|
1361
|
+
)
|
|
1362
|
+
);
|
|
1363
|
+
const message = `${header}.${payload}`;
|
|
1364
|
+
const privateKey = await crypto.subtle.importKey(
|
|
1365
|
+
"jwk",
|
|
1366
|
+
privateKeyJwk,
|
|
1367
|
+
{ name: "Ed25519" },
|
|
1368
|
+
false,
|
|
1369
|
+
["sign"]
|
|
1370
|
+
);
|
|
1371
|
+
const sig = await crypto.subtle.sign(
|
|
1372
|
+
"Ed25519",
|
|
1373
|
+
privateKey,
|
|
1374
|
+
new TextEncoder().encode(message)
|
|
1375
|
+
);
|
|
1376
|
+
const signature = base64url(Buffer.from(sig));
|
|
1377
|
+
return `${message}.${signature}`;
|
|
1378
|
+
}
|
|
1379
|
+
|
|
1380
|
+
// src/commands/dev.ts
|
|
1381
|
+
function findServerDir() {
|
|
1382
|
+
const cliDir = path4.dirname(fileURLToPath(import.meta.url));
|
|
1383
|
+
return path4.resolve(cliDir, "../../server");
|
|
1384
|
+
}
|
|
1385
|
+
async function runDev(port = 8787) {
|
|
1386
|
+
const serverDir = findServerDir();
|
|
1387
|
+
if (!fs5.existsSync(path4.join(serverDir, "wrangler.toml"))) {
|
|
1388
|
+
throw new Error(
|
|
1389
|
+
`Server directory not found at ${serverDir}. Make sure you're running from the zooid monorepo.`
|
|
1390
|
+
);
|
|
1391
|
+
}
|
|
1392
|
+
const jwtSecret = crypto2.randomUUID();
|
|
1393
|
+
const devVarsPath = path4.join(serverDir, ".dev.vars");
|
|
1394
|
+
fs5.writeFileSync(devVarsPath, `ZOOID_JWT_SECRET=${jwtSecret}
|
|
1395
|
+
`);
|
|
1396
|
+
const adminToken = await createAdminToken(jwtSecret);
|
|
1397
|
+
const serverUrl = `http://localhost:${port}`;
|
|
1398
|
+
saveConfig({ admin_token: adminToken }, serverUrl);
|
|
1399
|
+
const schemaPath = path4.join(serverDir, "src/db/schema.sql");
|
|
1400
|
+
if (fs5.existsSync(schemaPath)) {
|
|
1401
|
+
try {
|
|
1402
|
+
execSync(
|
|
1403
|
+
`npx wrangler d1 execute zooid-db --local --file=${schemaPath}`,
|
|
1404
|
+
{
|
|
1405
|
+
cwd: serverDir,
|
|
1406
|
+
stdio: "pipe"
|
|
1407
|
+
}
|
|
1408
|
+
);
|
|
1409
|
+
printSuccess("Database schema initialized");
|
|
1410
|
+
} catch {
|
|
1411
|
+
printSuccess("Database ready (schema already exists)");
|
|
1412
|
+
}
|
|
1413
|
+
}
|
|
1414
|
+
printSuccess("Local server configured");
|
|
1415
|
+
printInfo("Server", serverUrl);
|
|
1416
|
+
printInfo("Admin token", adminToken.slice(0, 20) + "...");
|
|
1417
|
+
printInfo("Config saved to", "~/.zooid/state.json");
|
|
1418
|
+
console.log("");
|
|
1419
|
+
console.log("Starting wrangler dev...");
|
|
1420
|
+
console.log("");
|
|
1421
|
+
const child = spawn2(
|
|
1422
|
+
"npx",
|
|
1423
|
+
["wrangler", "dev", "--local", "--port", String(port)],
|
|
1424
|
+
{
|
|
1425
|
+
cwd: serverDir,
|
|
1426
|
+
stdio: "inherit",
|
|
1427
|
+
shell: true
|
|
1428
|
+
}
|
|
1429
|
+
);
|
|
1430
|
+
child.on("error", (err) => {
|
|
1431
|
+
console.error(`Failed to start local server: ${err.message}`);
|
|
1432
|
+
process.exit(1);
|
|
1433
|
+
});
|
|
1434
|
+
child.on("exit", (code) => {
|
|
1435
|
+
process.exit(code ?? 0);
|
|
1436
|
+
});
|
|
1437
|
+
}
|
|
1438
|
+
|
|
1439
|
+
// src/commands/init.ts
|
|
1440
|
+
import fs7 from "fs";
|
|
1441
|
+
import path6 from "path";
|
|
1442
|
+
import readline3 from "readline/promises";
|
|
1443
|
+
|
|
1444
|
+
// src/commands/use.ts
|
|
1445
|
+
import fs6 from "fs";
|
|
1446
|
+
import path5 from "path";
|
|
1447
|
+
var SLUG_RE2 = /^[a-z0-9][a-z0-9-]{1,62}[a-z0-9]$/;
|
|
1448
|
+
function deriveTemplateName(url) {
|
|
1449
|
+
const parts = parseGitHubUrl(url);
|
|
1450
|
+
if (!parts) throw new Error("Cannot derive template name from URL");
|
|
1451
|
+
if (parts.path) {
|
|
1452
|
+
const segments = parts.path.split("/").filter(Boolean);
|
|
1453
|
+
return segments[segments.length - 1];
|
|
1454
|
+
}
|
|
1455
|
+
return parts.repo;
|
|
1456
|
+
}
|
|
1457
|
+
function resolveTemplateName(url, workforce) {
|
|
1458
|
+
const meta = workforce.meta;
|
|
1459
|
+
if (meta?.slug) {
|
|
1460
|
+
const slug = meta.slug;
|
|
1461
|
+
if (!SLUG_RE2.test(slug)) {
|
|
1462
|
+
throw new Error(
|
|
1463
|
+
`Invalid meta.slug "${slug}" \u2014 must be lowercase alphanumeric + hyphens, 3-64 chars`
|
|
1464
|
+
);
|
|
1465
|
+
}
|
|
1466
|
+
return slug;
|
|
1467
|
+
}
|
|
1468
|
+
return deriveTemplateName(url);
|
|
1469
|
+
}
|
|
1470
|
+
function addToInclude(relativePath) {
|
|
1471
|
+
const zooidDir = getZooidDir();
|
|
1472
|
+
const workforcePath = path5.join(zooidDir, "workforce.json");
|
|
1473
|
+
let raw;
|
|
1474
|
+
if (fs6.existsSync(workforcePath)) {
|
|
1475
|
+
raw = JSON.parse(fs6.readFileSync(workforcePath, "utf-8"));
|
|
1476
|
+
} else {
|
|
1477
|
+
raw = { channels: {}, roles: {} };
|
|
1478
|
+
}
|
|
1479
|
+
const include = raw.include ?? [];
|
|
1480
|
+
if (!include.includes(relativePath)) {
|
|
1481
|
+
include.push(relativePath);
|
|
1482
|
+
}
|
|
1483
|
+
raw.include = include;
|
|
1484
|
+
fs6.writeFileSync(workforcePath, JSON.stringify(raw, null, 2) + "\n");
|
|
1485
|
+
}
|
|
1486
|
+
async function runUse(url, options) {
|
|
1487
|
+
printStep("Fetching template...");
|
|
1488
|
+
const zooidDir = getZooidDir();
|
|
1489
|
+
const tmpDir = fs6.mkdtempSync(path5.join(zooidDir, ".tmp-template-"));
|
|
1490
|
+
try {
|
|
1491
|
+
const { fetchTemplate } = await import("./template-T5IB4YWC.js");
|
|
1492
|
+
const result = await fetchTemplate(url, tmpDir, {
|
|
1493
|
+
fetch: options?.fetch
|
|
1494
|
+
});
|
|
1495
|
+
const fetchedZooidDir = path5.join(tmpDir, ".zooid");
|
|
1496
|
+
const fetchedWorkforce = path5.join(fetchedZooidDir, "workforce.json");
|
|
1497
|
+
if (!fs6.existsSync(fetchedWorkforce)) {
|
|
1498
|
+
throw new Error("Template has no .zooid/workforce.json");
|
|
1499
|
+
}
|
|
1500
|
+
const wfRaw = JSON.parse(fs6.readFileSync(fetchedWorkforce, "utf-8"));
|
|
1501
|
+
const slug = resolveTemplateName(url, wfRaw);
|
|
1502
|
+
const targetDir = path5.join(zooidDir, slug);
|
|
1503
|
+
if (fs6.existsSync(targetDir)) {
|
|
1504
|
+
fs6.rmSync(targetDir, { recursive: true });
|
|
1505
|
+
}
|
|
1506
|
+
fs6.cpSync(fetchedZooidDir, targetDir, { recursive: true });
|
|
1507
|
+
addToInclude(`./${slug}/workforce.json`);
|
|
1508
|
+
printSuccess(
|
|
1509
|
+
`Saved .zooid/${slug}/ (${result.channelCount} channel(s), ${result.roleCount} role(s))`
|
|
1510
|
+
);
|
|
1511
|
+
printInfo("Added to include", "workforce.json");
|
|
1512
|
+
} finally {
|
|
1513
|
+
fs6.rmSync(tmpDir, { recursive: true, force: true });
|
|
1514
|
+
}
|
|
1515
|
+
}
|
|
1516
|
+
|
|
1517
|
+
// src/commands/init.ts
|
|
1518
|
+
var CONFIG_FILENAME = "zooid.json";
|
|
1519
|
+
function getConfigPath() {
|
|
1520
|
+
return path6.join(process.cwd(), CONFIG_FILENAME);
|
|
361
1521
|
}
|
|
362
1522
|
function loadServerConfig() {
|
|
363
1523
|
const configPath = getConfigPath();
|
|
364
|
-
if (!
|
|
365
|
-
const raw =
|
|
1524
|
+
if (!fs7.existsSync(configPath)) return null;
|
|
1525
|
+
const raw = fs7.readFileSync(configPath, "utf-8");
|
|
366
1526
|
return JSON.parse(raw);
|
|
367
1527
|
}
|
|
368
1528
|
function saveServerConfig(config) {
|
|
369
1529
|
const configPath = getConfigPath();
|
|
370
|
-
|
|
1530
|
+
fs7.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
|
|
371
1531
|
}
|
|
372
|
-
async function runInit() {
|
|
1532
|
+
async function runInit(options) {
|
|
373
1533
|
const configPath = getConfigPath();
|
|
374
1534
|
const existing = loadServerConfig();
|
|
1535
|
+
if (existing?.url && options?.use) {
|
|
1536
|
+
const workforcePath = path6.join(process.cwd(), ".zooid", "workforce.json");
|
|
1537
|
+
if (!fs7.existsSync(workforcePath)) {
|
|
1538
|
+
fs7.mkdirSync(path6.join(process.cwd(), ".zooid"), { recursive: true });
|
|
1539
|
+
fs7.writeFileSync(
|
|
1540
|
+
workforcePath,
|
|
1541
|
+
JSON.stringify({ channels: {}, roles: {} }, null, 2) + "\n"
|
|
1542
|
+
);
|
|
1543
|
+
}
|
|
1544
|
+
await runUse(options.use);
|
|
1545
|
+
console.log("");
|
|
1546
|
+
printInfo("Server", existing.url);
|
|
1547
|
+
printInfo("Workforce", ".zooid/workforce.json");
|
|
1548
|
+
console.log("");
|
|
1549
|
+
printSuccess("Ready to deploy. Run: npx zooid deploy");
|
|
1550
|
+
console.log("");
|
|
1551
|
+
return;
|
|
1552
|
+
}
|
|
375
1553
|
if (existing) {
|
|
376
1554
|
printInfo("Found existing", configPath);
|
|
377
1555
|
console.log("");
|
|
378
1556
|
}
|
|
379
|
-
const rl =
|
|
1557
|
+
const rl = readline3.createInterface({
|
|
380
1558
|
input: process.stdin,
|
|
381
1559
|
output: process.stdout
|
|
382
1560
|
});
|
|
@@ -413,14 +1591,14 @@ async function runInit() {
|
|
|
413
1591
|
tags,
|
|
414
1592
|
url
|
|
415
1593
|
};
|
|
416
|
-
|
|
417
|
-
const
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
1594
|
+
fs7.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
|
|
1595
|
+
const workforcePath = path6.join(process.cwd(), ".zooid", "workforce.json");
|
|
1596
|
+
if (!fs7.existsSync(workforcePath)) {
|
|
1597
|
+
fs7.mkdirSync(path6.join(process.cwd(), ".zooid"), { recursive: true });
|
|
1598
|
+
fs7.writeFileSync(
|
|
1599
|
+
workforcePath,
|
|
1600
|
+
JSON.stringify({ channels: {}, roles: {} }, null, 2) + "\n"
|
|
1601
|
+
);
|
|
424
1602
|
}
|
|
425
1603
|
console.log("");
|
|
426
1604
|
printSuccess(`Saved ${CONFIG_FILENAME}`);
|
|
@@ -435,24 +1613,155 @@ async function runInit() {
|
|
|
435
1613
|
config.tags.length > 0 ? config.tags.join(", ") : "(none)"
|
|
436
1614
|
);
|
|
437
1615
|
printInfo("URL", config.url || "(not set)");
|
|
438
|
-
printInfo("
|
|
439
|
-
printInfo("Roles", "roles/");
|
|
1616
|
+
printInfo("Workforce", ".zooid/workforce.json");
|
|
440
1617
|
console.log("");
|
|
441
1618
|
} finally {
|
|
442
1619
|
rl.close();
|
|
443
1620
|
}
|
|
1621
|
+
if (options?.use) {
|
|
1622
|
+
await runUse(options.use);
|
|
1623
|
+
}
|
|
1624
|
+
}
|
|
1625
|
+
|
|
1626
|
+
// src/commands/deploy.ts
|
|
1627
|
+
import { execSync as execSync2, spawnSync } from "child_process";
|
|
1628
|
+
import crypto3 from "crypto";
|
|
1629
|
+
import fs9 from "fs";
|
|
1630
|
+
import os from "os";
|
|
1631
|
+
import path7 from "path";
|
|
1632
|
+
import readline5 from "readline/promises";
|
|
1633
|
+
import { createRequire } from "module";
|
|
1634
|
+
import { ZooidClient } from "@zooid/sdk";
|
|
1635
|
+
|
|
1636
|
+
// src/lib/channels.ts
|
|
1637
|
+
function loadChannelDefs() {
|
|
1638
|
+
const wf = loadWorkforce();
|
|
1639
|
+
return new Map(Object.entries(wf.channels));
|
|
1640
|
+
}
|
|
1641
|
+
|
|
1642
|
+
// src/lib/wrangler-vars.ts
|
|
1643
|
+
import fs8 from "fs";
|
|
1644
|
+
function setWranglerVar(tomlPath, key, value) {
|
|
1645
|
+
const content = fs8.readFileSync(tomlPath, "utf-8");
|
|
1646
|
+
const lines = content.split("\n");
|
|
1647
|
+
let varsStart = -1;
|
|
1648
|
+
let varsEnd = lines.length;
|
|
1649
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1650
|
+
if (/^\[vars\]\s*$/.test(lines[i])) {
|
|
1651
|
+
varsStart = i;
|
|
1652
|
+
for (let j = i + 1; j < lines.length; j++) {
|
|
1653
|
+
if (/^\[/.test(lines[j]) && !/^\[vars\]/.test(lines[j])) {
|
|
1654
|
+
varsEnd = j;
|
|
1655
|
+
break;
|
|
1656
|
+
}
|
|
1657
|
+
}
|
|
1658
|
+
break;
|
|
1659
|
+
}
|
|
1660
|
+
}
|
|
1661
|
+
const keyPattern = new RegExp(`^${key}\\s*=`);
|
|
1662
|
+
let existingLine = -1;
|
|
1663
|
+
const searchStart = varsStart >= 0 ? varsStart + 1 : 0;
|
|
1664
|
+
const searchEnd = varsStart >= 0 ? varsEnd : lines.length;
|
|
1665
|
+
for (let i = searchStart; i < searchEnd; i++) {
|
|
1666
|
+
if (keyPattern.test(lines[i])) {
|
|
1667
|
+
existingLine = i;
|
|
1668
|
+
break;
|
|
1669
|
+
}
|
|
1670
|
+
}
|
|
1671
|
+
if (value === null) {
|
|
1672
|
+
if (existingLine >= 0) {
|
|
1673
|
+
lines.splice(existingLine, 1);
|
|
1674
|
+
}
|
|
1675
|
+
} else {
|
|
1676
|
+
const newLine = `${key} = '${value}'`;
|
|
1677
|
+
if (existingLine >= 0) {
|
|
1678
|
+
lines[existingLine] = newLine;
|
|
1679
|
+
} else if (varsStart >= 0) {
|
|
1680
|
+
lines.splice(varsStart + 1, 0, newLine);
|
|
1681
|
+
} else {
|
|
1682
|
+
lines.push("", "[vars]", newLine);
|
|
1683
|
+
}
|
|
1684
|
+
}
|
|
1685
|
+
fs8.writeFileSync(tomlPath, lines.join("\n"));
|
|
1686
|
+
}
|
|
1687
|
+
|
|
1688
|
+
// src/lib/channel-sync.ts
|
|
1689
|
+
import readline4 from "readline/promises";
|
|
1690
|
+
async function syncChannelsToServer(client, options = {}) {
|
|
1691
|
+
const localDefs = loadChannelDefs();
|
|
1692
|
+
const remoteChannels = await client.listChannels();
|
|
1693
|
+
const remoteIds = new Set(remoteChannels.map((ch) => ch.id));
|
|
1694
|
+
const localIds = new Set(localDefs.keys());
|
|
1695
|
+
let created = 0;
|
|
1696
|
+
let updated = 0;
|
|
1697
|
+
let deleted = 0;
|
|
1698
|
+
for (const [id, def] of localDefs) {
|
|
1699
|
+
if (!remoteIds.has(id)) {
|
|
1700
|
+
await client.createChannel({
|
|
1701
|
+
id,
|
|
1702
|
+
name: def.name ?? id,
|
|
1703
|
+
description: def.description,
|
|
1704
|
+
is_public: def.visibility === "public",
|
|
1705
|
+
config: def.config
|
|
1706
|
+
});
|
|
1707
|
+
printSuccess(`Channel created: ${id}`);
|
|
1708
|
+
created++;
|
|
1709
|
+
}
|
|
1710
|
+
}
|
|
1711
|
+
for (const [id, def] of localDefs) {
|
|
1712
|
+
if (remoteIds.has(id)) {
|
|
1713
|
+
await client.updateChannel(id, {
|
|
1714
|
+
name: def.name,
|
|
1715
|
+
description: def.description,
|
|
1716
|
+
is_public: def.visibility === "public",
|
|
1717
|
+
config: def.config ?? {}
|
|
1718
|
+
});
|
|
1719
|
+
printSuccess(`Channel updated: ${id}`);
|
|
1720
|
+
updated++;
|
|
1721
|
+
}
|
|
1722
|
+
}
|
|
1723
|
+
const orphaned = remoteChannels.filter((ch) => !localIds.has(ch.id));
|
|
1724
|
+
if (orphaned.length > 0) {
|
|
1725
|
+
if (options.prune) {
|
|
1726
|
+
for (const ch of orphaned) {
|
|
1727
|
+
await client.deleteChannel(ch.id);
|
|
1728
|
+
printSuccess(`Channel deleted: ${ch.id}`);
|
|
1729
|
+
deleted++;
|
|
1730
|
+
}
|
|
1731
|
+
} else if (options.confirmDelete) {
|
|
1732
|
+
const confirmed = await options.confirmDelete(orphaned);
|
|
1733
|
+
if (confirmed) {
|
|
1734
|
+
for (const ch of orphaned) {
|
|
1735
|
+
await client.deleteChannel(ch.id);
|
|
1736
|
+
printSuccess(`Channel deleted: ${ch.id}`);
|
|
1737
|
+
deleted++;
|
|
1738
|
+
}
|
|
1739
|
+
} else {
|
|
1740
|
+
printInfo("Skipped", "Remote-only channels left unchanged");
|
|
1741
|
+
}
|
|
1742
|
+
} else {
|
|
1743
|
+
printInfo(
|
|
1744
|
+
"Warning",
|
|
1745
|
+
`${orphaned.length} channel(s) on server not in workforce.json (use --prune to remove)`
|
|
1746
|
+
);
|
|
1747
|
+
for (const ch of orphaned) {
|
|
1748
|
+
printInfo(" -", `${ch.id}${ch.name ? ` (${ch.name})` : ""}`);
|
|
1749
|
+
}
|
|
1750
|
+
}
|
|
1751
|
+
}
|
|
1752
|
+
return { created, updated, deleted };
|
|
444
1753
|
}
|
|
445
1754
|
|
|
446
1755
|
// src/commands/deploy.ts
|
|
447
1756
|
var require2 = createRequire(import.meta.url);
|
|
448
1757
|
function resolvePackageDir(packageName) {
|
|
449
1758
|
const pkgJson = require2.resolve(`${packageName}/package.json`);
|
|
450
|
-
return
|
|
1759
|
+
return path7.dirname(pkgJson);
|
|
451
1760
|
}
|
|
452
|
-
var USER_WRANGLER_TOML =
|
|
1761
|
+
var USER_WRANGLER_TOML = path7.join(process.cwd(), "wrangler.toml");
|
|
453
1762
|
function ejectWranglerToml(opts) {
|
|
454
1763
|
const serverDir = resolvePackageDir("@zooid/server");
|
|
455
|
-
let toml =
|
|
1764
|
+
let toml = fs9.readFileSync(path7.join(serverDir, "wrangler.toml"), "utf-8");
|
|
456
1765
|
toml = toml.replace(/directory\s*=\s*"[^"]*"/, 'directory = "./web-dist/"');
|
|
457
1766
|
toml = toml.replace(/name = "[^"]*"/, `name = "${opts.workerName}"`);
|
|
458
1767
|
toml = toml.replace(
|
|
@@ -467,58 +1776,58 @@ function ejectWranglerToml(opts) {
|
|
|
467
1776
|
/ZOOID_SERVER_ID = "[^"]*"/,
|
|
468
1777
|
`ZOOID_SERVER_ID = "${opts.serverSlug}"`
|
|
469
1778
|
);
|
|
470
|
-
|
|
1779
|
+
fs9.writeFileSync(USER_WRANGLER_TOML, toml);
|
|
471
1780
|
}
|
|
472
1781
|
function prepareStagingDir() {
|
|
473
1782
|
const serverDir = resolvePackageDir("@zooid/server");
|
|
474
|
-
const serverRequire = createRequire(
|
|
475
|
-
const webDir =
|
|
476
|
-
const webDistDir =
|
|
477
|
-
if (!
|
|
1783
|
+
const serverRequire = createRequire(path7.join(serverDir, "package.json"));
|
|
1784
|
+
const webDir = path7.dirname(serverRequire.resolve("@zooid/web/package.json"));
|
|
1785
|
+
const webDistDir = path7.join(webDir, "dist");
|
|
1786
|
+
if (!fs9.existsSync(webDistDir)) {
|
|
478
1787
|
throw new Error(`Web dashboard not built. Missing: ${webDistDir}`);
|
|
479
1788
|
}
|
|
480
|
-
const tmpDir =
|
|
481
|
-
copyDirSync(
|
|
482
|
-
copyDirSync(webDistDir,
|
|
483
|
-
if (
|
|
484
|
-
|
|
1789
|
+
const tmpDir = fs9.mkdtempSync(path7.join(os.tmpdir(), "zooid-deploy-"));
|
|
1790
|
+
copyDirSync(path7.join(serverDir, "src"), path7.join(tmpDir, "src"));
|
|
1791
|
+
copyDirSync(webDistDir, path7.join(tmpDir, "web-dist"));
|
|
1792
|
+
if (fs9.existsSync(USER_WRANGLER_TOML)) {
|
|
1793
|
+
fs9.copyFileSync(USER_WRANGLER_TOML, path7.join(tmpDir, "wrangler.toml"));
|
|
485
1794
|
} else {
|
|
486
|
-
if (!
|
|
1795
|
+
if (!fs9.existsSync(path7.join(serverDir, "wrangler.toml"))) {
|
|
487
1796
|
throw new Error(`Server package missing wrangler.toml at ${serverDir}`);
|
|
488
1797
|
}
|
|
489
|
-
let toml =
|
|
1798
|
+
let toml = fs9.readFileSync(path7.join(serverDir, "wrangler.toml"), "utf-8");
|
|
490
1799
|
toml = toml.replace(/directory\s*=\s*"[^"]*"/, 'directory = "./web-dist/"');
|
|
491
|
-
|
|
1800
|
+
fs9.writeFileSync(path7.join(tmpDir, "wrangler.toml"), toml);
|
|
492
1801
|
}
|
|
493
1802
|
const nodeModules = findServerNodeModules(serverDir);
|
|
494
1803
|
if (nodeModules) {
|
|
495
|
-
|
|
1804
|
+
fs9.symlinkSync(nodeModules, path7.join(tmpDir, "node_modules"), "junction");
|
|
496
1805
|
}
|
|
497
1806
|
return tmpDir;
|
|
498
1807
|
}
|
|
499
1808
|
function findServerNodeModules(serverDir) {
|
|
500
|
-
const local =
|
|
501
|
-
if (
|
|
502
|
-
const storeNodeModules =
|
|
503
|
-
if (
|
|
1809
|
+
const local = path7.join(serverDir, "node_modules");
|
|
1810
|
+
if (fs9.existsSync(path7.join(local, "hono"))) return local;
|
|
1811
|
+
const storeNodeModules = path7.resolve(serverDir, "..", "..");
|
|
1812
|
+
if (fs9.existsSync(path7.join(storeNodeModules, "hono")))
|
|
504
1813
|
return storeNodeModules;
|
|
505
1814
|
let dir = serverDir;
|
|
506
|
-
while (dir !==
|
|
507
|
-
dir =
|
|
508
|
-
const nm =
|
|
509
|
-
if (
|
|
1815
|
+
while (dir !== path7.dirname(dir)) {
|
|
1816
|
+
dir = path7.dirname(dir);
|
|
1817
|
+
const nm = path7.join(dir, "node_modules");
|
|
1818
|
+
if (fs9.existsSync(path7.join(nm, "hono"))) return nm;
|
|
510
1819
|
}
|
|
511
1820
|
return null;
|
|
512
1821
|
}
|
|
513
1822
|
function copyDirSync(src, dest) {
|
|
514
|
-
|
|
515
|
-
for (const entry of
|
|
516
|
-
const srcPath =
|
|
517
|
-
const destPath =
|
|
1823
|
+
fs9.mkdirSync(dest, { recursive: true });
|
|
1824
|
+
for (const entry of fs9.readdirSync(src, { withFileTypes: true })) {
|
|
1825
|
+
const srcPath = path7.join(src, entry.name);
|
|
1826
|
+
const destPath = path7.join(dest, entry.name);
|
|
518
1827
|
if (entry.isDirectory()) {
|
|
519
1828
|
copyDirSync(srcPath, destPath);
|
|
520
1829
|
} else {
|
|
521
|
-
|
|
1830
|
+
fs9.copyFileSync(srcPath, destPath);
|
|
522
1831
|
}
|
|
523
1832
|
}
|
|
524
1833
|
}
|
|
@@ -533,7 +1842,7 @@ function wranglerEnv(creds) {
|
|
|
533
1842
|
return env;
|
|
534
1843
|
}
|
|
535
1844
|
function wrangler(cmd, cwd, creds, opts) {
|
|
536
|
-
return
|
|
1845
|
+
return execSync2(`npx wrangler ${cmd}`, {
|
|
537
1846
|
cwd,
|
|
538
1847
|
stdio: "pipe",
|
|
539
1848
|
encoding: "utf-8",
|
|
@@ -572,9 +1881,9 @@ function parseDeployUrls(output) {
|
|
|
572
1881
|
};
|
|
573
1882
|
}
|
|
574
1883
|
function loadDotEnv() {
|
|
575
|
-
const envPath =
|
|
576
|
-
if (!
|
|
577
|
-
const content =
|
|
1884
|
+
const envPath = path7.join(process.cwd(), ".env");
|
|
1885
|
+
if (!fs9.existsSync(envPath)) return {};
|
|
1886
|
+
const content = fs9.readFileSync(envPath, "utf-8");
|
|
578
1887
|
const tokenMatch = content.match(/^CLOUDFLARE_API_TOKEN=(.+)$/m);
|
|
579
1888
|
const accountMatch = content.match(/^CLOUDFLARE_ACCOUNT_ID=(.+)$/m);
|
|
580
1889
|
return {
|
|
@@ -593,7 +1902,7 @@ async function getCfCredentials() {
|
|
|
593
1902
|
printInfo("Using credentials from", ".env");
|
|
594
1903
|
return { apiToken: dotEnv.apiToken, accountId: dotEnv.accountId };
|
|
595
1904
|
}
|
|
596
|
-
const rl =
|
|
1905
|
+
const rl = readline5.createInterface({
|
|
597
1906
|
input: process.stdin,
|
|
598
1907
|
output: process.stdout
|
|
599
1908
|
});
|
|
@@ -618,19 +1927,7 @@ async function getCfCredentials() {
|
|
|
618
1927
|
rl.close();
|
|
619
1928
|
}
|
|
620
1929
|
}
|
|
621
|
-
function
|
|
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
|
-
}
|
|
633
|
-
async function runDeploy() {
|
|
1930
|
+
async function runDeploy(opts) {
|
|
634
1931
|
let config = loadServerConfig();
|
|
635
1932
|
if (!config) {
|
|
636
1933
|
printInfo("No zooid.json found", "starting setup...");
|
|
@@ -642,6 +1939,11 @@ async function runDeploy() {
|
|
|
642
1939
|
printError("Failed to load zooid.json after init");
|
|
643
1940
|
process.exit(1);
|
|
644
1941
|
}
|
|
1942
|
+
const serverUrl = config.url;
|
|
1943
|
+
if (serverUrl && isZoonHosted(serverUrl)) {
|
|
1944
|
+
await deployZoonHosted(config, serverUrl, opts);
|
|
1945
|
+
return;
|
|
1946
|
+
}
|
|
645
1947
|
let stagingDir;
|
|
646
1948
|
try {
|
|
647
1949
|
stagingDir = prepareStagingDir();
|
|
@@ -653,7 +1955,7 @@ async function runDeploy() {
|
|
|
653
1955
|
const dbName = `zooid-db-${serverSlug}`;
|
|
654
1956
|
const workerName = `zooid-${serverSlug}`;
|
|
655
1957
|
try {
|
|
656
|
-
|
|
1958
|
+
execSync2("npx wrangler --version", { cwd: stagingDir, stdio: "pipe" });
|
|
657
1959
|
} catch {
|
|
658
1960
|
printError("wrangler not found. Install with: npm install -g wrangler");
|
|
659
1961
|
process.exit(1);
|
|
@@ -692,12 +1994,12 @@ async function runDeploy() {
|
|
|
692
1994
|
const databaseId = dbIdMatch[1];
|
|
693
1995
|
printSuccess(`D1 database created (${databaseId})`);
|
|
694
1996
|
ejectWranglerToml({ workerName, dbName, databaseId, serverSlug });
|
|
695
|
-
|
|
1997
|
+
fs9.copyFileSync(USER_WRANGLER_TOML, path7.join(stagingDir, "wrangler.toml"));
|
|
696
1998
|
printSuccess(
|
|
697
1999
|
"Created wrangler.toml (edit to add vars, observability, etc.)"
|
|
698
2000
|
);
|
|
699
|
-
const schemaPath =
|
|
700
|
-
if (
|
|
2001
|
+
const schemaPath = path7.join(stagingDir, "src/db/schema.sql");
|
|
2002
|
+
if (fs9.existsSync(schemaPath)) {
|
|
701
2003
|
printStep("Running database schema migration...");
|
|
702
2004
|
wrangler(
|
|
703
2005
|
`d1 execute ${dbName} --remote --file=${schemaPath}`,
|
|
@@ -707,23 +2009,23 @@ async function runDeploy() {
|
|
|
707
2009
|
printSuccess("Database schema initialized");
|
|
708
2010
|
}
|
|
709
2011
|
printStep("Generating secrets...");
|
|
710
|
-
const keyPair = await
|
|
2012
|
+
const keyPair = await crypto3.subtle.generateKey("Ed25519", true, [
|
|
711
2013
|
"sign",
|
|
712
2014
|
"verify"
|
|
713
2015
|
]);
|
|
714
|
-
const privateKeyRaw = await
|
|
2016
|
+
const privateKeyRaw = await crypto3.subtle.exportKey(
|
|
715
2017
|
"pkcs8",
|
|
716
2018
|
keyPair.privateKey
|
|
717
2019
|
);
|
|
718
|
-
const publicKeyRaw = await
|
|
2020
|
+
const publicKeyRaw = await crypto3.subtle.exportKey(
|
|
719
2021
|
"raw",
|
|
720
2022
|
keyPair.publicKey
|
|
721
2023
|
);
|
|
722
|
-
const privateKeyJwk = await
|
|
2024
|
+
const privateKeyJwk = await crypto3.subtle.exportKey(
|
|
723
2025
|
"jwk",
|
|
724
2026
|
keyPair.privateKey
|
|
725
2027
|
);
|
|
726
|
-
const publicKeyJwk = await
|
|
2028
|
+
const publicKeyJwk = await crypto3.subtle.exportKey(
|
|
727
2029
|
"jwk",
|
|
728
2030
|
keyPair.publicKey
|
|
729
2031
|
);
|
|
@@ -752,7 +2054,7 @@ async function runDeploy() {
|
|
|
752
2054
|
console.log("");
|
|
753
2055
|
printInfo("Deploy type", "Redeploying existing server");
|
|
754
2056
|
console.log("");
|
|
755
|
-
if (!
|
|
2057
|
+
if (!fs9.existsSync(USER_WRANGLER_TOML)) {
|
|
756
2058
|
printStep("Ejecting wrangler.toml...");
|
|
757
2059
|
let databaseId = "";
|
|
758
2060
|
try {
|
|
@@ -767,9 +2069,9 @@ async function runDeploy() {
|
|
|
767
2069
|
"Created wrangler.toml (edit to add vars, observability, etc.)"
|
|
768
2070
|
);
|
|
769
2071
|
}
|
|
770
|
-
|
|
771
|
-
const schemaPath =
|
|
772
|
-
if (
|
|
2072
|
+
fs9.copyFileSync(USER_WRANGLER_TOML, path7.join(stagingDir, "wrangler.toml"));
|
|
2073
|
+
const schemaPath = path7.join(stagingDir, "src/db/schema.sql");
|
|
2074
|
+
if (fs9.existsSync(schemaPath)) {
|
|
773
2075
|
printStep("Running schema migration...");
|
|
774
2076
|
wrangler(
|
|
775
2077
|
`d1 execute ${dbName} --remote --file=${schemaPath}`,
|
|
@@ -778,11 +2080,11 @@ async function runDeploy() {
|
|
|
778
2080
|
);
|
|
779
2081
|
printSuccess("Schema up to date");
|
|
780
2082
|
}
|
|
781
|
-
const migrationsDir =
|
|
782
|
-
if (
|
|
783
|
-
const migrationFiles =
|
|
2083
|
+
const migrationsDir = path7.join(stagingDir, "src/db/migrations");
|
|
2084
|
+
if (fs9.existsSync(migrationsDir)) {
|
|
2085
|
+
const migrationFiles = fs9.readdirSync(migrationsDir).filter((f) => f.endsWith(".sql")).sort();
|
|
784
2086
|
for (const file of migrationFiles) {
|
|
785
|
-
const migrationPath =
|
|
2087
|
+
const migrationPath = path7.join(migrationsDir, file);
|
|
786
2088
|
try {
|
|
787
2089
|
wrangler(
|
|
788
2090
|
`d1 execute ${dbName} --remote --file=${migrationPath}`,
|
|
@@ -803,23 +2105,23 @@ async function runDeploy() {
|
|
|
803
2105
|
const hasLocalKey = keysResult?.[0]?.results?.length > 0;
|
|
804
2106
|
if (!hasLocalKey) {
|
|
805
2107
|
printStep("Upgrading to EdDSA auth...");
|
|
806
|
-
const keyPair = await
|
|
2108
|
+
const keyPair = await crypto3.subtle.generateKey("Ed25519", true, [
|
|
807
2109
|
"sign",
|
|
808
2110
|
"verify"
|
|
809
2111
|
]);
|
|
810
|
-
const privateKeyRaw = await
|
|
2112
|
+
const privateKeyRaw = await crypto3.subtle.exportKey(
|
|
811
2113
|
"pkcs8",
|
|
812
2114
|
keyPair.privateKey
|
|
813
2115
|
);
|
|
814
|
-
const publicKeyRaw = await
|
|
2116
|
+
const publicKeyRaw = await crypto3.subtle.exportKey(
|
|
815
2117
|
"raw",
|
|
816
2118
|
keyPair.publicKey
|
|
817
2119
|
);
|
|
818
|
-
const privateKeyJwk = await
|
|
2120
|
+
const privateKeyJwk = await crypto3.subtle.exportKey(
|
|
819
2121
|
"jwk",
|
|
820
2122
|
keyPair.privateKey
|
|
821
2123
|
);
|
|
822
|
-
const publicKeyJwk = await
|
|
2124
|
+
const publicKeyJwk = await crypto3.subtle.exportKey(
|
|
823
2125
|
"jwk",
|
|
824
2126
|
keyPair.publicKey
|
|
825
2127
|
);
|
|
@@ -862,6 +2164,17 @@ async function runDeploy() {
|
|
|
862
2164
|
process.exit(1);
|
|
863
2165
|
}
|
|
864
2166
|
}
|
|
2167
|
+
const roles = loadRoleDefs();
|
|
2168
|
+
if (roles.size > 0) {
|
|
2169
|
+
printStep("Syncing roles to wrangler.toml...");
|
|
2170
|
+
const mapping = rolesToScopeMapping(roles);
|
|
2171
|
+
setWranglerVar(USER_WRANGLER_TOML, "ZOOID_SCOPE_MAPPING", mapping);
|
|
2172
|
+
fs9.copyFileSync(USER_WRANGLER_TOML, path7.join(stagingDir, "wrangler.toml"));
|
|
2173
|
+
printSuccess(`${roles.size} role(s) synced to ZOOID_SCOPE_MAPPING`);
|
|
2174
|
+
} else {
|
|
2175
|
+
setWranglerVar(USER_WRANGLER_TOML, "ZOOID_SCOPE_MAPPING", null);
|
|
2176
|
+
fs9.copyFileSync(USER_WRANGLER_TOML, path7.join(stagingDir, "wrangler.toml"));
|
|
2177
|
+
}
|
|
865
2178
|
printStep("Deploying worker...");
|
|
866
2179
|
const deployOutput = wranglerVerbose("deploy", stagingDir, creds);
|
|
867
2180
|
const { workerUrl, customDomain } = parseDeployUrls(deployOutput);
|
|
@@ -924,14 +2237,33 @@ async function runDeploy() {
|
|
|
924
2237
|
}
|
|
925
2238
|
printInfo("Config", "~/.zooid/state.json");
|
|
926
2239
|
console.log("");
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
2240
|
+
if (canonicalUrl && adminToken) {
|
|
2241
|
+
const channelDefs = loadChannelDefs();
|
|
2242
|
+
if (channelDefs.size > 0 || !isFirstDeploy) {
|
|
2243
|
+
printStep("Syncing channels to server...");
|
|
2244
|
+
try {
|
|
2245
|
+
const client = new ZooidClient({
|
|
2246
|
+
server: canonicalUrl,
|
|
2247
|
+
token: adminToken
|
|
2248
|
+
});
|
|
2249
|
+
const result = await syncChannelsToServer(client, {
|
|
2250
|
+
prune: opts?.prune
|
|
2251
|
+
});
|
|
2252
|
+
if (result.created || result.updated || result.deleted) {
|
|
2253
|
+
printSuccess(
|
|
2254
|
+
`Channels synced (${result.created} created, ${result.updated} updated, ${result.deleted} deleted)`
|
|
2255
|
+
);
|
|
2256
|
+
} else {
|
|
2257
|
+
printSuccess("Channels up to date");
|
|
2258
|
+
}
|
|
2259
|
+
} catch (err) {
|
|
2260
|
+
printError(
|
|
2261
|
+
`Failed to sync channels: ${err instanceof Error ? err.message : err}`
|
|
2262
|
+
);
|
|
2263
|
+
}
|
|
2264
|
+
}
|
|
2265
|
+
}
|
|
2266
|
+
if (isFirstDeploy) {
|
|
935
2267
|
console.log(" Next steps:");
|
|
936
2268
|
console.log(" Edit wrangler.toml to add env vars, observability, etc.");
|
|
937
2269
|
console.log(" npx zooid channel create my-channel");
|
|
@@ -943,622 +2275,748 @@ async function runDeploy() {
|
|
|
943
2275
|
if (canonicalUrl) {
|
|
944
2276
|
try {
|
|
945
2277
|
const openCmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
|
|
946
|
-
|
|
2278
|
+
execSync2(`${openCmd} ${canonicalUrl}`, { stdio: "ignore" });
|
|
947
2279
|
} catch {
|
|
948
2280
|
}
|
|
949
2281
|
}
|
|
950
2282
|
}
|
|
951
2283
|
function cleanup(dir) {
|
|
952
2284
|
try {
|
|
953
|
-
|
|
2285
|
+
fs9.rmSync(dir, { recursive: true, force: true });
|
|
954
2286
|
} catch {
|
|
955
2287
|
}
|
|
956
2288
|
}
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
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
|
-
}
|
|
2289
|
+
async function deployZoonHosted(config, serverUrl, opts) {
|
|
2290
|
+
const file = loadConfigFile();
|
|
2291
|
+
const entry = file.servers?.[serverUrl];
|
|
2292
|
+
const adminToken = entry?.admin_token;
|
|
2293
|
+
const platformToken = entry?.platform_token;
|
|
2294
|
+
if (!platformToken) {
|
|
2295
|
+
printError("Not authenticated with Zoon platform. Run: npx zooid login");
|
|
2296
|
+
process.exit(1);
|
|
1021
2297
|
}
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
} else {
|
|
1033
|
-
printInfo("Skipped", "Remote-only channels left unchanged");
|
|
2298
|
+
console.log("");
|
|
2299
|
+
printInfo("Deploy type", `Zoon-hosted (${serverUrl})`);
|
|
2300
|
+
console.log("");
|
|
2301
|
+
const roles = loadRoleDefs();
|
|
2302
|
+
const roleDefs = Array.from(roles.entries()).filter(([id]) => id !== "owner").map(([id, def]) => {
|
|
2303
|
+
if (id === "public") {
|
|
2304
|
+
printInfo(
|
|
2305
|
+
"Deprecation",
|
|
2306
|
+
'The "public" role has been renamed to "authenticated". Please update your workforce.json.'
|
|
2307
|
+
);
|
|
1034
2308
|
}
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
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"
|
|
2309
|
+
return {
|
|
2310
|
+
slug: id === "public" ? "authenticated" : id,
|
|
2311
|
+
...def.name ? { name: def.name } : {},
|
|
2312
|
+
scopes: def.scopes,
|
|
2313
|
+
...def.description ? { description: def.description } : {}
|
|
1063
2314
|
};
|
|
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
2315
|
});
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
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));
|
|
2316
|
+
if (roleDefs.length > 0) {
|
|
2317
|
+
printStep("Syncing roles to Zoon...");
|
|
2318
|
+
try {
|
|
2319
|
+
const result = await syncRolesToZoon(serverUrl, platformToken, roleDefs);
|
|
2320
|
+
printSuccess(
|
|
2321
|
+
`Roles synced (${result.synced} synced, ${result.deleted} deleted)`
|
|
2322
|
+
);
|
|
2323
|
+
} catch (err) {
|
|
2324
|
+
printError(
|
|
2325
|
+
`Failed to sync roles: ${err instanceof Error ? err.message : err}`
|
|
2326
|
+
);
|
|
2327
|
+
}
|
|
1137
2328
|
}
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
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
|
|
2329
|
+
if (adminToken) {
|
|
2330
|
+
const channelDefs = loadChannelDefs();
|
|
2331
|
+
if (channelDefs.size > 0) {
|
|
2332
|
+
printStep("Syncing channels to server...");
|
|
2333
|
+
try {
|
|
2334
|
+
const client = new ZooidClient({
|
|
2335
|
+
server: serverUrl,
|
|
2336
|
+
token: adminToken
|
|
2337
|
+
});
|
|
2338
|
+
const result = await syncChannelsToServer(client, {
|
|
2339
|
+
prune: opts?.prune
|
|
1185
2340
|
});
|
|
2341
|
+
if (result.created || result.updated || result.deleted) {
|
|
2342
|
+
printSuccess(
|
|
2343
|
+
`Channels synced (${result.created} created, ${result.updated} updated, ${result.deleted} deleted)`
|
|
2344
|
+
);
|
|
2345
|
+
} else {
|
|
2346
|
+
printSuccess("Channels up to date");
|
|
2347
|
+
}
|
|
2348
|
+
} catch (err) {
|
|
2349
|
+
printError(
|
|
2350
|
+
`Failed to sync channels: ${err instanceof Error ? err.message : err}`
|
|
2351
|
+
);
|
|
1186
2352
|
}
|
|
1187
2353
|
}
|
|
1188
2354
|
}
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
);
|
|
1192
|
-
return entries;
|
|
2355
|
+
console.log("");
|
|
2356
|
+
printSuccess("Deploy complete (Zoon-hosted \u2014 no wrangler deploy needed)");
|
|
2357
|
+
console.log("");
|
|
1193
2358
|
}
|
|
1194
2359
|
|
|
1195
|
-
// src/commands/
|
|
1196
|
-
import
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
}
|
|
1210
|
-
async function ensureDirectoryToken() {
|
|
1211
|
-
const existing = getDirectoryToken();
|
|
1212
|
-
if (existing) return existing;
|
|
1213
|
-
return deviceAuth();
|
|
2360
|
+
// src/commands/destroy.ts
|
|
2361
|
+
import fs10 from "fs";
|
|
2362
|
+
import path8 from "path";
|
|
2363
|
+
import readline6 from "readline/promises";
|
|
2364
|
+
import { ZooidClient as ZooidClient2 } from "@zooid/sdk";
|
|
2365
|
+
function parseWranglerToml(content) {
|
|
2366
|
+
const nameMatch = content.match(/^name\s*=\s*"([^"]+)"/m);
|
|
2367
|
+
const dbNameMatch = content.match(/database_name\s*=\s*"([^"]+)"/);
|
|
2368
|
+
const dbIdMatch = content.match(/database_id\s*=\s*"([^"]+)"/);
|
|
2369
|
+
return {
|
|
2370
|
+
workerName: nameMatch?.[1] ?? null,
|
|
2371
|
+
dbName: dbNameMatch?.[1] ?? null,
|
|
2372
|
+
databaseId: dbIdMatch?.[1] ?? null
|
|
2373
|
+
};
|
|
1214
2374
|
}
|
|
1215
|
-
|
|
1216
|
-
const
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
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
|
-
);
|
|
2375
|
+
function removeServerFromState(serverUrl) {
|
|
2376
|
+
const statePath = getStatePath();
|
|
2377
|
+
if (!fs10.existsSync(statePath)) return;
|
|
2378
|
+
const file = JSON.parse(fs10.readFileSync(statePath, "utf-8"));
|
|
2379
|
+
if (file.servers) {
|
|
2380
|
+
delete file.servers[serverUrl];
|
|
1237
2381
|
}
|
|
1238
|
-
|
|
1239
|
-
|
|
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
|
-
}
|
|
2382
|
+
if (file.current === serverUrl) {
|
|
2383
|
+
delete file.current;
|
|
1254
2384
|
}
|
|
1255
|
-
|
|
1256
|
-
|
|
2385
|
+
fs10.writeFileSync(statePath, JSON.stringify(file, null, 2) + "\n");
|
|
2386
|
+
}
|
|
2387
|
+
async function cfApiFetch(apiPath, apiToken, opts) {
|
|
2388
|
+
return fetch(`https://api.cloudflare.com/client/v4${apiPath}`, {
|
|
2389
|
+
...opts,
|
|
2390
|
+
headers: {
|
|
2391
|
+
Authorization: `Bearer ${apiToken}`,
|
|
2392
|
+
"Content-Type": "application/json",
|
|
2393
|
+
...opts?.headers ?? {}
|
|
2394
|
+
}
|
|
2395
|
+
});
|
|
2396
|
+
}
|
|
2397
|
+
async function deleteD1Database(accountId, databaseId, apiToken) {
|
|
2398
|
+
const res = await cfApiFetch(
|
|
2399
|
+
`/accounts/${accountId}/d1/database/${databaseId}`,
|
|
2400
|
+
apiToken,
|
|
2401
|
+
{ method: "DELETE" }
|
|
1257
2402
|
);
|
|
2403
|
+
return res.ok;
|
|
1258
2404
|
}
|
|
1259
|
-
async function
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
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;
|
|
2405
|
+
async function deleteWorker(accountId, scriptName, apiToken) {
|
|
2406
|
+
const res = await cfApiFetch(
|
|
2407
|
+
`/accounts/${accountId}/workers/scripts/${scriptName}`,
|
|
2408
|
+
apiToken,
|
|
2409
|
+
{ method: "DELETE" }
|
|
2410
|
+
);
|
|
2411
|
+
return res.ok;
|
|
1274
2412
|
}
|
|
1275
|
-
function
|
|
1276
|
-
|
|
2413
|
+
function loadDotEnvValue(key) {
|
|
2414
|
+
const envPath = path8.join(process.cwd(), ".env");
|
|
2415
|
+
if (!fs10.existsSync(envPath)) return void 0;
|
|
2416
|
+
const content = fs10.readFileSync(envPath, "utf-8");
|
|
2417
|
+
const match = content.match(new RegExp(`^${key}=(.+)$`, "m"));
|
|
2418
|
+
return match?.[1]?.trim();
|
|
1277
2419
|
}
|
|
1278
|
-
async function
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
if (parts.length > 0) msg = parts.join(": ");
|
|
1286
|
-
} catch {
|
|
2420
|
+
async function runDestroy(opts = {}) {
|
|
2421
|
+
const config = loadServerConfig();
|
|
2422
|
+
if (!config) {
|
|
2423
|
+
printError(
|
|
2424
|
+
"No zooid.json found. Run this from your Zooid project directory."
|
|
2425
|
+
);
|
|
2426
|
+
process.exit(1);
|
|
1287
2427
|
}
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
const
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
2428
|
+
const serverUrl = config.url;
|
|
2429
|
+
if (serverUrl && isZoonHosted(serverUrl)) {
|
|
2430
|
+
printError("Zoon-hosted server destroy is not yet supported.");
|
|
2431
|
+
printInfo("Use the Zoon dashboard to delete your server", serverUrl);
|
|
2432
|
+
process.exit(1);
|
|
2433
|
+
}
|
|
2434
|
+
const wranglerPath = path8.join(process.cwd(), "wrangler.toml");
|
|
2435
|
+
if (!fs10.existsSync(wranglerPath)) {
|
|
2436
|
+
printError("No wrangler.toml found. Is this a deployed Zooid project?");
|
|
2437
|
+
process.exit(1);
|
|
2438
|
+
}
|
|
2439
|
+
const wranglerContent = fs10.readFileSync(wranglerPath, "utf-8");
|
|
2440
|
+
const wrangler2 = parseWranglerToml(wranglerContent);
|
|
2441
|
+
if (!wrangler2.workerName) {
|
|
2442
|
+
printError("Could not determine Worker name from wrangler.toml");
|
|
2443
|
+
process.exit(1);
|
|
2444
|
+
}
|
|
2445
|
+
const apiToken = process.env.CLOUDFLARE_API_TOKEN || loadDotEnvValue("CLOUDFLARE_API_TOKEN");
|
|
2446
|
+
const accountId = process.env.CLOUDFLARE_ACCOUNT_ID || loadDotEnvValue("CLOUDFLARE_ACCOUNT_ID");
|
|
2447
|
+
if (!apiToken) {
|
|
2448
|
+
printError(
|
|
2449
|
+
"Cloudflare credentials required. Set CLOUDFLARE_API_TOKEN and CLOUDFLARE_ACCOUNT_ID."
|
|
1299
2450
|
);
|
|
2451
|
+
process.exit(1);
|
|
1300
2452
|
}
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
2453
|
+
if (!accountId) {
|
|
2454
|
+
printError(
|
|
2455
|
+
"CLOUDFLARE_ACCOUNT_ID required. Set it in environment or .env file."
|
|
2456
|
+
);
|
|
2457
|
+
process.exit(1);
|
|
1305
2458
|
}
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
const
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
}
|
|
1316
|
-
|
|
2459
|
+
const configFile = loadConfigFile();
|
|
2460
|
+
const serverEntry = serverUrl ? configFile.servers?.[serverUrl] : void 0;
|
|
2461
|
+
const adminToken = serverEntry?.admin_token;
|
|
2462
|
+
let channelCount = 0;
|
|
2463
|
+
if (adminToken && serverUrl) {
|
|
2464
|
+
try {
|
|
2465
|
+
const client = new ZooidClient2({
|
|
2466
|
+
server: serverUrl,
|
|
2467
|
+
token: adminToken
|
|
2468
|
+
});
|
|
2469
|
+
const channels = await client.listChannels();
|
|
2470
|
+
channelCount = channels.length;
|
|
2471
|
+
} catch {
|
|
2472
|
+
}
|
|
2473
|
+
}
|
|
2474
|
+
if (!opts.force) {
|
|
2475
|
+
console.log("");
|
|
2476
|
+
console.log(
|
|
2477
|
+
" \u26A0 This will permanently delete your Zooid server and all its data."
|
|
2478
|
+
);
|
|
2479
|
+
console.log("");
|
|
2480
|
+
printInfo("Worker", wrangler2.workerName);
|
|
2481
|
+
if (wrangler2.dbName) printInfo("Database", wrangler2.dbName);
|
|
2482
|
+
if (channelCount > 0) printInfo("Channels", `${channelCount}`);
|
|
2483
|
+
console.log("");
|
|
2484
|
+
console.log(" This action cannot be undone.");
|
|
2485
|
+
console.log("");
|
|
2486
|
+
const serverSlug = wrangler2.workerName.replace(/^zooid-/, "");
|
|
2487
|
+
const rl = readline6.createInterface({
|
|
2488
|
+
input: process.stdin,
|
|
2489
|
+
output: process.stdout
|
|
2490
|
+
});
|
|
2491
|
+
try {
|
|
2492
|
+
const answer = await rl.question(
|
|
2493
|
+
` Type the server name to confirm (${serverSlug}): `
|
|
2494
|
+
);
|
|
2495
|
+
if (answer.trim() !== serverSlug) {
|
|
2496
|
+
printError("Name does not match. Aborting.");
|
|
2497
|
+
process.exit(1);
|
|
1317
2498
|
}
|
|
2499
|
+
} finally {
|
|
2500
|
+
rl.close();
|
|
1318
2501
|
}
|
|
1319
|
-
|
|
1320
|
-
|
|
2502
|
+
}
|
|
2503
|
+
if (adminToken && serverUrl) {
|
|
2504
|
+
printStep("Destroying Durable Objects...");
|
|
2505
|
+
try {
|
|
2506
|
+
const res = await fetch(`${serverUrl}/api/v1/admin/destroy`, {
|
|
2507
|
+
method: "POST",
|
|
2508
|
+
headers: { Authorization: `Bearer ${adminToken}` }
|
|
2509
|
+
});
|
|
2510
|
+
if (res.ok) {
|
|
2511
|
+
const body = await res.json();
|
|
2512
|
+
printSuccess(`${body.destroyed} Durable Object(s) destroyed`);
|
|
2513
|
+
} else {
|
|
2514
|
+
console.warn(
|
|
2515
|
+
" Warning: Could not destroy DOs \u2014 server may be unreachable."
|
|
2516
|
+
);
|
|
2517
|
+
}
|
|
2518
|
+
} catch {
|
|
2519
|
+
console.warn(
|
|
2520
|
+
" Warning: Server unreachable \u2014 Durable Objects may not be fully cleaned up."
|
|
2521
|
+
);
|
|
1321
2522
|
}
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
2523
|
+
}
|
|
2524
|
+
if (wrangler2.databaseId) {
|
|
2525
|
+
printStep("Deleting D1 database...");
|
|
2526
|
+
const dbOk = await deleteD1Database(
|
|
2527
|
+
accountId,
|
|
2528
|
+
wrangler2.databaseId,
|
|
2529
|
+
apiToken
|
|
2530
|
+
);
|
|
2531
|
+
if (dbOk) {
|
|
2532
|
+
printSuccess(`Deleted ${wrangler2.dbName ?? wrangler2.databaseId}`);
|
|
2533
|
+
} else {
|
|
2534
|
+
console.warn(
|
|
2535
|
+
" Warning: Could not delete D1 database (may already be deleted)."
|
|
1326
2536
|
);
|
|
1327
2537
|
}
|
|
1328
|
-
}
|
|
1329
|
-
|
|
2538
|
+
}
|
|
2539
|
+
printStep("Deleting Worker...");
|
|
2540
|
+
const workerOk = await deleteWorker(accountId, wrangler2.workerName, apiToken);
|
|
2541
|
+
if (workerOk) {
|
|
2542
|
+
printSuccess(`Deleted ${wrangler2.workerName}`);
|
|
1330
2543
|
} else {
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
2544
|
+
console.warn(
|
|
2545
|
+
" Warning: Could not delete Worker (may already be deleted)."
|
|
2546
|
+
);
|
|
2547
|
+
}
|
|
2548
|
+
if (!opts.keepLocal) {
|
|
2549
|
+
printStep("Cleaning up local files...");
|
|
2550
|
+
if (fs10.existsSync(wranglerPath)) {
|
|
2551
|
+
fs10.unlinkSync(wranglerPath);
|
|
2552
|
+
printSuccess("Removed wrangler.toml");
|
|
2553
|
+
}
|
|
2554
|
+
if (serverUrl) {
|
|
2555
|
+
removeServerFromState(serverUrl);
|
|
2556
|
+
printSuccess("Removed server entry from ~/.zooid/state.json");
|
|
1334
2557
|
}
|
|
1335
2558
|
}
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
2559
|
+
console.log("");
|
|
2560
|
+
printSuccess("Server destroyed.");
|
|
2561
|
+
console.log(
|
|
2562
|
+
" If you configured a custom domain, remember to remove the DNS record."
|
|
2563
|
+
);
|
|
2564
|
+
}
|
|
2565
|
+
|
|
2566
|
+
// src/commands/login.ts
|
|
2567
|
+
import fs11 from "fs";
|
|
2568
|
+
import path9 from "path";
|
|
2569
|
+
|
|
2570
|
+
// src/lib/device-auth.ts
|
|
2571
|
+
import { exec } from "child_process";
|
|
2572
|
+
function defaultOpenBrowser(url) {
|
|
2573
|
+
const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
|
|
2574
|
+
try {
|
|
2575
|
+
exec(`${cmd} ${JSON.stringify(url)}`);
|
|
2576
|
+
} catch {
|
|
2577
|
+
}
|
|
2578
|
+
}
|
|
2579
|
+
async function pollDeviceAuth(accountsUrl, options) {
|
|
2580
|
+
const _fetch = options?.fetch ?? globalThis.fetch;
|
|
2581
|
+
const openBrowser = options?.openBrowser ?? defaultOpenBrowser;
|
|
2582
|
+
const initRes = await _fetch(`${accountsUrl}/api/auth/device/code`, {
|
|
2583
|
+
method: "POST",
|
|
2584
|
+
headers: { "Content-Type": "application/json" },
|
|
2585
|
+
body: JSON.stringify({ client_id: "zooid-cli" })
|
|
2586
|
+
});
|
|
2587
|
+
if (!initRes.ok) {
|
|
2588
|
+
throw new Error(`Failed to initiate device auth: ${initRes.status}`);
|
|
2589
|
+
}
|
|
2590
|
+
const init = await initRes.json();
|
|
2591
|
+
process.stderr.write(
|
|
2592
|
+
`If the browser doesn't open, visit:
|
|
2593
|
+
${init.verification_uri_complete}
|
|
2594
|
+
`
|
|
2595
|
+
);
|
|
2596
|
+
openBrowser(init.verification_uri_complete);
|
|
2597
|
+
const deadline = Date.now() + init.expires_in * 1e3;
|
|
2598
|
+
const interval = (init.interval ?? 5) * 1e3;
|
|
2599
|
+
return new Promise((resolve, reject) => {
|
|
2600
|
+
const poll = async () => {
|
|
2601
|
+
if (Date.now() > deadline) {
|
|
2602
|
+
reject(new Error("Authentication timed out. Please try again."));
|
|
2603
|
+
return;
|
|
2604
|
+
}
|
|
2605
|
+
try {
|
|
2606
|
+
const res = await _fetch(`${accountsUrl}/api/auth/device/token`, {
|
|
2607
|
+
method: "POST",
|
|
2608
|
+
headers: { "Content-Type": "application/json" },
|
|
2609
|
+
body: JSON.stringify({
|
|
2610
|
+
grant_type: "urn:ietf:params:oauth:grant-type:device_code",
|
|
2611
|
+
device_code: init.device_code,
|
|
2612
|
+
client_id: "zooid-cli"
|
|
2613
|
+
})
|
|
2614
|
+
});
|
|
2615
|
+
if (res.status === 200) {
|
|
2616
|
+
const data = await res.json();
|
|
2617
|
+
const sessionRes = await _fetch(
|
|
2618
|
+
`${accountsUrl}/api/auth/get-session`,
|
|
2619
|
+
{
|
|
2620
|
+
headers: { Authorization: `Bearer ${data.access_token}` }
|
|
2621
|
+
}
|
|
2622
|
+
);
|
|
2623
|
+
let user = {
|
|
2624
|
+
email: "unknown",
|
|
2625
|
+
name: void 0
|
|
2626
|
+
};
|
|
2627
|
+
if (sessionRes.ok) {
|
|
2628
|
+
const session = await sessionRes.json();
|
|
2629
|
+
if (session.user) {
|
|
2630
|
+
user = { email: session.user.email, name: session.user.name };
|
|
2631
|
+
}
|
|
2632
|
+
}
|
|
2633
|
+
resolve({ sessionToken: data.access_token, user });
|
|
2634
|
+
return;
|
|
2635
|
+
}
|
|
2636
|
+
if (res.status === 400) {
|
|
2637
|
+
const error = await res.json();
|
|
2638
|
+
if (error.error === "expired_token") {
|
|
2639
|
+
reject(new Error("Device code expired. Please try again."));
|
|
2640
|
+
return;
|
|
2641
|
+
}
|
|
2642
|
+
}
|
|
2643
|
+
} catch {
|
|
2644
|
+
}
|
|
2645
|
+
setTimeout(poll, interval);
|
|
1344
2646
|
};
|
|
1345
|
-
|
|
1346
|
-
if (details.tags.length > 0) entry.tags = details.tags;
|
|
1347
|
-
return entry;
|
|
2647
|
+
setTimeout(poll, interval);
|
|
1348
2648
|
});
|
|
1349
|
-
|
|
2649
|
+
}
|
|
2650
|
+
async function exchangeToken(accountsUrl, sessionToken, serverUrl, options) {
|
|
2651
|
+
const _fetch = options?.fetch ?? globalThis.fetch;
|
|
2652
|
+
const res = await _fetch(`${accountsUrl}/api/auth/device-code/exchange`, {
|
|
1350
2653
|
method: "POST",
|
|
1351
|
-
|
|
2654
|
+
headers: {
|
|
2655
|
+
Authorization: `Bearer ${sessionToken}`,
|
|
2656
|
+
"Content-Type": "application/json"
|
|
2657
|
+
},
|
|
2658
|
+
body: JSON.stringify({ server_url: serverUrl })
|
|
1352
2659
|
});
|
|
1353
2660
|
if (!res.ok) {
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
for (const ch of selected) {
|
|
1359
|
-
console.log(` ${ch.id} \u2192 ${serverUrl}/${ch.id}`);
|
|
1360
|
-
}
|
|
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]}`);
|
|
2661
|
+
const error = await res.json().catch(() => ({}));
|
|
2662
|
+
throw new Error(
|
|
2663
|
+
`Token exchange failed: ${error.message || error.error || res.status}`
|
|
2664
|
+
);
|
|
1369
2665
|
}
|
|
1370
|
-
|
|
2666
|
+
return await res.json();
|
|
1371
2667
|
}
|
|
1372
|
-
async function
|
|
1373
|
-
const
|
|
1374
|
-
const
|
|
1375
|
-
|
|
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
|
-
}
|
|
2668
|
+
async function fetchServers(accountsUrl, sessionToken, options) {
|
|
2669
|
+
const _fetch = options?.fetch ?? globalThis.fetch;
|
|
2670
|
+
const res = await _fetch(`${accountsUrl}/api/auth/device-code/servers`, {
|
|
2671
|
+
headers: { Authorization: `Bearer ${sessionToken}` }
|
|
1385
2672
|
});
|
|
1386
|
-
|
|
1387
|
-
|
|
2673
|
+
if (!res.ok) return [];
|
|
2674
|
+
const data = await res.json();
|
|
2675
|
+
return data.servers;
|
|
1388
2676
|
}
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
tags: ch.tags
|
|
1396
|
-
});
|
|
1397
|
-
}
|
|
1398
|
-
return result;
|
|
1399
|
-
}
|
|
1400
|
-
const rl = readline4.createInterface({
|
|
1401
|
-
input: process.stdin,
|
|
1402
|
-
output: process.stdout
|
|
1403
|
-
});
|
|
2677
|
+
|
|
2678
|
+
// src/commands/login.ts
|
|
2679
|
+
var ACCOUNTS_URL = "https://accounts.zooid.dev";
|
|
2680
|
+
function writeProjectConfig(serverUrl) {
|
|
2681
|
+
const configPath = path9.join(process.cwd(), "zooid.json");
|
|
2682
|
+
let existing = {};
|
|
1404
2683
|
try {
|
|
1405
|
-
|
|
1406
|
-
|
|
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(", ")
|
|
1415
|
-
);
|
|
1416
|
-
const tags = tagsRaw.split(",").map((t) => t.trim()).filter(Boolean);
|
|
1417
|
-
result.set(ch.id, { description: desc, tags });
|
|
1418
|
-
console.log("");
|
|
1419
|
-
}
|
|
1420
|
-
} finally {
|
|
1421
|
-
rl.close();
|
|
2684
|
+
existing = JSON.parse(fs11.readFileSync(configPath, "utf-8"));
|
|
2685
|
+
} catch {
|
|
1422
2686
|
}
|
|
1423
|
-
|
|
2687
|
+
existing.url = serverUrl;
|
|
2688
|
+
fs11.writeFileSync(configPath, JSON.stringify(existing, null, 2) + "\n");
|
|
1424
2689
|
}
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
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
|
-
);
|
|
1435
|
-
}
|
|
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));
|
|
2690
|
+
async function runLogin(url, options) {
|
|
2691
|
+
const _fetch = options?.fetch ?? globalThis.fetch;
|
|
2692
|
+
if (url) {
|
|
2693
|
+
return loginToServer(normalizeServerUrl(url), _fetch);
|
|
1448
2694
|
}
|
|
2695
|
+
return loginToZoon(_fetch);
|
|
1449
2696
|
}
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
const
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
2697
|
+
async function loginToZoon(_fetch) {
|
|
2698
|
+
process.stderr.write("\nOpening browser to authenticate with Zoon...\n");
|
|
2699
|
+
const result = await pollDeviceAuth(ACCOUNTS_URL, { fetch: _fetch });
|
|
2700
|
+
process.stderr.write(
|
|
2701
|
+
`
|
|
2702
|
+
Logged in as ${result.user.name || result.user.email}
|
|
2703
|
+
`
|
|
2704
|
+
);
|
|
2705
|
+
const servers = await fetchServers(ACCOUNTS_URL, result.sessionToken, {
|
|
2706
|
+
fetch: _fetch
|
|
2707
|
+
});
|
|
2708
|
+
let targetServer;
|
|
2709
|
+
try {
|
|
2710
|
+
const fs13 = await import("fs");
|
|
2711
|
+
const path10 = await import("path");
|
|
2712
|
+
const configPath = path10.join(process.cwd(), "zooid.json");
|
|
2713
|
+
if (fs13.existsSync(configPath)) {
|
|
2714
|
+
const raw = JSON.parse(fs13.readFileSync(configPath, "utf-8"));
|
|
2715
|
+
if (raw.url) {
|
|
2716
|
+
const hasAccess = servers.some((s) => s.url === raw.url);
|
|
2717
|
+
if (hasAccess) {
|
|
2718
|
+
targetServer = raw.url;
|
|
2719
|
+
} else {
|
|
2720
|
+
printInfo(
|
|
2721
|
+
"Note",
|
|
2722
|
+
`zooid.json references ${raw.url} but you don't have access to it`
|
|
2723
|
+
);
|
|
2724
|
+
}
|
|
2725
|
+
}
|
|
2726
|
+
}
|
|
2727
|
+
} catch {
|
|
1462
2728
|
}
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
2729
|
+
if (!targetServer && servers.length > 0) {
|
|
2730
|
+
targetServer = servers[0].url;
|
|
2731
|
+
}
|
|
2732
|
+
if (!targetServer) {
|
|
2733
|
+
saveConfig(
|
|
2734
|
+
{
|
|
2735
|
+
platform_token: result.sessionToken,
|
|
2736
|
+
auth_method: "oidc"
|
|
2737
|
+
},
|
|
2738
|
+
ACCOUNTS_URL,
|
|
2739
|
+
{ setCurrent: false }
|
|
2740
|
+
);
|
|
2741
|
+
printSuccess("Authenticated with Zoon");
|
|
2742
|
+
printInfo("Note", "No servers found. Create one at app.zooid.dev");
|
|
2743
|
+
process.stderr.write("\n");
|
|
1466
2744
|
return;
|
|
1467
2745
|
}
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
2746
|
+
const exchangeResult = await exchangeToken(
|
|
2747
|
+
ACCOUNTS_URL,
|
|
2748
|
+
result.sessionToken,
|
|
2749
|
+
targetServer,
|
|
2750
|
+
{ fetch: _fetch }
|
|
2751
|
+
);
|
|
2752
|
+
saveConfig(
|
|
2753
|
+
{
|
|
2754
|
+
admin_token: exchangeResult.token,
|
|
2755
|
+
platform_token: result.sessionToken,
|
|
2756
|
+
auth_method: "oidc"
|
|
2757
|
+
},
|
|
2758
|
+
targetServer,
|
|
2759
|
+
{ setCurrent: true }
|
|
2760
|
+
);
|
|
2761
|
+
writeProjectConfig(targetServer);
|
|
2762
|
+
printSuccess(`Server: ${targetServer} (set as current)`);
|
|
2763
|
+
printInfo("Project", `zooid.json \u2192 ${targetServer}`);
|
|
2764
|
+
process.stderr.write("\n");
|
|
2765
|
+
}
|
|
2766
|
+
async function loginToServer(serverUrl, _fetch) {
|
|
2767
|
+
const url = new URL(serverUrl);
|
|
2768
|
+
if (url.hostname.endsWith(".zoon.eco")) {
|
|
2769
|
+
process.stderr.write("\nOpening browser to authenticate with Zoon...\n");
|
|
2770
|
+
const result = await pollDeviceAuth(ACCOUNTS_URL, { fetch: _fetch });
|
|
2771
|
+
process.stderr.write(
|
|
2772
|
+
`
|
|
2773
|
+
Logged in as ${result.user.name || result.user.email}
|
|
2774
|
+
`
|
|
2775
|
+
);
|
|
2776
|
+
const exchangeResult = await exchangeToken(
|
|
2777
|
+
ACCOUNTS_URL,
|
|
2778
|
+
result.sessionToken,
|
|
2779
|
+
serverUrl,
|
|
2780
|
+
{ fetch: _fetch }
|
|
2781
|
+
);
|
|
2782
|
+
saveConfig(
|
|
2783
|
+
{
|
|
2784
|
+
admin_token: exchangeResult.token,
|
|
2785
|
+
platform_token: result.sessionToken,
|
|
2786
|
+
auth_method: "oidc"
|
|
2787
|
+
},
|
|
2788
|
+
serverUrl,
|
|
2789
|
+
{ setCurrent: true }
|
|
2790
|
+
);
|
|
2791
|
+
writeProjectConfig(serverUrl);
|
|
2792
|
+
printSuccess(`Server: ${serverUrl} (set as current)`);
|
|
2793
|
+
printInfo("Project", `zooid.json \u2192 ${serverUrl}`);
|
|
2794
|
+
return;
|
|
1474
2795
|
}
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
2796
|
+
throw new Error(
|
|
2797
|
+
"CLI login for self-hosted servers with external OIDC is coming soon.\nFor now, use `npx zooid token mint` to create a token manually."
|
|
2798
|
+
);
|
|
1478
2799
|
}
|
|
1479
2800
|
|
|
1480
|
-
// src/commands/
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
2801
|
+
// src/commands/logout.ts
|
|
2802
|
+
import fs12 from "fs";
|
|
2803
|
+
async function runLogout(options) {
|
|
2804
|
+
const file = loadConfigFile();
|
|
2805
|
+
if (options.all) {
|
|
2806
|
+
for (const url of Object.keys(file.servers ?? {})) {
|
|
2807
|
+
clearServerAuth(file, url);
|
|
2808
|
+
}
|
|
2809
|
+
process.stderr.write("Logged out of all servers\n");
|
|
2810
|
+
} else {
|
|
2811
|
+
const server = resolveServer();
|
|
2812
|
+
if (!server) {
|
|
2813
|
+
throw new Error("No server configured.");
|
|
2814
|
+
}
|
|
2815
|
+
clearServerAuth(file, server);
|
|
2816
|
+
process.stderr.write(`Logged out of ${server}
|
|
2817
|
+
`);
|
|
2818
|
+
}
|
|
2819
|
+
const dir = getConfigDir();
|
|
2820
|
+
fs12.mkdirSync(dir, { recursive: true });
|
|
2821
|
+
fs12.writeFileSync(getStatePath(), JSON.stringify(file, null, 2) + "\n");
|
|
1484
2822
|
}
|
|
1485
|
-
|
|
1486
|
-
const
|
|
1487
|
-
|
|
2823
|
+
function clearServerAuth(file, url) {
|
|
2824
|
+
const entry = file.servers?.[url];
|
|
2825
|
+
if (!entry) return;
|
|
2826
|
+
delete entry.admin_token;
|
|
2827
|
+
delete entry.refresh_token;
|
|
2828
|
+
delete entry.auth_method;
|
|
1488
2829
|
}
|
|
1489
2830
|
|
|
1490
|
-
// src/commands/
|
|
1491
|
-
async function
|
|
2831
|
+
// src/commands/whoami.ts
|
|
2832
|
+
async function runWhoami() {
|
|
2833
|
+
const config = loadConfig();
|
|
2834
|
+
const file = loadConfigFile();
|
|
2835
|
+
if (!config.server) {
|
|
2836
|
+
throw new Error("No server configured. Run: npx zooid login");
|
|
2837
|
+
}
|
|
1492
2838
|
const client = createClient();
|
|
1493
|
-
const
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
2839
|
+
const claims = await client.getTokenClaims();
|
|
2840
|
+
const entry = file.servers?.[config.server];
|
|
2841
|
+
return {
|
|
2842
|
+
server: config.server,
|
|
2843
|
+
sub: claims.sub ?? "unknown",
|
|
2844
|
+
name: claims.name,
|
|
2845
|
+
scopes: claims.scopes ?? [],
|
|
2846
|
+
exp: claims.exp,
|
|
2847
|
+
authMethod: entry?.auth_method
|
|
2848
|
+
};
|
|
1498
2849
|
}
|
|
1499
2850
|
|
|
1500
|
-
// src/commands/
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
2851
|
+
// src/commands/credentials.ts
|
|
2852
|
+
function requireZoonServer() {
|
|
2853
|
+
const server = resolveServer();
|
|
2854
|
+
if (!server) {
|
|
2855
|
+
throw new Error("No server configured. Run: npx zooid login");
|
|
2856
|
+
}
|
|
2857
|
+
if (!isZoonHosted(server)) {
|
|
2858
|
+
throw new Error(
|
|
2859
|
+
"Credentials are only available for Zoon-hosted servers (*.zoon.eco)"
|
|
2860
|
+
);
|
|
2861
|
+
}
|
|
2862
|
+
const file = loadConfigFile();
|
|
2863
|
+
const entry = file.servers?.[server];
|
|
2864
|
+
if (!entry?.platform_token) {
|
|
2865
|
+
throw new Error(
|
|
2866
|
+
"Not authenticated with Zoon platform. Run: npx zooid login"
|
|
2867
|
+
);
|
|
2868
|
+
}
|
|
2869
|
+
return { server, platformToken: entry.platform_token };
|
|
2870
|
+
}
|
|
2871
|
+
function formatEnv(server, clientId, clientSecret) {
|
|
2872
|
+
return [
|
|
2873
|
+
`ZOOID_SERVER=${server}`,
|
|
2874
|
+
`ZOOID_CLIENT_ID=${clientId}`,
|
|
2875
|
+
`ZOOID_CLIENT_SECRET=${clientSecret}`
|
|
2876
|
+
].join("\n");
|
|
2877
|
+
}
|
|
2878
|
+
async function runCredentialsCreate(name, options) {
|
|
2879
|
+
const { server, platformToken } = requireZoonServer();
|
|
2880
|
+
let roleNames = options.role;
|
|
2881
|
+
if (!roleNames || roleNames.length === 0) {
|
|
2882
|
+
const wf = loadWorkforce();
|
|
2883
|
+
if (wf.roles[name]) {
|
|
2884
|
+
roleNames = [name];
|
|
2885
|
+
} else {
|
|
2886
|
+
throw new Error(
|
|
2887
|
+
`No roles specified and no matching agent/role "${name}" found in workforce.json`
|
|
2888
|
+
);
|
|
2889
|
+
}
|
|
2890
|
+
}
|
|
2891
|
+
const result = await createCredential(server, platformToken, name, roleNames);
|
|
2892
|
+
process.stderr.write(
|
|
2893
|
+
`
|
|
2894
|
+
Created credential "${name}" on ${server} (roles: ${roleNames.join(", ")})
|
|
2895
|
+
|
|
2896
|
+
`
|
|
2897
|
+
);
|
|
2898
|
+
return formatEnv(server, result.client_id, result.client_secret);
|
|
1509
2899
|
}
|
|
1510
|
-
async function
|
|
1511
|
-
const
|
|
1512
|
-
|
|
2900
|
+
async function runCredentialsList() {
|
|
2901
|
+
const { server, platformToken } = requireZoonServer();
|
|
2902
|
+
return listCredentials(server, platformToken);
|
|
2903
|
+
}
|
|
2904
|
+
async function resolveClientId(nameOrId) {
|
|
2905
|
+
const { server, platformToken } = requireZoonServer();
|
|
2906
|
+
const creds = await listCredentials(server, platformToken);
|
|
2907
|
+
const match = creds.find((c) => c.name === nameOrId) || creds.find((c) => c.client_id === nameOrId);
|
|
2908
|
+
if (!match) {
|
|
1513
2909
|
throw new Error(
|
|
1514
|
-
`
|
|
2910
|
+
`Credential "${nameOrId}" not found. Run: npx zooid credentials list`
|
|
1515
2911
|
);
|
|
1516
2912
|
}
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
2913
|
+
return { clientId: match.client_id, name: match.name };
|
|
2914
|
+
}
|
|
2915
|
+
async function runCredentialsRotate(nameOrId) {
|
|
2916
|
+
const { server, platformToken } = requireZoonServer();
|
|
2917
|
+
const { clientId, name } = await resolveClientId(nameOrId);
|
|
2918
|
+
const result = await rotateCredential(server, platformToken, clientId);
|
|
2919
|
+
process.stderr.write(`
|
|
2920
|
+
Rotated credential "${name}" on ${server}
|
|
2921
|
+
|
|
1520
2922
|
`);
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
const
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
2923
|
+
return formatEnv(server, result.client_id, result.client_secret);
|
|
2924
|
+
}
|
|
2925
|
+
async function runCredentialsRevoke(nameOrId) {
|
|
2926
|
+
const { server, platformToken } = requireZoonServer();
|
|
2927
|
+
const { clientId, name } = await resolveClientId(nameOrId);
|
|
2928
|
+
await revokeCredential(server, platformToken, clientId);
|
|
2929
|
+
process.stderr.write(`
|
|
2930
|
+
Revoked credential "${name}" on ${server}
|
|
2931
|
+
`);
|
|
2932
|
+
}
|
|
2933
|
+
|
|
2934
|
+
// src/lib/auto-refresh.ts
|
|
2935
|
+
function decodeJwtPayload(jwt) {
|
|
2936
|
+
try {
|
|
2937
|
+
const parts = jwt.split(".");
|
|
2938
|
+
if (parts.length !== 3) return null;
|
|
2939
|
+
const payload = atob(parts[1]);
|
|
2940
|
+
return JSON.parse(payload);
|
|
2941
|
+
} catch {
|
|
2942
|
+
return null;
|
|
2943
|
+
}
|
|
2944
|
+
}
|
|
2945
|
+
async function maybeRefreshToken(serverConfig, _serverUrl, _options) {
|
|
2946
|
+
if (serverConfig.auth_method !== "oidc") return;
|
|
2947
|
+
if (!serverConfig.admin_token) return;
|
|
2948
|
+
const payload = decodeJwtPayload(serverConfig.admin_token);
|
|
2949
|
+
if (!payload?.exp) return;
|
|
2950
|
+
const expiresAt = payload.exp * 1e3;
|
|
2951
|
+
const twoMinutes = 2 * 60 * 1e3;
|
|
2952
|
+
if (Date.now() < expiresAt - twoMinutes) return;
|
|
2953
|
+
process.stderr.write(
|
|
2954
|
+
"Session expired. Run `npx zooid login` to re-authenticate.\n"
|
|
2955
|
+
);
|
|
2956
|
+
}
|
|
2957
|
+
|
|
2958
|
+
// src/commands/role.ts
|
|
2959
|
+
function runRoleCreate(id, options) {
|
|
2960
|
+
const wf = loadWorkforce();
|
|
2961
|
+
if (id in wf.roles) {
|
|
2962
|
+
throw new Error(
|
|
2963
|
+
`Role "${id}" already exists. Use "zooid role update" to modify it.`
|
|
2964
|
+
);
|
|
2965
|
+
}
|
|
2966
|
+
const def = { scopes: options.scopes };
|
|
2967
|
+
if (options.name) def.name = options.name;
|
|
2968
|
+
if (options.description) def.description = options.description;
|
|
2969
|
+
wf.roles[id] = def;
|
|
2970
|
+
saveWorkforce(wf);
|
|
2971
|
+
}
|
|
2972
|
+
function runRoleList() {
|
|
2973
|
+
const wf = loadWorkforce();
|
|
2974
|
+
return Object.keys(wf.roles);
|
|
2975
|
+
}
|
|
2976
|
+
function runRoleUpdate(id, fields) {
|
|
2977
|
+
const wf = loadWorkforce();
|
|
2978
|
+
const existing = wf.roles[id];
|
|
2979
|
+
if (!existing) {
|
|
2980
|
+
throw new Error(`Role "${id}" not found. Use "zooid role create" first.`);
|
|
2981
|
+
}
|
|
2982
|
+
if (fields.name !== void 0) {
|
|
2983
|
+
if (fields.name === null) {
|
|
2984
|
+
delete existing.name;
|
|
2985
|
+
} else {
|
|
2986
|
+
existing.name = fields.name;
|
|
1537
2987
|
}
|
|
1538
2988
|
}
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
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
|
|
2989
|
+
if (fields.description !== void 0) {
|
|
2990
|
+
if (fields.description === null) {
|
|
2991
|
+
delete existing.description;
|
|
2992
|
+
} else {
|
|
2993
|
+
existing.description = fields.description;
|
|
1553
2994
|
}
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
}
|
|
2995
|
+
}
|
|
2996
|
+
if (fields.scopes !== void 0) {
|
|
2997
|
+
existing.scopes = fields.scopes;
|
|
2998
|
+
}
|
|
2999
|
+
const targetFile = wf.provenance.roles[id];
|
|
3000
|
+
if (targetFile) {
|
|
3001
|
+
updateInFile(targetFile, "roles", id, existing);
|
|
3002
|
+
} else {
|
|
3003
|
+
wf.roles[id] = existing;
|
|
3004
|
+
saveWorkforce(wf);
|
|
3005
|
+
}
|
|
3006
|
+
return existing;
|
|
3007
|
+
}
|
|
3008
|
+
function runRoleDelete(id) {
|
|
3009
|
+
const wf = loadWorkforce();
|
|
3010
|
+
if (!(id in wf.roles)) {
|
|
3011
|
+
throw new Error(`Role "${id}" not found in .zooid/workforce.json`);
|
|
3012
|
+
}
|
|
3013
|
+
const targetFile = wf.provenance.roles[id];
|
|
3014
|
+
if (targetFile) {
|
|
3015
|
+
removeFromFile(targetFile, "roles", id);
|
|
3016
|
+
} else {
|
|
3017
|
+
delete wf.roles[id];
|
|
3018
|
+
saveWorkforce(wf);
|
|
3019
|
+
}
|
|
1562
3020
|
}
|
|
1563
3021
|
|
|
1564
3022
|
// src/index.ts
|
|
@@ -1584,7 +3042,7 @@ async function resolveAndRecord(channel, opts) {
|
|
|
1584
3042
|
return result;
|
|
1585
3043
|
}
|
|
1586
3044
|
var program = new Command();
|
|
1587
|
-
program.name("zooid").description("\u{1FAB8} Pub/sub for AI agents").version("0.
|
|
3045
|
+
program.name("zooid").description("\u{1FAB8} Pub/sub for AI agents").version("0.6.1");
|
|
1588
3046
|
var telemetryCtx = { startTime: 0 };
|
|
1589
3047
|
function setTelemetryChannel(channelId) {
|
|
1590
3048
|
telemetryCtx.channelId = channelId;
|
|
@@ -1608,11 +3066,22 @@ function handleError(commandName, err) {
|
|
|
1608
3066
|
printError(message);
|
|
1609
3067
|
process.exit(1);
|
|
1610
3068
|
}
|
|
1611
|
-
program.hook("preAction", () => {
|
|
3069
|
+
program.hook("preAction", async () => {
|
|
1612
3070
|
telemetryCtx.startTime = Date.now();
|
|
1613
3071
|
if (isEnabled()) {
|
|
1614
3072
|
showNoticeIfNeeded();
|
|
1615
3073
|
}
|
|
3074
|
+
try {
|
|
3075
|
+
const file = loadConfigFile();
|
|
3076
|
+
const server = resolveServer();
|
|
3077
|
+
if (server && file.servers?.[server]) {
|
|
3078
|
+
const entry = file.servers[server];
|
|
3079
|
+
await maybeRefreshToken(entry, server, {
|
|
3080
|
+
save: (partial) => saveConfig(partial, server)
|
|
3081
|
+
});
|
|
3082
|
+
}
|
|
3083
|
+
} catch {
|
|
3084
|
+
}
|
|
1616
3085
|
});
|
|
1617
3086
|
function sendTelemetry(commandName, exitCode, error) {
|
|
1618
3087
|
if (!isEnabled()) return;
|
|
@@ -1651,20 +3120,65 @@ program.command("dev").description("Start local development server").option("--p
|
|
|
1651
3120
|
handleError("dev", err);
|
|
1652
3121
|
}
|
|
1653
3122
|
});
|
|
1654
|
-
program.command("init").description("Create zooid
|
|
3123
|
+
program.command("init").description("Create zooid.json with server identity").option("--use <url>", "Include a template from a GitHub URL").action(async (opts) => {
|
|
1655
3124
|
try {
|
|
1656
|
-
await runInit();
|
|
3125
|
+
await runInit({ use: opts.use });
|
|
1657
3126
|
} catch (err) {
|
|
1658
3127
|
handleError("init", err);
|
|
1659
3128
|
}
|
|
1660
3129
|
});
|
|
1661
|
-
program.command("
|
|
3130
|
+
program.command("use <url>").description("Add a template to your workforce via include").action(async (url) => {
|
|
3131
|
+
try {
|
|
3132
|
+
await runUse(url);
|
|
3133
|
+
} catch (err) {
|
|
3134
|
+
handleError("use", err);
|
|
3135
|
+
}
|
|
3136
|
+
});
|
|
3137
|
+
program.command("deploy").description("Deploy Zooid server to Cloudflare Workers").option("--prune", "Delete server resources not in workforce.json").action(async (opts) => {
|
|
1662
3138
|
try {
|
|
1663
|
-
await runDeploy();
|
|
3139
|
+
await runDeploy(opts);
|
|
1664
3140
|
} catch (err) {
|
|
1665
3141
|
handleError("deploy", err);
|
|
1666
3142
|
}
|
|
1667
3143
|
});
|
|
3144
|
+
program.command("destroy").description("Destroy a deployed Zooid server and all its data").option("--force", "Skip confirmation prompt").option("--keep-local", "Keep wrangler.toml and state entries").action(async (options) => {
|
|
3145
|
+
try {
|
|
3146
|
+
await runDestroy({
|
|
3147
|
+
force: options.force,
|
|
3148
|
+
keepLocal: options.keepLocal
|
|
3149
|
+
});
|
|
3150
|
+
} catch (err) {
|
|
3151
|
+
handleError("destroy", err);
|
|
3152
|
+
}
|
|
3153
|
+
});
|
|
3154
|
+
program.command("login").argument("[url]", "Server URL (e.g., https://beno.zoon.eco)").description("Authenticate with Zoon or a specific server").action(async (url) => {
|
|
3155
|
+
try {
|
|
3156
|
+
await runLogin(url);
|
|
3157
|
+
} catch (err) {
|
|
3158
|
+
handleError("login", err);
|
|
3159
|
+
}
|
|
3160
|
+
});
|
|
3161
|
+
program.command("logout").option("--all", "Log out of all servers").description("Clear authentication for the current server").action(async (opts) => {
|
|
3162
|
+
try {
|
|
3163
|
+
await runLogout(opts);
|
|
3164
|
+
} catch (err) {
|
|
3165
|
+
handleError("logout", err);
|
|
3166
|
+
}
|
|
3167
|
+
});
|
|
3168
|
+
program.command("whoami").description("Show current identity and auth status").action(async () => {
|
|
3169
|
+
try {
|
|
3170
|
+
const result = await runWhoami();
|
|
3171
|
+
console.log(`Server: ${result.server}`);
|
|
3172
|
+
console.log(`User: ${result.name || result.sub}`);
|
|
3173
|
+
console.log(`Scopes: ${result.scopes.join(", ")}`);
|
|
3174
|
+
if (result.authMethod) {
|
|
3175
|
+
const expStr = result.exp ? ` (expires ${new Date(result.exp * 1e3).toISOString()})` : "";
|
|
3176
|
+
console.log(`Auth: ${result.authMethod}${expStr}`);
|
|
3177
|
+
}
|
|
3178
|
+
} catch (err) {
|
|
3179
|
+
handleError("whoami", err);
|
|
3180
|
+
}
|
|
3181
|
+
});
|
|
1668
3182
|
var configCmd = program.command("config").description("Manage Zooid configuration");
|
|
1669
3183
|
configCmd.command("set <key> <value>").description("Set a config value (server, admin-token, telemetry)").action((key, value) => {
|
|
1670
3184
|
try {
|
|
@@ -1687,9 +3201,9 @@ configCmd.command("get <key>").description("Get a config value").action((key) =>
|
|
|
1687
3201
|
}
|
|
1688
3202
|
});
|
|
1689
3203
|
var channelCmd = program.command("channel").description("Manage channels");
|
|
1690
|
-
channelCmd.command("create <id>").description("Create a new channel").option("--name <name>", "Display name (defaults to id)").option("--description <desc>", "Channel description").option("--public", "Make channel public
|
|
3204
|
+
channelCmd.command("create <id>").description("Create a new channel").option("--name <name>", "Display name (defaults to id)").option("--description <desc>", "Channel description").option("--public", "Make channel public").option("--private", "Make channel private (default)", true).option("--strict", "Enable strict schema validation on publish").option(
|
|
1691
3205
|
"--config <file>",
|
|
1692
|
-
"Path to channel config JSON file (
|
|
3206
|
+
"Path to channel config JSON file (types, policies, storage)"
|
|
1693
3207
|
).option(
|
|
1694
3208
|
"--schema <file>",
|
|
1695
3209
|
"Path to JSON schema file (map of event types to JSON schemas)"
|
|
@@ -1697,12 +3211,12 @@ channelCmd.command("create <id>").description("Create a new channel").option("--
|
|
|
1697
3211
|
try {
|
|
1698
3212
|
let config;
|
|
1699
3213
|
if (opts.config) {
|
|
1700
|
-
const
|
|
1701
|
-
const raw =
|
|
3214
|
+
const fs13 = await import("fs");
|
|
3215
|
+
const raw = fs13.readFileSync(opts.config, "utf-8");
|
|
1702
3216
|
config = JSON.parse(raw);
|
|
1703
3217
|
} else if (opts.schema) {
|
|
1704
|
-
const
|
|
1705
|
-
const raw =
|
|
3218
|
+
const fs13 = await import("fs");
|
|
3219
|
+
const raw = fs13.readFileSync(opts.schema, "utf-8");
|
|
1706
3220
|
const parsed = JSON.parse(raw);
|
|
1707
3221
|
const types = {};
|
|
1708
3222
|
for (const [eventType, schemaDef] of Object.entries(parsed)) {
|
|
@@ -1713,7 +3227,7 @@ channelCmd.command("create <id>").description("Create a new channel").option("--
|
|
|
1713
3227
|
const result = await runChannelCreate(id, {
|
|
1714
3228
|
name: opts.name,
|
|
1715
3229
|
description: opts.description,
|
|
1716
|
-
public: opts.
|
|
3230
|
+
public: opts.public ? true : false,
|
|
1717
3231
|
strict: opts.strict,
|
|
1718
3232
|
config
|
|
1719
3233
|
});
|
|
@@ -1725,7 +3239,7 @@ channelCmd.command("create <id>").description("Create a new channel").option("--
|
|
|
1725
3239
|
});
|
|
1726
3240
|
channelCmd.command("update <id>").description("Update a channel").option("--name <name>", "Display name").option("--description <desc>", "Channel description").option("--tags <tags>", "Comma-separated tags").option("--public", "Make channel public").option("--private", "Make channel private").option("--strict", "Enable strict schema validation on publish").option("--no-strict", "Disable strict schema validation").option(
|
|
1727
3241
|
"--config <file>",
|
|
1728
|
-
"Path to channel config JSON file (
|
|
3242
|
+
"Path to channel config JSON file (types, policies, storage)"
|
|
1729
3243
|
).option(
|
|
1730
3244
|
"--schema <file>",
|
|
1731
3245
|
"Path to JSON schema file (map of event types to JSON schemas)"
|
|
@@ -1739,12 +3253,12 @@ channelCmd.command("update <id>").description("Update a channel").option("--name
|
|
|
1739
3253
|
if (opts.public) fields.is_public = true;
|
|
1740
3254
|
if (opts.private) fields.is_public = false;
|
|
1741
3255
|
if (opts.config) {
|
|
1742
|
-
const
|
|
1743
|
-
const raw =
|
|
3256
|
+
const fs13 = await import("fs");
|
|
3257
|
+
const raw = fs13.readFileSync(opts.config, "utf-8");
|
|
1744
3258
|
fields.config = JSON.parse(raw);
|
|
1745
3259
|
} else if (opts.schema) {
|
|
1746
|
-
const
|
|
1747
|
-
const raw =
|
|
3260
|
+
const fs13 = await import("fs");
|
|
3261
|
+
const raw = fs13.readFileSync(opts.schema, "utf-8");
|
|
1748
3262
|
const parsed = JSON.parse(raw);
|
|
1749
3263
|
const types = {};
|
|
1750
3264
|
for (const [eventType, schemaDef] of Object.entries(parsed)) {
|
|
@@ -1790,8 +3304,8 @@ channelCmd.command("list").description("List all channels").action(async () => {
|
|
|
1790
3304
|
channelCmd.command("delete <id>").description("Delete a channel and all its data").option("-y, --yes", "Skip confirmation prompt").action(async (id, opts) => {
|
|
1791
3305
|
try {
|
|
1792
3306
|
if (!opts.yes) {
|
|
1793
|
-
const
|
|
1794
|
-
const rl =
|
|
3307
|
+
const readline7 = await import("readline");
|
|
3308
|
+
const rl = readline7.createInterface({
|
|
1795
3309
|
input: process.stdin,
|
|
1796
3310
|
output: process.stdout
|
|
1797
3311
|
});
|
|
@@ -1813,21 +3327,21 @@ channelCmd.command("delete <id>").description("Delete a channel and all its data
|
|
|
1813
3327
|
handleError("channel delete", err);
|
|
1814
3328
|
}
|
|
1815
3329
|
});
|
|
1816
|
-
program.command("
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
} catch (err) {
|
|
1820
|
-
handleError("push", err);
|
|
1821
|
-
}
|
|
1822
|
-
});
|
|
1823
|
-
program.command("pull").description("Pull channel definitions from server into channels/").action(async () => {
|
|
3330
|
+
program.command("pull").description(
|
|
3331
|
+
"Pull channel and role definitions from server into workforce.json"
|
|
3332
|
+
).action(async () => {
|
|
1824
3333
|
try {
|
|
1825
3334
|
await runPull();
|
|
1826
3335
|
} catch (err) {
|
|
1827
3336
|
handleError("pull", err);
|
|
1828
3337
|
}
|
|
1829
3338
|
});
|
|
1830
|
-
program.command("publish <channel>
|
|
3339
|
+
program.command("publish <channel> [data]").description(
|
|
3340
|
+
"Publish an event to a channel (accepts JSON arg, --data, --file, or stdin)"
|
|
3341
|
+
).option("--type <type>", "Event type").option("--data <json>", "Event data as JSON string").option("--file <path>", "Read event from JSON file").option(
|
|
3342
|
+
"--stream",
|
|
3343
|
+
"Stream mode: read newline-delimited JSON from stdin, publish each line"
|
|
3344
|
+
).option("--token <token>", "Auth token (for remote/private channels)").action(async (channel, dataArg, opts) => {
|
|
1831
3345
|
try {
|
|
1832
3346
|
const { client, channelId, tokenSaved } = resolveChannel(channel, {
|
|
1833
3347
|
token: opts.token,
|
|
@@ -1840,8 +3354,19 @@ program.command("publish <channel>").description("Publish an event to a channel"
|
|
|
1840
3354
|
`for ${channelId} \u2014 won't need --token next time`
|
|
1841
3355
|
);
|
|
1842
3356
|
}
|
|
1843
|
-
|
|
1844
|
-
|
|
3357
|
+
if (opts.stream) {
|
|
3358
|
+
const { published, errors } = await runPublishStream(
|
|
3359
|
+
channelId,
|
|
3360
|
+
opts,
|
|
3361
|
+
client,
|
|
3362
|
+
(event) => printSuccess(`Published event: ${event.id}`)
|
|
3363
|
+
);
|
|
3364
|
+
const summary = `${published} published`;
|
|
3365
|
+
printSuccess(errors ? `${summary}, ${errors} failed` : summary);
|
|
3366
|
+
} else {
|
|
3367
|
+
const event = await runPublish(channelId, opts, client, dataArg);
|
|
3368
|
+
printSuccess(`Published event: ${event.id}`);
|
|
3369
|
+
}
|
|
1845
3370
|
} catch (err) {
|
|
1846
3371
|
handleError("publish", err);
|
|
1847
3372
|
}
|
|
@@ -1852,8 +3377,8 @@ program.command("delete-event <channel> <event-id>").description("Delete a singl
|
|
|
1852
3377
|
tokenType: "publish"
|
|
1853
3378
|
});
|
|
1854
3379
|
if (!opts.yes) {
|
|
1855
|
-
const
|
|
1856
|
-
const rl =
|
|
3380
|
+
const readline7 = await import("readline");
|
|
3381
|
+
const rl = readline7.createInterface({
|
|
1857
3382
|
input: process.stdin,
|
|
1858
3383
|
output: process.stdout
|
|
1859
3384
|
});
|
|
@@ -2036,21 +3561,32 @@ var tokenCmd = program.command("token").description("Manage tokens");
|
|
|
2036
3561
|
tokenCmd.command("mint").description(
|
|
2037
3562
|
"Mint a new token. Scopes: admin, pub:<channel>, sub:<channel>. Wildcards: pub:*, sub:prefix-*"
|
|
2038
3563
|
).argument(
|
|
2039
|
-
"
|
|
3564
|
+
"[scopes...]",
|
|
2040
3565
|
"Scopes to grant (e.g. admin, pub:my-channel, sub:*)"
|
|
3566
|
+
).option(
|
|
3567
|
+
"--role <roles...>",
|
|
3568
|
+
"Mint with scopes from named roles (reads workforce.json)"
|
|
2041
3569
|
).option("--sub <sub>", "Subject identifier (e.g. publisher ID)").option("--name <name>", "Display name (used for publisher identity)").option("--expires-in <duration>", "Token expiry (e.g. 5m, 1h, 7d, 30d)").action(async (scopes, opts) => {
|
|
2042
3570
|
try {
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
3571
|
+
if (!opts.role?.length && (!scopes || scopes.length === 0)) {
|
|
3572
|
+
printError("Provide scopes or --role");
|
|
3573
|
+
process.exit(1);
|
|
3574
|
+
}
|
|
3575
|
+
if (!opts.role?.length) {
|
|
3576
|
+
for (const s of scopes) {
|
|
3577
|
+
if (s !== "admin" && !s.startsWith("pub:") && !s.startsWith("sub:")) {
|
|
3578
|
+
printError(
|
|
3579
|
+
`Invalid scope "${s}". Must be "admin", "pub:<channel>", or "sub:<channel>"`
|
|
3580
|
+
);
|
|
3581
|
+
process.exit(1);
|
|
3582
|
+
}
|
|
2048
3583
|
}
|
|
2049
3584
|
}
|
|
2050
|
-
const result = await runTokenMint(scopes, {
|
|
3585
|
+
const result = await runTokenMint(scopes ?? [], {
|
|
2051
3586
|
sub: opts.sub,
|
|
2052
3587
|
name: opts.name,
|
|
2053
|
-
expiresIn: opts.expiresIn
|
|
3588
|
+
expiresIn: opts.expiresIn,
|
|
3589
|
+
role: opts.role
|
|
2054
3590
|
});
|
|
2055
3591
|
console.log(result.token);
|
|
2056
3592
|
} catch (err) {
|
|
@@ -2128,6 +3664,130 @@ program.command("unshare <channel>").description("Remove a channel from the Zooi
|
|
|
2128
3664
|
handleError("unshare", err);
|
|
2129
3665
|
}
|
|
2130
3666
|
});
|
|
3667
|
+
var roleCmd = program.command("role").description("Manage role definitions");
|
|
3668
|
+
roleCmd.command("create <id>").description("Create a role definition in workforce.json").option("--name <name>", "Display name").option("--description <desc>", "Role description").argument("<scopes...>", "Scopes to grant (e.g. pub:signals sub:market-data)").action((id, scopes, opts) => {
|
|
3669
|
+
try {
|
|
3670
|
+
for (const s of scopes) {
|
|
3671
|
+
if (s !== "admin" && !s.startsWith("pub:") && !s.startsWith("sub:")) {
|
|
3672
|
+
printError(
|
|
3673
|
+
`Invalid scope "${s}". Must be "admin", "pub:<channel>", or "sub:<channel>"`
|
|
3674
|
+
);
|
|
3675
|
+
process.exit(1);
|
|
3676
|
+
}
|
|
3677
|
+
}
|
|
3678
|
+
runRoleCreate(id, {
|
|
3679
|
+
name: opts.name,
|
|
3680
|
+
description: opts.description,
|
|
3681
|
+
scopes
|
|
3682
|
+
});
|
|
3683
|
+
printSuccess(`Created role "${id}" in workforce.json`);
|
|
3684
|
+
printInfo("Next", "Run `npx zooid deploy` to sync to server");
|
|
3685
|
+
} catch (err) {
|
|
3686
|
+
handleError("role create", err);
|
|
3687
|
+
}
|
|
3688
|
+
});
|
|
3689
|
+
roleCmd.command("list").description("List role definitions in workforce.json").action(() => {
|
|
3690
|
+
try {
|
|
3691
|
+
const ids = runRoleList();
|
|
3692
|
+
if (ids.length === 0) {
|
|
3693
|
+
console.log(
|
|
3694
|
+
"No roles defined. Create one with: npx zooid role create <id> <scopes...>"
|
|
3695
|
+
);
|
|
3696
|
+
} else {
|
|
3697
|
+
for (const id of ids) {
|
|
3698
|
+
console.log(` ${id}`);
|
|
3699
|
+
}
|
|
3700
|
+
}
|
|
3701
|
+
} catch (err) {
|
|
3702
|
+
handleError("role list", err);
|
|
3703
|
+
}
|
|
3704
|
+
});
|
|
3705
|
+
roleCmd.command("update <id>").description("Update a role definition in workforce.json").option("--name <name>", "Display name").option("--description <desc>", "Role description").option("--scopes <scopes...>", "Replace scopes").action((id, opts) => {
|
|
3706
|
+
try {
|
|
3707
|
+
const fields = {};
|
|
3708
|
+
if (opts.name !== void 0) fields.name = opts.name;
|
|
3709
|
+
if (opts.description !== void 0) fields.description = opts.description;
|
|
3710
|
+
if (opts.scopes !== void 0) fields.scopes = opts.scopes;
|
|
3711
|
+
if (Object.keys(fields).length === 0) {
|
|
3712
|
+
throw new Error(
|
|
3713
|
+
"No fields specified. Use --name, --description, or --scopes."
|
|
3714
|
+
);
|
|
3715
|
+
}
|
|
3716
|
+
runRoleUpdate(id, fields);
|
|
3717
|
+
printSuccess(`Updated role "${id}" in workforce.json`);
|
|
3718
|
+
printInfo("Next", "Run `npx zooid deploy` to sync to server");
|
|
3719
|
+
} catch (err) {
|
|
3720
|
+
handleError("role update", err);
|
|
3721
|
+
}
|
|
3722
|
+
});
|
|
3723
|
+
roleCmd.command("delete <id>").description("Delete a role definition from workforce.json").option("-y, --yes", "Skip confirmation prompt").action(async (id, opts) => {
|
|
3724
|
+
try {
|
|
3725
|
+
if (!opts.yes) {
|
|
3726
|
+
const readline7 = await import("readline");
|
|
3727
|
+
const rl = readline7.createInterface({
|
|
3728
|
+
input: process.stdin,
|
|
3729
|
+
output: process.stdout
|
|
3730
|
+
});
|
|
3731
|
+
const answer = await new Promise((resolve) => {
|
|
3732
|
+
rl.question(
|
|
3733
|
+
`Delete role "${id}" from workforce.json? [y/N] `,
|
|
3734
|
+
resolve
|
|
3735
|
+
);
|
|
3736
|
+
});
|
|
3737
|
+
rl.close();
|
|
3738
|
+
if (answer.toLowerCase() !== "y") {
|
|
3739
|
+
console.log("Aborted.");
|
|
3740
|
+
return;
|
|
3741
|
+
}
|
|
3742
|
+
}
|
|
3743
|
+
runRoleDelete(id);
|
|
3744
|
+
printSuccess(`Deleted role "${id}" from workforce.json`);
|
|
3745
|
+
printInfo("Next", "Run `npx zooid deploy` to sync to server");
|
|
3746
|
+
} catch (err) {
|
|
3747
|
+
handleError("role delete", err);
|
|
3748
|
+
}
|
|
3749
|
+
});
|
|
3750
|
+
var credentialsCmd = program.command("credentials").description("Manage M2M agent credentials");
|
|
3751
|
+
credentialsCmd.command("create <name>").option("--role <role...>", "Role names to assign").description("Create a new credential (outputs .env to stdout)").action(async (name, opts) => {
|
|
3752
|
+
try {
|
|
3753
|
+
const env = await runCredentialsCreate(name, opts);
|
|
3754
|
+
process.stdout.write(env + "\n");
|
|
3755
|
+
} catch (err) {
|
|
3756
|
+
handleError("credentials create", err);
|
|
3757
|
+
}
|
|
3758
|
+
});
|
|
3759
|
+
credentialsCmd.command("list").description("List all credentials for the current server").action(async () => {
|
|
3760
|
+
try {
|
|
3761
|
+
const creds = await runCredentialsList();
|
|
3762
|
+
if (creds.length === 0) {
|
|
3763
|
+
printInfo("No credentials", "found for this server");
|
|
3764
|
+
return;
|
|
3765
|
+
}
|
|
3766
|
+
for (const c of creds) {
|
|
3767
|
+
const roleNames = c.roles.map((r) => r.name ?? r).join(", ");
|
|
3768
|
+
console.log(
|
|
3769
|
+
` ${c.name.padEnd(20)} ${c.client_id.padEnd(35)} roles: ${roleNames}`
|
|
3770
|
+
);
|
|
3771
|
+
}
|
|
3772
|
+
} catch (err) {
|
|
3773
|
+
handleError("credentials list", err);
|
|
3774
|
+
}
|
|
3775
|
+
});
|
|
3776
|
+
credentialsCmd.command("rotate <name>").description("Rotate credential secret (outputs .env to stdout)").action(async (name) => {
|
|
3777
|
+
try {
|
|
3778
|
+
const env = await runCredentialsRotate(name);
|
|
3779
|
+
process.stdout.write(env + "\n");
|
|
3780
|
+
} catch (err) {
|
|
3781
|
+
handleError("credentials rotate", err);
|
|
3782
|
+
}
|
|
3783
|
+
});
|
|
3784
|
+
credentialsCmd.command("revoke <name>").description("Revoke (delete) a credential").action(async (name) => {
|
|
3785
|
+
try {
|
|
3786
|
+
await runCredentialsRevoke(name);
|
|
3787
|
+
} catch (err) {
|
|
3788
|
+
handleError("credentials revoke", err);
|
|
3789
|
+
}
|
|
3790
|
+
});
|
|
2131
3791
|
program.parse();
|
|
2132
3792
|
export {
|
|
2133
3793
|
setTelemetryChannel
|