@neta-art/cohub-cli 1.7.1 → 1.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,21 +2,36 @@ import { randomUUID } from "node:crypto";
2
2
  import { createReadStream } from "node:fs";
3
3
  import { readdir, stat } from "node:fs/promises";
4
4
  import { basename, dirname, relative, resolve, sep } from "node:path";
5
- import { createClient } from "../client.js";
5
+ import { resolveCohubEnvironment } from "@neta-art/cohub";
6
6
  import { uploadAvatarAsset } from "../avatar.js";
7
- import { table, json as outJson, ok, error, handleHttp } from "../output.js";
8
- function requireSpace(program) {
9
- let current = program;
10
- while (current) {
11
- const opts = current.opts();
12
- if (opts.space)
13
- return String(opts.space);
14
- current = current.parent ?? null;
15
- }
16
- return error("Missing required option", "Add -s, --space <id> to target a space");
17
- }
18
- const cliEnv = (process.env.ENV === "prod" ? "prod" : "dev");
7
+ import { createClient } from "../client.js";
8
+ import { table, json as outJson, jsonRequested, ok, error, handleHttp } from "../output.js";
9
+ import { resolveSpace } from "../space.js";
10
+ const cliEnv = resolveCohubEnvironment();
19
11
  const defaultIdleTtlSeconds = cliEnv === "prod" ? 12 * 60 * 60 : 10 * 60;
12
+ const SPACE_ROLES = ["host", "builder", "guest"];
13
+ function parseInteger(value, name, options = {}) {
14
+ if (!/^-?\d+$/.test(value.trim()))
15
+ return error(`Invalid ${name}`, `${name} must be an integer`);
16
+ const parsed = Number.parseInt(value, 10);
17
+ if (!Number.isSafeInteger(parsed))
18
+ return error(`Invalid ${name}`, `${name} must be a safe integer`);
19
+ if (options.min !== undefined && parsed < options.min)
20
+ return error(`Invalid ${name}`, `${name} must be at least ${options.min}`);
21
+ if (options.max !== undefined && parsed > options.max)
22
+ return error(`Invalid ${name}`, `${name} must be at most ${options.max}`);
23
+ return parsed;
24
+ }
25
+ function parseChoice(value, name, choices) {
26
+ if (choices.includes(value))
27
+ return value;
28
+ return error(`Invalid ${name}`, `Use one of: ${choices.join(", ")}`);
29
+ }
30
+ function parseNullableRole(value, name) {
31
+ if (value === undefined || value === "null")
32
+ return null;
33
+ return parseChoice(value, name, SPACE_ROLES);
34
+ }
20
35
  const parseAutoDestroy = (opts) => {
21
36
  const mode = opts.autoDestroy ?? (opts.idleTtl ? "idle" : undefined);
22
37
  if (!mode)
@@ -25,10 +40,7 @@ const parseAutoDestroy = (opts) => {
25
40
  return { mode: "never" };
26
41
  if (mode !== "idle")
27
42
  return error("Invalid auto destroy mode", "Use --auto-destroy idle or --auto-destroy never");
28
- const ttlSeconds = Number.parseInt(opts.idleTtl ?? String(defaultIdleTtlSeconds), 10);
29
- if (!Number.isSafeInteger(ttlSeconds) || ttlSeconds < 60 || ttlSeconds > 30 * 24 * 60 * 60) {
30
- return error("Invalid idle TTL", "--idle-ttl must be an integer between 60 and 2592000 seconds");
31
- }
43
+ const ttlSeconds = parseInteger(opts.idleTtl ?? String(defaultIdleTtlSeconds), "idle TTL", { min: 60, max: 30 * 24 * 60 * 60 });
32
44
  return { mode: "idle", ttlSeconds };
33
45
  };
34
46
  const formatAutoDestroy = (policy) => {
@@ -93,7 +105,7 @@ async function putUploadEntry(entry, uploadUrl, headers) {
93
105
  }
94
106
  }
95
107
  async function uploadFiles(command, paths, opts) {
96
- const spaceId = requireSpace(command);
108
+ const spaceId = resolveSpace(command);
97
109
  const client = createClient();
98
110
  try {
99
111
  const files = await collectUploadFiles(paths);
@@ -117,7 +129,7 @@ async function uploadFiles(command, paths, opts) {
117
129
  const result = await client.space(spaceId).files.completeUpload(plan.uploadId, {
118
130
  entries: plan.entries.map((entry) => ({ id: entry.id })),
119
131
  });
120
- if (opts.json)
132
+ if (jsonRequested(opts))
121
133
  return outJson({ ...result, uploadId: plan.uploadId, files: files.length });
122
134
  ok(`Uploaded ${files.length} file${files.length === 1 ? "" : "s"}`);
123
135
  }
@@ -159,11 +171,11 @@ async function sendPrompt(command, words, opts) {
159
171
  return error("Conflicting schedule", "Use only one of --delay-ms, --at, or --cron");
160
172
  if (opts.cron && !opts.timezone)
161
173
  return error("Missing timezone", "--timezone is required with --cron");
162
- const spaceId = requireSpace(command);
174
+ const spaceId = resolveSpace(command);
163
175
  const client = createClient();
164
176
  try {
165
177
  const schedule = opts.delayMs
166
- ? { mode: "delay", delayMs: Number.parseInt(opts.delayMs, 10) }
178
+ ? { mode: "delay", delayMs: parseInteger(opts.delayMs, "delay", { min: 1 }) }
167
179
  : opts.at
168
180
  ? { mode: "at", sendAt: opts.at }
169
181
  : opts.cron
@@ -177,7 +189,7 @@ async function sendPrompt(command, words, opts) {
177
189
  provider: opts.provider,
178
190
  schedule,
179
191
  });
180
- if (opts.json)
192
+ if (jsonRequested(opts))
181
193
  return outJson(result);
182
194
  if (result.mode === "immediate")
183
195
  return ok(`Prompt sent — sessionId: ${result.sessionId}, turnId: ${result.turnId}`);
@@ -216,7 +228,7 @@ export function registerSpaces(program) {
216
228
  const client = createClient();
217
229
  try {
218
230
  const items = await client.spaces.list();
219
- if (opts.json)
231
+ if (jsonRequested(opts))
220
232
  return outJson(items);
221
233
  table(items, [
222
234
  { key: "id", label: "ID" },
@@ -237,7 +249,7 @@ export function registerSpaces(program) {
237
249
  const client = createClient();
238
250
  try {
239
251
  const space = await client.spaces.get(id);
240
- if (opts.json)
252
+ if (jsonRequested(opts))
241
253
  return outJson(space);
242
254
  table([space], [
243
255
  { key: "id", label: "ID" },
@@ -269,7 +281,7 @@ export function registerSpaces(program) {
269
281
  description: opts.description,
270
282
  ...(autoDestroy ? { config: { sandbox: { autoDestroy } } } : {}),
271
283
  });
272
- if (opts.json)
284
+ if (jsonRequested(opts))
273
285
  return outJson(result);
274
286
  ok(`Space created: ${result.space.id}`);
275
287
  table([result.space], [
@@ -302,12 +314,12 @@ export function registerSpaces(program) {
302
314
  .description("Upload the space avatar")
303
315
  .option("--json", "Output as JSON")
304
316
  .action(async (path, opts) => {
305
- const spaceId = requireSpace(spacesCmd);
317
+ const spaceId = resolveSpace(spacesCmd);
306
318
  const client = createClient();
307
319
  try {
308
320
  const asset = await uploadAvatarAsset({ client, purpose: "space_avatar", spaceId, path });
309
321
  const result = await client.space(spaceId).profile({ avatarUrl: asset.publicUrl });
310
- if (opts.json)
322
+ if (jsonRequested(opts))
311
323
  return outJson({ ...result, asset });
312
324
  ok("Space avatar updated");
313
325
  }
@@ -328,13 +340,13 @@ export function registerSpaces(program) {
328
340
  const autoDestroy = parseAutoDestroy(opts);
329
341
  if (autoDestroy) {
330
342
  const result = await client.space(id).updateConfig({ sandbox: { autoDestroy } });
331
- if (opts.json)
343
+ if (jsonRequested(opts))
332
344
  return outJson(result);
333
345
  ok(`Space config updated — sandbox auto destroy: ${formatAutoDestroy(autoDestroy)}`);
334
346
  return;
335
347
  }
336
348
  const result = await client.space(id).getConfig();
337
- if (opts.json)
349
+ if (jsonRequested(opts))
338
350
  return outJson(result);
339
351
  table([{ key: "sandbox.autoDestroy", value: formatAutoDestroy(result.config.sandbox.autoDestroy) }], [
340
352
  { key: "key", label: "Key" },
@@ -378,11 +390,11 @@ export function registerSpaces(program) {
378
390
  .description("Space usage statistics (default: 30 days)")
379
391
  .option("--json", "Output as JSON")
380
392
  .action(async (days, opts) => {
381
- const spaceId = requireSpace(spacesCmd);
393
+ const spaceId = resolveSpace(spacesCmd);
382
394
  const client = createClient();
383
395
  try {
384
- const usage = await client.space(spaceId).usage.get(Number.parseInt(days ?? "30", 10));
385
- if (opts.json)
396
+ const usage = await client.space(spaceId).usage.get(parseInteger(days ?? "30", "days", { min: 1 }));
397
+ if (jsonRequested(opts))
386
398
  return outJson(usage);
387
399
  console.log("\n Summary:");
388
400
  table([usage.summary], [
@@ -402,18 +414,18 @@ function registerMods(spacesCmd) {
402
414
  const modsCmd = spacesCmd
403
415
  .command("mods")
404
416
  .description("Manage space mods")
405
- .hook("preAction", () => { requireSpace(spacesCmd); });
417
+ .hook("preAction", () => { resolveSpace(spacesCmd); });
406
418
  modsCmd
407
419
  .command("ls")
408
420
  .alias("list")
409
421
  .description("List mods")
410
422
  .option("--json", "Output as JSON")
411
423
  .action(async (opts) => {
412
- const spaceId = requireSpace(spacesCmd);
424
+ const spaceId = resolveSpace(spacesCmd);
413
425
  const client = createClient();
414
426
  try {
415
427
  const result = await client.space(spaceId).mods.list();
416
- if (opts.json)
428
+ if (jsonRequested(opts))
417
429
  return outJson(result.items);
418
430
  table(result.items, [
419
431
  { key: "id", label: "ID" },
@@ -435,11 +447,11 @@ function registerMods(spacesCmd) {
435
447
  .option("--json", "Output as JSON")
436
448
  .action(async (modSpaceId, opts) => {
437
449
  await confirmRestart(opts);
438
- const spaceId = requireSpace(spacesCmd);
450
+ const spaceId = resolveSpace(spacesCmd);
439
451
  const client = createClient();
440
452
  try {
441
453
  const result = await client.space(spaceId).mods.create({ modSpaceId, name: opts.name, mountSlug: opts.slug });
442
- if (opts.json)
454
+ if (jsonRequested(opts))
443
455
  return outJson(result);
444
456
  ok(`Mod added — ${result.item.mountPath}; sandbox restarting`);
445
457
  }
@@ -454,11 +466,11 @@ function registerMods(spacesCmd) {
454
466
  .option("--json", "Output as JSON")
455
467
  .action(async (modId, opts) => {
456
468
  await confirmRestart(opts);
457
- const spaceId = requireSpace(spacesCmd);
469
+ const spaceId = resolveSpace(spacesCmd);
458
470
  const client = createClient();
459
471
  try {
460
472
  const result = await client.space(spaceId).mods.update(modId, { enabled: true });
461
- if (opts.json)
473
+ if (jsonRequested(opts))
462
474
  return outJson(result);
463
475
  ok("Mod enabled; sandbox restarting");
464
476
  }
@@ -473,11 +485,11 @@ function registerMods(spacesCmd) {
473
485
  .option("--json", "Output as JSON")
474
486
  .action(async (modId, opts) => {
475
487
  await confirmRestart(opts);
476
- const spaceId = requireSpace(spacesCmd);
488
+ const spaceId = resolveSpace(spacesCmd);
477
489
  const client = createClient();
478
490
  try {
479
491
  const result = await client.space(spaceId).mods.update(modId, { enabled: false });
480
- if (opts.json)
492
+ if (jsonRequested(opts))
481
493
  return outJson(result);
482
494
  ok("Mod disabled; sandbox restarting");
483
495
  }
@@ -493,11 +505,11 @@ function registerMods(spacesCmd) {
493
505
  .option("--json", "Output as JSON")
494
506
  .action(async (modId, opts) => {
495
507
  await confirmRestart(opts);
496
- const spaceId = requireSpace(spacesCmd);
508
+ const spaceId = resolveSpace(spacesCmd);
497
509
  const client = createClient();
498
510
  try {
499
511
  const result = await client.space(spaceId).mods.remove(modId);
500
- if (opts.json)
512
+ if (jsonRequested(opts))
501
513
  return outJson(result);
502
514
  ok("Mod removed; sandbox restarting");
503
515
  }
@@ -511,18 +523,18 @@ function registerFiles(spacesCmd) {
511
523
  const filesCmd = spacesCmd
512
524
  .command("files")
513
525
  .description("File operations")
514
- .hook("preAction", () => { requireSpace(spacesCmd); });
526
+ .hook("preAction", () => { resolveSpace(spacesCmd); });
515
527
  filesCmd
516
528
  .command("ls [path]")
517
529
  .alias("list")
518
530
  .description("List directory tree")
519
531
  .option("--json", "Output as JSON")
520
532
  .action(async (path, opts) => {
521
- const spaceId = requireSpace(spacesCmd);
533
+ const spaceId = resolveSpace(spacesCmd);
522
534
  const client = createClient();
523
535
  try {
524
536
  const tree = await client.space(spaceId).files.list(path ?? "");
525
- if (opts.json)
537
+ if (jsonRequested(opts))
526
538
  return outJson(tree);
527
539
  if (tree.entries.length === 0) {
528
540
  console.log(" (empty)");
@@ -543,7 +555,7 @@ function registerFiles(spacesCmd) {
543
555
  .command("cat <path>")
544
556
  .description("Read file content")
545
557
  .action(async (path) => {
546
- const spaceId = requireSpace(spacesCmd);
558
+ const spaceId = resolveSpace(spacesCmd);
547
559
  const client = createClient();
548
560
  try {
549
561
  const file = await client.space(spaceId).files.read(path);
@@ -573,7 +585,7 @@ function registerFiles(spacesCmd) {
573
585
  }
574
586
  if (!content)
575
587
  return error("No content provided", "Use -c or pipe via stdin");
576
- const spaceId = requireSpace(spacesCmd);
588
+ const spaceId = resolveSpace(spacesCmd);
577
589
  const client = createClient();
578
590
  try {
579
591
  const result = await client.space(spaceId).files.write({
@@ -597,7 +609,7 @@ function registerFiles(spacesCmd) {
597
609
  .command("mkdir <path>")
598
610
  .description("Create a directory")
599
611
  .action(async (path) => {
600
- const spaceId = requireSpace(spacesCmd);
612
+ const spaceId = resolveSpace(spacesCmd);
601
613
  const client = createClient();
602
614
  try {
603
615
  await client.space(spaceId).files.createDir(path);
@@ -612,7 +624,7 @@ function registerFiles(spacesCmd) {
612
624
  .description("Delete a file or directory")
613
625
  .option("-r, --recursive", "Delete recursively")
614
626
  .action(async (path, opts) => {
615
- const spaceId = requireSpace(spacesCmd);
627
+ const spaceId = resolveSpace(spacesCmd);
616
628
  const client = createClient();
617
629
  try {
618
630
  await client.space(spaceId).files.delete(path, opts.recursive ?? false);
@@ -626,7 +638,7 @@ function registerFiles(spacesCmd) {
626
638
  .command("mv <from> <to>")
627
639
  .description("Move or rename")
628
640
  .action(async (from, to) => {
629
- const spaceId = requireSpace(spacesCmd);
641
+ const spaceId = resolveSpace(spacesCmd);
630
642
  const client = createClient();
631
643
  try {
632
644
  await client.space(spaceId).files.move({ fromPath: from, toPath: to });
@@ -642,18 +654,18 @@ function registerSessions(spacesCmd) {
642
654
  const sessionsCmd = spacesCmd
643
655
  .command("sessions")
644
656
  .description("Browse sessions and turns")
645
- .hook("preAction", () => { requireSpace(spacesCmd); });
657
+ .hook("preAction", () => { resolveSpace(spacesCmd); });
646
658
  sessionsCmd
647
659
  .command("ls")
648
660
  .alias("list")
649
661
  .description("List sessions")
650
662
  .option("--json", "Output as JSON")
651
663
  .action(async (opts) => {
652
- const spaceId = requireSpace(spacesCmd);
664
+ const spaceId = resolveSpace(spacesCmd);
653
665
  const client = createClient();
654
666
  try {
655
667
  const result = await client.space(spaceId).sessions.list();
656
- if (opts.json)
668
+ if (jsonRequested(opts))
657
669
  return outJson(result);
658
670
  if (result.sessions.length === 0) {
659
671
  console.log(" (empty)");
@@ -675,11 +687,11 @@ function registerSessions(spacesCmd) {
675
687
  .description("Create a session")
676
688
  .option("--json", "Output as JSON")
677
689
  .action(async (title, opts) => {
678
- const spaceId = requireSpace(spacesCmd);
690
+ const spaceId = resolveSpace(spacesCmd);
679
691
  const client = createClient();
680
692
  try {
681
693
  const result = await client.space(spaceId).sessions.create({ title });
682
- if (opts.json)
694
+ if (jsonRequested(opts))
683
695
  return outJson(result);
684
696
  ok(`Session created: ${result.session.id}`);
685
697
  table([result.session], [
@@ -696,11 +708,11 @@ function registerSessions(spacesCmd) {
696
708
  .description("Session details")
697
709
  .option("--json", "Output as JSON")
698
710
  .action(async (id, opts) => {
699
- const spaceId = requireSpace(spacesCmd);
711
+ const spaceId = resolveSpace(spacesCmd);
700
712
  const client = createClient();
701
713
  try {
702
714
  const result = await client.space(spaceId).session(id).get();
703
- if (opts.json)
715
+ if (jsonRequested(opts))
704
716
  return outJson(result);
705
717
  table([result.session], [
706
718
  { key: "id", label: "ID" },
@@ -718,7 +730,7 @@ function registerSessions(spacesCmd) {
718
730
  .command("rename <id> <name>")
719
731
  .description("Rename a session")
720
732
  .action(async (id, name) => {
721
- const spaceId = requireSpace(spacesCmd);
733
+ const spaceId = resolveSpace(spacesCmd);
722
734
  const client = createClient();
723
735
  try {
724
736
  await client.space(spaceId).session(id).rename(name);
@@ -734,13 +746,13 @@ function registerSessions(spacesCmd) {
734
746
  .description("Stream realtime session events")
735
747
  .option("--json", "Output as JSON")
736
748
  .action(async (id, opts) => {
737
- const spaceId = requireSpace(spacesCmd);
749
+ const spaceId = resolveSpace(spacesCmd);
738
750
  const client = createClient();
739
751
  const session = client.space(spaceId).session(id);
740
752
  process.stdout.write(" Listening for events...\n\n");
741
753
  let lastAppendPath = null;
742
754
  session.on("turn.patch", (e) => {
743
- if (opts.json) {
755
+ if (jsonRequested(opts)) {
744
756
  console.log(JSON.stringify(e));
745
757
  }
746
758
  else {
@@ -767,7 +779,7 @@ function registerSessions(spacesCmd) {
767
779
  });
768
780
  session.on("turn.error", (e) => {
769
781
  process.stderr.write(`\n ✗ Error\n`);
770
- if (opts.json)
782
+ if (jsonRequested(opts))
771
783
  process.stderr.write(`${JSON.stringify(e)}\n`);
772
784
  process.exit(1);
773
785
  });
@@ -789,15 +801,15 @@ function registerTurns(sessionsCmd) {
789
801
  .option("--limit <n>", "Page size", "30")
790
802
  .option("--json", "Output as JSON")
791
803
  .action(async (sessionId, opts) => {
792
- const spaceId = requireSpace(sessionsCmd);
804
+ const spaceId = resolveSpace(sessionsCmd);
793
805
  const client = createClient();
794
806
  try {
795
807
  const result = await client.space(spaceId).session(sessionId).turns.listPaginated({
796
- cursor: opts.cursor === undefined ? undefined : Number.parseInt(opts.cursor, 10),
797
- direction: opts.direction,
798
- limit: Number.parseInt(opts.limit ?? "30", 10),
808
+ cursor: opts.cursor === undefined ? undefined : parseInteger(opts.cursor, "cursor", { min: 0 }),
809
+ direction: parseChoice(opts.direction ?? "older", "direction", ["older", "newer"]),
810
+ limit: parseInteger(opts.limit ?? "30", "limit", { min: 1, max: 100 }),
799
811
  });
800
- if (opts.json)
812
+ if (jsonRequested(opts))
801
813
  return outJson(result);
802
814
  if (result.turns.length === 0)
803
815
  return console.log(" No turns found");
@@ -821,11 +833,11 @@ function registerTurns(sessionsCmd) {
821
833
  .description("Show turn details")
822
834
  .option("--json", "Output as JSON")
823
835
  .action(async (sessionId, turnId, opts) => {
824
- const spaceId = requireSpace(sessionsCmd);
836
+ const spaceId = resolveSpace(sessionsCmd);
825
837
  const client = createClient();
826
838
  try {
827
839
  const result = await client.space(spaceId).session(sessionId).turns.get(turnId);
828
- if (opts.json)
840
+ if (jsonRequested(opts))
829
841
  return outJson(result);
830
842
  table([result.turn], [
831
843
  { key: "sequence", label: "Seq" },
@@ -852,14 +864,14 @@ function registerTurns(sessionsCmd) {
852
864
  .option("--limit <n>", "Page size", "100")
853
865
  .option("--json", "Output as JSON")
854
866
  .action(async (sessionId, opts) => {
855
- const spaceId = requireSpace(sessionsCmd);
867
+ const spaceId = resolveSpace(sessionsCmd);
856
868
  const client = createClient();
857
869
  try {
858
870
  const result = await client.space(spaceId).session(sessionId).turns.index({
859
- cursor: opts.cursor === undefined ? undefined : Number.parseInt(opts.cursor, 10),
860
- limit: Number.parseInt(opts.limit ?? "100", 10),
871
+ cursor: opts.cursor === undefined ? undefined : parseInteger(opts.cursor, "cursor", { min: 0 }),
872
+ limit: parseInteger(opts.limit ?? "100", "limit", { min: 1, max: 500 }),
861
873
  });
862
- if (opts.json)
874
+ if (jsonRequested(opts))
863
875
  return outJson(result);
864
876
  if (result.turns.length === 0)
865
877
  return console.log(" No turns found");
@@ -886,18 +898,18 @@ function registerTurns(sessionsCmd) {
886
898
  .option("--after <n>", "Turns after anchor", "20")
887
899
  .option("--json", "Output as JSON")
888
900
  .action(async (sessionId, opts) => {
889
- const spaceId = requireSpace(sessionsCmd);
901
+ const spaceId = resolveSpace(sessionsCmd);
890
902
  if (!opts.sequence && !opts.turn)
891
903
  return error("Missing anchor", "Use --sequence <n> or --turn <id>");
892
904
  const client = createClient();
893
905
  try {
894
906
  const result = await client.space(spaceId).session(sessionId).turns.window({
895
- sequence: opts.sequence === undefined ? undefined : Number.parseInt(opts.sequence, 10),
907
+ sequence: opts.sequence === undefined ? undefined : parseInteger(opts.sequence, "sequence", { min: 0 }),
896
908
  turnId: opts.turn,
897
- before: Number.parseInt(opts.before ?? "10", 10),
898
- after: Number.parseInt(opts.after ?? "20", 10),
909
+ before: parseInteger(opts.before ?? "10", "before", { min: 0, max: 200 }),
910
+ after: parseInteger(opts.after ?? "20", "after", { min: 0, max: 200 }),
899
911
  });
900
- if (opts.json)
912
+ if (jsonRequested(opts))
901
913
  return outJson(result);
902
914
  if (result.turns.length === 0)
903
915
  return console.log(" No turns found");
@@ -926,7 +938,7 @@ function registerSessionAccess(sessionsCmd) {
926
938
  const client = createClient();
927
939
  try {
928
940
  const policy = await client.sessionAccess.get(id);
929
- if (opts.json)
941
+ if (jsonRequested(opts))
930
942
  return outJson(policy);
931
943
  table([policy], [
932
944
  { key: "signed_in_user", label: "Signed-in" },
@@ -946,9 +958,9 @@ function registerSessionAccess(sessionsCmd) {
946
958
  const client = createClient();
947
959
  try {
948
960
  const policy = await client.sessionAccess.set(id, {
949
- anonymous_user: (opts.anonymous ?? null),
961
+ anonymous_user: parseNullableRole(opts.anonymous, "anonymous role"),
950
962
  });
951
- if (opts.json)
963
+ if (jsonRequested(opts))
952
964
  return outJson(policy);
953
965
  ok("Session access updated");
954
966
  table([policy], [
@@ -979,18 +991,18 @@ function registerMembers(spacesCmd) {
979
991
  const memCmd = spacesCmd
980
992
  .command("members")
981
993
  .description("Member management")
982
- .hook("preAction", () => { requireSpace(spacesCmd); });
994
+ .hook("preAction", () => { resolveSpace(spacesCmd); });
983
995
  memCmd
984
996
  .command("ls")
985
997
  .alias("list")
986
998
  .description("List space members")
987
999
  .option("--json", "Output as JSON")
988
1000
  .action(async (opts) => {
989
- const spaceId = requireSpace(spacesCmd);
1001
+ const spaceId = resolveSpace(spacesCmd);
990
1002
  const client = createClient();
991
1003
  try {
992
1004
  const result = await client.space(spaceId).members.list();
993
- if (opts.json)
1005
+ if (jsonRequested(opts))
994
1006
  return outJson(result);
995
1007
  if (result.items.length === 0) {
996
1008
  console.log(" (empty)");
@@ -1010,10 +1022,10 @@ function registerMembers(spacesCmd) {
1010
1022
  .command("update <userId> <role>")
1011
1023
  .description("Change member role (host | builder | guest)")
1012
1024
  .action(async (userId, role) => {
1013
- const spaceId = requireSpace(spacesCmd);
1025
+ const spaceId = resolveSpace(spacesCmd);
1014
1026
  const client = createClient();
1015
1027
  try {
1016
- await client.space(spaceId).members.update(userId, role);
1028
+ await client.space(spaceId).members.update(userId, parseChoice(role, "role", SPACE_ROLES));
1017
1029
  ok(`${userId} → ${role}`);
1018
1030
  }
1019
1031
  catch (e) {
@@ -1024,7 +1036,7 @@ function registerMembers(spacesCmd) {
1024
1036
  .command("remove <userId>")
1025
1037
  .description("Remove a member")
1026
1038
  .action(async (userId) => {
1027
- const spaceId = requireSpace(spacesCmd);
1039
+ const spaceId = resolveSpace(spacesCmd);
1028
1040
  const client = createClient();
1029
1041
  try {
1030
1042
  await client.space(spaceId).members.remove(userId);
@@ -1040,17 +1052,17 @@ function registerAccess(spacesCmd) {
1040
1052
  const accCmd = spacesCmd
1041
1053
  .command("access")
1042
1054
  .description("Access control")
1043
- .hook("preAction", () => { requireSpace(spacesCmd); });
1055
+ .hook("preAction", () => { resolveSpace(spacesCmd); });
1044
1056
  accCmd
1045
1057
  .command("get")
1046
1058
  .description("Get access policy")
1047
1059
  .option("--json", "Output as JSON")
1048
1060
  .action(async (opts) => {
1049
- const spaceId = requireSpace(spacesCmd);
1061
+ const spaceId = resolveSpace(spacesCmd);
1050
1062
  const client = createClient();
1051
1063
  try {
1052
1064
  const policy = await client.space(spaceId).access.get();
1053
- if (opts.json)
1065
+ if (jsonRequested(opts))
1054
1066
  return outJson(policy);
1055
1067
  table([policy], [
1056
1068
  { key: "signed_in_user", label: "Signed-in" },
@@ -1068,14 +1080,14 @@ function registerAccess(spacesCmd) {
1068
1080
  .option("--anonymous <role>", "Role for anonymous users (host|builder|guest|null)")
1069
1081
  .option("--json", "Output as JSON")
1070
1082
  .action(async (opts) => {
1071
- const spaceId = requireSpace(spacesCmd);
1083
+ const spaceId = resolveSpace(spacesCmd);
1072
1084
  const client = createClient();
1073
1085
  try {
1074
1086
  const policy = await client.space(spaceId).access.set({
1075
- signed_in_user: (opts.signedIn ?? null),
1076
- anonymous_user: (opts.anonymous ?? null),
1087
+ signed_in_user: parseNullableRole(opts.signedIn, "signed-in role"),
1088
+ anonymous_user: parseNullableRole(opts.anonymous, "anonymous role"),
1077
1089
  });
1078
- if (opts.json)
1090
+ if (jsonRequested(opts))
1079
1091
  return outJson(policy);
1080
1092
  ok("Access policy updated");
1081
1093
  table([policy], [
@@ -1093,18 +1105,18 @@ function registerCheckpoints(spacesCmd) {
1093
1105
  const cpCmd = spacesCmd
1094
1106
  .command("checkpoints")
1095
1107
  .description("Checkpoint management")
1096
- .hook("preAction", () => { requireSpace(spacesCmd); });
1108
+ .hook("preAction", () => { resolveSpace(spacesCmd); });
1097
1109
  cpCmd
1098
1110
  .command("ls")
1099
1111
  .alias("list")
1100
1112
  .description("List checkpoints")
1101
1113
  .option("--json", "Output as JSON")
1102
1114
  .action(async (opts) => {
1103
- const spaceId = requireSpace(spacesCmd);
1115
+ const spaceId = resolveSpace(spacesCmd);
1104
1116
  const client = createClient();
1105
1117
  try {
1106
1118
  const result = await client.space(spaceId).checkpoints.list();
1107
- if (opts.json)
1119
+ if (jsonRequested(opts))
1108
1120
  return outJson(result);
1109
1121
  if (result.checkpoints.length === 0) {
1110
1122
  console.log(" (empty)");
@@ -1126,11 +1138,11 @@ function registerCheckpoints(spacesCmd) {
1126
1138
  .description("Checkpoint details")
1127
1139
  .option("--json", "Output as JSON")
1128
1140
  .action(async (id, opts) => {
1129
- const spaceId = requireSpace(spacesCmd);
1141
+ const spaceId = resolveSpace(spacesCmd);
1130
1142
  const client = createClient();
1131
1143
  try {
1132
1144
  const result = await client.space(spaceId).checkpoints.get(id);
1133
- if (opts.json)
1145
+ if (jsonRequested(opts))
1134
1146
  return outJson(result);
1135
1147
  table([result.checkpoint], [
1136
1148
  { key: "id", label: "ID" },
@@ -1149,11 +1161,11 @@ function registerCheckpoints(spacesCmd) {
1149
1161
  .description("Create a checkpoint")
1150
1162
  .option("--json", "Output as JSON")
1151
1163
  .action(async (description, opts) => {
1152
- const spaceId = requireSpace(spacesCmd);
1164
+ const spaceId = resolveSpace(spacesCmd);
1153
1165
  const client = createClient();
1154
1166
  try {
1155
1167
  const result = await client.space(spaceId).checkpoints.create(description ?? null);
1156
- if (opts.json)
1168
+ if (jsonRequested(opts))
1157
1169
  return outJson(result);
1158
1170
  ok(`Checkpoint created — taskRunId: ${result.taskRunId}`);
1159
1171
  }
@@ -1,5 +1,5 @@
1
1
  import { createClient } from "../client.js";
2
- import { table, json as outJson, handleHttp } from "../output.js";
2
+ import { table, json as outJson, jsonRequested, handleHttp } from "../output.js";
3
3
  export function registerTasks(program) {
4
4
  const cmd = program.command("tasks", { hidden: true }).description("Task runs");
5
5
  cmd
@@ -18,7 +18,7 @@ export function registerTasks(program) {
18
18
  if (opts.space)
19
19
  filters.spaceId = opts.space;
20
20
  const result = await client.tasks.list(filters);
21
- if (opts.json)
21
+ if (jsonRequested(opts))
22
22
  return outJson(result);
23
23
  if (result.runs.length === 0)
24
24
  return console.log(" (empty)");
@@ -41,7 +41,7 @@ export function registerTasks(program) {
41
41
  const client = createClient();
42
42
  try {
43
43
  const result = await client.tasks.get(id);
44
- if (opts.json)
44
+ if (jsonRequested(opts))
45
45
  return outJson(result);
46
46
  table([result.run], [
47
47
  { key: "id", label: "ID" },
package/dist/output.d.ts CHANGED
@@ -4,10 +4,14 @@ export declare function table(rows: Row[], columns: {
4
4
  label: string;
5
5
  }[]): void;
6
6
  export declare function json(data: unknown): void;
7
+ export declare function jsonRequested(opts?: {
8
+ json?: boolean;
9
+ }): boolean;
7
10
  export declare function ok(msg: string): void;
8
11
  export declare function error(msg: string, detail?: string): never;
9
12
  export declare function handleHttp(e: unknown): never;
10
13
  export declare function spinner(): {
11
14
  start(msg: string): void;
15
+ update(msg: string): void;
12
16
  stop(msg: string): void;
13
17
  };