aicomputer 0.1.6 → 0.1.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +1233 -213
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -1,16 +1,15 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/index.ts
4
- import { Command as Command9 } from "commander";
5
- import chalk8 from "chalk";
4
+ import { Command as Command11 } from "commander";
5
+ import chalk11 from "chalk";
6
6
  import { readFileSync as readFileSync3 } from "fs";
7
7
  import { basename as basename2 } from "path";
8
8
 
9
9
  // src/commands/access.ts
10
10
  import { spawn } from "child_process";
11
- import { select } from "@inquirer/prompts";
12
11
  import { Command } from "commander";
13
- import chalk2 from "chalk";
12
+ import chalk3 from "chalk";
14
13
  import ora from "ora";
15
14
 
16
15
  // src/lib/config.ts
@@ -159,6 +158,11 @@ async function createComputer(input) {
159
158
  body: JSON.stringify(input)
160
159
  });
161
160
  }
161
+ async function deleteComputer(computerID) {
162
+ return api(`/v1/computers/${computerID}`, {
163
+ method: "DELETE"
164
+ });
165
+ }
162
166
  async function getFilesystemSettings() {
163
167
  return api("/v1/me/filesystem");
164
168
  }
@@ -236,6 +240,79 @@ function normalizePrimaryPath(primaryPath) {
236
240
  return trimmed.startsWith("/") ? trimmed : `/${trimmed}`;
237
241
  }
238
242
 
243
+ // src/lib/computer-picker.ts
244
+ import { select } from "@inquirer/prompts";
245
+ import chalk2 from "chalk";
246
+
247
+ // src/lib/format.ts
248
+ import chalk from "chalk";
249
+ function padEnd(str, len) {
250
+ const visible = str.replace(/\u001b\[[0-9;]*m/g, "");
251
+ return str + " ".repeat(Math.max(0, len - visible.length));
252
+ }
253
+ function timeAgo(dateStr) {
254
+ const seconds = Math.floor((Date.now() - new Date(dateStr).getTime()) / 1e3);
255
+ if (seconds < 60) return `${seconds}s ago`;
256
+ const minutes = Math.floor(seconds / 60);
257
+ if (minutes < 60) return `${minutes}m ago`;
258
+ const hours = Math.floor(minutes / 60);
259
+ if (hours < 24) return `${hours}h ago`;
260
+ const days = Math.floor(hours / 24);
261
+ return `${days}d ago`;
262
+ }
263
+ function formatStatus(status) {
264
+ switch (status) {
265
+ case "running":
266
+ return chalk.green(status);
267
+ case "pending":
268
+ case "provisioning":
269
+ case "starting":
270
+ return chalk.yellow(status);
271
+ case "stopping":
272
+ case "stopped":
273
+ case "deleted":
274
+ return chalk.gray(status);
275
+ case "error":
276
+ return chalk.red(status);
277
+ default:
278
+ return status;
279
+ }
280
+ }
281
+
282
+ // src/lib/computer-picker.ts
283
+ async function promptForSSHComputer(computers, message) {
284
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
285
+ throw new Error("computer id or handle is required when not running interactively");
286
+ }
287
+ const available = computers.filter(isSSHSelectable);
288
+ if (available.length === 0) {
289
+ if (computers.length === 0) {
290
+ throw new Error("no computers found");
291
+ }
292
+ throw new Error("no running computers with SSH enabled");
293
+ }
294
+ const handleWidth = Math.max(6, ...available.map((computer) => computer.handle.length));
295
+ const selectedID = await select({
296
+ message,
297
+ pageSize: Math.min(available.length, 10),
298
+ choices: available.map((computer) => ({
299
+ name: `${padEnd(chalk2.white(computer.handle), handleWidth + 2)}${padEnd(formatStatus(computer.status), 12)}${chalk2.dim(describeSSHChoice(computer))}`,
300
+ value: computer.id
301
+ }))
302
+ });
303
+ return available.find((computer) => computer.id === selectedID) ?? available[0];
304
+ }
305
+ function isSSHSelectable(computer) {
306
+ return computer.ssh_enabled && computer.status === "running";
307
+ }
308
+ function describeSSHChoice(computer) {
309
+ const displayName = computer.display_name.trim();
310
+ if (displayName && displayName !== computer.handle) {
311
+ return `${displayName} ${timeAgo(computer.updated_at)}`;
312
+ }
313
+ return `${computer.runtime_family} ${timeAgo(computer.updated_at)}`;
314
+ }
315
+
239
316
  // src/lib/ssh-config.ts
240
317
  import { homedir as homedir2 } from "os";
241
318
  import { join as join2 } from "path";
@@ -370,41 +447,6 @@ async function generateSSHKey() {
370
447
  return { publicKeyPath, privateKeyPath };
371
448
  }
372
449
 
373
- // src/lib/format.ts
374
- import chalk from "chalk";
375
- function padEnd(str, len) {
376
- const visible = str.replace(/\u001b\[[0-9;]*m/g, "");
377
- return str + " ".repeat(Math.max(0, len - visible.length));
378
- }
379
- function timeAgo(dateStr) {
380
- const seconds = Math.floor((Date.now() - new Date(dateStr).getTime()) / 1e3);
381
- if (seconds < 60) return `${seconds}s ago`;
382
- const minutes = Math.floor(seconds / 60);
383
- if (minutes < 60) return `${minutes}m ago`;
384
- const hours = Math.floor(minutes / 60);
385
- if (hours < 24) return `${hours}h ago`;
386
- const days = Math.floor(hours / 24);
387
- return `${days}d ago`;
388
- }
389
- function formatStatus(status) {
390
- switch (status) {
391
- case "running":
392
- return chalk.green(status);
393
- case "pending":
394
- case "provisioning":
395
- case "starting":
396
- return chalk.yellow(status);
397
- case "stopping":
398
- case "stopped":
399
- case "deleted":
400
- return chalk.gray(status);
401
- case "error":
402
- return chalk.red(status);
403
- default:
404
- return status;
405
- }
406
- }
407
-
408
450
  // src/lib/open-browser.ts
409
451
  import { constants } from "fs";
410
452
  import { access } from "fs/promises";
@@ -466,9 +508,9 @@ var openCommand = new Command("open").description("Open a computer in your brows
466
508
  throw new Error("VNC is not available for this computer");
467
509
  }
468
510
  const url = info.connection.vnc_url;
469
- spinner.succeed(`Opening VNC for ${chalk2.bold(computer.handle)}`);
511
+ spinner.succeed(`Opening VNC for ${chalk3.bold(computer.handle)}`);
470
512
  await openBrowserURL(url);
471
- console.log(chalk2.dim(` ${url}`));
513
+ console.log(chalk3.dim(` ${url}`));
472
514
  return;
473
515
  }
474
516
  if (options.terminal) {
@@ -476,15 +518,15 @@ var openCommand = new Command("open").description("Open a computer in your brows
476
518
  throw new Error("Terminal access is not available for this computer");
477
519
  }
478
520
  const access3 = await createTerminalAccess(computer.id);
479
- spinner.succeed(`Opening terminal for ${chalk2.bold(computer.handle)}`);
521
+ spinner.succeed(`Opening terminal for ${chalk3.bold(computer.handle)}`);
480
522
  await openBrowserURL(access3.access_url);
481
- console.log(chalk2.dim(` ${access3.access_url}`));
523
+ console.log(chalk3.dim(` ${access3.access_url}`));
482
524
  return;
483
525
  }
484
526
  const access2 = await createBrowserAccess(computer.id);
485
- spinner.succeed(`Opening ${chalk2.bold(computer.handle)}`);
527
+ spinner.succeed(`Opening ${chalk3.bold(computer.handle)}`);
486
528
  await openBrowserURL(access2.access_url);
487
- console.log(chalk2.dim(` ${access2.access_url}`));
529
+ console.log(chalk3.dim(` ${access2.access_url}`));
488
530
  } catch (error) {
489
531
  spinner.fail(error instanceof Error ? error.message : "Failed to open computer");
490
532
  process.exit(1);
@@ -505,8 +547,8 @@ var sshCommand = new Command("ssh").description("Open an SSH session to a comput
505
547
  throw new Error("SSH is not available for this computer");
506
548
  }
507
549
  const registered = await ensureDefaultSSHKeyRegistered();
508
- spinner.succeed(`Connecting to ${chalk2.bold(computer.handle)}`);
509
- console.log(chalk2.dim(` ${formatSSHCommand(info.connection.ssh_user, info.connection.ssh_host, info.connection.ssh_port)}`));
550
+ spinner.succeed(`Connecting to ${chalk3.bold(computer.handle)}`);
551
+ console.log(chalk3.dim(` ${formatSSHCommand(info.connection.ssh_user, info.connection.ssh_host, info.connection.ssh_port)}`));
510
552
  console.log();
511
553
  await runSSH([
512
554
  "-i",
@@ -529,17 +571,17 @@ portsCommand.command("ls").description("List published ports for a computer").ar
529
571
  spinner.stop();
530
572
  if (ports.length === 0) {
531
573
  console.log();
532
- console.log(chalk2.dim(" No published ports."));
574
+ console.log(chalk3.dim(" No published ports."));
533
575
  console.log();
534
576
  return;
535
577
  }
536
578
  const subWidth = Math.max(10, ...ports.map((p) => p.subdomain.length));
537
579
  console.log();
538
580
  console.log(
539
- ` ${chalk2.dim(padEnd("Subdomain", subWidth + 2))}${chalk2.dim(padEnd("Port", 8))}${chalk2.dim("Protocol")}`
581
+ ` ${chalk3.dim(padEnd("Subdomain", subWidth + 2))}${chalk3.dim(padEnd("Port", 8))}${chalk3.dim("Protocol")}`
540
582
  );
541
583
  console.log(
542
- ` ${chalk2.dim("-".repeat(subWidth + 2))}${chalk2.dim("-".repeat(8))}${chalk2.dim("-".repeat(8))}`
584
+ ` ${chalk3.dim("-".repeat(subWidth + 2))}${chalk3.dim("-".repeat(8))}${chalk3.dim("-".repeat(8))}`
543
585
  );
544
586
  for (const port of ports) {
545
587
  const url = `https://${port.subdomain}--${computer.handle}.computer.agentcomputer.ai`;
@@ -547,7 +589,7 @@ portsCommand.command("ls").description("List published ports for a computer").ar
547
589
  ` ${padEnd(port.subdomain, subWidth + 2)}${padEnd(String(port.target_port), 8)}${port.protocol}`
548
590
  );
549
591
  console.log(
550
- ` ${chalk2.dim(url)}`
592
+ ` ${chalk3.dim(url)}`
551
593
  );
552
594
  }
553
595
  console.log();
@@ -570,8 +612,8 @@ portsCommand.command("publish").description("Publish an HTTP app port").argument
570
612
  protocol: options.protocol
571
613
  });
572
614
  const url = `https://${published.subdomain}--${computer.handle}.computer.agentcomputer.ai`;
573
- spinner.succeed(`Published port ${chalk2.bold(String(published.target_port))}`);
574
- console.log(chalk2.dim(` ${url}`));
615
+ spinner.succeed(`Published port ${chalk3.bold(String(published.target_port))}`);
616
+ console.log(chalk3.dim(` ${url}`));
575
617
  } catch (error) {
576
618
  spinner.fail(error instanceof Error ? error.message : "Failed to publish port");
577
619
  process.exit(1);
@@ -586,7 +628,7 @@ portsCommand.command("rm").description("Unpublish an app port").argument("<id-or
586
628
  }
587
629
  const computer = await resolveComputer(identifier);
588
630
  await deletePublishedPort(computer.id, targetPort);
589
- spinner.succeed(`Removed port ${chalk2.bold(String(targetPort))}`);
631
+ spinner.succeed(`Removed port ${chalk3.bold(String(targetPort))}`);
590
632
  } catch (error) {
591
633
  spinner.fail(error instanceof Error ? error.message : "Failed to remove port");
592
634
  process.exit(1);
@@ -623,11 +665,11 @@ async function setupSSHAlias(options) {
623
665
  });
624
666
  spinner.succeed(`SSH alias '${alias}' is ready`);
625
667
  console.log();
626
- console.log(chalk2.dim(` SSH config: ${configResult.configPath}`));
627
- console.log(chalk2.dim(` Identity: ${registered.privateKeyPath}`));
668
+ console.log(chalk3.dim(` SSH config: ${configResult.configPath}`));
669
+ console.log(chalk3.dim(` Identity: ${registered.privateKeyPath}`));
628
670
  console.log();
629
- console.log(` ${chalk2.bold("Shell:")} ssh ${alias}`);
630
- console.log(` ${chalk2.bold("Direct:")} ssh <handle>@${alias}`);
671
+ console.log(` ${chalk3.bold("Shell:")} ssh ${alias}`);
672
+ console.log(` ${chalk3.bold("Direct:")} ssh <handle>@${alias}`);
631
673
  console.log();
632
674
  } catch (error) {
633
675
  spinner.fail(error instanceof Error ? error.message : "Failed to configure SSH alias");
@@ -675,43 +717,13 @@ async function resolveSSHComputer(identifier, spinner) {
675
717
  if (trimmed) {
676
718
  return resolveComputer(trimmed);
677
719
  }
678
- if (!process.stdin.isTTY || !process.stdout.isTTY) {
679
- throw new Error("computer id or handle is required when not running interactively");
680
- }
681
720
  const computers = await listComputers();
682
- const available = computers.filter(isSSHSelectable);
683
- if (available.length === 0) {
684
- if (computers.length === 0) {
685
- throw new Error("no computers found");
686
- }
687
- throw new Error("no running computers with SSH enabled");
688
- }
689
- const handleWidth = Math.max(6, ...available.map((computer) => computer.handle.length));
690
721
  spinner.stop();
691
- let selectedID;
692
722
  try {
693
- selectedID = await select({
694
- message: "Select a computer to SSH into",
695
- pageSize: Math.min(available.length, 10),
696
- choices: available.map((computer) => ({
697
- name: `${padEnd(chalk2.white(computer.handle), handleWidth + 2)}${padEnd(formatStatus(computer.status), 12)}${chalk2.dim(describeSSHChoice(computer))}`,
698
- value: computer.id
699
- }))
700
- });
723
+ return await promptForSSHComputer(computers, "Select a computer to SSH into");
701
724
  } finally {
702
725
  spinner.start("Preparing SSH access...");
703
726
  }
704
- return available.find((computer) => computer.id === selectedID) ?? available[0];
705
- }
706
- function isSSHSelectable(computer) {
707
- return computer.ssh_enabled && computer.status === "running";
708
- }
709
- function describeSSHChoice(computer) {
710
- const displayName = computer.display_name.trim();
711
- if (displayName && displayName !== computer.handle) {
712
- return `${displayName} ${timeAgo(computer.updated_at)}`;
713
- }
714
- return `${computer.runtime_family} ${timeAgo(computer.updated_at)}`;
715
727
  }
716
728
 
717
729
  // src/commands/acp.ts
@@ -1062,22 +1074,22 @@ acpCommand.command("serve").description("Serve a local stdio ACP bridge backed b
1062
1074
 
1063
1075
  // src/commands/agent.ts
1064
1076
  import { Command as Command3 } from "commander";
1065
- import chalk3 from "chalk";
1077
+ import chalk4 from "chalk";
1066
1078
  import ora2 from "ora";
1067
1079
  function formatAgentSessionStatus(status) {
1068
1080
  switch (status) {
1069
1081
  case "idle":
1070
- return chalk3.green(status);
1082
+ return chalk4.green(status);
1071
1083
  case "running":
1072
- return chalk3.blue(status);
1084
+ return chalk4.blue(status);
1073
1085
  case "cancelling":
1074
- return chalk3.yellow(status);
1086
+ return chalk4.yellow(status);
1075
1087
  case "interrupted":
1076
- return chalk3.yellow(status);
1088
+ return chalk4.yellow(status);
1077
1089
  case "failed":
1078
- return chalk3.red(status);
1090
+ return chalk4.red(status);
1079
1091
  case "closed":
1080
- return chalk3.gray(status);
1092
+ return chalk4.gray(status);
1081
1093
  default:
1082
1094
  return status;
1083
1095
  }
@@ -1086,14 +1098,14 @@ function printAgents(agents) {
1086
1098
  const idWidth = Math.max(5, ...agents.map((agent) => agent.id.length));
1087
1099
  console.log();
1088
1100
  console.log(
1089
- ` ${chalk3.dim(padEnd("Agent", idWidth + 2))}${chalk3.dim(padEnd("Installed", 12))}${chalk3.dim(padEnd("Creds", 8))}${chalk3.dim("Version")}`
1101
+ ` ${chalk4.dim(padEnd("Agent", idWidth + 2))}${chalk4.dim(padEnd("Installed", 12))}${chalk4.dim(padEnd("Creds", 8))}${chalk4.dim("Version")}`
1090
1102
  );
1091
1103
  console.log(
1092
- ` ${chalk3.dim("-".repeat(idWidth + 2))}${chalk3.dim("-".repeat(12))}${chalk3.dim("-".repeat(8))}${chalk3.dim("-".repeat(12))}`
1104
+ ` ${chalk4.dim("-".repeat(idWidth + 2))}${chalk4.dim("-".repeat(12))}${chalk4.dim("-".repeat(8))}${chalk4.dim("-".repeat(12))}`
1093
1105
  );
1094
1106
  for (const agent of agents) {
1095
1107
  console.log(
1096
- ` ${padEnd(agent.id, idWidth + 2)}${padEnd(agent.installed ? chalk3.green("yes") : chalk3.gray("no"), 12)}${padEnd(agent.credentialsAvailable ? chalk3.green("yes") : chalk3.yellow("no"), 8)}${agent.version ?? chalk3.dim("unknown")}`
1108
+ ` ${padEnd(agent.id, idWidth + 2)}${padEnd(agent.installed ? chalk4.green("yes") : chalk4.gray("no"), 12)}${padEnd(agent.credentialsAvailable ? chalk4.green("yes") : chalk4.yellow("no"), 8)}${agent.version ?? chalk4.dim("unknown")}`
1097
1109
  );
1098
1110
  }
1099
1111
  console.log();
@@ -1101,7 +1113,7 @@ function printAgents(agents) {
1101
1113
  function printSessions(sessions, handleByComputerID = /* @__PURE__ */ new Map()) {
1102
1114
  if (sessions.length === 0) {
1103
1115
  console.log();
1104
- console.log(chalk3.dim(" No agent sessions found."));
1116
+ console.log(chalk4.dim(" No agent sessions found."));
1105
1117
  console.log();
1106
1118
  return;
1107
1119
  }
@@ -1110,19 +1122,19 @@ function printSessions(sessions, handleByComputerID = /* @__PURE__ */ new Map())
1110
1122
  const statusWidth = 13;
1111
1123
  console.log();
1112
1124
  console.log(
1113
- ` ${chalk3.dim(padEnd("Session", 14))}${chalk3.dim(padEnd("Name", nameWidth + 2))}${chalk3.dim(padEnd("Agent", agentWidth + 2))}${chalk3.dim(padEnd("Status", statusWidth + 2))}${chalk3.dim("Location")}`
1125
+ ` ${chalk4.dim(padEnd("Session", 14))}${chalk4.dim(padEnd("Name", nameWidth + 2))}${chalk4.dim(padEnd("Agent", agentWidth + 2))}${chalk4.dim(padEnd("Status", statusWidth + 2))}${chalk4.dim("Location")}`
1114
1126
  );
1115
1127
  console.log(
1116
- ` ${chalk3.dim("-".repeat(14))}${chalk3.dim("-".repeat(nameWidth + 2))}${chalk3.dim("-".repeat(agentWidth + 2))}${chalk3.dim("-".repeat(statusWidth + 2))}${chalk3.dim("-".repeat(20))}`
1128
+ ` ${chalk4.dim("-".repeat(14))}${chalk4.dim("-".repeat(nameWidth + 2))}${chalk4.dim("-".repeat(agentWidth + 2))}${chalk4.dim("-".repeat(statusWidth + 2))}${chalk4.dim("-".repeat(20))}`
1117
1129
  );
1118
1130
  for (const session of sessions) {
1119
1131
  const location = handleByComputerID.get(session.computer_id) ?? session.computer_id;
1120
1132
  console.log(
1121
1133
  ` ${padEnd(session.id.slice(0, 12), 14)}${padEnd(session.name || "default", nameWidth + 2)}${padEnd(session.agent, agentWidth + 2)}${padEnd(formatAgentSessionStatus(session.status), statusWidth + 2)}${location}`
1122
1134
  );
1123
- console.log(` ${chalk3.dim(` cwd=${session.cwd} updated=${timeAgo(session.updated_at)}`)}`);
1135
+ console.log(` ${chalk4.dim(` cwd=${session.cwd} updated=${timeAgo(session.updated_at)}`)}`);
1124
1136
  if (session.last_stop_reason || session.last_error) {
1125
- console.log(` ${chalk3.dim(` ${session.last_error ? `error=${session.last_error}` : `stop=${session.last_stop_reason}`}`)}`);
1137
+ console.log(` ${chalk4.dim(` ${session.last_error ? `error=${session.last_error}` : `stop=${session.last_stop_reason}`}`)}`);
1126
1138
  }
1127
1139
  }
1128
1140
  console.log();
@@ -1136,7 +1148,7 @@ var StreamPrinter = class {
1136
1148
  }
1137
1149
  writeDim(text) {
1138
1150
  this.ensureBreak();
1139
- process.stdout.write(chalk3.dim(text));
1151
+ process.stdout.write(chalk4.dim(text));
1140
1152
  this.inlineOpen = !text.endsWith("\n");
1141
1153
  }
1142
1154
  writeLine(text) {
@@ -1204,7 +1216,7 @@ function renderSSEChunk(chunk, printer, asJson) {
1204
1216
  try {
1205
1217
  envelope = JSON.parse(payload);
1206
1218
  } catch {
1207
- printer.writeLine(chalk3.dim(payload));
1219
+ printer.writeLine(chalk4.dim(payload));
1208
1220
  return;
1209
1221
  }
1210
1222
  const method = typeof envelope.method === "string" ? envelope.method : "";
@@ -1231,14 +1243,14 @@ function renderSSEChunk(chunk, printer, asJson) {
1231
1243
  case "tool_call_update": {
1232
1244
  const title = typeof update?.title === "string" && update.title ? update.title : "tool";
1233
1245
  const status = typeof update?.status === "string" && update.status ? update.status : "in_progress";
1234
- printer.writeLine(chalk3.dim(`[tool:${status}] ${title}`));
1246
+ printer.writeLine(chalk4.dim(`[tool:${status}] ${title}`));
1235
1247
  return;
1236
1248
  }
1237
1249
  case "plan": {
1238
1250
  const entries = Array.isArray(update?.entries) ? update.entries : [];
1239
1251
  const detail = entries.filter((entry) => typeof entry === "object" && entry !== null).map((entry) => `- [${entry.status ?? "pending"}] ${entry.content ?? ""}`).join("\n");
1240
1252
  if (detail) {
1241
- printer.writeLine(chalk3.dim(`Plan
1253
+ printer.writeLine(chalk4.dim(`Plan
1242
1254
  ${detail}`));
1243
1255
  }
1244
1256
  return;
@@ -1248,7 +1260,7 @@ ${detail}`));
1248
1260
  }
1249
1261
  }
1250
1262
  if (method === "session/request_permission") {
1251
- printer.writeLine(chalk3.yellow("Permission requested by remote agent"));
1263
+ printer.writeLine(chalk4.yellow("Permission requested by remote agent"));
1252
1264
  return;
1253
1265
  }
1254
1266
  }
@@ -1376,12 +1388,12 @@ agentCommand.command("prompt").description("Send a prompt to a machine agent ses
1376
1388
  return;
1377
1389
  }
1378
1390
  console.log();
1379
- console.log(` ${chalk3.bold(session.name || "default")} ${formatAgentSessionStatus(settled.status)}`);
1391
+ console.log(` ${chalk4.bold(session.name || "default")} ${formatAgentSessionStatus(settled.status)}`);
1380
1392
  if (settled.last_stop_reason) {
1381
- console.log(chalk3.dim(` stop_reason=${settled.last_stop_reason}`));
1393
+ console.log(chalk4.dim(` stop_reason=${settled.last_stop_reason}`));
1382
1394
  }
1383
1395
  if (settled.last_error) {
1384
- console.log(chalk3.red(` error=${settled.last_error}`));
1396
+ console.log(chalk4.red(` error=${settled.last_error}`));
1385
1397
  }
1386
1398
  console.log();
1387
1399
  } catch (error) {
@@ -1511,11 +1523,558 @@ fleetCommand.command("status").description("List open agent sessions across all
1511
1523
  }
1512
1524
  });
1513
1525
 
1514
- // src/commands/computers.ts
1526
+ // src/commands/claude-auth.ts
1527
+ import { randomBytes, createHash } from "crypto";
1528
+ import { spawn as spawn2 } from "child_process";
1529
+ import { input as textInput } from "@inquirer/prompts";
1515
1530
  import { Command as Command4 } from "commander";
1516
- import chalk4 from "chalk";
1531
+ import chalk5 from "chalk";
1517
1532
  import ora3 from "ora";
1518
- import { select as select2, input as textInput, confirm } from "@inquirer/prompts";
1533
+ var CLAUDE_OAUTH_CLIENT_ID = process.env.CLAUDE_OAUTH_CLIENT_ID ?? "9d1c250a-e61b-44d9-88ed-5944d1962f5e";
1534
+ var CLAUDE_OAUTH_AUTHORIZE_URL = process.env.CLAUDE_OAUTH_AUTHORIZE_URL ?? "https://claude.ai/oauth/authorize";
1535
+ var CLAUDE_OAUTH_TOKEN_URL = process.env.CLAUDE_OAUTH_TOKEN_URL ?? "https://platform.claude.com/v1/oauth/token";
1536
+ var CLAUDE_OAUTH_REDIRECT_URL = process.env.CLAUDE_OAUTH_REDIRECT_URL ?? "https://platform.claude.com/oauth/code/callback";
1537
+ var CLAUDE_OAUTH_SCOPES = (process.env.CLAUDE_OAUTH_SCOPES ?? "user:profile user:inference user:sessions:claude_code user:mcp_servers user:file_upload").split(/\s+/).filter(Boolean);
1538
+ var readyPollIntervalMs = 2e3;
1539
+ var readyPollTimeoutMs = 18e4;
1540
+ var claudeAuthCommand = new Command4("claude-auth").alias("claude-login").description("Authenticate Claude Code on a computer").option("--machine <id-or-handle>", "Use a specific computer").option("--keep-helper", "Keep a temporary helper machine if one is created").option("--skip-cross-check", "Skip verification on a second shared machine").action(async (options) => {
1541
+ const todos = createTodoList();
1542
+ let target = null;
1543
+ let helperCreated = false;
1544
+ let sharedInstall = false;
1545
+ let activeTodoID = "target";
1546
+ let failureMessage = null;
1547
+ console.log();
1548
+ console.log(chalk5.cyan("Authenticating with Claude Code...\n"));
1549
+ try {
1550
+ const prepared = await prepareTargetMachine(options);
1551
+ target = prepared.computer;
1552
+ helperCreated = prepared.helperCreated;
1553
+ sharedInstall = prepared.sharedInstall;
1554
+ markTodo(
1555
+ todos,
1556
+ "target",
1557
+ "done",
1558
+ prepared.detail
1559
+ );
1560
+ activeTodoID = "ready";
1561
+ target = await waitForRunning(target);
1562
+ markTodo(todos, "ready", "done", `${target.handle} is running`);
1563
+ activeTodoID = "oauth";
1564
+ const oauth = await runManualOAuthFlow();
1565
+ markTodo(todos, "oauth", "done", "browser flow completed");
1566
+ activeTodoID = "install";
1567
+ const sshTarget = await resolveSSHTarget(target);
1568
+ await installClaudeAuth(sshTarget, oauth);
1569
+ markTodo(
1570
+ todos,
1571
+ "install",
1572
+ "done",
1573
+ sharedInstall ? `installed Claude login on shared home via ${target.handle}` : `installed Claude login on ${target.handle}`
1574
+ );
1575
+ activeTodoID = "verify-primary";
1576
+ await verifyStoredAuth(sshTarget);
1577
+ markTodo(
1578
+ todos,
1579
+ "verify-primary",
1580
+ "done",
1581
+ `${target.handle} fresh login shell sees Claude auth`
1582
+ );
1583
+ activeTodoID = "verify-shared";
1584
+ const sharedCheck = await verifySecondaryMachine(
1585
+ target.id,
1586
+ sharedInstall,
1587
+ Boolean(options.skipCrossCheck)
1588
+ );
1589
+ if (sharedCheck.status === "verified") {
1590
+ markTodo(
1591
+ todos,
1592
+ "verify-shared",
1593
+ "done",
1594
+ `${sharedCheck.handle} also sees stored Claude auth`
1595
+ );
1596
+ } else {
1597
+ markTodo(todos, "verify-shared", "skipped", sharedCheck.reason);
1598
+ }
1599
+ } catch (error) {
1600
+ failureMessage = error instanceof Error ? error.message : "Failed to authenticate Claude";
1601
+ markTodo(todos, activeTodoID, "failed", failureMessage);
1602
+ } finally {
1603
+ if (helperCreated && target && !options.keepHelper) {
1604
+ try {
1605
+ await deleteComputer(target.id);
1606
+ markTodo(
1607
+ todos,
1608
+ "cleanup",
1609
+ "done",
1610
+ `removed temporary helper ${target.handle}`
1611
+ );
1612
+ } catch (error) {
1613
+ const message = error instanceof Error ? error.message : "failed to remove helper";
1614
+ markTodo(todos, "cleanup", "failed", message);
1615
+ }
1616
+ } else if (helperCreated && target && options.keepHelper) {
1617
+ markTodo(
1618
+ todos,
1619
+ "cleanup",
1620
+ "skipped",
1621
+ `kept helper ${target.handle}`
1622
+ );
1623
+ } else {
1624
+ markTodo(todos, "cleanup", "skipped", "no helper created");
1625
+ }
1626
+ printTodoList(todos);
1627
+ }
1628
+ if (failureMessage) {
1629
+ console.error(chalk5.red(`
1630
+ ${failureMessage}`));
1631
+ process.exit(1);
1632
+ }
1633
+ if (target) {
1634
+ console.log(chalk5.green(`Claude is ready on ${chalk5.bold(target.handle)}.`));
1635
+ console.log();
1636
+ }
1637
+ });
1638
+ function createTodoList() {
1639
+ return [
1640
+ { id: "target", label: "Pick target computer", state: "pending" },
1641
+ { id: "ready", label: "Wait for machine readiness", state: "pending" },
1642
+ { id: "oauth", label: "Complete Claude browser auth", state: "pending" },
1643
+ { id: "install", label: "Install stored Claude login", state: "pending" },
1644
+ { id: "verify-primary", label: "Verify on target machine", state: "pending" },
1645
+ { id: "verify-shared", label: "Verify shared-home propagation", state: "pending" },
1646
+ { id: "cleanup", label: "Clean up temporary helper", state: "pending" }
1647
+ ];
1648
+ }
1649
+ function markTodo(items, id, state, detail) {
1650
+ const item = items.find((entry) => entry.id === id);
1651
+ if (!item) {
1652
+ return;
1653
+ }
1654
+ item.state = state;
1655
+ item.detail = detail;
1656
+ }
1657
+ function printTodoList(items) {
1658
+ console.log();
1659
+ console.log(chalk5.dim("TODO"));
1660
+ console.log();
1661
+ for (const item of items) {
1662
+ const marker = item.state === "done" ? chalk5.green("[x]") : item.state === "skipped" ? chalk5.yellow("[-]") : item.state === "failed" ? chalk5.red("[!]") : chalk5.dim("[ ]");
1663
+ const detail = item.detail ? chalk5.dim(` ${item.detail}`) : "";
1664
+ console.log(` ${marker} ${item.label}${detail ? ` ${detail}` : ""}`);
1665
+ }
1666
+ console.log();
1667
+ }
1668
+ async function prepareTargetMachine(options) {
1669
+ if (options.machine?.trim()) {
1670
+ const computer2 = await resolveComputer(options.machine.trim());
1671
+ assertClaudeAuthTarget(computer2);
1672
+ return {
1673
+ computer: computer2,
1674
+ helperCreated: false,
1675
+ sharedInstall: isSharedInstallTarget(computer2),
1676
+ detail: describeTarget(computer2, false)
1677
+ };
1678
+ }
1679
+ const computers = await listComputers();
1680
+ const filesystemSettings = await getFilesystemSettings().catch(() => null);
1681
+ if (filesystemSettings?.shared_enabled) {
1682
+ const existing = pickSharedRunningComputer(computers);
1683
+ if (existing) {
1684
+ return {
1685
+ computer: existing,
1686
+ helperCreated: false,
1687
+ sharedInstall: true,
1688
+ detail: describeTarget(existing, false)
1689
+ };
1690
+ }
1691
+ const spinner = ora3("Creating temporary shared helper...").start();
1692
+ try {
1693
+ const helper = await createComputer({
1694
+ handle: `claude-auth-${randomSuffix(6)}`,
1695
+ display_name: "Claude Auth Helper",
1696
+ runtime_family: "managed-worker",
1697
+ use_platform_default: true,
1698
+ ssh_enabled: true,
1699
+ vnc_enabled: false
1700
+ });
1701
+ spinner.succeed(`Created temporary helper ${chalk5.bold(helper.handle)}`);
1702
+ return {
1703
+ computer: helper,
1704
+ helperCreated: true,
1705
+ sharedInstall: true,
1706
+ detail: describeTarget(helper, true)
1707
+ };
1708
+ } catch (error) {
1709
+ spinner.fail(
1710
+ error instanceof Error ? error.message : "Failed to create temporary helper"
1711
+ );
1712
+ throw error;
1713
+ }
1714
+ }
1715
+ const computer = await promptForSSHComputer(
1716
+ computers,
1717
+ "Select a computer for Claude auth"
1718
+ );
1719
+ return {
1720
+ computer,
1721
+ helperCreated: false,
1722
+ sharedInstall: isSharedInstallTarget(computer),
1723
+ detail: describeTarget(computer, false)
1724
+ };
1725
+ }
1726
+ function pickSharedRunningComputer(computers) {
1727
+ const candidates = computers.filter(
1728
+ (computer) => computer.runtime_family === "managed-worker" && computer.filesystem_mode === "shared" && computer.ssh_enabled && computer.status === "running"
1729
+ ).sort(
1730
+ (left, right) => new Date(right.updated_at).getTime() - new Date(left.updated_at).getTime()
1731
+ );
1732
+ return candidates[0] ?? null;
1733
+ }
1734
+ function assertClaudeAuthTarget(computer) {
1735
+ if (!computer.ssh_enabled) {
1736
+ throw new Error(`${computer.handle} does not have SSH enabled`);
1737
+ }
1738
+ }
1739
+ function isSharedInstallTarget(computer) {
1740
+ return computer.runtime_family === "managed-worker" && computer.filesystem_mode === "shared";
1741
+ }
1742
+ function describeTarget(computer, helperCreated) {
1743
+ if (helperCreated) {
1744
+ return `created temporary helper ${computer.handle}`;
1745
+ }
1746
+ if (isSharedInstallTarget(computer)) {
1747
+ return `using shared machine ${computer.handle}`;
1748
+ }
1749
+ return `using ${computer.handle}`;
1750
+ }
1751
+ async function waitForRunning(initial) {
1752
+ if (initial.status === "running") {
1753
+ return initial;
1754
+ }
1755
+ const spinner = ora3(`Waiting for ${chalk5.bold(initial.handle)} to be ready...`).start();
1756
+ const deadline = Date.now() + readyPollTimeoutMs;
1757
+ let lastStatus = initial.status;
1758
+ while (Date.now() < deadline) {
1759
+ const current = await getComputerByID(initial.id);
1760
+ if (current.status === "running") {
1761
+ spinner.succeed(`${chalk5.bold(current.handle)} is ready`);
1762
+ return current;
1763
+ }
1764
+ if (current.status !== lastStatus) {
1765
+ lastStatus = current.status;
1766
+ spinner.text = `Waiting for ${chalk5.bold(current.handle)}... ${chalk5.dim(current.status)}`;
1767
+ }
1768
+ if (current.status === "error" || current.status === "deleted" || current.status === "stopped") {
1769
+ spinner.fail(`${current.handle} entered ${current.status}`);
1770
+ throw new Error(current.last_error || `${current.handle} entered ${current.status}`);
1771
+ }
1772
+ await delay(readyPollIntervalMs);
1773
+ }
1774
+ spinner.fail(`Timed out waiting for ${initial.handle}`);
1775
+ throw new Error(`timed out waiting for ${initial.handle} to be ready`);
1776
+ }
1777
+ async function runManualOAuthFlow() {
1778
+ const codeVerifier = base64url(randomBytes(32));
1779
+ const state = randomBytes(16).toString("hex");
1780
+ const codeChallenge = base64url(createHash("sha256").update(codeVerifier).digest());
1781
+ const url = buildAuthorizationURL(codeChallenge, state);
1782
+ console.log("We will open your browser so you can authenticate with Claude.");
1783
+ console.log("If the browser does not open automatically, use the URL below:\n");
1784
+ console.log(url);
1785
+ console.log();
1786
+ try {
1787
+ await openBrowserURL(url);
1788
+ } catch {
1789
+ console.log(chalk5.yellow("Unable to open the browser automatically."));
1790
+ }
1791
+ console.log(
1792
+ "After completing authentication, copy the code shown on the success page."
1793
+ );
1794
+ console.log("You can paste either the full URL, or a value formatted as CODE#STATE.\n");
1795
+ const pasted = (await textInput({
1796
+ message: "Paste the authorization code (or URL) here:"
1797
+ })).trim();
1798
+ if (!pasted) {
1799
+ throw new Error("no authorization code provided");
1800
+ }
1801
+ const parsed = parseAuthorizationInput(pasted, state);
1802
+ const spinner = ora3("Exchanging authorization code...").start();
1803
+ try {
1804
+ const response = await fetch(CLAUDE_OAUTH_TOKEN_URL, {
1805
+ method: "POST",
1806
+ headers: {
1807
+ "Content-Type": "application/json"
1808
+ },
1809
+ body: JSON.stringify({
1810
+ grant_type: "authorization_code",
1811
+ code: parsed.code,
1812
+ state: parsed.state,
1813
+ redirect_uri: CLAUDE_OAUTH_REDIRECT_URL,
1814
+ client_id: CLAUDE_OAUTH_CLIENT_ID,
1815
+ code_verifier: codeVerifier
1816
+ })
1817
+ });
1818
+ if (!response.ok) {
1819
+ throw new Error(
1820
+ `token exchange failed: ${response.status} ${await response.text()}`
1821
+ );
1822
+ }
1823
+ const payload = await response.json();
1824
+ if (!payload.refresh_token || !payload.scope) {
1825
+ throw new Error("token exchange returned an incomplete response");
1826
+ }
1827
+ spinner.succeed("Authorization code exchanged");
1828
+ return {
1829
+ refreshToken: payload.refresh_token,
1830
+ scope: payload.scope
1831
+ };
1832
+ } catch (error) {
1833
+ spinner.fail(
1834
+ error instanceof Error ? error.message : "Failed to exchange authorization code"
1835
+ );
1836
+ throw error;
1837
+ }
1838
+ }
1839
+ function buildAuthorizationURL(codeChallenge, state) {
1840
+ const params = new URLSearchParams({
1841
+ code: "true",
1842
+ client_id: CLAUDE_OAUTH_CLIENT_ID,
1843
+ response_type: "code",
1844
+ redirect_uri: CLAUDE_OAUTH_REDIRECT_URL,
1845
+ scope: CLAUDE_OAUTH_SCOPES.join(" "),
1846
+ code_challenge: codeChallenge,
1847
+ code_challenge_method: "S256",
1848
+ state
1849
+ });
1850
+ return `${CLAUDE_OAUTH_AUTHORIZE_URL}?${params.toString()}`;
1851
+ }
1852
+ function parseAuthorizationInput(value, expectedState) {
1853
+ if (value.startsWith("http://") || value.startsWith("https://")) {
1854
+ const parsed = new URL(value);
1855
+ const code2 = parsed.searchParams.get("code");
1856
+ const state2 = parsed.searchParams.get("state");
1857
+ if (!code2 || !state2) {
1858
+ throw new Error("pasted URL is missing code or state");
1859
+ }
1860
+ if (state2 !== expectedState) {
1861
+ throw new Error("state mismatch detected; restart the authentication flow");
1862
+ }
1863
+ return { code: code2, state: state2 };
1864
+ }
1865
+ const [code, state] = value.split("#", 2).map((part) => part?.trim() ?? "");
1866
+ if (!code || !state) {
1867
+ throw new Error("expected a full URL or a CODE#STATE value");
1868
+ }
1869
+ if (state !== expectedState) {
1870
+ throw new Error("state mismatch detected; restart the authentication flow");
1871
+ }
1872
+ return { code, state };
1873
+ }
1874
+ async function resolveSSHTarget(computer) {
1875
+ const registered = await ensureDefaultSSHKeyRegistered();
1876
+ const info = await getConnectionInfo(computer.id);
1877
+ if (!info.connection.ssh_available) {
1878
+ throw new Error(`SSH is not available for ${computer.handle}`);
1879
+ }
1880
+ return {
1881
+ handle: computer.handle,
1882
+ host: info.connection.ssh_host,
1883
+ port: info.connection.ssh_port,
1884
+ user: info.connection.ssh_user,
1885
+ identityFile: registered.privateKeyPath
1886
+ };
1887
+ }
1888
+ async function installClaudeAuth(target, oauth) {
1889
+ const spinner = ora3(`Installing Claude auth on ${chalk5.bold(target.handle)}...`).start();
1890
+ try {
1891
+ const installScript = buildInstallScript(oauth.refreshToken, oauth.scope);
1892
+ const result = await runRemoteBash(target, ["-s"], installScript);
1893
+ if (result.stdout.trim()) {
1894
+ spinner.succeed(`Installed Claude auth on ${chalk5.bold(target.handle)}`);
1895
+ return;
1896
+ }
1897
+ spinner.succeed(`Installed Claude auth on ${chalk5.bold(target.handle)}`);
1898
+ } catch (error) {
1899
+ spinner.fail(
1900
+ error instanceof Error ? error.message : `Failed to install Claude auth on ${target.handle}`
1901
+ );
1902
+ throw error;
1903
+ }
1904
+ }
1905
+ function buildInstallScript(refreshToken, scopes) {
1906
+ const tokenMarker = `TOKEN_${randomSuffix(12)}`;
1907
+ const scopeMarker = `SCOPES_${randomSuffix(12)}`;
1908
+ return [
1909
+ "set -euo pipefail",
1910
+ 'command -v claude >/dev/null 2>&1 || { echo "claude is not installed on this computer" >&2; exit 1; }',
1911
+ `export CLAUDE_CODE_OAUTH_REFRESH_TOKEN="$(cat <<'` + tokenMarker + "'",
1912
+ refreshToken,
1913
+ tokenMarker,
1914
+ ')"',
1915
+ `export CLAUDE_CODE_OAUTH_SCOPES="$(cat <<'` + scopeMarker + "'",
1916
+ scopes,
1917
+ scopeMarker,
1918
+ ')"',
1919
+ "claude auth login",
1920
+ "unset CLAUDE_CODE_OAUTH_REFRESH_TOKEN",
1921
+ "unset CLAUDE_CODE_OAUTH_SCOPES"
1922
+ ].join("\n");
1923
+ }
1924
+ async function verifyStoredAuth(target) {
1925
+ const command = freshShellCommand("claude auth status --json");
1926
+ const result = await runRemoteBash(target, ["-lc", command]);
1927
+ const payload = parseStatusOutput(result.stdout);
1928
+ if (!payload.loggedIn) {
1929
+ throw new Error(`Claude auth is not visible on ${target.handle}`);
1930
+ }
1931
+ }
1932
+ function freshShellCommand(inner) {
1933
+ return [
1934
+ "env -i",
1935
+ 'HOME="$HOME"',
1936
+ 'USER="${USER:-node}"',
1937
+ 'SHELL="/bin/bash"',
1938
+ 'PATH="$HOME/.local/bin:/usr/local/bin:/usr/bin:/bin"',
1939
+ "bash -lc",
1940
+ shellQuote(inner)
1941
+ ].join(" ");
1942
+ }
1943
+ function parseStatusOutput(stdout) {
1944
+ const start = stdout.indexOf("{");
1945
+ const end = stdout.lastIndexOf("}");
1946
+ if (start === -1 || end === -1 || end <= start) {
1947
+ throw new Error("could not parse claude auth status output");
1948
+ }
1949
+ const parsed = JSON.parse(stdout.slice(start, end + 1));
1950
+ return { loggedIn: parsed.loggedIn === true };
1951
+ }
1952
+ async function verifySecondaryMachine(primaryComputerID, sharedInstall, skip) {
1953
+ if (!sharedInstall) {
1954
+ return { status: "skipped", reason: "target uses isolated filesystem" };
1955
+ }
1956
+ if (skip) {
1957
+ return { status: "skipped", reason: "cross-check skipped by flag" };
1958
+ }
1959
+ const secondary = (await listComputers()).filter(
1960
+ (computer) => computer.id !== primaryComputerID && computer.runtime_family === "managed-worker" && computer.filesystem_mode === "shared" && computer.ssh_enabled && computer.status === "running"
1961
+ ).sort(
1962
+ (left, right) => new Date(right.updated_at).getTime() - new Date(left.updated_at).getTime()
1963
+ )[0];
1964
+ if (!secondary) {
1965
+ return {
1966
+ status: "skipped",
1967
+ reason: "no second running shared managed-worker was available"
1968
+ };
1969
+ }
1970
+ const sshTarget = await resolveSSHTarget(secondary);
1971
+ await verifyStoredAuth(sshTarget);
1972
+ return { status: "verified", handle: secondary.handle };
1973
+ }
1974
+ async function runRemoteBash(target, bashArgs, script) {
1975
+ const args = [
1976
+ "-T",
1977
+ "-i",
1978
+ target.identityFile,
1979
+ "-p",
1980
+ String(target.port),
1981
+ `${target.user}@${target.host}`,
1982
+ "bash",
1983
+ ...bashArgs
1984
+ ];
1985
+ return new Promise((resolve, reject) => {
1986
+ const child = spawn2("ssh", args, {
1987
+ stdio: ["pipe", "pipe", "pipe"]
1988
+ });
1989
+ let stdout = "";
1990
+ let stderr = "";
1991
+ child.stdout.on("data", (chunk) => {
1992
+ stdout += chunk.toString();
1993
+ });
1994
+ child.stderr.on("data", (chunk) => {
1995
+ stderr += chunk.toString();
1996
+ });
1997
+ child.on("error", reject);
1998
+ child.on("exit", (code) => {
1999
+ if (code === 0) {
2000
+ resolve({ stdout, stderr });
2001
+ return;
2002
+ }
2003
+ const message = stderr.trim() || stdout.trim() || `ssh exited with code ${code ?? 1}`;
2004
+ reject(new Error(message));
2005
+ });
2006
+ if (script !== void 0) {
2007
+ child.stdin.end(script);
2008
+ } else {
2009
+ child.stdin.end();
2010
+ }
2011
+ });
2012
+ }
2013
+ function shellQuote(value) {
2014
+ return `'${value.replace(/'/g, `'\\''`)}'`;
2015
+ }
2016
+ function base64url(buffer) {
2017
+ return buffer.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
2018
+ }
2019
+ function randomSuffix(length) {
2020
+ return randomBytes(Math.ceil(length / 2)).toString("hex").slice(0, length);
2021
+ }
2022
+ function delay(ms) {
2023
+ return new Promise((resolve) => {
2024
+ setTimeout(resolve, ms);
2025
+ });
2026
+ }
2027
+
2028
+ // src/commands/computers.ts
2029
+ import { Command as Command5 } from "commander";
2030
+ import chalk6 from "chalk";
2031
+ import ora4 from "ora";
2032
+ import { select as select2, input as textInput2, confirm } from "@inquirer/prompts";
2033
+
2034
+ // src/lib/machine-sources.ts
2035
+ async function getMachineSourceSettings() {
2036
+ return api("/v1/me/machine-source");
2037
+ }
2038
+ async function upsertMachineSource(input) {
2039
+ return api("/v1/me/machine-source", {
2040
+ method: "PUT",
2041
+ body: JSON.stringify(input)
2042
+ });
2043
+ }
2044
+ async function clearMachineSourceDefault() {
2045
+ return api("/v1/me/machine-source", {
2046
+ method: "DELETE"
2047
+ });
2048
+ }
2049
+ async function rebuildMachineSource(sourceID) {
2050
+ return api(`/v1/me/machine-source/${encodeURIComponent(sourceID)}/rebuild`, {
2051
+ method: "POST"
2052
+ });
2053
+ }
2054
+ async function deleteMachineSource(sourceID) {
2055
+ return api(`/v1/me/machine-source/${encodeURIComponent(sourceID)}`, {
2056
+ method: "DELETE"
2057
+ });
2058
+ }
2059
+ function summarizeMachineSourceSelection(settings) {
2060
+ if (settings.platform_default || !settings.default_machine_source) {
2061
+ return "AgentComputer platform default image";
2062
+ }
2063
+ return summarizeMachineSource(settings.default_machine_source);
2064
+ }
2065
+ function summarizeMachineSource(source) {
2066
+ if (source.kind === "oci-image") {
2067
+ return source.requested_ref || "OCI image";
2068
+ }
2069
+ const parts = [
2070
+ source.git_url || "Nix git source",
2071
+ source.git_ref ? `ref ${source.git_ref}` : "",
2072
+ source.git_subpath ? `subpath ${source.git_subpath}` : ""
2073
+ ].filter(Boolean);
2074
+ return parts.join(" | ");
2075
+ }
2076
+
2077
+ // src/commands/computers.ts
1519
2078
  function isInternalCondition(value) {
1520
2079
  return /^[A-Z][a-zA-Z]+$/.test(value);
1521
2080
  }
@@ -1525,32 +2084,38 @@ function printComputer(computer) {
1525
2084
  const ssh = computer.ssh_enabled ? formatSSHCommand2(computer.handle, computer.ssh_host, computer.ssh_port) : "disabled";
1526
2085
  const isCustom = computer.runtime_family === "custom-machine";
1527
2086
  console.log();
1528
- console.log(` ${chalk4.bold.white(computer.handle)} ${formatStatus(computer.status)}`);
2087
+ console.log(` ${chalk6.bold.white(computer.handle)} ${formatStatus(computer.status)}`);
1529
2088
  console.log();
1530
- console.log(` ${chalk4.dim("ID")} ${computer.id}`);
1531
- console.log(` ${chalk4.dim("Tier")} ${computer.tier}`);
2089
+ console.log(` ${chalk6.dim("ID")} ${computer.id}`);
2090
+ console.log(` ${chalk6.dim("Tier")} ${computer.tier}`);
1532
2091
  if (isCustom) {
1533
- console.log(` ${chalk4.dim("Runtime")} ${computer.runtime_family}`);
1534
- console.log(` ${chalk4.dim("Source")} ${computer.source_kind}`);
1535
- console.log(` ${chalk4.dim("Image")} ${computer.image_family}`);
2092
+ console.log(` ${chalk6.dim("Runtime")} ${computer.runtime_family}`);
2093
+ console.log(` ${chalk6.dim("Source")} ${computer.source_kind}`);
2094
+ console.log(` ${chalk6.dim("Image")} ${computer.image_family}`);
2095
+ } else {
2096
+ console.log(` ${chalk6.dim("Runtime")} ${computer.runtime_family}`);
2097
+ console.log(` ${chalk6.dim("Launch")} ${formatManagedWorkerLaunchSource(computer)}`);
2098
+ if (computer.user_source_id && computer.resolved_image_ref) {
2099
+ console.log(` ${chalk6.dim("Image")} ${computer.resolved_image_ref}`);
2100
+ }
1536
2101
  }
1537
- console.log(` ${chalk4.dim("Primary")} :${computer.primary_port}${computer.primary_path}`);
1538
- console.log(` ${chalk4.dim("Health")} ${computer.healthcheck_type}${computer.healthcheck_value ? ` (${computer.healthcheck_value})` : ""}`);
2102
+ console.log(` ${chalk6.dim("Primary")} :${computer.primary_port}${computer.primary_path}`);
2103
+ console.log(` ${chalk6.dim("Health")} ${computer.healthcheck_type}${computer.healthcheck_value ? ` (${computer.healthcheck_value})` : ""}`);
1539
2104
  console.log();
1540
- console.log(` ${chalk4.dim("Gateway")} ${chalk4.cyan(webURL(computer))}`);
1541
- console.log(` ${chalk4.dim("VNC")} ${vnc ? chalk4.cyan(vnc) : chalk4.dim("not available")}`);
1542
- console.log(` ${chalk4.dim("Terminal")} ${terminal ? chalk4.cyan(terminal) : chalk4.dim("not available")}`);
1543
- console.log(` ${chalk4.dim("SSH")} ${computer.ssh_enabled ? chalk4.white(ssh) : chalk4.dim(ssh)}`);
2105
+ console.log(` ${chalk6.dim("Gateway")} ${chalk6.cyan(webURL(computer))}`);
2106
+ console.log(` ${chalk6.dim("VNC")} ${vnc ? chalk6.cyan(vnc) : chalk6.dim("not available")}`);
2107
+ console.log(` ${chalk6.dim("Terminal")} ${terminal ? chalk6.cyan(terminal) : chalk6.dim("not available")}`);
2108
+ console.log(` ${chalk6.dim("SSH")} ${computer.ssh_enabled ? chalk6.white(ssh) : chalk6.dim(ssh)}`);
1544
2109
  if (computer.last_error) {
1545
2110
  console.log();
1546
2111
  if (isInternalCondition(computer.last_error)) {
1547
- console.log(` ${chalk4.dim("Condition")} ${chalk4.dim(computer.last_error)}`);
2112
+ console.log(` ${chalk6.dim("Condition")} ${chalk6.dim(computer.last_error)}`);
1548
2113
  } else {
1549
- console.log(` ${chalk4.dim("Error")} ${chalk4.red(computer.last_error)}`);
2114
+ console.log(` ${chalk6.dim("Error")} ${chalk6.red(computer.last_error)}`);
1550
2115
  }
1551
2116
  }
1552
2117
  console.log();
1553
- console.log(` ${chalk4.dim("Created")} ${timeAgo(computer.created_at)}`);
2118
+ console.log(` ${chalk6.dim("Created")} ${timeAgo(computer.created_at)}`);
1554
2119
  console.log();
1555
2120
  }
1556
2121
  function formatSSHCommand2(user, host, port) {
@@ -1562,22 +2127,41 @@ function formatSSHCommand2(user, host, port) {
1562
2127
  }
1563
2128
  return `ssh -p ${port} ${user}@${host}`;
1564
2129
  }
2130
+ function formatManagedWorkerLaunchSource(computer) {
2131
+ if (!computer.user_source_id) {
2132
+ return "AgentComputer managed-worker platform default";
2133
+ }
2134
+ if (computer.source_repo_url) {
2135
+ let value = computer.source_repo_url;
2136
+ if (computer.source_ref) {
2137
+ value += ` @ ${computer.source_ref}`;
2138
+ }
2139
+ if (computer.source_subpath) {
2140
+ value += ` # ${computer.source_subpath}`;
2141
+ }
2142
+ return `saved nix-git source ${computer.user_source_id} (${value})`;
2143
+ }
2144
+ if (computer.source_ref) {
2145
+ return `saved oci-image source ${computer.user_source_id} (${computer.source_ref})`;
2146
+ }
2147
+ return `saved custom source ${computer.user_source_id}`;
2148
+ }
1565
2149
  function printComputerTable(computers) {
1566
2150
  const handleWidth = Math.max(6, ...computers.map((c) => c.handle.length));
1567
2151
  const statusWidth = 12;
1568
2152
  const createdWidth = 10;
1569
2153
  console.log();
1570
2154
  console.log(
1571
- ` ${chalk4.dim(padEnd("Handle", handleWidth + 2))}${chalk4.dim(padEnd("Status", statusWidth + 2))}${chalk4.dim(padEnd("Created", createdWidth + 2))}${chalk4.dim("URL")}`
2155
+ ` ${chalk6.dim(padEnd("Handle", handleWidth + 2))}${chalk6.dim(padEnd("Status", statusWidth + 2))}${chalk6.dim(padEnd("Created", createdWidth + 2))}${chalk6.dim("URL")}`
1572
2156
  );
1573
2157
  console.log(
1574
- ` ${chalk4.dim("-".repeat(handleWidth + 2))}${chalk4.dim("-".repeat(statusWidth + 2))}${chalk4.dim("-".repeat(createdWidth + 2))}${chalk4.dim("-".repeat(20))}`
2158
+ ` ${chalk6.dim("-".repeat(handleWidth + 2))}${chalk6.dim("-".repeat(statusWidth + 2))}${chalk6.dim("-".repeat(createdWidth + 2))}${chalk6.dim("-".repeat(20))}`
1575
2159
  );
1576
2160
  for (const computer of computers) {
1577
2161
  const status = formatStatus(computer.status);
1578
- const created = chalk4.dim(timeAgo(computer.created_at));
2162
+ const created = chalk6.dim(timeAgo(computer.created_at));
1579
2163
  console.log(
1580
- ` ${chalk4.white(padEnd(computer.handle, handleWidth + 2))}${padEnd(status, statusWidth + 2)}${padEnd(created, createdWidth + 2)}${chalk4.cyan(webURL(computer))}`
2164
+ ` ${chalk6.white(padEnd(computer.handle, handleWidth + 2))}${padEnd(status, statusWidth + 2)}${padEnd(created, createdWidth + 2)}${chalk6.cyan(webURL(computer))}`
1581
2165
  );
1582
2166
  }
1583
2167
  console.log();
@@ -1587,29 +2171,29 @@ function printComputerTableVerbose(computers) {
1587
2171
  const statusWidth = 10;
1588
2172
  console.log();
1589
2173
  console.log(
1590
- ` ${chalk4.dim(padEnd("Handle", handleWidth + 2))}${chalk4.dim(padEnd("Status", statusWidth + 2))}${chalk4.dim("URLs")}`
2174
+ ` ${chalk6.dim(padEnd("Handle", handleWidth + 2))}${chalk6.dim(padEnd("Status", statusWidth + 2))}${chalk6.dim("URLs")}`
1591
2175
  );
1592
2176
  console.log(
1593
- ` ${chalk4.dim("-".repeat(handleWidth + 2))}${chalk4.dim("-".repeat(statusWidth + 2))}${chalk4.dim("-".repeat(20))}`
2177
+ ` ${chalk6.dim("-".repeat(handleWidth + 2))}${chalk6.dim("-".repeat(statusWidth + 2))}${chalk6.dim("-".repeat(20))}`
1594
2178
  );
1595
2179
  for (const computer of computers) {
1596
2180
  const status = formatStatus(computer.status);
1597
2181
  const vnc = vncURL(computer);
1598
2182
  const terminal = terminalURL(computer);
1599
2183
  console.log(
1600
- ` ${chalk4.white(padEnd(computer.handle, handleWidth + 2))}${padEnd(status, statusWidth + 2)}${chalk4.cyan(webURL(computer))}`
2184
+ ` ${chalk6.white(padEnd(computer.handle, handleWidth + 2))}${padEnd(status, statusWidth + 2)}${chalk6.cyan(webURL(computer))}`
1601
2185
  );
1602
2186
  console.log(
1603
- ` ${padEnd("", handleWidth + 2)}${padEnd("", statusWidth + 2)}${chalk4.dim(vnc ?? "VNC not available")}`
2187
+ ` ${padEnd("", handleWidth + 2)}${padEnd("", statusWidth + 2)}${chalk6.dim(vnc ?? "VNC not available")}`
1604
2188
  );
1605
2189
  console.log(
1606
- ` ${padEnd("", handleWidth + 2)}${padEnd("", statusWidth + 2)}${chalk4.dim(terminal ?? "Terminal not available")}`
2190
+ ` ${padEnd("", handleWidth + 2)}${padEnd("", statusWidth + 2)}${chalk6.dim(terminal ?? "Terminal not available")}`
1607
2191
  );
1608
2192
  }
1609
2193
  console.log();
1610
2194
  }
1611
- var lsCommand = new Command4("ls").description("List computers").option("--json", "Print raw JSON").option("-v, --verbose", "Show all URLs for each computer").action(async (options) => {
1612
- const spinner = options.json ? null : ora3("Fetching computers...").start();
2195
+ var lsCommand = new Command5("ls").description("List computers").option("--json", "Print raw JSON").option("-v, --verbose", "Show all URLs for each computer").action(async (options) => {
2196
+ const spinner = options.json ? null : ora4("Fetching computers...").start();
1613
2197
  try {
1614
2198
  const computers = await listComputers();
1615
2199
  spinner?.stop();
@@ -1619,7 +2203,7 @@ var lsCommand = new Command4("ls").description("List computers").option("--json"
1619
2203
  }
1620
2204
  if (computers.length === 0) {
1621
2205
  console.log();
1622
- console.log(chalk4.dim(" No computers found."));
2206
+ console.log(chalk6.dim(" No computers found."));
1623
2207
  console.log();
1624
2208
  return;
1625
2209
  }
@@ -1639,8 +2223,8 @@ var lsCommand = new Command4("ls").description("List computers").option("--json"
1639
2223
  process.exit(1);
1640
2224
  }
1641
2225
  });
1642
- var getCommand = new Command4("get").description("Show computer details").argument("<id-or-handle>", "Computer id or handle").option("--json", "Print raw JSON").action(async (identifier, options) => {
1643
- const spinner = options.json ? null : ora3("Fetching computer...").start();
2226
+ var getCommand = new Command5("get").description("Show computer details").argument("<id-or-handle>", "Computer id or handle").option("--json", "Print raw JSON").action(async (identifier, options) => {
2227
+ const spinner = options.json ? null : ora4("Fetching computer...").start();
1644
2228
  try {
1645
2229
  const computer = await resolveComputer(identifier);
1646
2230
  spinner?.stop();
@@ -1660,19 +2244,28 @@ var getCommand = new Command4("get").description("Show computer details").argume
1660
2244
  process.exit(1);
1661
2245
  }
1662
2246
  });
1663
- var createCommand = new Command4("create").description("Create a computer").argument("[handle]", "Optional computer handle").option("--name <display-name>", "Display name").option("--tier <tier>", "Tier override").option("--interactive", "Prompt for runtime choices").option("--runtime-family <runtime-family>", "managed-worker or custom-machine").option("--source-kind <source-kind>", "none or oci-image").option("--image-family <family>", "Image family override").option("--image-ref <image>", "Resolved image override").option("--primary-port <port>", "Primary app port").option("--primary-path <path>", "Primary app path").option("--healthcheck-type <type>", "http or tcp").option("--healthcheck-value <value>", "Health check path or port").option("--ssh-enabled", "Enable SSH access").option("--ssh-disabled", "Disable SSH access").option("--vnc-enabled", "Enable VNC access").option("--vnc-disabled", "Disable VNC access").action(async (handle, options) => {
2247
+ var createCommand = new Command5("create").description("Create a computer").argument("[handle]", "Optional computer handle").option("--name <display-name>", "Display name").option("--tier <tier>", "Tier override").option("--interactive", "Prompt for runtime choices").option("--runtime-family <runtime-family>", "managed-worker or custom-machine").option("--source-kind <source-kind>", "none or oci-image").option("--image-family <family>", "Image family override").option("--image-ref <image>", "Resolved image override").option("--use-platform-default", "Use the AgentComputer platform default image").option("--primary-port <port>", "Primary app port").option("--primary-path <path>", "Primary app path").option("--healthcheck-type <type>", "http or tcp").option("--healthcheck-value <value>", "Health check path or port").option("--ssh-enabled", "Enable SSH access").option("--ssh-disabled", "Disable SSH access").option("--vnc-enabled", "Enable VNC access").option("--vnc-disabled", "Disable VNC access").action(async (handle, options) => {
1664
2248
  let spinner;
1665
2249
  let timer;
1666
2250
  let startTime = 0;
1667
2251
  try {
1668
2252
  const selectedOptions = await resolveCreateOptions(options);
1669
2253
  const runtimeFamily = effectiveRuntimeFamily(selectedOptions.runtimeFamily);
2254
+ const machineSourceSettings = runtimeFamily === "managed-worker" ? await getMachineSourceSettings().catch(() => null) : null;
1670
2255
  const filesystemSettings = await loadFilesystemSettingsForCreate(runtimeFamily);
2256
+ const machineSourceNote = createMachineSourceNote(
2257
+ runtimeFamily,
2258
+ machineSourceSettings,
2259
+ Boolean(selectedOptions.usePlatformDefault)
2260
+ );
2261
+ if (machineSourceNote) {
2262
+ console.log(chalk6.dim(machineSourceNote));
2263
+ }
1671
2264
  const provisioningNote = createProvisioningNote(runtimeFamily, filesystemSettings);
1672
2265
  if (provisioningNote) {
1673
- console.log(chalk4.dim(provisioningNote));
2266
+ console.log(chalk6.dim(provisioningNote));
1674
2267
  }
1675
- spinner = ora3(createSpinnerText(runtimeFamily, filesystemSettings, 0)).start();
2268
+ spinner = ora4(createSpinnerText(runtimeFamily, filesystemSettings, 0)).start();
1676
2269
  startTime = Date.now();
1677
2270
  timer = setInterval(() => {
1678
2271
  const elapsed2 = (Date.now() - startTime) / 1e3;
@@ -1688,6 +2281,7 @@ var createCommand = new Command4("create").description("Create a computer").argu
1688
2281
  source_kind: parseSourceKindOption(selectedOptions.sourceKind),
1689
2282
  image_family: selectedOptions.imageFamily,
1690
2283
  image_ref: selectedOptions.imageRef,
2284
+ use_platform_default: selectedOptions.usePlatformDefault,
1691
2285
  primary_port: parseOptionalPort(selectedOptions.primaryPort),
1692
2286
  primary_path: selectedOptions.primaryPath,
1693
2287
  healthcheck_type: parseHealthcheckTypeOption(selectedOptions.healthcheckType),
@@ -1708,7 +2302,7 @@ var createCommand = new Command4("create").description("Create a computer").argu
1708
2302
  }
1709
2303
  const elapsed = ((Date.now() - startTime) / 1e3).toFixed(1);
1710
2304
  spinner.succeed(
1711
- chalk4.green(`Created ${chalk4.bold(computer.handle)} ${chalk4.dim(`[${elapsed}s]`)}`)
2305
+ chalk6.green(`Created ${chalk6.bold(computer.handle)} ${chalk6.dim(`[${elapsed}s]`)}`)
1712
2306
  );
1713
2307
  printComputer(computer);
1714
2308
  } catch (error) {
@@ -1717,18 +2311,17 @@ var createCommand = new Command4("create").description("Create a computer").argu
1717
2311
  }
1718
2312
  const message = error instanceof Error ? error.message : "Failed to create computer";
1719
2313
  if (spinner) {
1720
- const suffix = startTime ? ` ${chalk4.dim(`[${((Date.now() - startTime) / 1e3).toFixed(1)}s]`)}` : "";
2314
+ const suffix = startTime ? ` ${chalk6.dim(`[${((Date.now() - startTime) / 1e3).toFixed(1)}s]`)}` : "";
1721
2315
  spinner.fail(`${message}${suffix}`);
1722
2316
  } else {
1723
- console.error(chalk4.red(message));
2317
+ console.error(chalk6.red(message));
1724
2318
  }
1725
2319
  process.exit(1);
1726
2320
  }
1727
2321
  });
1728
2322
  async function resolveCreateOptions(options) {
1729
2323
  const selectedOptions = { ...options };
1730
- const hasExplicitRuntimeSelection = Boolean(selectedOptions.runtimeFamily) || Boolean(selectedOptions.imageRef) || Boolean(selectedOptions.sourceKind);
1731
- const shouldPrompt = Boolean(selectedOptions.interactive) || !hasExplicitRuntimeSelection;
2324
+ const shouldPrompt = Boolean(selectedOptions.interactive);
1732
2325
  if (!process.stdin.isTTY || !process.stdout.isTTY || !shouldPrompt) {
1733
2326
  validateCreateOptions(selectedOptions);
1734
2327
  return selectedOptions;
@@ -1750,9 +2343,9 @@ async function resolveCreateOptions(options) {
1750
2343
  if (runtimeChoice === "custom-machine") {
1751
2344
  selectedOptions.runtimeFamily = "custom-machine";
1752
2345
  selectedOptions.sourceKind = "oci-image";
1753
- selectedOptions.imageRef = (await textInput({ message: "OCI image ref (required):" })).trim();
1754
- selectedOptions.primaryPort = (await textInput({ message: "Primary port:", default: "3000" })).trim();
1755
- selectedOptions.primaryPath = (await textInput({ message: "Primary path:", default: "/" })).trim();
2346
+ selectedOptions.imageRef = (await textInput2({ message: "OCI image ref (required):" })).trim();
2347
+ selectedOptions.primaryPort = (await textInput2({ message: "Primary port:", default: "3000" })).trim();
2348
+ selectedOptions.primaryPath = (await textInput2({ message: "Primary path:", default: "/" })).trim();
1756
2349
  selectedOptions.healthcheckType = await select2({
1757
2350
  message: "Healthcheck type",
1758
2351
  choices: [
@@ -1761,7 +2354,7 @@ async function resolveCreateOptions(options) {
1761
2354
  ],
1762
2355
  default: "tcp"
1763
2356
  });
1764
- selectedOptions.healthcheckValue = (await textInput({ message: "Healthcheck value (optional):" })).trim();
2357
+ selectedOptions.healthcheckValue = (await textInput2({ message: "Healthcheck value (optional):" })).trim();
1765
2358
  selectedOptions.sshEnabled = await confirm({
1766
2359
  message: "Enable SSH?",
1767
2360
  default: false
@@ -1775,28 +2368,28 @@ async function resolveCreateOptions(options) {
1775
2368
  validateCreateOptions(selectedOptions);
1776
2369
  return selectedOptions;
1777
2370
  }
1778
- var removeCommand = new Command4("rm").description("Delete a computer").argument("<id-or-handle>", "Computer id or handle").option("-y, --yes", "Skip confirmation prompt").action(async (identifier, options, cmd) => {
2371
+ var removeCommand = new Command5("rm").description("Delete a computer").argument("<id-or-handle>", "Computer id or handle").option("-y, --yes", "Skip confirmation prompt").action(async (identifier, options, cmd) => {
1779
2372
  const globalYes = cmd.parent?.opts()?.yes;
1780
2373
  const skipConfirm = Boolean(options.yes || globalYes);
1781
- const spinner = ora3("Resolving computer...").start();
2374
+ const spinner = ora4("Resolving computer...").start();
1782
2375
  try {
1783
2376
  const computer = await resolveComputer(identifier);
1784
2377
  spinner.stop();
1785
2378
  if (!skipConfirm && process.stdin.isTTY) {
1786
2379
  const confirmed = await confirm({
1787
- message: `Delete computer ${chalk4.bold(computer.handle)}?`,
2380
+ message: `Delete computer ${chalk6.bold(computer.handle)}?`,
1788
2381
  default: false
1789
2382
  });
1790
2383
  if (!confirmed) {
1791
- console.log(chalk4.dim(" Cancelled."));
2384
+ console.log(chalk6.dim(" Cancelled."));
1792
2385
  return;
1793
2386
  }
1794
2387
  }
1795
- const deleteSpinner = ora3("Deleting computer...").start();
2388
+ const deleteSpinner = ora4("Deleting computer...").start();
1796
2389
  await api(`/v1/computers/${computer.id}`, {
1797
2390
  method: "DELETE"
1798
2391
  });
1799
- deleteSpinner.succeed(chalk4.green(`Deleted ${chalk4.bold(computer.handle)}`));
2392
+ deleteSpinner.succeed(chalk6.green(`Deleted ${chalk6.bold(computer.handle)}`));
1800
2393
  } catch (error) {
1801
2394
  spinner.fail(
1802
2395
  error instanceof Error ? error.message : "Failed to delete computer"
@@ -1836,17 +2429,33 @@ function createProvisioningNote(runtimeFamily, filesystemSettings) {
1836
2429
  }
1837
2430
  return "Using isolated storage for this desktop.";
1838
2431
  }
2432
+ function createMachineSourceNote(runtimeFamily, machineSourceSettings, usePlatformDefault) {
2433
+ if (runtimeFamily !== "managed-worker") {
2434
+ return null;
2435
+ }
2436
+ if (usePlatformDefault) {
2437
+ return "Using the AgentComputer platform default image for this machine.";
2438
+ }
2439
+ if (!machineSourceSettings) {
2440
+ return null;
2441
+ }
2442
+ if (machineSourceSettings.platform_default || !machineSourceSettings.default_machine_source) {
2443
+ return "Using the AgentComputer platform default image.";
2444
+ }
2445
+ return `Using managed-worker image source: ${summarizeMachineSourceSelection(machineSourceSettings)}.`;
2446
+ }
1839
2447
  function createSpinnerText(runtimeFamily, filesystemSettings, elapsedSeconds) {
1840
- const elapsedLabel = chalk4.dim(`${elapsedSeconds.toFixed(1)}s`);
2448
+ const elapsedLabel = chalk6.dim(`${elapsedSeconds.toFixed(1)}s`);
1841
2449
  if (runtimeFamily === "managed-worker" && filesystemSettings?.shared_enabled && elapsedSeconds >= 5) {
1842
- return `Creating computer... ${elapsedLabel} ${chalk4.dim("mounting shared home")}`;
2450
+ return `Creating computer... ${elapsedLabel} ${chalk6.dim("mounting shared home")}`;
1843
2451
  }
1844
2452
  return `Creating computer... ${elapsedLabel}`;
1845
2453
  }
1846
2454
  function validateCreateOptions(options) {
1847
2455
  const runtimeFamily = parseRuntimeFamilyOption(options.runtimeFamily);
1848
2456
  const sourceKind = parseSourceKindOption(options.sourceKind);
1849
- if (runtimeFamily === "custom-machine" && (typeof options.imageRef !== "string" || options.imageRef.trim() === "")) {
2457
+ const imageRef = options.imageRef?.trim();
2458
+ if (runtimeFamily === "custom-machine" && !imageRef) {
1850
2459
  throw new Error("--image-ref is required for --runtime-family custom-machine");
1851
2460
  }
1852
2461
  if (runtimeFamily === "custom-machine" && sourceKind === "none") {
@@ -1855,6 +2464,12 @@ function validateCreateOptions(options) {
1855
2464
  if (runtimeFamily === "custom-machine" && options.vncEnabled) {
1856
2465
  throw new Error("custom-machine does not support platform VNC");
1857
2466
  }
2467
+ if (runtimeFamily === "custom-machine" && options.usePlatformDefault) {
2468
+ throw new Error("--use-platform-default cannot be used with --runtime-family custom-machine");
2469
+ }
2470
+ if (imageRef && options.usePlatformDefault) {
2471
+ throw new Error("choose either --image-ref or --use-platform-default");
2472
+ }
1858
2473
  }
1859
2474
  function parseSourceKindOption(value) {
1860
2475
  switch (value) {
@@ -1900,7 +2515,7 @@ function resolveOptionalToggle(enabled, disabled, label) {
1900
2515
  }
1901
2516
 
1902
2517
  // src/commands/completion.ts
1903
- import { Command as Command5 } from "commander";
2518
+ import { Command as Command6 } from "commander";
1904
2519
  var ZSH_SCRIPT = `#compdef computer agentcomputer aicomputer
1905
2520
 
1906
2521
  _computer() {
@@ -1909,9 +2524,12 @@ _computer() {
1909
2524
  'login:Authenticate the CLI'
1910
2525
  'logout:Remove stored API key'
1911
2526
  'whoami:Show current user'
2527
+ 'claude-auth:Authenticate Claude Code on a computer'
2528
+ 'claude-login:Alias for claude-auth'
1912
2529
  'create:Create a computer'
1913
2530
  'ls:List computers'
1914
2531
  'get:Show computer details'
2532
+ 'image:Manage machine image sources'
1915
2533
  'open:Open in browser'
1916
2534
  'ssh:SSH into a computer'
1917
2535
  'ports:Manage published ports'
@@ -1930,6 +2548,15 @@ _computer() {
1930
2548
  'rm:Unpublish an app port'
1931
2549
  )
1932
2550
 
2551
+ local -a image_commands
2552
+ image_commands=(
2553
+ 'ls:List machine image sources'
2554
+ 'save:Create or update a machine image source'
2555
+ 'default:Set the default machine image source'
2556
+ 'rebuild:Rebuild a machine image source'
2557
+ 'rm:Delete a machine image source'
2558
+ )
2559
+
1933
2560
  _arguments -C \\
1934
2561
  '(-h --help)'{-h,--help}'[Display help]' \\
1935
2562
  '(-V --version)'{-V,--version}'[Show version]' \\
@@ -1951,6 +2578,12 @@ _computer() {
1951
2578
  whoami)
1952
2579
  _arguments '--json[Print raw JSON]'
1953
2580
  ;;
2581
+ claude-auth|claude-login)
2582
+ _arguments \\
2583
+ '--machine[Use a specific computer]:computer:_computer_handles' \\
2584
+ '--keep-helper[Keep a temporary helper machine]' \\
2585
+ '--skip-cross-check[Skip second-machine verification]'
2586
+ ;;
1954
2587
  create)
1955
2588
  _arguments \\
1956
2589
  '--name[Display name]:name:' \\
@@ -1960,6 +2593,7 @@ _computer() {
1960
2593
  '--source-kind[Source kind]:kind:(none oci-image)' \\
1961
2594
  '--image-family[Image family]:family:' \\
1962
2595
  '--image-ref[Image reference]:image:' \\
2596
+ '--use-platform-default[Use platform default image]' \\
1963
2597
  '--primary-port[Primary app port]:port:' \\
1964
2598
  '--primary-path[Primary app path]:path:' \\
1965
2599
  '--healthcheck-type[Healthcheck type]:type:(http tcp)' \\
@@ -1980,6 +2614,50 @@ _computer() {
1980
2614
  '--json[Print raw JSON]' \\
1981
2615
  '1:computer:_computer_handles'
1982
2616
  ;;
2617
+ image)
2618
+ _arguments -C \\
2619
+ '1:command:->image_command' \\
2620
+ '*::arg:->image_args'
2621
+ case "$state" in
2622
+ image_command)
2623
+ _describe -t commands 'image command' image_commands
2624
+ ;;
2625
+ image_args)
2626
+ case "$words[2]" in
2627
+ ls)
2628
+ _arguments '--json[Print raw JSON]'
2629
+ ;;
2630
+ save)
2631
+ _arguments \\
2632
+ '--id[source id]:id:' \\
2633
+ '--kind[source kind]:kind:(oci-image nix-git)' \\
2634
+ '--requested-ref[OCI image ref or resolved ref]:ref:' \\
2635
+ '--git-url[Git URL]:url:' \\
2636
+ '--git-ref[Git ref]:ref:' \\
2637
+ '--git-subpath[Git subpath]:path:' \\
2638
+ '--set-as-default[Select as default after saving]' \\
2639
+ '--json[Print raw JSON]'
2640
+ ;;
2641
+ default)
2642
+ _arguments \\
2643
+ '--json[Print raw JSON]' \\
2644
+ '1:source id:_machine_source_ids'
2645
+ ;;
2646
+ rebuild)
2647
+ _arguments \\
2648
+ '--json[Print raw JSON]' \\
2649
+ '1:source id:_machine_source_ids'
2650
+ ;;
2651
+ rm)
2652
+ _arguments \\
2653
+ '--json[Print raw JSON]' \\
2654
+ '(-y --yes)'{-y,--yes}'[Skip confirmation]' \\
2655
+ '1:source id:_machine_source_ids'
2656
+ ;;
2657
+ esac
2658
+ ;;
2659
+ esac
2660
+ ;;
1983
2661
  open)
1984
2662
  _arguments \\
1985
2663
  '--vnc[Open VNC desktop]' \\
@@ -2039,13 +2717,22 @@ _computer_handles() {
2039
2717
  fi
2040
2718
  }
2041
2719
 
2720
+ _machine_source_ids() {
2721
+ local -a ids
2722
+ local cli="\${words[1]:-computer}"
2723
+ if ids=(\${(f)"$(\${cli} image ls --json 2>/dev/null | grep '"id"' | sed 's/.*"id": "\\([^"]*\\)".*/\\1/')"}); then
2724
+ _describe -t ids 'machine image source' ids
2725
+ fi
2726
+ }
2727
+
2042
2728
  _computer "$@"`;
2043
2729
  var BASH_SCRIPT = `_computer() {
2044
2730
  local cur prev words cword
2045
2731
  _init_completion || return
2046
2732
 
2047
- local commands="login logout whoami create ls get open ssh ports agent fleet acp rm completion help"
2733
+ local commands="login logout whoami claude-auth claude-login create ls get image open ssh ports agent fleet acp rm completion help"
2048
2734
  local ports_commands="ls publish rm"
2735
+ local image_commands="ls save default rebuild rm"
2049
2736
 
2050
2737
  if [[ $cword -eq 1 ]]; then
2051
2738
  COMPREPLY=($(compgen -W "$commands" -- "$cur"))
@@ -2061,12 +2748,60 @@ var BASH_SCRIPT = `_computer() {
2061
2748
  whoami)
2062
2749
  COMPREPLY=($(compgen -W "--json" -- "$cur"))
2063
2750
  ;;
2751
+ claude-auth|claude-login)
2752
+ COMPREPLY=($(compgen -W "--machine --keep-helper --skip-cross-check" -- "$cur"))
2753
+ ;;
2064
2754
  create)
2065
- COMPREPLY=($(compgen -W "--name --tier --interactive --runtime-family --source-kind --image-family --image-ref --primary-port --primary-path --healthcheck-type --healthcheck-value --ssh-enabled --ssh-disabled --vnc-enabled --vnc-disabled" -- "$cur"))
2755
+ COMPREPLY=($(compgen -W "--name --tier --interactive --runtime-family --source-kind --image-family --image-ref --use-platform-default --primary-port --primary-path --healthcheck-type --healthcheck-value --ssh-enabled --ssh-disabled --vnc-enabled --vnc-disabled" -- "$cur"))
2066
2756
  ;;
2067
2757
  ls)
2068
2758
  COMPREPLY=($(compgen -W "--json --verbose -v" -- "$cur"))
2069
2759
  ;;
2760
+ image)
2761
+ if [[ $cword -eq 2 ]]; then
2762
+ COMPREPLY=($(compgen -W "$image_commands" -- "$cur"))
2763
+ else
2764
+ case "\${words[2]}" in
2765
+ ls)
2766
+ COMPREPLY=($(compgen -W "--json" -- "$cur"))
2767
+ ;;
2768
+ save)
2769
+ COMPREPLY=($(compgen -W "--id --kind --requested-ref --git-url --git-ref --git-subpath --set-as-default --json" -- "$cur"))
2770
+ ;;
2771
+ default|rebuild|rm)
2772
+ case "\${words[2]}" in
2773
+ default)
2774
+ if [[ "$cur" == -* ]]; then
2775
+ COMPREPLY=($(compgen -W "--json" -- "$cur"))
2776
+ else
2777
+ local source_ids cli="\${words[0]:-computer}"
2778
+ source_ids=$(\${cli} image ls --json 2>/dev/null | grep '"id"' | sed 's/.*"id": "([^"]*)".*/\\1/')
2779
+ COMPREPLY=($(compgen -W "$source_ids" -- "$cur"))
2780
+ fi
2781
+ ;;
2782
+ rebuild)
2783
+ if [[ "$cur" == -* ]]; then
2784
+ COMPREPLY=($(compgen -W "--json" -- "$cur"))
2785
+ else
2786
+ local source_ids cli="\${words[0]:-computer}"
2787
+ source_ids=$(\${cli} image ls --json 2>/dev/null | grep '"id"' | sed 's/.*"id": "([^"]*)".*/\\1/')
2788
+ COMPREPLY=($(compgen -W "$source_ids" -- "$cur"))
2789
+ fi
2790
+ ;;
2791
+ rm)
2792
+ if [[ "$cur" == -* ]]; then
2793
+ COMPREPLY=($(compgen -W "--json --yes -y" -- "$cur"))
2794
+ else
2795
+ local source_ids cli="\${words[0]:-computer}"
2796
+ source_ids=$(\${cli} image ls --json 2>/dev/null | grep '"id"' | sed 's/.*"id": "([^"]*)".*/\\1/')
2797
+ COMPREPLY=($(compgen -W "$source_ids" -- "$cur"))
2798
+ fi
2799
+ ;;
2800
+ esac
2801
+ ;;
2802
+ esac
2803
+ fi
2804
+ ;;
2070
2805
  get|open|ssh|rm)
2071
2806
  if [[ $cword -eq 2 ]]; then
2072
2807
  local handles cli="\${words[0]:-computer}"
@@ -2098,7 +2833,7 @@ var BASH_SCRIPT = `_computer() {
2098
2833
  }
2099
2834
 
2100
2835
  complete -F _computer computer agentcomputer aicomputer`;
2101
- var completionCommand = new Command5("completion").description("Generate shell completions").argument("<shell>", "Shell type (bash or zsh)").action((shell) => {
2836
+ var completionCommand = new Command6("completion").description("Generate shell completions").argument("<shell>", "Shell type (bash or zsh)").action((shell) => {
2102
2837
  switch (shell) {
2103
2838
  case "zsh":
2104
2839
  console.log(ZSH_SCRIPT);
@@ -2112,19 +2847,299 @@ var completionCommand = new Command5("completion").description("Generate shell c
2112
2847
  }
2113
2848
  });
2114
2849
 
2850
+ // src/commands/images.ts
2851
+ import { confirm as confirm2, input as textInput3, select as select3 } from "@inquirer/prompts";
2852
+ import { Command as Command7 } from "commander";
2853
+ import chalk7 from "chalk";
2854
+ import ora5 from "ora";
2855
+ var imageCommand = new Command7("image").description("Manage machine image sources");
2856
+ imageCommand.command("ls").description("List machine image sources").option("--json", "Print raw JSON").action(async (options) => {
2857
+ const spinner = options.json ? null : ora5("Fetching machine images...").start();
2858
+ try {
2859
+ const settings = await getMachineSourceSettings();
2860
+ spinner?.stop();
2861
+ if (options.json) {
2862
+ console.log(JSON.stringify(settings, null, 2));
2863
+ return;
2864
+ }
2865
+ printMachineSourceSettings(settings);
2866
+ } catch (error) {
2867
+ if (spinner) {
2868
+ spinner.fail(error instanceof Error ? error.message : "Failed to fetch machine images");
2869
+ } else {
2870
+ console.error(error instanceof Error ? error.message : "Failed to fetch machine images");
2871
+ }
2872
+ process.exit(1);
2873
+ }
2874
+ });
2875
+ imageCommand.command("save").description("Create or update a machine image source").option("--id <id>", "Existing source id to update").option("--kind <kind>", "oci-image or nix-git").option("--requested-ref <ref>", "OCI image ref or explicit resolved ref").option("--git-url <url>", "Repository URL for a Nix git source").option("--git-ref <ref>", "Git ref for a Nix git source").option("--git-subpath <path>", "Subdirectory for a Nix git source").option("--set-as-default", "Select this source as the default after saving").option("--json", "Print raw JSON").action(async (options) => {
2876
+ const spinner = options.json ? null : ora5("Saving machine image source...").start();
2877
+ try {
2878
+ const input = await resolveSaveInput(options);
2879
+ const settings = await upsertMachineSource(input);
2880
+ spinner?.stop();
2881
+ if (options.json) {
2882
+ console.log(JSON.stringify(settings, null, 2));
2883
+ return;
2884
+ }
2885
+ console.log();
2886
+ console.log(chalk7.green("Saved machine image source."));
2887
+ printMachineSourceSettings(settings);
2888
+ } catch (error) {
2889
+ if (spinner) {
2890
+ spinner.fail(error instanceof Error ? error.message : "Failed to save machine image source");
2891
+ } else {
2892
+ console.error(error instanceof Error ? error.message : "Failed to save machine image source");
2893
+ }
2894
+ process.exit(1);
2895
+ }
2896
+ });
2897
+ imageCommand.command("default").description("Set the default machine image source").argument("[source-id]", "Source id to select; use 'platform' or omit for the platform default").option("--json", "Print raw JSON").action(async (sourceID, options) => {
2898
+ const usePlatformDefault = !sourceID || sourceID === "platform";
2899
+ const spinner = options.json ? null : ora5("Updating machine image default...").start();
2900
+ try {
2901
+ let settings;
2902
+ if (usePlatformDefault) {
2903
+ settings = await clearMachineSourceDefault();
2904
+ } else {
2905
+ const current = await getMachineSourceSettings();
2906
+ const source = current.sources.find((entry) => entry.id === sourceID);
2907
+ if (!source) {
2908
+ throw new Error(`machine image source '${sourceID}' not found`);
2909
+ }
2910
+ settings = await upsertMachineSource({
2911
+ id: source.id,
2912
+ kind: source.kind,
2913
+ set_as_default: true
2914
+ });
2915
+ }
2916
+ spinner?.stop();
2917
+ if (options.json) {
2918
+ console.log(JSON.stringify(settings, null, 2));
2919
+ return;
2920
+ }
2921
+ console.log();
2922
+ if (!usePlatformDefault) {
2923
+ const selected = settings.default_machine_source ?? void 0;
2924
+ const label = selected ? summarizeMachineSource(selected) : sourceID;
2925
+ console.log(chalk7.green(`Selected ${chalk7.bold(label)} as the default machine image.`));
2926
+ } else {
2927
+ console.log(chalk7.green("Using the AgentComputer platform default image."));
2928
+ }
2929
+ printMachineSourceSettings(settings);
2930
+ } catch (error) {
2931
+ if (spinner) {
2932
+ spinner.fail(error instanceof Error ? error.message : "Failed to update machine image default");
2933
+ } else {
2934
+ console.error(error instanceof Error ? error.message : "Failed to update machine image default");
2935
+ }
2936
+ process.exit(1);
2937
+ }
2938
+ });
2939
+ imageCommand.command("rebuild").description("Rebuild a machine image source").argument("<source-id>", "Source id to rebuild").option("--json", "Print raw JSON").action(async (sourceID, options) => {
2940
+ const spinner = options.json ? null : ora5("Queueing machine image rebuild...").start();
2941
+ try {
2942
+ const settings = await rebuildMachineSource(sourceID);
2943
+ spinner?.stop();
2944
+ if (options.json) {
2945
+ console.log(JSON.stringify(settings, null, 2));
2946
+ return;
2947
+ }
2948
+ console.log();
2949
+ console.log(chalk7.green(`Queued rebuild for ${chalk7.bold(sourceID)}.`));
2950
+ printMachineSourceSettings(settings);
2951
+ } catch (error) {
2952
+ if (spinner) {
2953
+ spinner.fail(error instanceof Error ? error.message : "Failed to rebuild machine image source");
2954
+ } else {
2955
+ console.error(error instanceof Error ? error.message : "Failed to rebuild machine image source");
2956
+ }
2957
+ process.exit(1);
2958
+ }
2959
+ });
2960
+ imageCommand.command("rm").description("Delete a machine image source").argument("<source-id>", "Source id to delete").option("-y, --yes", "Skip confirmation prompt").option("--json", "Print raw JSON").action(async (sourceID, options, cmd) => {
2961
+ const globalYes = cmd.parent?.parent?.opts()?.yes;
2962
+ const skipConfirm = Boolean(options.yes || globalYes);
2963
+ let spinner = null;
2964
+ try {
2965
+ if (!skipConfirm && process.stdin.isTTY) {
2966
+ const confirmed = await confirmDeletion(sourceID);
2967
+ if (!confirmed) {
2968
+ console.log(chalk7.dim(" Cancelled."));
2969
+ return;
2970
+ }
2971
+ }
2972
+ spinner = options.json ? null : ora5("Deleting machine image source...").start();
2973
+ const settings = await deleteMachineSource(sourceID);
2974
+ spinner?.stop();
2975
+ if (options.json) {
2976
+ console.log(JSON.stringify(settings, null, 2));
2977
+ return;
2978
+ }
2979
+ console.log();
2980
+ console.log(chalk7.green(`Deleted machine image source ${chalk7.bold(sourceID)}.`));
2981
+ printMachineSourceSettings(settings);
2982
+ } catch (error) {
2983
+ if (spinner) {
2984
+ spinner.fail(error instanceof Error ? error.message : "Failed to delete machine image source");
2985
+ } else {
2986
+ console.error(error instanceof Error ? error.message : "Failed to delete machine image source");
2987
+ }
2988
+ process.exit(1);
2989
+ }
2990
+ });
2991
+ function printMachineSourceSettings(settings) {
2992
+ console.log(` ${chalk7.dim("Default")} ${chalk7.white(summarizeMachineSourceSelection(settings))}`);
2993
+ console.log();
2994
+ if (settings.sources.length === 0) {
2995
+ console.log(chalk7.dim(" No custom machine images configured yet."));
2996
+ console.log();
2997
+ return;
2998
+ }
2999
+ for (const source of settings.sources) {
3000
+ printMachineSourceCard(source, settings.default_machine_source_id === source.id);
3001
+ }
3002
+ }
3003
+ function printMachineSourceCard(source, isDefault) {
3004
+ console.log(` ${chalk7.bold(machineSourceTitle(source))}${isDefault ? chalk7.green(" (default)") : ""}`);
3005
+ console.log(` ${chalk7.dim(" ID")} ${source.id}`);
3006
+ console.log(` ${chalk7.dim(" Kind")} ${source.kind}`);
3007
+ console.log(` ${chalk7.dim(" Status")} ${source.status}${source.latest_build ? ` | latest build ${source.latest_build.status}` : ""}`);
3008
+ if (source.resolved_image_ref) {
3009
+ console.log(` ${chalk7.dim(" Resolved")} ${source.resolved_image_ref}`);
3010
+ }
3011
+ if (source.error) {
3012
+ console.log(` ${chalk7.dim(" Error")} ${chalk7.red(source.error)}`);
3013
+ }
3014
+ console.log(` ${chalk7.dim(" Source")} ${summarizeMachineSource(source)}`);
3015
+ console.log();
3016
+ }
3017
+ function machineSourceTitle(source) {
3018
+ if (source.kind === "oci-image") {
3019
+ return source.requested_ref || "OCI image";
3020
+ }
3021
+ return source.git_url || "Nix git source";
3022
+ }
3023
+ function parseMachineSourceKind(value) {
3024
+ switch (value) {
3025
+ case void 0:
3026
+ case "oci-image":
3027
+ case "nix-git":
3028
+ return value;
3029
+ default:
3030
+ throw new Error("--kind must be oci-image or nix-git");
3031
+ }
3032
+ }
3033
+ function hasTTY() {
3034
+ return process.stdin.isTTY && process.stdout.isTTY;
3035
+ }
3036
+ async function resolveSaveInput(options) {
3037
+ const existing = await loadExistingSource(options.id);
3038
+ const selectedKind = parseMachineSourceKind(options.kind) ?? existing?.kind;
3039
+ let kind = selectedKind;
3040
+ if (!kind) {
3041
+ if (!hasTTY()) {
3042
+ throw new Error("--kind is required");
3043
+ }
3044
+ kind = await selectMachineSourceKind();
3045
+ }
3046
+ const input = {
3047
+ id: options.id,
3048
+ kind,
3049
+ set_as_default: options.setAsDefault
3050
+ };
3051
+ switch (kind) {
3052
+ case "oci-image": {
3053
+ const existingSameKind = existing?.kind === kind;
3054
+ const requestedRef = normalizeValue(options.requestedRef) ?? (existingSameKind ? existing?.requested_ref : void 0);
3055
+ if (!requestedRef) {
3056
+ if (!hasTTY()) {
3057
+ throw new Error("--requested-ref is required for oci-image sources");
3058
+ }
3059
+ const promptedRef = normalizeValue(await textInput3({ message: "OCI image ref:" }));
3060
+ if (!promptedRef) {
3061
+ throw new Error("OCI image ref is required");
3062
+ }
3063
+ input.requested_ref = promptedRef;
3064
+ break;
3065
+ }
3066
+ input.requested_ref = requestedRef;
3067
+ break;
3068
+ }
3069
+ case "nix-git": {
3070
+ const existingSameKind = existing?.kind === kind;
3071
+ const gitUrl = normalizeValue(options.gitUrl) ?? (existingSameKind ? existing?.git_url : void 0);
3072
+ const gitRef = normalizeValue(options.gitRef) ?? (existingSameKind ? existing?.git_ref : void 0);
3073
+ const gitSubpath = normalizeValue(options.gitSubpath) ?? (existingSameKind ? existing?.git_subpath : void 0);
3074
+ if (!gitUrl) {
3075
+ if (!hasTTY()) {
3076
+ throw new Error("--git-url is required for nix-git sources");
3077
+ }
3078
+ const promptedGitURL = normalizeValue(await textInput3({ message: "Git URL:" }));
3079
+ if (!promptedGitURL) {
3080
+ throw new Error("Git URL is required");
3081
+ }
3082
+ input.git_url = promptedGitURL;
3083
+ break;
3084
+ }
3085
+ input.git_url = gitUrl;
3086
+ if (gitRef) {
3087
+ input.git_ref = gitRef;
3088
+ }
3089
+ if (gitSubpath) {
3090
+ input.git_subpath = gitSubpath;
3091
+ }
3092
+ break;
3093
+ }
3094
+ }
3095
+ return input;
3096
+ }
3097
+ async function selectMachineSourceKind() {
3098
+ const kind = await select3({
3099
+ message: "Select machine image kind",
3100
+ choices: [
3101
+ { name: "oci-image - resolve an OCI image digest", value: "oci-image" },
3102
+ { name: "nix-git - build a Nix source into OCI", value: "nix-git" }
3103
+ ],
3104
+ default: "oci-image"
3105
+ });
3106
+ return kind;
3107
+ }
3108
+ async function loadExistingSource(sourceID) {
3109
+ if (!sourceID) {
3110
+ return void 0;
3111
+ }
3112
+ const settings = await getMachineSourceSettings();
3113
+ const source = settings.sources.find((entry) => entry.id === sourceID);
3114
+ if (!source) {
3115
+ throw new Error(`machine image source '${sourceID}' not found`);
3116
+ }
3117
+ return source;
3118
+ }
3119
+ function normalizeValue(value) {
3120
+ const trimmed = value?.trim();
3121
+ return trimmed ? trimmed : void 0;
3122
+ }
3123
+ async function confirmDeletion(sourceID) {
3124
+ return confirm2({
3125
+ message: `Delete machine image source ${sourceID}?`,
3126
+ default: false
3127
+ });
3128
+ }
3129
+
2115
3130
  // src/commands/login.ts
2116
- import { Command as Command6 } from "commander";
2117
- import chalk5 from "chalk";
2118
- import ora4 from "ora";
3131
+ import { Command as Command8 } from "commander";
3132
+ import chalk8 from "chalk";
3133
+ import ora6 from "ora";
2119
3134
 
2120
3135
  // src/lib/browser-login.ts
2121
- import { randomBytes } from "crypto";
3136
+ import { randomBytes as randomBytes2 } from "crypto";
2122
3137
  import { createServer } from "http";
2123
3138
  var CALLBACK_HOST = "127.0.0.1";
2124
3139
  var CALLBACK_PATH = "/callback";
2125
3140
  var LOGIN_TIMEOUT_MS = 5 * 60 * 1e3;
2126
3141
  async function createBrowserLoginAttempt() {
2127
- const state = randomBytes(16).toString("hex");
3142
+ const state = randomBytes2(16).toString("hex");
2128
3143
  const deferred = createDeferred();
2129
3144
  let callbackURL = "";
2130
3145
  let closed = false;
@@ -2343,11 +3358,11 @@ function escapeHTML(value) {
2343
3358
  }
2344
3359
 
2345
3360
  // src/commands/login.ts
2346
- var loginCommand = new Command6("login").description("Authenticate the CLI").option("--api-key <key>", "API key starting with ac_live_").option("--stdin", "Read the API key from stdin").option("-f, --force", "Overwrite an existing stored API key").action(async (options) => {
3361
+ var loginCommand = new Command8("login").description("Authenticate the CLI").option("--api-key <key>", "API key starting with ac_live_").option("--stdin", "Read the API key from stdin").option("-f, --force", "Overwrite an existing stored API key").action(async (options) => {
2347
3362
  const existingKey = getStoredAPIKey();
2348
3363
  if (existingKey && !options.force) {
2349
3364
  console.log();
2350
- console.log(chalk5.yellow(" Already logged in. Use --force to overwrite."));
3365
+ console.log(chalk8.yellow(" Already logged in. Use --force to overwrite."));
2351
3366
  console.log();
2352
3367
  return;
2353
3368
  }
@@ -2355,8 +3370,8 @@ var loginCommand = new Command6("login").description("Authenticate the CLI").opt
2355
3370
  const apiKey = await resolveAPIKeyInput(options.apiKey, options.stdin);
2356
3371
  if (!apiKey && wantsManualLogin) {
2357
3372
  console.log();
2358
- console.log(chalk5.dim(" Usage: computer login --api-key <ac_live_...>"));
2359
- console.log(chalk5.dim(` API: ${getBaseURL()}`));
3373
+ console.log(chalk8.dim(" Usage: computer login --api-key <ac_live_...>"));
3374
+ console.log(chalk8.dim(` API: ${getBaseURL()}`));
2360
3375
  console.log();
2361
3376
  process.exit(1);
2362
3377
  }
@@ -2366,15 +3381,15 @@ var loginCommand = new Command6("login").description("Authenticate the CLI").opt
2366
3381
  }
2367
3382
  if (!apiKey.startsWith("ac_live_")) {
2368
3383
  console.log();
2369
- console.log(chalk5.red(" API key must start with ac_live_"));
3384
+ console.log(chalk8.red(" API key must start with ac_live_"));
2370
3385
  console.log();
2371
3386
  process.exit(1);
2372
3387
  }
2373
- const spinner = ora4("Authenticating...").start();
3388
+ const spinner = ora6("Authenticating...").start();
2374
3389
  try {
2375
3390
  const me = await apiWithKey(apiKey, "/v1/me");
2376
3391
  setAPIKey(apiKey);
2377
- spinner.succeed(`Logged in as ${chalk5.bold(me.user.email)}`);
3392
+ spinner.succeed(`Logged in as ${chalk8.bold(me.user.email)}`);
2378
3393
  } catch (error) {
2379
3394
  spinner.fail(
2380
3395
  error instanceof Error ? error.message : "Failed to validate API key"
@@ -2383,7 +3398,7 @@ var loginCommand = new Command6("login").description("Authenticate the CLI").opt
2383
3398
  }
2384
3399
  });
2385
3400
  async function runBrowserLogin() {
2386
- const spinner = ora4("Starting browser login...").start();
3401
+ const spinner = ora6("Starting browser login...").start();
2387
3402
  let attempt = null;
2388
3403
  try {
2389
3404
  attempt = await createBrowserLoginAttempt();
@@ -2393,14 +3408,14 @@ async function runBrowserLogin() {
2393
3408
  } catch {
2394
3409
  spinner.stop();
2395
3410
  console.log();
2396
- console.log(chalk5.yellow(" Browser auto-open failed. Open this URL to continue:"));
2397
- console.log(chalk5.dim(` ${attempt.loginURL}`));
3411
+ console.log(chalk8.yellow(" Browser auto-open failed. Open this URL to continue:"));
3412
+ console.log(chalk8.dim(` ${attempt.loginURL}`));
2398
3413
  console.log();
2399
3414
  spinner.start("Waiting for browser login...");
2400
3415
  }
2401
3416
  spinner.text = "Waiting for browser login...";
2402
3417
  const result = await attempt.waitForResult();
2403
- spinner.succeed(`Logged in as ${chalk5.bold(result.me.user.email)}`);
3418
+ spinner.succeed(`Logged in as ${chalk8.bold(result.me.user.email)}`);
2404
3419
  } catch (error) {
2405
3420
  spinner.fail(error instanceof Error ? error.message : "Browser login failed");
2406
3421
  process.exit(1);
@@ -2426,33 +3441,33 @@ async function resolveAPIKeyInput(flagValue, readFromStdin) {
2426
3441
  }
2427
3442
 
2428
3443
  // src/commands/logout.ts
2429
- import { Command as Command7 } from "commander";
2430
- import chalk6 from "chalk";
2431
- var logoutCommand = new Command7("logout").description("Remove stored API key").action(() => {
3444
+ import { Command as Command9 } from "commander";
3445
+ import chalk9 from "chalk";
3446
+ var logoutCommand = new Command9("logout").description("Remove stored API key").action(() => {
2432
3447
  if (!getStoredAPIKey()) {
2433
3448
  console.log();
2434
- console.log(chalk6.dim(" Not logged in."));
3449
+ console.log(chalk9.dim(" Not logged in."));
2435
3450
  if (hasEnvAPIKey()) {
2436
- console.log(chalk6.dim(" Environment API key is still active in this shell."));
3451
+ console.log(chalk9.dim(" Environment API key is still active in this shell."));
2437
3452
  }
2438
3453
  console.log();
2439
3454
  return;
2440
3455
  }
2441
3456
  clearAPIKey();
2442
3457
  console.log();
2443
- console.log(chalk6.green(" Logged out."));
3458
+ console.log(chalk9.green(" Logged out."));
2444
3459
  if (hasEnvAPIKey()) {
2445
- console.log(chalk6.dim(" Environment API key is still active in this shell."));
3460
+ console.log(chalk9.dim(" Environment API key is still active in this shell."));
2446
3461
  }
2447
3462
  console.log();
2448
3463
  });
2449
3464
 
2450
3465
  // src/commands/whoami.ts
2451
- import { Command as Command8 } from "commander";
2452
- import chalk7 from "chalk";
2453
- import ora5 from "ora";
2454
- var whoamiCommand = new Command8("whoami").description("Show current user").option("--json", "Print raw JSON").action(async (options) => {
2455
- const spinner = options.json ? null : ora5("Loading user...").start();
3466
+ import { Command as Command10 } from "commander";
3467
+ import chalk10 from "chalk";
3468
+ import ora7 from "ora";
3469
+ var whoamiCommand = new Command10("whoami").description("Show current user").option("--json", "Print raw JSON").action(async (options) => {
3470
+ const spinner = options.json ? null : ora7("Loading user...").start();
2456
3471
  try {
2457
3472
  const me = await api("/v1/me");
2458
3473
  spinner?.stop();
@@ -2461,14 +3476,14 @@ var whoamiCommand = new Command8("whoami").description("Show current user").opti
2461
3476
  return;
2462
3477
  }
2463
3478
  console.log();
2464
- console.log(` ${chalk7.bold.white(me.user.display_name || me.user.email)}`);
3479
+ console.log(` ${chalk10.bold.white(me.user.display_name || me.user.email)}`);
2465
3480
  if (me.user.display_name) {
2466
- console.log(` ${chalk7.dim(me.user.email)}`);
3481
+ console.log(` ${chalk10.dim(me.user.email)}`);
2467
3482
  }
2468
3483
  if (me.api_key.name) {
2469
- console.log(` ${chalk7.dim("Key:")} ${me.api_key.name}`);
3484
+ console.log(` ${chalk10.dim("Key:")} ${me.api_key.name}`);
2470
3485
  }
2471
- console.log(` ${chalk7.dim("API:")} ${chalk7.dim(getBaseURL())}`);
3486
+ console.log(` ${chalk10.dim("API:")} ${chalk10.dim(getBaseURL())}`);
2472
3487
  console.log();
2473
3488
  } catch (error) {
2474
3489
  if (spinner) {
@@ -2485,15 +3500,15 @@ var pkg2 = JSON.parse(
2485
3500
  readFileSync3(new URL("../package.json", import.meta.url), "utf8")
2486
3501
  );
2487
3502
  var cliName = process.argv[1] ? basename2(process.argv[1]) : "agentcomputer";
2488
- var program = new Command9();
3503
+ var program = new Command11();
2489
3504
  function appendTextSection(lines, title, values) {
2490
3505
  if (values.length === 0) {
2491
3506
  return;
2492
3507
  }
2493
- lines.push(` ${chalk8.dim(title)}`);
3508
+ lines.push(` ${chalk11.dim(title)}`);
2494
3509
  lines.push("");
2495
3510
  for (const value of values) {
2496
- lines.push(` ${chalk8.white(value)}`);
3511
+ lines.push(` ${chalk11.white(value)}`);
2497
3512
  }
2498
3513
  lines.push("");
2499
3514
  }
@@ -2502,10 +3517,10 @@ function appendTableSection(lines, title, entries) {
2502
3517
  return;
2503
3518
  }
2504
3519
  const width = Math.max(...entries.map((entry) => entry.term.length), 0) + 2;
2505
- lines.push(` ${chalk8.dim(title)}`);
3520
+ lines.push(` ${chalk11.dim(title)}`);
2506
3521
  lines.push("");
2507
3522
  for (const entry of entries) {
2508
- lines.push(` ${chalk8.white(padEnd(entry.term, width))}${chalk8.dim(entry.desc)}`);
3523
+ lines.push(` ${chalk11.white(padEnd(entry.term, width))}${chalk11.dim(entry.desc)}`);
2509
3524
  }
2510
3525
  lines.push("");
2511
3526
  }
@@ -2524,29 +3539,32 @@ function formatRootHelp(cmd) {
2524
3539
  const groups = [
2525
3540
  ["Auth", []],
2526
3541
  ["Computers", []],
3542
+ ["Images", []],
2527
3543
  ["Access", []],
2528
3544
  ["Agents", []],
2529
3545
  ["Other", []]
2530
3546
  ];
2531
3547
  const otherGroup = groups.find(([name]) => name === "Other")[1];
2532
- lines.push(`${chalk8.bold(cliName)} ${chalk8.dim(`v${version}`)}`);
3548
+ lines.push(`${chalk11.bold(cliName)} ${chalk11.dim(`v${version}`)}`);
2533
3549
  lines.push("");
2534
3550
  if (cmd.description()) {
2535
- lines.push(` ${chalk8.dim(cmd.description())}`);
3551
+ lines.push(` ${chalk11.dim(cmd.description())}`);
2536
3552
  lines.push("");
2537
3553
  }
2538
3554
  appendTextSection(lines, "Usage", [`${cliName} <command> [options]`]);
2539
3555
  for (const sub of cmd.commands) {
2540
3556
  const name = sub.name();
2541
3557
  const entry = { term: name, desc: sub.description() };
2542
- if (["login", "logout", "whoami"].includes(name)) {
3558
+ if (["login", "logout", "whoami", "claude-auth"].includes(name)) {
2543
3559
  groups[0][1].push(entry);
2544
3560
  } else if (["create", "ls", "get", "rm"].includes(name)) {
2545
3561
  groups[1][1].push(entry);
2546
- } else if (["open", "ssh", "ports"].includes(name)) {
3562
+ } else if (name === "image") {
2547
3563
  groups[2][1].push(entry);
2548
- } else if (["agent", "fleet", "acp"].includes(name)) {
3564
+ } else if (["open", "ssh", "ports"].includes(name)) {
2549
3565
  groups[3][1].push(entry);
3566
+ } else if (["agent", "fleet", "acp"].includes(name)) {
3567
+ groups[4][1].push(entry);
2550
3568
  } else {
2551
3569
  otherGroup.push(entry);
2552
3570
  }
@@ -2581,10 +3599,10 @@ function formatSubcommandHelp(cmd, helper) {
2581
3599
  term: helper.optionTerm(option),
2582
3600
  desc: helper.optionDescription(option)
2583
3601
  }));
2584
- lines.push(chalk8.bold(commandPath(cmd)));
3602
+ lines.push(chalk11.bold(commandPath(cmd)));
2585
3603
  lines.push("");
2586
3604
  if (description) {
2587
- lines.push(` ${chalk8.dim(description)}`);
3605
+ lines.push(` ${chalk11.dim(description)}`);
2588
3606
  lines.push("");
2589
3607
  }
2590
3608
  appendTextSection(lines, "Usage", [helper.commandUsage(cmd)]);
@@ -2611,9 +3629,11 @@ program.name(cliName).description("Agent Computer CLI").version(pkg2.version ??
2611
3629
  program.addCommand(loginCommand);
2612
3630
  program.addCommand(logoutCommand);
2613
3631
  program.addCommand(whoamiCommand);
3632
+ program.addCommand(claudeAuthCommand);
2614
3633
  program.addCommand(createCommand);
2615
3634
  program.addCommand(lsCommand);
2616
3635
  program.addCommand(getCommand);
3636
+ program.addCommand(imageCommand);
2617
3637
  program.addCommand(agentCommand);
2618
3638
  program.addCommand(fleetCommand);
2619
3639
  program.addCommand(acpCommand);