aicomputer 0.1.6 → 0.1.8

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 +1268 -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,592 @@ 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").option("--verbose", "Show step-by-step auth diagnostics").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
+ const primaryVerification = await verifyStoredAuth(sshTarget);
1577
+ markVerificationTodo(
1578
+ todos,
1579
+ "verify-primary",
1580
+ primaryVerification,
1581
+ `${target.handle} fresh login shell sees Claude auth`
1582
+ );
1583
+ activeTodoID = "verify-shared";
1584
+ const sharedCheck = primaryVerification.status === "verified" ? await verifySecondaryMachine(
1585
+ target.id,
1586
+ sharedInstall,
1587
+ Boolean(options.skipCrossCheck)
1588
+ ) : {
1589
+ status: "skipped",
1590
+ reason: "primary verification was inconclusive"
1591
+ };
1592
+ if (sharedCheck.status === "verified") {
1593
+ markTodo(
1594
+ todos,
1595
+ "verify-shared",
1596
+ "done",
1597
+ `${sharedCheck.handle} also sees stored Claude auth`
1598
+ );
1599
+ } else {
1600
+ markTodo(todos, "verify-shared", "skipped", sharedCheck.reason);
1601
+ }
1602
+ } catch (error) {
1603
+ failureMessage = error instanceof Error ? error.message : "Failed to authenticate Claude";
1604
+ markTodo(todos, activeTodoID, "failed", failureMessage);
1605
+ } finally {
1606
+ if (helperCreated && target && !options.keepHelper) {
1607
+ try {
1608
+ await deleteComputer(target.id);
1609
+ markTodo(
1610
+ todos,
1611
+ "cleanup",
1612
+ "done",
1613
+ `removed temporary helper ${target.handle}`
1614
+ );
1615
+ } catch (error) {
1616
+ const message = error instanceof Error ? error.message : "failed to remove helper";
1617
+ markTodo(todos, "cleanup", "failed", message);
1618
+ }
1619
+ } else if (helperCreated && target && options.keepHelper) {
1620
+ markTodo(
1621
+ todos,
1622
+ "cleanup",
1623
+ "skipped",
1624
+ `kept helper ${target.handle}`
1625
+ );
1626
+ } else {
1627
+ markTodo(todos, "cleanup", "skipped", "no helper created");
1628
+ }
1629
+ if (options.verbose) {
1630
+ printTodoList(todos);
1631
+ }
1632
+ }
1633
+ if (failureMessage) {
1634
+ console.error(chalk5.red(`
1635
+ ${failureMessage}`));
1636
+ process.exit(1);
1637
+ }
1638
+ if (target) {
1639
+ console.log(
1640
+ chalk5.green(`Claude login installed on ${chalk5.bold(target.handle)}.`)
1641
+ );
1642
+ console.log();
1643
+ }
1644
+ });
1645
+ function createTodoList() {
1646
+ return [
1647
+ { id: "target", label: "Pick target computer", state: "pending" },
1648
+ { id: "ready", label: "Wait for machine readiness", state: "pending" },
1649
+ { id: "oauth", label: "Complete Claude browser auth", state: "pending" },
1650
+ { id: "install", label: "Install stored Claude login", state: "pending" },
1651
+ { id: "verify-primary", label: "Verify on target machine", state: "pending" },
1652
+ { id: "verify-shared", label: "Verify shared-home propagation", state: "pending" },
1653
+ { id: "cleanup", label: "Clean up temporary helper", state: "pending" }
1654
+ ];
1655
+ }
1656
+ function markTodo(items, id, state, detail) {
1657
+ const item = items.find((entry) => entry.id === id);
1658
+ if (!item) {
1659
+ return;
1660
+ }
1661
+ item.state = state;
1662
+ item.detail = detail;
1663
+ }
1664
+ function markVerificationTodo(items, id, result, successDetail) {
1665
+ if (result.status === "verified") {
1666
+ markTodo(items, id, "done", successDetail);
1667
+ return;
1668
+ }
1669
+ markTodo(items, id, "skipped", result.detail);
1670
+ }
1671
+ function printTodoList(items) {
1672
+ console.log();
1673
+ console.log(chalk5.dim("TODO"));
1674
+ console.log();
1675
+ for (const item of items) {
1676
+ const marker = item.state === "done" ? chalk5.green("[x]") : item.state === "skipped" ? chalk5.yellow("[-]") : item.state === "failed" ? chalk5.red("[!]") : chalk5.dim("[ ]");
1677
+ const detail = item.detail ? chalk5.dim(` ${item.detail}`) : "";
1678
+ console.log(` ${marker} ${item.label}${detail ? ` ${detail}` : ""}`);
1679
+ }
1680
+ console.log();
1681
+ }
1682
+ async function prepareTargetMachine(options) {
1683
+ if (options.machine?.trim()) {
1684
+ const computer2 = await resolveComputer(options.machine.trim());
1685
+ assertClaudeAuthTarget(computer2);
1686
+ return {
1687
+ computer: computer2,
1688
+ helperCreated: false,
1689
+ sharedInstall: isSharedInstallTarget(computer2),
1690
+ detail: describeTarget(computer2, false)
1691
+ };
1692
+ }
1693
+ const computers = await listComputers();
1694
+ const filesystemSettings = await getFilesystemSettings().catch(() => null);
1695
+ if (filesystemSettings?.shared_enabled) {
1696
+ const existing = pickSharedRunningComputer(computers);
1697
+ if (existing) {
1698
+ return {
1699
+ computer: existing,
1700
+ helperCreated: false,
1701
+ sharedInstall: true,
1702
+ detail: describeTarget(existing, false)
1703
+ };
1704
+ }
1705
+ const spinner = ora3("Creating temporary shared helper...").start();
1706
+ try {
1707
+ const helper = await createComputer({
1708
+ handle: `claude-auth-${randomSuffix(6)}`,
1709
+ display_name: "Claude Auth Helper",
1710
+ runtime_family: "managed-worker",
1711
+ use_platform_default: true,
1712
+ ssh_enabled: true,
1713
+ vnc_enabled: false
1714
+ });
1715
+ spinner.succeed(`Created temporary helper ${chalk5.bold(helper.handle)}`);
1716
+ return {
1717
+ computer: helper,
1718
+ helperCreated: true,
1719
+ sharedInstall: true,
1720
+ detail: describeTarget(helper, true)
1721
+ };
1722
+ } catch (error) {
1723
+ spinner.fail(
1724
+ error instanceof Error ? error.message : "Failed to create temporary helper"
1725
+ );
1726
+ throw error;
1727
+ }
1728
+ }
1729
+ const computer = await promptForSSHComputer(
1730
+ computers,
1731
+ "Select a computer for Claude auth"
1732
+ );
1733
+ return {
1734
+ computer,
1735
+ helperCreated: false,
1736
+ sharedInstall: isSharedInstallTarget(computer),
1737
+ detail: describeTarget(computer, false)
1738
+ };
1739
+ }
1740
+ function pickSharedRunningComputer(computers) {
1741
+ const candidates = computers.filter(
1742
+ (computer) => computer.runtime_family === "managed-worker" && computer.filesystem_mode === "shared" && computer.ssh_enabled && computer.status === "running"
1743
+ ).sort(
1744
+ (left, right) => new Date(right.updated_at).getTime() - new Date(left.updated_at).getTime()
1745
+ );
1746
+ return candidates[0] ?? null;
1747
+ }
1748
+ function assertClaudeAuthTarget(computer) {
1749
+ if (!computer.ssh_enabled) {
1750
+ throw new Error(`${computer.handle} does not have SSH enabled`);
1751
+ }
1752
+ }
1753
+ function isSharedInstallTarget(computer) {
1754
+ return computer.runtime_family === "managed-worker" && computer.filesystem_mode === "shared";
1755
+ }
1756
+ function describeTarget(computer, helperCreated) {
1757
+ if (helperCreated) {
1758
+ return `created temporary helper ${computer.handle}`;
1759
+ }
1760
+ if (isSharedInstallTarget(computer)) {
1761
+ return `using shared machine ${computer.handle}`;
1762
+ }
1763
+ return `using ${computer.handle}`;
1764
+ }
1765
+ async function waitForRunning(initial) {
1766
+ if (initial.status === "running") {
1767
+ return initial;
1768
+ }
1769
+ const spinner = ora3(`Waiting for ${chalk5.bold(initial.handle)} to be ready...`).start();
1770
+ const deadline = Date.now() + readyPollTimeoutMs;
1771
+ let lastStatus = initial.status;
1772
+ while (Date.now() < deadline) {
1773
+ const current = await getComputerByID(initial.id);
1774
+ if (current.status === "running") {
1775
+ spinner.succeed(`${chalk5.bold(current.handle)} is ready`);
1776
+ return current;
1777
+ }
1778
+ if (current.status !== lastStatus) {
1779
+ lastStatus = current.status;
1780
+ spinner.text = `Waiting for ${chalk5.bold(current.handle)}... ${chalk5.dim(current.status)}`;
1781
+ }
1782
+ if (current.status === "error" || current.status === "deleted" || current.status === "stopped") {
1783
+ spinner.fail(`${current.handle} entered ${current.status}`);
1784
+ throw new Error(current.last_error || `${current.handle} entered ${current.status}`);
1785
+ }
1786
+ await delay(readyPollIntervalMs);
1787
+ }
1788
+ spinner.fail(`Timed out waiting for ${initial.handle}`);
1789
+ throw new Error(`timed out waiting for ${initial.handle} to be ready`);
1790
+ }
1791
+ async function runManualOAuthFlow() {
1792
+ const codeVerifier = base64url(randomBytes(32));
1793
+ const state = randomBytes(16).toString("hex");
1794
+ const codeChallenge = base64url(createHash("sha256").update(codeVerifier).digest());
1795
+ const url = buildAuthorizationURL(codeChallenge, state);
1796
+ console.log("We will open your browser so you can authenticate with Claude.");
1797
+ console.log("If the browser does not open automatically, use the URL below:\n");
1798
+ console.log(url);
1799
+ console.log();
1800
+ try {
1801
+ await openBrowserURL(url);
1802
+ } catch {
1803
+ console.log(chalk5.yellow("Unable to open the browser automatically."));
1804
+ }
1805
+ console.log(
1806
+ "After completing authentication, copy the code shown on the success page."
1807
+ );
1808
+ console.log("You can paste either the full URL, or a value formatted as CODE#STATE.\n");
1809
+ const pasted = (await textInput({
1810
+ message: "Paste the authorization code (or URL) here:"
1811
+ })).trim();
1812
+ if (!pasted) {
1813
+ throw new Error("no authorization code provided");
1814
+ }
1815
+ const parsed = parseAuthorizationInput(pasted, state);
1816
+ const spinner = ora3("Exchanging authorization code...").start();
1817
+ try {
1818
+ const response = await fetch(CLAUDE_OAUTH_TOKEN_URL, {
1819
+ method: "POST",
1820
+ headers: {
1821
+ "Content-Type": "application/json"
1822
+ },
1823
+ body: JSON.stringify({
1824
+ grant_type: "authorization_code",
1825
+ code: parsed.code,
1826
+ state: parsed.state,
1827
+ redirect_uri: CLAUDE_OAUTH_REDIRECT_URL,
1828
+ client_id: CLAUDE_OAUTH_CLIENT_ID,
1829
+ code_verifier: codeVerifier
1830
+ })
1831
+ });
1832
+ if (!response.ok) {
1833
+ throw new Error(
1834
+ `token exchange failed: ${response.status} ${await response.text()}`
1835
+ );
1836
+ }
1837
+ const payload = await response.json();
1838
+ if (!payload.refresh_token || !payload.scope) {
1839
+ throw new Error("token exchange returned an incomplete response");
1840
+ }
1841
+ spinner.succeed("Authorization code exchanged");
1842
+ return {
1843
+ refreshToken: payload.refresh_token,
1844
+ scope: payload.scope
1845
+ };
1846
+ } catch (error) {
1847
+ spinner.fail(
1848
+ error instanceof Error ? error.message : "Failed to exchange authorization code"
1849
+ );
1850
+ throw error;
1851
+ }
1852
+ }
1853
+ function buildAuthorizationURL(codeChallenge, state) {
1854
+ const params = new URLSearchParams({
1855
+ code: "true",
1856
+ client_id: CLAUDE_OAUTH_CLIENT_ID,
1857
+ response_type: "code",
1858
+ redirect_uri: CLAUDE_OAUTH_REDIRECT_URL,
1859
+ scope: CLAUDE_OAUTH_SCOPES.join(" "),
1860
+ code_challenge: codeChallenge,
1861
+ code_challenge_method: "S256",
1862
+ state
1863
+ });
1864
+ return `${CLAUDE_OAUTH_AUTHORIZE_URL}?${params.toString()}`;
1865
+ }
1866
+ function parseAuthorizationInput(value, expectedState) {
1867
+ if (value.startsWith("http://") || value.startsWith("https://")) {
1868
+ const parsed = new URL(value);
1869
+ const code2 = parsed.searchParams.get("code");
1870
+ const state2 = parsed.searchParams.get("state");
1871
+ if (!code2 || !state2) {
1872
+ throw new Error("pasted URL is missing code or state");
1873
+ }
1874
+ if (state2 !== expectedState) {
1875
+ throw new Error("state mismatch detected; restart the authentication flow");
1876
+ }
1877
+ return { code: code2, state: state2 };
1878
+ }
1879
+ const [code, state] = value.split("#", 2).map((part) => part?.trim() ?? "");
1880
+ if (!code || !state) {
1881
+ throw new Error("expected a full URL or a CODE#STATE value");
1882
+ }
1883
+ if (state !== expectedState) {
1884
+ throw new Error("state mismatch detected; restart the authentication flow");
1885
+ }
1886
+ return { code, state };
1887
+ }
1888
+ async function resolveSSHTarget(computer) {
1889
+ const registered = await ensureDefaultSSHKeyRegistered();
1890
+ const info = await getConnectionInfo(computer.id);
1891
+ if (!info.connection.ssh_available) {
1892
+ throw new Error(`SSH is not available for ${computer.handle}`);
1893
+ }
1894
+ return {
1895
+ handle: computer.handle,
1896
+ host: info.connection.ssh_host,
1897
+ port: info.connection.ssh_port,
1898
+ user: info.connection.ssh_user,
1899
+ identityFile: registered.privateKeyPath
1900
+ };
1901
+ }
1902
+ async function installClaudeAuth(target, oauth) {
1903
+ const spinner = ora3(`Installing Claude auth on ${chalk5.bold(target.handle)}...`).start();
1904
+ try {
1905
+ const installScript = buildInstallScript(oauth.refreshToken, oauth.scope);
1906
+ const result = await runRemoteCommand(target, ["bash", "-s"], installScript);
1907
+ if (result.stdout.trim()) {
1908
+ spinner.succeed(`Installed Claude auth on ${chalk5.bold(target.handle)}`);
1909
+ return;
1910
+ }
1911
+ spinner.succeed(`Installed Claude auth on ${chalk5.bold(target.handle)}`);
1912
+ } catch (error) {
1913
+ spinner.fail(
1914
+ error instanceof Error ? error.message : `Failed to install Claude auth on ${target.handle}`
1915
+ );
1916
+ throw error;
1917
+ }
1918
+ }
1919
+ function buildInstallScript(refreshToken, scopes) {
1920
+ const tokenMarker = `TOKEN_${randomSuffix(12)}`;
1921
+ const scopeMarker = `SCOPES_${randomSuffix(12)}`;
1922
+ return [
1923
+ "set -euo pipefail",
1924
+ 'command -v claude >/dev/null 2>&1 || { echo "claude is not installed on this computer" >&2; exit 1; }',
1925
+ `export CLAUDE_CODE_OAUTH_REFRESH_TOKEN="$(cat <<'` + tokenMarker + "'",
1926
+ refreshToken,
1927
+ tokenMarker,
1928
+ ')"',
1929
+ `export CLAUDE_CODE_OAUTH_SCOPES="$(cat <<'` + scopeMarker + "'",
1930
+ scopes,
1931
+ scopeMarker,
1932
+ ')"',
1933
+ "claude auth login",
1934
+ "unset CLAUDE_CODE_OAUTH_REFRESH_TOKEN",
1935
+ "unset CLAUDE_CODE_OAUTH_SCOPES"
1936
+ ].join("\n");
1937
+ }
1938
+ async function verifyStoredAuth(target) {
1939
+ try {
1940
+ const result = await runRemoteCommand(target, [
1941
+ "bash",
1942
+ "--noprofile",
1943
+ "--norc",
1944
+ "-lc",
1945
+ "claude auth status --json 2>/dev/null || claude auth status"
1946
+ ]);
1947
+ const payload = parseStatusOutput(result.stdout, result.stderr);
1948
+ if (payload.loggedIn) {
1949
+ return { status: "verified", detail: "verified" };
1950
+ }
1951
+ return {
1952
+ status: "failed",
1953
+ detail: payload.detail ? `verification failed: ${payload.detail}` : "verification failed"
1954
+ };
1955
+ } catch (error) {
1956
+ return {
1957
+ status: "inconclusive",
1958
+ detail: error instanceof Error ? error.message : "verification command did not complete cleanly"
1959
+ };
1960
+ }
1961
+ }
1962
+ function parseStatusOutput(stdout, stderr) {
1963
+ const combined = [stdout, stderr].map((value) => value.trim()).filter(Boolean).join("\n");
1964
+ const start = combined.indexOf("{");
1965
+ const end = combined.lastIndexOf("}");
1966
+ if (start === -1 || end === -1 || end <= start) {
1967
+ const normalized = combined.toLowerCase();
1968
+ if (normalized.includes("logged in")) {
1969
+ return { loggedIn: true };
1970
+ }
1971
+ if (normalized.includes("not logged in") || normalized.includes("logged out")) {
1972
+ return { loggedIn: false, detail: firstStatusLine(combined) };
1973
+ }
1974
+ throw new Error(
1975
+ combined ? `could not verify Claude auth from status output: ${firstStatusLine(combined)}` : "could not verify Claude auth from empty status output"
1976
+ );
1977
+ }
1978
+ const parsed = JSON.parse(combined.slice(start, end + 1));
1979
+ return {
1980
+ loggedIn: parsed.loggedIn === true,
1981
+ detail: parsed.loggedIn === true ? void 0 : parsed.error
1982
+ };
1983
+ }
1984
+ function firstStatusLine(value) {
1985
+ return value.split(/\r?\n/).map((line) => line.trim()).find(Boolean) ?? "unknown output";
1986
+ }
1987
+ async function verifySecondaryMachine(primaryComputerID, sharedInstall, skip) {
1988
+ if (!sharedInstall) {
1989
+ return { status: "skipped", reason: "target uses isolated filesystem" };
1990
+ }
1991
+ if (skip) {
1992
+ return { status: "skipped", reason: "cross-check skipped by flag" };
1993
+ }
1994
+ const secondary = (await listComputers()).filter(
1995
+ (computer) => computer.id !== primaryComputerID && computer.runtime_family === "managed-worker" && computer.filesystem_mode === "shared" && computer.ssh_enabled && computer.status === "running"
1996
+ ).sort(
1997
+ (left, right) => new Date(right.updated_at).getTime() - new Date(left.updated_at).getTime()
1998
+ )[0];
1999
+ if (!secondary) {
2000
+ return {
2001
+ status: "skipped",
2002
+ reason: "no second running shared managed-worker was available"
2003
+ };
2004
+ }
2005
+ const sshTarget = await resolveSSHTarget(secondary);
2006
+ const verification = await verifyStoredAuth(sshTarget);
2007
+ if (verification.status !== "verified") {
2008
+ return { status: "skipped", reason: verification.detail };
2009
+ }
2010
+ return { status: "verified", handle: secondary.handle };
2011
+ }
2012
+ async function runRemoteCommand(target, remoteArgs, script) {
2013
+ const args = [
2014
+ "-T",
2015
+ "-i",
2016
+ target.identityFile,
2017
+ "-p",
2018
+ String(target.port),
2019
+ `${target.user}@${target.host}`,
2020
+ ...remoteArgs
2021
+ ];
2022
+ return new Promise((resolve, reject) => {
2023
+ const child = spawn2("ssh", args, {
2024
+ stdio: ["pipe", "pipe", "pipe"]
2025
+ });
2026
+ let stdout = "";
2027
+ let stderr = "";
2028
+ child.stdout.on("data", (chunk) => {
2029
+ stdout += chunk.toString();
2030
+ });
2031
+ child.stderr.on("data", (chunk) => {
2032
+ stderr += chunk.toString();
2033
+ });
2034
+ child.on("error", reject);
2035
+ child.on("exit", (code) => {
2036
+ if (code === 0) {
2037
+ resolve({ stdout, stderr });
2038
+ return;
2039
+ }
2040
+ const message = stderr.trim() || stdout.trim() || `ssh exited with code ${code ?? 1}`;
2041
+ reject(new Error(message));
2042
+ });
2043
+ if (script !== void 0) {
2044
+ child.stdin.end(script);
2045
+ } else {
2046
+ child.stdin.end();
2047
+ }
2048
+ });
2049
+ }
2050
+ function base64url(buffer) {
2051
+ return buffer.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
2052
+ }
2053
+ function randomSuffix(length) {
2054
+ return randomBytes(Math.ceil(length / 2)).toString("hex").slice(0, length);
2055
+ }
2056
+ function delay(ms) {
2057
+ return new Promise((resolve) => {
2058
+ setTimeout(resolve, ms);
2059
+ });
2060
+ }
2061
+
2062
+ // src/commands/computers.ts
2063
+ import { Command as Command5 } from "commander";
2064
+ import chalk6 from "chalk";
2065
+ import ora4 from "ora";
2066
+ import { select as select2, input as textInput2, confirm } from "@inquirer/prompts";
2067
+
2068
+ // src/lib/machine-sources.ts
2069
+ async function getMachineSourceSettings() {
2070
+ return api("/v1/me/machine-source");
2071
+ }
2072
+ async function upsertMachineSource(input) {
2073
+ return api("/v1/me/machine-source", {
2074
+ method: "PUT",
2075
+ body: JSON.stringify(input)
2076
+ });
2077
+ }
2078
+ async function clearMachineSourceDefault() {
2079
+ return api("/v1/me/machine-source", {
2080
+ method: "DELETE"
2081
+ });
2082
+ }
2083
+ async function rebuildMachineSource(sourceID) {
2084
+ return api(`/v1/me/machine-source/${encodeURIComponent(sourceID)}/rebuild`, {
2085
+ method: "POST"
2086
+ });
2087
+ }
2088
+ async function deleteMachineSource(sourceID) {
2089
+ return api(`/v1/me/machine-source/${encodeURIComponent(sourceID)}`, {
2090
+ method: "DELETE"
2091
+ });
2092
+ }
2093
+ function summarizeMachineSourceSelection(settings) {
2094
+ if (settings.platform_default || !settings.default_machine_source) {
2095
+ return "AgentComputer platform default image";
2096
+ }
2097
+ return summarizeMachineSource(settings.default_machine_source);
2098
+ }
2099
+ function summarizeMachineSource(source) {
2100
+ if (source.kind === "oci-image") {
2101
+ return source.requested_ref || "OCI image";
2102
+ }
2103
+ const parts = [
2104
+ source.git_url || "Nix git source",
2105
+ source.git_ref ? `ref ${source.git_ref}` : "",
2106
+ source.git_subpath ? `subpath ${source.git_subpath}` : ""
2107
+ ].filter(Boolean);
2108
+ return parts.join(" | ");
2109
+ }
2110
+
2111
+ // src/commands/computers.ts
1519
2112
  function isInternalCondition(value) {
1520
2113
  return /^[A-Z][a-zA-Z]+$/.test(value);
1521
2114
  }
@@ -1525,32 +2118,38 @@ function printComputer(computer) {
1525
2118
  const ssh = computer.ssh_enabled ? formatSSHCommand2(computer.handle, computer.ssh_host, computer.ssh_port) : "disabled";
1526
2119
  const isCustom = computer.runtime_family === "custom-machine";
1527
2120
  console.log();
1528
- console.log(` ${chalk4.bold.white(computer.handle)} ${formatStatus(computer.status)}`);
2121
+ console.log(` ${chalk6.bold.white(computer.handle)} ${formatStatus(computer.status)}`);
1529
2122
  console.log();
1530
- console.log(` ${chalk4.dim("ID")} ${computer.id}`);
1531
- console.log(` ${chalk4.dim("Tier")} ${computer.tier}`);
2123
+ console.log(` ${chalk6.dim("ID")} ${computer.id}`);
2124
+ console.log(` ${chalk6.dim("Tier")} ${computer.tier}`);
1532
2125
  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}`);
2126
+ console.log(` ${chalk6.dim("Runtime")} ${computer.runtime_family}`);
2127
+ console.log(` ${chalk6.dim("Source")} ${computer.source_kind}`);
2128
+ console.log(` ${chalk6.dim("Image")} ${computer.image_family}`);
2129
+ } else {
2130
+ console.log(` ${chalk6.dim("Runtime")} ${computer.runtime_family}`);
2131
+ console.log(` ${chalk6.dim("Launch")} ${formatManagedWorkerLaunchSource(computer)}`);
2132
+ if (computer.user_source_id && computer.resolved_image_ref) {
2133
+ console.log(` ${chalk6.dim("Image")} ${computer.resolved_image_ref}`);
2134
+ }
1536
2135
  }
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})` : ""}`);
2136
+ console.log(` ${chalk6.dim("Primary")} :${computer.primary_port}${computer.primary_path}`);
2137
+ console.log(` ${chalk6.dim("Health")} ${computer.healthcheck_type}${computer.healthcheck_value ? ` (${computer.healthcheck_value})` : ""}`);
1539
2138
  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)}`);
2139
+ console.log(` ${chalk6.dim("Gateway")} ${chalk6.cyan(webURL(computer))}`);
2140
+ console.log(` ${chalk6.dim("VNC")} ${vnc ? chalk6.cyan(vnc) : chalk6.dim("not available")}`);
2141
+ console.log(` ${chalk6.dim("Terminal")} ${terminal ? chalk6.cyan(terminal) : chalk6.dim("not available")}`);
2142
+ console.log(` ${chalk6.dim("SSH")} ${computer.ssh_enabled ? chalk6.white(ssh) : chalk6.dim(ssh)}`);
1544
2143
  if (computer.last_error) {
1545
2144
  console.log();
1546
2145
  if (isInternalCondition(computer.last_error)) {
1547
- console.log(` ${chalk4.dim("Condition")} ${chalk4.dim(computer.last_error)}`);
2146
+ console.log(` ${chalk6.dim("Condition")} ${chalk6.dim(computer.last_error)}`);
1548
2147
  } else {
1549
- console.log(` ${chalk4.dim("Error")} ${chalk4.red(computer.last_error)}`);
2148
+ console.log(` ${chalk6.dim("Error")} ${chalk6.red(computer.last_error)}`);
1550
2149
  }
1551
2150
  }
1552
2151
  console.log();
1553
- console.log(` ${chalk4.dim("Created")} ${timeAgo(computer.created_at)}`);
2152
+ console.log(` ${chalk6.dim("Created")} ${timeAgo(computer.created_at)}`);
1554
2153
  console.log();
1555
2154
  }
1556
2155
  function formatSSHCommand2(user, host, port) {
@@ -1562,22 +2161,41 @@ function formatSSHCommand2(user, host, port) {
1562
2161
  }
1563
2162
  return `ssh -p ${port} ${user}@${host}`;
1564
2163
  }
2164
+ function formatManagedWorkerLaunchSource(computer) {
2165
+ if (!computer.user_source_id) {
2166
+ return "AgentComputer managed-worker platform default";
2167
+ }
2168
+ if (computer.source_repo_url) {
2169
+ let value = computer.source_repo_url;
2170
+ if (computer.source_ref) {
2171
+ value += ` @ ${computer.source_ref}`;
2172
+ }
2173
+ if (computer.source_subpath) {
2174
+ value += ` # ${computer.source_subpath}`;
2175
+ }
2176
+ return `saved nix-git source ${computer.user_source_id} (${value})`;
2177
+ }
2178
+ if (computer.source_ref) {
2179
+ return `saved oci-image source ${computer.user_source_id} (${computer.source_ref})`;
2180
+ }
2181
+ return `saved custom source ${computer.user_source_id}`;
2182
+ }
1565
2183
  function printComputerTable(computers) {
1566
2184
  const handleWidth = Math.max(6, ...computers.map((c) => c.handle.length));
1567
2185
  const statusWidth = 12;
1568
2186
  const createdWidth = 10;
1569
2187
  console.log();
1570
2188
  console.log(
1571
- ` ${chalk4.dim(padEnd("Handle", handleWidth + 2))}${chalk4.dim(padEnd("Status", statusWidth + 2))}${chalk4.dim(padEnd("Created", createdWidth + 2))}${chalk4.dim("URL")}`
2189
+ ` ${chalk6.dim(padEnd("Handle", handleWidth + 2))}${chalk6.dim(padEnd("Status", statusWidth + 2))}${chalk6.dim(padEnd("Created", createdWidth + 2))}${chalk6.dim("URL")}`
1572
2190
  );
1573
2191
  console.log(
1574
- ` ${chalk4.dim("-".repeat(handleWidth + 2))}${chalk4.dim("-".repeat(statusWidth + 2))}${chalk4.dim("-".repeat(createdWidth + 2))}${chalk4.dim("-".repeat(20))}`
2192
+ ` ${chalk6.dim("-".repeat(handleWidth + 2))}${chalk6.dim("-".repeat(statusWidth + 2))}${chalk6.dim("-".repeat(createdWidth + 2))}${chalk6.dim("-".repeat(20))}`
1575
2193
  );
1576
2194
  for (const computer of computers) {
1577
2195
  const status = formatStatus(computer.status);
1578
- const created = chalk4.dim(timeAgo(computer.created_at));
2196
+ const created = chalk6.dim(timeAgo(computer.created_at));
1579
2197
  console.log(
1580
- ` ${chalk4.white(padEnd(computer.handle, handleWidth + 2))}${padEnd(status, statusWidth + 2)}${padEnd(created, createdWidth + 2)}${chalk4.cyan(webURL(computer))}`
2198
+ ` ${chalk6.white(padEnd(computer.handle, handleWidth + 2))}${padEnd(status, statusWidth + 2)}${padEnd(created, createdWidth + 2)}${chalk6.cyan(webURL(computer))}`
1581
2199
  );
1582
2200
  }
1583
2201
  console.log();
@@ -1587,29 +2205,29 @@ function printComputerTableVerbose(computers) {
1587
2205
  const statusWidth = 10;
1588
2206
  console.log();
1589
2207
  console.log(
1590
- ` ${chalk4.dim(padEnd("Handle", handleWidth + 2))}${chalk4.dim(padEnd("Status", statusWidth + 2))}${chalk4.dim("URLs")}`
2208
+ ` ${chalk6.dim(padEnd("Handle", handleWidth + 2))}${chalk6.dim(padEnd("Status", statusWidth + 2))}${chalk6.dim("URLs")}`
1591
2209
  );
1592
2210
  console.log(
1593
- ` ${chalk4.dim("-".repeat(handleWidth + 2))}${chalk4.dim("-".repeat(statusWidth + 2))}${chalk4.dim("-".repeat(20))}`
2211
+ ` ${chalk6.dim("-".repeat(handleWidth + 2))}${chalk6.dim("-".repeat(statusWidth + 2))}${chalk6.dim("-".repeat(20))}`
1594
2212
  );
1595
2213
  for (const computer of computers) {
1596
2214
  const status = formatStatus(computer.status);
1597
2215
  const vnc = vncURL(computer);
1598
2216
  const terminal = terminalURL(computer);
1599
2217
  console.log(
1600
- ` ${chalk4.white(padEnd(computer.handle, handleWidth + 2))}${padEnd(status, statusWidth + 2)}${chalk4.cyan(webURL(computer))}`
2218
+ ` ${chalk6.white(padEnd(computer.handle, handleWidth + 2))}${padEnd(status, statusWidth + 2)}${chalk6.cyan(webURL(computer))}`
1601
2219
  );
1602
2220
  console.log(
1603
- ` ${padEnd("", handleWidth + 2)}${padEnd("", statusWidth + 2)}${chalk4.dim(vnc ?? "VNC not available")}`
2221
+ ` ${padEnd("", handleWidth + 2)}${padEnd("", statusWidth + 2)}${chalk6.dim(vnc ?? "VNC not available")}`
1604
2222
  );
1605
2223
  console.log(
1606
- ` ${padEnd("", handleWidth + 2)}${padEnd("", statusWidth + 2)}${chalk4.dim(terminal ?? "Terminal not available")}`
2224
+ ` ${padEnd("", handleWidth + 2)}${padEnd("", statusWidth + 2)}${chalk6.dim(terminal ?? "Terminal not available")}`
1607
2225
  );
1608
2226
  }
1609
2227
  console.log();
1610
2228
  }
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();
2229
+ 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) => {
2230
+ const spinner = options.json ? null : ora4("Fetching computers...").start();
1613
2231
  try {
1614
2232
  const computers = await listComputers();
1615
2233
  spinner?.stop();
@@ -1619,7 +2237,7 @@ var lsCommand = new Command4("ls").description("List computers").option("--json"
1619
2237
  }
1620
2238
  if (computers.length === 0) {
1621
2239
  console.log();
1622
- console.log(chalk4.dim(" No computers found."));
2240
+ console.log(chalk6.dim(" No computers found."));
1623
2241
  console.log();
1624
2242
  return;
1625
2243
  }
@@ -1639,8 +2257,8 @@ var lsCommand = new Command4("ls").description("List computers").option("--json"
1639
2257
  process.exit(1);
1640
2258
  }
1641
2259
  });
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();
2260
+ 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) => {
2261
+ const spinner = options.json ? null : ora4("Fetching computer...").start();
1644
2262
  try {
1645
2263
  const computer = await resolveComputer(identifier);
1646
2264
  spinner?.stop();
@@ -1660,19 +2278,28 @@ var getCommand = new Command4("get").description("Show computer details").argume
1660
2278
  process.exit(1);
1661
2279
  }
1662
2280
  });
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) => {
2281
+ 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
2282
  let spinner;
1665
2283
  let timer;
1666
2284
  let startTime = 0;
1667
2285
  try {
1668
2286
  const selectedOptions = await resolveCreateOptions(options);
1669
2287
  const runtimeFamily = effectiveRuntimeFamily(selectedOptions.runtimeFamily);
2288
+ const machineSourceSettings = runtimeFamily === "managed-worker" ? await getMachineSourceSettings().catch(() => null) : null;
1670
2289
  const filesystemSettings = await loadFilesystemSettingsForCreate(runtimeFamily);
2290
+ const machineSourceNote = createMachineSourceNote(
2291
+ runtimeFamily,
2292
+ machineSourceSettings,
2293
+ Boolean(selectedOptions.usePlatformDefault)
2294
+ );
2295
+ if (machineSourceNote) {
2296
+ console.log(chalk6.dim(machineSourceNote));
2297
+ }
1671
2298
  const provisioningNote = createProvisioningNote(runtimeFamily, filesystemSettings);
1672
2299
  if (provisioningNote) {
1673
- console.log(chalk4.dim(provisioningNote));
2300
+ console.log(chalk6.dim(provisioningNote));
1674
2301
  }
1675
- spinner = ora3(createSpinnerText(runtimeFamily, filesystemSettings, 0)).start();
2302
+ spinner = ora4(createSpinnerText(runtimeFamily, filesystemSettings, 0)).start();
1676
2303
  startTime = Date.now();
1677
2304
  timer = setInterval(() => {
1678
2305
  const elapsed2 = (Date.now() - startTime) / 1e3;
@@ -1688,6 +2315,7 @@ var createCommand = new Command4("create").description("Create a computer").argu
1688
2315
  source_kind: parseSourceKindOption(selectedOptions.sourceKind),
1689
2316
  image_family: selectedOptions.imageFamily,
1690
2317
  image_ref: selectedOptions.imageRef,
2318
+ use_platform_default: selectedOptions.usePlatformDefault,
1691
2319
  primary_port: parseOptionalPort(selectedOptions.primaryPort),
1692
2320
  primary_path: selectedOptions.primaryPath,
1693
2321
  healthcheck_type: parseHealthcheckTypeOption(selectedOptions.healthcheckType),
@@ -1708,7 +2336,7 @@ var createCommand = new Command4("create").description("Create a computer").argu
1708
2336
  }
1709
2337
  const elapsed = ((Date.now() - startTime) / 1e3).toFixed(1);
1710
2338
  spinner.succeed(
1711
- chalk4.green(`Created ${chalk4.bold(computer.handle)} ${chalk4.dim(`[${elapsed}s]`)}`)
2339
+ chalk6.green(`Created ${chalk6.bold(computer.handle)} ${chalk6.dim(`[${elapsed}s]`)}`)
1712
2340
  );
1713
2341
  printComputer(computer);
1714
2342
  } catch (error) {
@@ -1717,18 +2345,17 @@ var createCommand = new Command4("create").description("Create a computer").argu
1717
2345
  }
1718
2346
  const message = error instanceof Error ? error.message : "Failed to create computer";
1719
2347
  if (spinner) {
1720
- const suffix = startTime ? ` ${chalk4.dim(`[${((Date.now() - startTime) / 1e3).toFixed(1)}s]`)}` : "";
2348
+ const suffix = startTime ? ` ${chalk6.dim(`[${((Date.now() - startTime) / 1e3).toFixed(1)}s]`)}` : "";
1721
2349
  spinner.fail(`${message}${suffix}`);
1722
2350
  } else {
1723
- console.error(chalk4.red(message));
2351
+ console.error(chalk6.red(message));
1724
2352
  }
1725
2353
  process.exit(1);
1726
2354
  }
1727
2355
  });
1728
2356
  async function resolveCreateOptions(options) {
1729
2357
  const selectedOptions = { ...options };
1730
- const hasExplicitRuntimeSelection = Boolean(selectedOptions.runtimeFamily) || Boolean(selectedOptions.imageRef) || Boolean(selectedOptions.sourceKind);
1731
- const shouldPrompt = Boolean(selectedOptions.interactive) || !hasExplicitRuntimeSelection;
2358
+ const shouldPrompt = Boolean(selectedOptions.interactive);
1732
2359
  if (!process.stdin.isTTY || !process.stdout.isTTY || !shouldPrompt) {
1733
2360
  validateCreateOptions(selectedOptions);
1734
2361
  return selectedOptions;
@@ -1750,9 +2377,9 @@ async function resolveCreateOptions(options) {
1750
2377
  if (runtimeChoice === "custom-machine") {
1751
2378
  selectedOptions.runtimeFamily = "custom-machine";
1752
2379
  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();
2380
+ selectedOptions.imageRef = (await textInput2({ message: "OCI image ref (required):" })).trim();
2381
+ selectedOptions.primaryPort = (await textInput2({ message: "Primary port:", default: "3000" })).trim();
2382
+ selectedOptions.primaryPath = (await textInput2({ message: "Primary path:", default: "/" })).trim();
1756
2383
  selectedOptions.healthcheckType = await select2({
1757
2384
  message: "Healthcheck type",
1758
2385
  choices: [
@@ -1761,7 +2388,7 @@ async function resolveCreateOptions(options) {
1761
2388
  ],
1762
2389
  default: "tcp"
1763
2390
  });
1764
- selectedOptions.healthcheckValue = (await textInput({ message: "Healthcheck value (optional):" })).trim();
2391
+ selectedOptions.healthcheckValue = (await textInput2({ message: "Healthcheck value (optional):" })).trim();
1765
2392
  selectedOptions.sshEnabled = await confirm({
1766
2393
  message: "Enable SSH?",
1767
2394
  default: false
@@ -1775,28 +2402,28 @@ async function resolveCreateOptions(options) {
1775
2402
  validateCreateOptions(selectedOptions);
1776
2403
  return selectedOptions;
1777
2404
  }
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) => {
2405
+ 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
2406
  const globalYes = cmd.parent?.opts()?.yes;
1780
2407
  const skipConfirm = Boolean(options.yes || globalYes);
1781
- const spinner = ora3("Resolving computer...").start();
2408
+ const spinner = ora4("Resolving computer...").start();
1782
2409
  try {
1783
2410
  const computer = await resolveComputer(identifier);
1784
2411
  spinner.stop();
1785
2412
  if (!skipConfirm && process.stdin.isTTY) {
1786
2413
  const confirmed = await confirm({
1787
- message: `Delete computer ${chalk4.bold(computer.handle)}?`,
2414
+ message: `Delete computer ${chalk6.bold(computer.handle)}?`,
1788
2415
  default: false
1789
2416
  });
1790
2417
  if (!confirmed) {
1791
- console.log(chalk4.dim(" Cancelled."));
2418
+ console.log(chalk6.dim(" Cancelled."));
1792
2419
  return;
1793
2420
  }
1794
2421
  }
1795
- const deleteSpinner = ora3("Deleting computer...").start();
2422
+ const deleteSpinner = ora4("Deleting computer...").start();
1796
2423
  await api(`/v1/computers/${computer.id}`, {
1797
2424
  method: "DELETE"
1798
2425
  });
1799
- deleteSpinner.succeed(chalk4.green(`Deleted ${chalk4.bold(computer.handle)}`));
2426
+ deleteSpinner.succeed(chalk6.green(`Deleted ${chalk6.bold(computer.handle)}`));
1800
2427
  } catch (error) {
1801
2428
  spinner.fail(
1802
2429
  error instanceof Error ? error.message : "Failed to delete computer"
@@ -1836,17 +2463,33 @@ function createProvisioningNote(runtimeFamily, filesystemSettings) {
1836
2463
  }
1837
2464
  return "Using isolated storage for this desktop.";
1838
2465
  }
2466
+ function createMachineSourceNote(runtimeFamily, machineSourceSettings, usePlatformDefault) {
2467
+ if (runtimeFamily !== "managed-worker") {
2468
+ return null;
2469
+ }
2470
+ if (usePlatformDefault) {
2471
+ return "Using the AgentComputer platform default image for this machine.";
2472
+ }
2473
+ if (!machineSourceSettings) {
2474
+ return null;
2475
+ }
2476
+ if (machineSourceSettings.platform_default || !machineSourceSettings.default_machine_source) {
2477
+ return "Using the AgentComputer platform default image.";
2478
+ }
2479
+ return `Using managed-worker image source: ${summarizeMachineSourceSelection(machineSourceSettings)}.`;
2480
+ }
1839
2481
  function createSpinnerText(runtimeFamily, filesystemSettings, elapsedSeconds) {
1840
- const elapsedLabel = chalk4.dim(`${elapsedSeconds.toFixed(1)}s`);
2482
+ const elapsedLabel = chalk6.dim(`${elapsedSeconds.toFixed(1)}s`);
1841
2483
  if (runtimeFamily === "managed-worker" && filesystemSettings?.shared_enabled && elapsedSeconds >= 5) {
1842
- return `Creating computer... ${elapsedLabel} ${chalk4.dim("mounting shared home")}`;
2484
+ return `Creating computer... ${elapsedLabel} ${chalk6.dim("mounting shared home")}`;
1843
2485
  }
1844
2486
  return `Creating computer... ${elapsedLabel}`;
1845
2487
  }
1846
2488
  function validateCreateOptions(options) {
1847
2489
  const runtimeFamily = parseRuntimeFamilyOption(options.runtimeFamily);
1848
2490
  const sourceKind = parseSourceKindOption(options.sourceKind);
1849
- if (runtimeFamily === "custom-machine" && (typeof options.imageRef !== "string" || options.imageRef.trim() === "")) {
2491
+ const imageRef = options.imageRef?.trim();
2492
+ if (runtimeFamily === "custom-machine" && !imageRef) {
1850
2493
  throw new Error("--image-ref is required for --runtime-family custom-machine");
1851
2494
  }
1852
2495
  if (runtimeFamily === "custom-machine" && sourceKind === "none") {
@@ -1855,6 +2498,12 @@ function validateCreateOptions(options) {
1855
2498
  if (runtimeFamily === "custom-machine" && options.vncEnabled) {
1856
2499
  throw new Error("custom-machine does not support platform VNC");
1857
2500
  }
2501
+ if (runtimeFamily === "custom-machine" && options.usePlatformDefault) {
2502
+ throw new Error("--use-platform-default cannot be used with --runtime-family custom-machine");
2503
+ }
2504
+ if (imageRef && options.usePlatformDefault) {
2505
+ throw new Error("choose either --image-ref or --use-platform-default");
2506
+ }
1858
2507
  }
1859
2508
  function parseSourceKindOption(value) {
1860
2509
  switch (value) {
@@ -1900,7 +2549,7 @@ function resolveOptionalToggle(enabled, disabled, label) {
1900
2549
  }
1901
2550
 
1902
2551
  // src/commands/completion.ts
1903
- import { Command as Command5 } from "commander";
2552
+ import { Command as Command6 } from "commander";
1904
2553
  var ZSH_SCRIPT = `#compdef computer agentcomputer aicomputer
1905
2554
 
1906
2555
  _computer() {
@@ -1909,9 +2558,12 @@ _computer() {
1909
2558
  'login:Authenticate the CLI'
1910
2559
  'logout:Remove stored API key'
1911
2560
  'whoami:Show current user'
2561
+ 'claude-auth:Authenticate Claude Code on a computer'
2562
+ 'claude-login:Alias for claude-auth'
1912
2563
  'create:Create a computer'
1913
2564
  'ls:List computers'
1914
2565
  'get:Show computer details'
2566
+ 'image:Manage machine image sources'
1915
2567
  'open:Open in browser'
1916
2568
  'ssh:SSH into a computer'
1917
2569
  'ports:Manage published ports'
@@ -1930,6 +2582,15 @@ _computer() {
1930
2582
  'rm:Unpublish an app port'
1931
2583
  )
1932
2584
 
2585
+ local -a image_commands
2586
+ image_commands=(
2587
+ 'ls:List machine image sources'
2588
+ 'save:Create or update a machine image source'
2589
+ 'default:Set the default machine image source'
2590
+ 'rebuild:Rebuild a machine image source'
2591
+ 'rm:Delete a machine image source'
2592
+ )
2593
+
1933
2594
  _arguments -C \\
1934
2595
  '(-h --help)'{-h,--help}'[Display help]' \\
1935
2596
  '(-V --version)'{-V,--version}'[Show version]' \\
@@ -1951,6 +2612,13 @@ _computer() {
1951
2612
  whoami)
1952
2613
  _arguments '--json[Print raw JSON]'
1953
2614
  ;;
2615
+ claude-auth|claude-login)
2616
+ _arguments \\
2617
+ '--machine[Use a specific computer]:computer:_computer_handles' \\
2618
+ '--keep-helper[Keep a temporary helper machine]' \\
2619
+ '--skip-cross-check[Skip second-machine verification]' \\
2620
+ '--verbose[Show step-by-step auth diagnostics]'
2621
+ ;;
1954
2622
  create)
1955
2623
  _arguments \\
1956
2624
  '--name[Display name]:name:' \\
@@ -1960,6 +2628,7 @@ _computer() {
1960
2628
  '--source-kind[Source kind]:kind:(none oci-image)' \\
1961
2629
  '--image-family[Image family]:family:' \\
1962
2630
  '--image-ref[Image reference]:image:' \\
2631
+ '--use-platform-default[Use platform default image]' \\
1963
2632
  '--primary-port[Primary app port]:port:' \\
1964
2633
  '--primary-path[Primary app path]:path:' \\
1965
2634
  '--healthcheck-type[Healthcheck type]:type:(http tcp)' \\
@@ -1980,6 +2649,50 @@ _computer() {
1980
2649
  '--json[Print raw JSON]' \\
1981
2650
  '1:computer:_computer_handles'
1982
2651
  ;;
2652
+ image)
2653
+ _arguments -C \\
2654
+ '1:command:->image_command' \\
2655
+ '*::arg:->image_args'
2656
+ case "$state" in
2657
+ image_command)
2658
+ _describe -t commands 'image command' image_commands
2659
+ ;;
2660
+ image_args)
2661
+ case "$words[2]" in
2662
+ ls)
2663
+ _arguments '--json[Print raw JSON]'
2664
+ ;;
2665
+ save)
2666
+ _arguments \\
2667
+ '--id[source id]:id:' \\
2668
+ '--kind[source kind]:kind:(oci-image nix-git)' \\
2669
+ '--requested-ref[OCI image ref or resolved ref]:ref:' \\
2670
+ '--git-url[Git URL]:url:' \\
2671
+ '--git-ref[Git ref]:ref:' \\
2672
+ '--git-subpath[Git subpath]:path:' \\
2673
+ '--set-as-default[Select as default after saving]' \\
2674
+ '--json[Print raw JSON]'
2675
+ ;;
2676
+ default)
2677
+ _arguments \\
2678
+ '--json[Print raw JSON]' \\
2679
+ '1:source id:_machine_source_ids'
2680
+ ;;
2681
+ rebuild)
2682
+ _arguments \\
2683
+ '--json[Print raw JSON]' \\
2684
+ '1:source id:_machine_source_ids'
2685
+ ;;
2686
+ rm)
2687
+ _arguments \\
2688
+ '--json[Print raw JSON]' \\
2689
+ '(-y --yes)'{-y,--yes}'[Skip confirmation]' \\
2690
+ '1:source id:_machine_source_ids'
2691
+ ;;
2692
+ esac
2693
+ ;;
2694
+ esac
2695
+ ;;
1983
2696
  open)
1984
2697
  _arguments \\
1985
2698
  '--vnc[Open VNC desktop]' \\
@@ -2039,13 +2752,22 @@ _computer_handles() {
2039
2752
  fi
2040
2753
  }
2041
2754
 
2755
+ _machine_source_ids() {
2756
+ local -a ids
2757
+ local cli="\${words[1]:-computer}"
2758
+ if ids=(\${(f)"$(\${cli} image ls --json 2>/dev/null | grep '"id"' | sed 's/.*"id": "\\([^"]*\\)".*/\\1/')"}); then
2759
+ _describe -t ids 'machine image source' ids
2760
+ fi
2761
+ }
2762
+
2042
2763
  _computer "$@"`;
2043
2764
  var BASH_SCRIPT = `_computer() {
2044
2765
  local cur prev words cword
2045
2766
  _init_completion || return
2046
2767
 
2047
- local commands="login logout whoami create ls get open ssh ports agent fleet acp rm completion help"
2768
+ local commands="login logout whoami claude-auth claude-login create ls get image open ssh ports agent fleet acp rm completion help"
2048
2769
  local ports_commands="ls publish rm"
2770
+ local image_commands="ls save default rebuild rm"
2049
2771
 
2050
2772
  if [[ $cword -eq 1 ]]; then
2051
2773
  COMPREPLY=($(compgen -W "$commands" -- "$cur"))
@@ -2061,12 +2783,60 @@ var BASH_SCRIPT = `_computer() {
2061
2783
  whoami)
2062
2784
  COMPREPLY=($(compgen -W "--json" -- "$cur"))
2063
2785
  ;;
2786
+ claude-auth|claude-login)
2787
+ COMPREPLY=($(compgen -W "--machine --keep-helper --skip-cross-check --verbose" -- "$cur"))
2788
+ ;;
2064
2789
  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"))
2790
+ 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
2791
  ;;
2067
2792
  ls)
2068
2793
  COMPREPLY=($(compgen -W "--json --verbose -v" -- "$cur"))
2069
2794
  ;;
2795
+ image)
2796
+ if [[ $cword -eq 2 ]]; then
2797
+ COMPREPLY=($(compgen -W "$image_commands" -- "$cur"))
2798
+ else
2799
+ case "\${words[2]}" in
2800
+ ls)
2801
+ COMPREPLY=($(compgen -W "--json" -- "$cur"))
2802
+ ;;
2803
+ save)
2804
+ COMPREPLY=($(compgen -W "--id --kind --requested-ref --git-url --git-ref --git-subpath --set-as-default --json" -- "$cur"))
2805
+ ;;
2806
+ default|rebuild|rm)
2807
+ case "\${words[2]}" in
2808
+ default)
2809
+ if [[ "$cur" == -* ]]; then
2810
+ COMPREPLY=($(compgen -W "--json" -- "$cur"))
2811
+ else
2812
+ local source_ids cli="\${words[0]:-computer}"
2813
+ source_ids=$(\${cli} image ls --json 2>/dev/null | grep '"id"' | sed 's/.*"id": "([^"]*)".*/\\1/')
2814
+ COMPREPLY=($(compgen -W "$source_ids" -- "$cur"))
2815
+ fi
2816
+ ;;
2817
+ rebuild)
2818
+ if [[ "$cur" == -* ]]; then
2819
+ COMPREPLY=($(compgen -W "--json" -- "$cur"))
2820
+ else
2821
+ local source_ids cli="\${words[0]:-computer}"
2822
+ source_ids=$(\${cli} image ls --json 2>/dev/null | grep '"id"' | sed 's/.*"id": "([^"]*)".*/\\1/')
2823
+ COMPREPLY=($(compgen -W "$source_ids" -- "$cur"))
2824
+ fi
2825
+ ;;
2826
+ rm)
2827
+ if [[ "$cur" == -* ]]; then
2828
+ COMPREPLY=($(compgen -W "--json --yes -y" -- "$cur"))
2829
+ else
2830
+ local source_ids cli="\${words[0]:-computer}"
2831
+ source_ids=$(\${cli} image ls --json 2>/dev/null | grep '"id"' | sed 's/.*"id": "([^"]*)".*/\\1/')
2832
+ COMPREPLY=($(compgen -W "$source_ids" -- "$cur"))
2833
+ fi
2834
+ ;;
2835
+ esac
2836
+ ;;
2837
+ esac
2838
+ fi
2839
+ ;;
2070
2840
  get|open|ssh|rm)
2071
2841
  if [[ $cword -eq 2 ]]; then
2072
2842
  local handles cli="\${words[0]:-computer}"
@@ -2098,7 +2868,7 @@ var BASH_SCRIPT = `_computer() {
2098
2868
  }
2099
2869
 
2100
2870
  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) => {
2871
+ var completionCommand = new Command6("completion").description("Generate shell completions").argument("<shell>", "Shell type (bash or zsh)").action((shell) => {
2102
2872
  switch (shell) {
2103
2873
  case "zsh":
2104
2874
  console.log(ZSH_SCRIPT);
@@ -2112,19 +2882,299 @@ var completionCommand = new Command5("completion").description("Generate shell c
2112
2882
  }
2113
2883
  });
2114
2884
 
2885
+ // src/commands/images.ts
2886
+ import { confirm as confirm2, input as textInput3, select as select3 } from "@inquirer/prompts";
2887
+ import { Command as Command7 } from "commander";
2888
+ import chalk7 from "chalk";
2889
+ import ora5 from "ora";
2890
+ var imageCommand = new Command7("image").description("Manage machine image sources");
2891
+ imageCommand.command("ls").description("List machine image sources").option("--json", "Print raw JSON").action(async (options) => {
2892
+ const spinner = options.json ? null : ora5("Fetching machine images...").start();
2893
+ try {
2894
+ const settings = await getMachineSourceSettings();
2895
+ spinner?.stop();
2896
+ if (options.json) {
2897
+ console.log(JSON.stringify(settings, null, 2));
2898
+ return;
2899
+ }
2900
+ printMachineSourceSettings(settings);
2901
+ } catch (error) {
2902
+ if (spinner) {
2903
+ spinner.fail(error instanceof Error ? error.message : "Failed to fetch machine images");
2904
+ } else {
2905
+ console.error(error instanceof Error ? error.message : "Failed to fetch machine images");
2906
+ }
2907
+ process.exit(1);
2908
+ }
2909
+ });
2910
+ 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) => {
2911
+ const spinner = options.json ? null : ora5("Saving machine image source...").start();
2912
+ try {
2913
+ const input = await resolveSaveInput(options);
2914
+ const settings = await upsertMachineSource(input);
2915
+ spinner?.stop();
2916
+ if (options.json) {
2917
+ console.log(JSON.stringify(settings, null, 2));
2918
+ return;
2919
+ }
2920
+ console.log();
2921
+ console.log(chalk7.green("Saved machine image source."));
2922
+ printMachineSourceSettings(settings);
2923
+ } catch (error) {
2924
+ if (spinner) {
2925
+ spinner.fail(error instanceof Error ? error.message : "Failed to save machine image source");
2926
+ } else {
2927
+ console.error(error instanceof Error ? error.message : "Failed to save machine image source");
2928
+ }
2929
+ process.exit(1);
2930
+ }
2931
+ });
2932
+ 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) => {
2933
+ const usePlatformDefault = !sourceID || sourceID === "platform";
2934
+ const spinner = options.json ? null : ora5("Updating machine image default...").start();
2935
+ try {
2936
+ let settings;
2937
+ if (usePlatformDefault) {
2938
+ settings = await clearMachineSourceDefault();
2939
+ } else {
2940
+ const current = await getMachineSourceSettings();
2941
+ const source = current.sources.find((entry) => entry.id === sourceID);
2942
+ if (!source) {
2943
+ throw new Error(`machine image source '${sourceID}' not found`);
2944
+ }
2945
+ settings = await upsertMachineSource({
2946
+ id: source.id,
2947
+ kind: source.kind,
2948
+ set_as_default: true
2949
+ });
2950
+ }
2951
+ spinner?.stop();
2952
+ if (options.json) {
2953
+ console.log(JSON.stringify(settings, null, 2));
2954
+ return;
2955
+ }
2956
+ console.log();
2957
+ if (!usePlatformDefault) {
2958
+ const selected = settings.default_machine_source ?? void 0;
2959
+ const label = selected ? summarizeMachineSource(selected) : sourceID;
2960
+ console.log(chalk7.green(`Selected ${chalk7.bold(label)} as the default machine image.`));
2961
+ } else {
2962
+ console.log(chalk7.green("Using the AgentComputer platform default image."));
2963
+ }
2964
+ printMachineSourceSettings(settings);
2965
+ } catch (error) {
2966
+ if (spinner) {
2967
+ spinner.fail(error instanceof Error ? error.message : "Failed to update machine image default");
2968
+ } else {
2969
+ console.error(error instanceof Error ? error.message : "Failed to update machine image default");
2970
+ }
2971
+ process.exit(1);
2972
+ }
2973
+ });
2974
+ 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) => {
2975
+ const spinner = options.json ? null : ora5("Queueing machine image rebuild...").start();
2976
+ try {
2977
+ const settings = await rebuildMachineSource(sourceID);
2978
+ spinner?.stop();
2979
+ if (options.json) {
2980
+ console.log(JSON.stringify(settings, null, 2));
2981
+ return;
2982
+ }
2983
+ console.log();
2984
+ console.log(chalk7.green(`Queued rebuild for ${chalk7.bold(sourceID)}.`));
2985
+ printMachineSourceSettings(settings);
2986
+ } catch (error) {
2987
+ if (spinner) {
2988
+ spinner.fail(error instanceof Error ? error.message : "Failed to rebuild machine image source");
2989
+ } else {
2990
+ console.error(error instanceof Error ? error.message : "Failed to rebuild machine image source");
2991
+ }
2992
+ process.exit(1);
2993
+ }
2994
+ });
2995
+ 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) => {
2996
+ const globalYes = cmd.parent?.parent?.opts()?.yes;
2997
+ const skipConfirm = Boolean(options.yes || globalYes);
2998
+ let spinner = null;
2999
+ try {
3000
+ if (!skipConfirm && process.stdin.isTTY) {
3001
+ const confirmed = await confirmDeletion(sourceID);
3002
+ if (!confirmed) {
3003
+ console.log(chalk7.dim(" Cancelled."));
3004
+ return;
3005
+ }
3006
+ }
3007
+ spinner = options.json ? null : ora5("Deleting machine image source...").start();
3008
+ const settings = await deleteMachineSource(sourceID);
3009
+ spinner?.stop();
3010
+ if (options.json) {
3011
+ console.log(JSON.stringify(settings, null, 2));
3012
+ return;
3013
+ }
3014
+ console.log();
3015
+ console.log(chalk7.green(`Deleted machine image source ${chalk7.bold(sourceID)}.`));
3016
+ printMachineSourceSettings(settings);
3017
+ } catch (error) {
3018
+ if (spinner) {
3019
+ spinner.fail(error instanceof Error ? error.message : "Failed to delete machine image source");
3020
+ } else {
3021
+ console.error(error instanceof Error ? error.message : "Failed to delete machine image source");
3022
+ }
3023
+ process.exit(1);
3024
+ }
3025
+ });
3026
+ function printMachineSourceSettings(settings) {
3027
+ console.log(` ${chalk7.dim("Default")} ${chalk7.white(summarizeMachineSourceSelection(settings))}`);
3028
+ console.log();
3029
+ if (settings.sources.length === 0) {
3030
+ console.log(chalk7.dim(" No custom machine images configured yet."));
3031
+ console.log();
3032
+ return;
3033
+ }
3034
+ for (const source of settings.sources) {
3035
+ printMachineSourceCard(source, settings.default_machine_source_id === source.id);
3036
+ }
3037
+ }
3038
+ function printMachineSourceCard(source, isDefault) {
3039
+ console.log(` ${chalk7.bold(machineSourceTitle(source))}${isDefault ? chalk7.green(" (default)") : ""}`);
3040
+ console.log(` ${chalk7.dim(" ID")} ${source.id}`);
3041
+ console.log(` ${chalk7.dim(" Kind")} ${source.kind}`);
3042
+ console.log(` ${chalk7.dim(" Status")} ${source.status}${source.latest_build ? ` | latest build ${source.latest_build.status}` : ""}`);
3043
+ if (source.resolved_image_ref) {
3044
+ console.log(` ${chalk7.dim(" Resolved")} ${source.resolved_image_ref}`);
3045
+ }
3046
+ if (source.error) {
3047
+ console.log(` ${chalk7.dim(" Error")} ${chalk7.red(source.error)}`);
3048
+ }
3049
+ console.log(` ${chalk7.dim(" Source")} ${summarizeMachineSource(source)}`);
3050
+ console.log();
3051
+ }
3052
+ function machineSourceTitle(source) {
3053
+ if (source.kind === "oci-image") {
3054
+ return source.requested_ref || "OCI image";
3055
+ }
3056
+ return source.git_url || "Nix git source";
3057
+ }
3058
+ function parseMachineSourceKind(value) {
3059
+ switch (value) {
3060
+ case void 0:
3061
+ case "oci-image":
3062
+ case "nix-git":
3063
+ return value;
3064
+ default:
3065
+ throw new Error("--kind must be oci-image or nix-git");
3066
+ }
3067
+ }
3068
+ function hasTTY() {
3069
+ return process.stdin.isTTY && process.stdout.isTTY;
3070
+ }
3071
+ async function resolveSaveInput(options) {
3072
+ const existing = await loadExistingSource(options.id);
3073
+ const selectedKind = parseMachineSourceKind(options.kind) ?? existing?.kind;
3074
+ let kind = selectedKind;
3075
+ if (!kind) {
3076
+ if (!hasTTY()) {
3077
+ throw new Error("--kind is required");
3078
+ }
3079
+ kind = await selectMachineSourceKind();
3080
+ }
3081
+ const input = {
3082
+ id: options.id,
3083
+ kind,
3084
+ set_as_default: options.setAsDefault
3085
+ };
3086
+ switch (kind) {
3087
+ case "oci-image": {
3088
+ const existingSameKind = existing?.kind === kind;
3089
+ const requestedRef = normalizeValue(options.requestedRef) ?? (existingSameKind ? existing?.requested_ref : void 0);
3090
+ if (!requestedRef) {
3091
+ if (!hasTTY()) {
3092
+ throw new Error("--requested-ref is required for oci-image sources");
3093
+ }
3094
+ const promptedRef = normalizeValue(await textInput3({ message: "OCI image ref:" }));
3095
+ if (!promptedRef) {
3096
+ throw new Error("OCI image ref is required");
3097
+ }
3098
+ input.requested_ref = promptedRef;
3099
+ break;
3100
+ }
3101
+ input.requested_ref = requestedRef;
3102
+ break;
3103
+ }
3104
+ case "nix-git": {
3105
+ const existingSameKind = existing?.kind === kind;
3106
+ const gitUrl = normalizeValue(options.gitUrl) ?? (existingSameKind ? existing?.git_url : void 0);
3107
+ const gitRef = normalizeValue(options.gitRef) ?? (existingSameKind ? existing?.git_ref : void 0);
3108
+ const gitSubpath = normalizeValue(options.gitSubpath) ?? (existingSameKind ? existing?.git_subpath : void 0);
3109
+ if (!gitUrl) {
3110
+ if (!hasTTY()) {
3111
+ throw new Error("--git-url is required for nix-git sources");
3112
+ }
3113
+ const promptedGitURL = normalizeValue(await textInput3({ message: "Git URL:" }));
3114
+ if (!promptedGitURL) {
3115
+ throw new Error("Git URL is required");
3116
+ }
3117
+ input.git_url = promptedGitURL;
3118
+ break;
3119
+ }
3120
+ input.git_url = gitUrl;
3121
+ if (gitRef) {
3122
+ input.git_ref = gitRef;
3123
+ }
3124
+ if (gitSubpath) {
3125
+ input.git_subpath = gitSubpath;
3126
+ }
3127
+ break;
3128
+ }
3129
+ }
3130
+ return input;
3131
+ }
3132
+ async function selectMachineSourceKind() {
3133
+ const kind = await select3({
3134
+ message: "Select machine image kind",
3135
+ choices: [
3136
+ { name: "oci-image - resolve an OCI image digest", value: "oci-image" },
3137
+ { name: "nix-git - build a Nix source into OCI", value: "nix-git" }
3138
+ ],
3139
+ default: "oci-image"
3140
+ });
3141
+ return kind;
3142
+ }
3143
+ async function loadExistingSource(sourceID) {
3144
+ if (!sourceID) {
3145
+ return void 0;
3146
+ }
3147
+ const settings = await getMachineSourceSettings();
3148
+ const source = settings.sources.find((entry) => entry.id === sourceID);
3149
+ if (!source) {
3150
+ throw new Error(`machine image source '${sourceID}' not found`);
3151
+ }
3152
+ return source;
3153
+ }
3154
+ function normalizeValue(value) {
3155
+ const trimmed = value?.trim();
3156
+ return trimmed ? trimmed : void 0;
3157
+ }
3158
+ async function confirmDeletion(sourceID) {
3159
+ return confirm2({
3160
+ message: `Delete machine image source ${sourceID}?`,
3161
+ default: false
3162
+ });
3163
+ }
3164
+
2115
3165
  // src/commands/login.ts
2116
- import { Command as Command6 } from "commander";
2117
- import chalk5 from "chalk";
2118
- import ora4 from "ora";
3166
+ import { Command as Command8 } from "commander";
3167
+ import chalk8 from "chalk";
3168
+ import ora6 from "ora";
2119
3169
 
2120
3170
  // src/lib/browser-login.ts
2121
- import { randomBytes } from "crypto";
3171
+ import { randomBytes as randomBytes2 } from "crypto";
2122
3172
  import { createServer } from "http";
2123
3173
  var CALLBACK_HOST = "127.0.0.1";
2124
3174
  var CALLBACK_PATH = "/callback";
2125
3175
  var LOGIN_TIMEOUT_MS = 5 * 60 * 1e3;
2126
3176
  async function createBrowserLoginAttempt() {
2127
- const state = randomBytes(16).toString("hex");
3177
+ const state = randomBytes2(16).toString("hex");
2128
3178
  const deferred = createDeferred();
2129
3179
  let callbackURL = "";
2130
3180
  let closed = false;
@@ -2343,11 +3393,11 @@ function escapeHTML(value) {
2343
3393
  }
2344
3394
 
2345
3395
  // 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) => {
3396
+ 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
3397
  const existingKey = getStoredAPIKey();
2348
3398
  if (existingKey && !options.force) {
2349
3399
  console.log();
2350
- console.log(chalk5.yellow(" Already logged in. Use --force to overwrite."));
3400
+ console.log(chalk8.yellow(" Already logged in. Use --force to overwrite."));
2351
3401
  console.log();
2352
3402
  return;
2353
3403
  }
@@ -2355,8 +3405,8 @@ var loginCommand = new Command6("login").description("Authenticate the CLI").opt
2355
3405
  const apiKey = await resolveAPIKeyInput(options.apiKey, options.stdin);
2356
3406
  if (!apiKey && wantsManualLogin) {
2357
3407
  console.log();
2358
- console.log(chalk5.dim(" Usage: computer login --api-key <ac_live_...>"));
2359
- console.log(chalk5.dim(` API: ${getBaseURL()}`));
3408
+ console.log(chalk8.dim(" Usage: computer login --api-key <ac_live_...>"));
3409
+ console.log(chalk8.dim(` API: ${getBaseURL()}`));
2360
3410
  console.log();
2361
3411
  process.exit(1);
2362
3412
  }
@@ -2366,15 +3416,15 @@ var loginCommand = new Command6("login").description("Authenticate the CLI").opt
2366
3416
  }
2367
3417
  if (!apiKey.startsWith("ac_live_")) {
2368
3418
  console.log();
2369
- console.log(chalk5.red(" API key must start with ac_live_"));
3419
+ console.log(chalk8.red(" API key must start with ac_live_"));
2370
3420
  console.log();
2371
3421
  process.exit(1);
2372
3422
  }
2373
- const spinner = ora4("Authenticating...").start();
3423
+ const spinner = ora6("Authenticating...").start();
2374
3424
  try {
2375
3425
  const me = await apiWithKey(apiKey, "/v1/me");
2376
3426
  setAPIKey(apiKey);
2377
- spinner.succeed(`Logged in as ${chalk5.bold(me.user.email)}`);
3427
+ spinner.succeed(`Logged in as ${chalk8.bold(me.user.email)}`);
2378
3428
  } catch (error) {
2379
3429
  spinner.fail(
2380
3430
  error instanceof Error ? error.message : "Failed to validate API key"
@@ -2383,7 +3433,7 @@ var loginCommand = new Command6("login").description("Authenticate the CLI").opt
2383
3433
  }
2384
3434
  });
2385
3435
  async function runBrowserLogin() {
2386
- const spinner = ora4("Starting browser login...").start();
3436
+ const spinner = ora6("Starting browser login...").start();
2387
3437
  let attempt = null;
2388
3438
  try {
2389
3439
  attempt = await createBrowserLoginAttempt();
@@ -2393,14 +3443,14 @@ async function runBrowserLogin() {
2393
3443
  } catch {
2394
3444
  spinner.stop();
2395
3445
  console.log();
2396
- console.log(chalk5.yellow(" Browser auto-open failed. Open this URL to continue:"));
2397
- console.log(chalk5.dim(` ${attempt.loginURL}`));
3446
+ console.log(chalk8.yellow(" Browser auto-open failed. Open this URL to continue:"));
3447
+ console.log(chalk8.dim(` ${attempt.loginURL}`));
2398
3448
  console.log();
2399
3449
  spinner.start("Waiting for browser login...");
2400
3450
  }
2401
3451
  spinner.text = "Waiting for browser login...";
2402
3452
  const result = await attempt.waitForResult();
2403
- spinner.succeed(`Logged in as ${chalk5.bold(result.me.user.email)}`);
3453
+ spinner.succeed(`Logged in as ${chalk8.bold(result.me.user.email)}`);
2404
3454
  } catch (error) {
2405
3455
  spinner.fail(error instanceof Error ? error.message : "Browser login failed");
2406
3456
  process.exit(1);
@@ -2426,33 +3476,33 @@ async function resolveAPIKeyInput(flagValue, readFromStdin) {
2426
3476
  }
2427
3477
 
2428
3478
  // 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(() => {
3479
+ import { Command as Command9 } from "commander";
3480
+ import chalk9 from "chalk";
3481
+ var logoutCommand = new Command9("logout").description("Remove stored API key").action(() => {
2432
3482
  if (!getStoredAPIKey()) {
2433
3483
  console.log();
2434
- console.log(chalk6.dim(" Not logged in."));
3484
+ console.log(chalk9.dim(" Not logged in."));
2435
3485
  if (hasEnvAPIKey()) {
2436
- console.log(chalk6.dim(" Environment API key is still active in this shell."));
3486
+ console.log(chalk9.dim(" Environment API key is still active in this shell."));
2437
3487
  }
2438
3488
  console.log();
2439
3489
  return;
2440
3490
  }
2441
3491
  clearAPIKey();
2442
3492
  console.log();
2443
- console.log(chalk6.green(" Logged out."));
3493
+ console.log(chalk9.green(" Logged out."));
2444
3494
  if (hasEnvAPIKey()) {
2445
- console.log(chalk6.dim(" Environment API key is still active in this shell."));
3495
+ console.log(chalk9.dim(" Environment API key is still active in this shell."));
2446
3496
  }
2447
3497
  console.log();
2448
3498
  });
2449
3499
 
2450
3500
  // 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();
3501
+ import { Command as Command10 } from "commander";
3502
+ import chalk10 from "chalk";
3503
+ import ora7 from "ora";
3504
+ var whoamiCommand = new Command10("whoami").description("Show current user").option("--json", "Print raw JSON").action(async (options) => {
3505
+ const spinner = options.json ? null : ora7("Loading user...").start();
2456
3506
  try {
2457
3507
  const me = await api("/v1/me");
2458
3508
  spinner?.stop();
@@ -2461,14 +3511,14 @@ var whoamiCommand = new Command8("whoami").description("Show current user").opti
2461
3511
  return;
2462
3512
  }
2463
3513
  console.log();
2464
- console.log(` ${chalk7.bold.white(me.user.display_name || me.user.email)}`);
3514
+ console.log(` ${chalk10.bold.white(me.user.display_name || me.user.email)}`);
2465
3515
  if (me.user.display_name) {
2466
- console.log(` ${chalk7.dim(me.user.email)}`);
3516
+ console.log(` ${chalk10.dim(me.user.email)}`);
2467
3517
  }
2468
3518
  if (me.api_key.name) {
2469
- console.log(` ${chalk7.dim("Key:")} ${me.api_key.name}`);
3519
+ console.log(` ${chalk10.dim("Key:")} ${me.api_key.name}`);
2470
3520
  }
2471
- console.log(` ${chalk7.dim("API:")} ${chalk7.dim(getBaseURL())}`);
3521
+ console.log(` ${chalk10.dim("API:")} ${chalk10.dim(getBaseURL())}`);
2472
3522
  console.log();
2473
3523
  } catch (error) {
2474
3524
  if (spinner) {
@@ -2485,15 +3535,15 @@ var pkg2 = JSON.parse(
2485
3535
  readFileSync3(new URL("../package.json", import.meta.url), "utf8")
2486
3536
  );
2487
3537
  var cliName = process.argv[1] ? basename2(process.argv[1]) : "agentcomputer";
2488
- var program = new Command9();
3538
+ var program = new Command11();
2489
3539
  function appendTextSection(lines, title, values) {
2490
3540
  if (values.length === 0) {
2491
3541
  return;
2492
3542
  }
2493
- lines.push(` ${chalk8.dim(title)}`);
3543
+ lines.push(` ${chalk11.dim(title)}`);
2494
3544
  lines.push("");
2495
3545
  for (const value of values) {
2496
- lines.push(` ${chalk8.white(value)}`);
3546
+ lines.push(` ${chalk11.white(value)}`);
2497
3547
  }
2498
3548
  lines.push("");
2499
3549
  }
@@ -2502,10 +3552,10 @@ function appendTableSection(lines, title, entries) {
2502
3552
  return;
2503
3553
  }
2504
3554
  const width = Math.max(...entries.map((entry) => entry.term.length), 0) + 2;
2505
- lines.push(` ${chalk8.dim(title)}`);
3555
+ lines.push(` ${chalk11.dim(title)}`);
2506
3556
  lines.push("");
2507
3557
  for (const entry of entries) {
2508
- lines.push(` ${chalk8.white(padEnd(entry.term, width))}${chalk8.dim(entry.desc)}`);
3558
+ lines.push(` ${chalk11.white(padEnd(entry.term, width))}${chalk11.dim(entry.desc)}`);
2509
3559
  }
2510
3560
  lines.push("");
2511
3561
  }
@@ -2524,29 +3574,32 @@ function formatRootHelp(cmd) {
2524
3574
  const groups = [
2525
3575
  ["Auth", []],
2526
3576
  ["Computers", []],
3577
+ ["Images", []],
2527
3578
  ["Access", []],
2528
3579
  ["Agents", []],
2529
3580
  ["Other", []]
2530
3581
  ];
2531
3582
  const otherGroup = groups.find(([name]) => name === "Other")[1];
2532
- lines.push(`${chalk8.bold(cliName)} ${chalk8.dim(`v${version}`)}`);
3583
+ lines.push(`${chalk11.bold(cliName)} ${chalk11.dim(`v${version}`)}`);
2533
3584
  lines.push("");
2534
3585
  if (cmd.description()) {
2535
- lines.push(` ${chalk8.dim(cmd.description())}`);
3586
+ lines.push(` ${chalk11.dim(cmd.description())}`);
2536
3587
  lines.push("");
2537
3588
  }
2538
3589
  appendTextSection(lines, "Usage", [`${cliName} <command> [options]`]);
2539
3590
  for (const sub of cmd.commands) {
2540
3591
  const name = sub.name();
2541
3592
  const entry = { term: name, desc: sub.description() };
2542
- if (["login", "logout", "whoami"].includes(name)) {
3593
+ if (["login", "logout", "whoami", "claude-auth"].includes(name)) {
2543
3594
  groups[0][1].push(entry);
2544
3595
  } else if (["create", "ls", "get", "rm"].includes(name)) {
2545
3596
  groups[1][1].push(entry);
2546
- } else if (["open", "ssh", "ports"].includes(name)) {
3597
+ } else if (name === "image") {
2547
3598
  groups[2][1].push(entry);
2548
- } else if (["agent", "fleet", "acp"].includes(name)) {
3599
+ } else if (["open", "ssh", "ports"].includes(name)) {
2549
3600
  groups[3][1].push(entry);
3601
+ } else if (["agent", "fleet", "acp"].includes(name)) {
3602
+ groups[4][1].push(entry);
2550
3603
  } else {
2551
3604
  otherGroup.push(entry);
2552
3605
  }
@@ -2581,10 +3634,10 @@ function formatSubcommandHelp(cmd, helper) {
2581
3634
  term: helper.optionTerm(option),
2582
3635
  desc: helper.optionDescription(option)
2583
3636
  }));
2584
- lines.push(chalk8.bold(commandPath(cmd)));
3637
+ lines.push(chalk11.bold(commandPath(cmd)));
2585
3638
  lines.push("");
2586
3639
  if (description) {
2587
- lines.push(` ${chalk8.dim(description)}`);
3640
+ lines.push(` ${chalk11.dim(description)}`);
2588
3641
  lines.push("");
2589
3642
  }
2590
3643
  appendTextSection(lines, "Usage", [helper.commandUsage(cmd)]);
@@ -2611,9 +3664,11 @@ program.name(cliName).description("Agent Computer CLI").version(pkg2.version ??
2611
3664
  program.addCommand(loginCommand);
2612
3665
  program.addCommand(logoutCommand);
2613
3666
  program.addCommand(whoamiCommand);
3667
+ program.addCommand(claudeAuthCommand);
2614
3668
  program.addCommand(createCommand);
2615
3669
  program.addCommand(lsCommand);
2616
3670
  program.addCommand(getCommand);
3671
+ program.addCommand(imageCommand);
2617
3672
  program.addCommand(agentCommand);
2618
3673
  program.addCommand(fleetCommand);
2619
3674
  program.addCommand(acpCommand);