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.
- package/dist/index.js +1268 -213
- 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
|
|
5
|
-
import
|
|
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
|
|
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 ${
|
|
511
|
+
spinner.succeed(`Opening VNC for ${chalk3.bold(computer.handle)}`);
|
|
470
512
|
await openBrowserURL(url);
|
|
471
|
-
console.log(
|
|
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 ${
|
|
521
|
+
spinner.succeed(`Opening terminal for ${chalk3.bold(computer.handle)}`);
|
|
480
522
|
await openBrowserURL(access3.access_url);
|
|
481
|
-
console.log(
|
|
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 ${
|
|
527
|
+
spinner.succeed(`Opening ${chalk3.bold(computer.handle)}`);
|
|
486
528
|
await openBrowserURL(access2.access_url);
|
|
487
|
-
console.log(
|
|
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 ${
|
|
509
|
-
console.log(
|
|
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(
|
|
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
|
-
` ${
|
|
581
|
+
` ${chalk3.dim(padEnd("Subdomain", subWidth + 2))}${chalk3.dim(padEnd("Port", 8))}${chalk3.dim("Protocol")}`
|
|
540
582
|
);
|
|
541
583
|
console.log(
|
|
542
|
-
` ${
|
|
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
|
-
` ${
|
|
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 ${
|
|
574
|
-
console.log(
|
|
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 ${
|
|
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(
|
|
627
|
-
console.log(
|
|
668
|
+
console.log(chalk3.dim(` SSH config: ${configResult.configPath}`));
|
|
669
|
+
console.log(chalk3.dim(` Identity: ${registered.privateKeyPath}`));
|
|
628
670
|
console.log();
|
|
629
|
-
console.log(` ${
|
|
630
|
-
console.log(` ${
|
|
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
|
-
|
|
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
|
|
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
|
|
1082
|
+
return chalk4.green(status);
|
|
1071
1083
|
case "running":
|
|
1072
|
-
return
|
|
1084
|
+
return chalk4.blue(status);
|
|
1073
1085
|
case "cancelling":
|
|
1074
|
-
return
|
|
1086
|
+
return chalk4.yellow(status);
|
|
1075
1087
|
case "interrupted":
|
|
1076
|
-
return
|
|
1088
|
+
return chalk4.yellow(status);
|
|
1077
1089
|
case "failed":
|
|
1078
|
-
return
|
|
1090
|
+
return chalk4.red(status);
|
|
1079
1091
|
case "closed":
|
|
1080
|
-
return
|
|
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
|
-
` ${
|
|
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
|
-
` ${
|
|
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 ?
|
|
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(
|
|
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
|
-
` ${
|
|
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
|
-
` ${
|
|
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(` ${
|
|
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(` ${
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(` ${
|
|
1391
|
+
console.log(` ${chalk4.bold(session.name || "default")} ${formatAgentSessionStatus(settled.status)}`);
|
|
1380
1392
|
if (settled.last_stop_reason) {
|
|
1381
|
-
console.log(
|
|
1393
|
+
console.log(chalk4.dim(` stop_reason=${settled.last_stop_reason}`));
|
|
1382
1394
|
}
|
|
1383
1395
|
if (settled.last_error) {
|
|
1384
|
-
console.log(
|
|
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/
|
|
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
|
|
1531
|
+
import chalk5 from "chalk";
|
|
1517
1532
|
import ora3 from "ora";
|
|
1518
|
-
|
|
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(` ${
|
|
2121
|
+
console.log(` ${chalk6.bold.white(computer.handle)} ${formatStatus(computer.status)}`);
|
|
1529
2122
|
console.log();
|
|
1530
|
-
console.log(` ${
|
|
1531
|
-
console.log(` ${
|
|
2123
|
+
console.log(` ${chalk6.dim("ID")} ${computer.id}`);
|
|
2124
|
+
console.log(` ${chalk6.dim("Tier")} ${computer.tier}`);
|
|
1532
2125
|
if (isCustom) {
|
|
1533
|
-
console.log(` ${
|
|
1534
|
-
console.log(` ${
|
|
1535
|
-
console.log(` ${
|
|
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(` ${
|
|
1538
|
-
console.log(` ${
|
|
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(` ${
|
|
1541
|
-
console.log(` ${
|
|
1542
|
-
console.log(` ${
|
|
1543
|
-
console.log(` ${
|
|
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(` ${
|
|
2146
|
+
console.log(` ${chalk6.dim("Condition")} ${chalk6.dim(computer.last_error)}`);
|
|
1548
2147
|
} else {
|
|
1549
|
-
console.log(` ${
|
|
2148
|
+
console.log(` ${chalk6.dim("Error")} ${chalk6.red(computer.last_error)}`);
|
|
1550
2149
|
}
|
|
1551
2150
|
}
|
|
1552
2151
|
console.log();
|
|
1553
|
-
console.log(` ${
|
|
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
|
-
` ${
|
|
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
|
-
` ${
|
|
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 =
|
|
2196
|
+
const created = chalk6.dim(timeAgo(computer.created_at));
|
|
1579
2197
|
console.log(
|
|
1580
|
-
` ${
|
|
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
|
-
` ${
|
|
2208
|
+
` ${chalk6.dim(padEnd("Handle", handleWidth + 2))}${chalk6.dim(padEnd("Status", statusWidth + 2))}${chalk6.dim("URLs")}`
|
|
1591
2209
|
);
|
|
1592
2210
|
console.log(
|
|
1593
|
-
` ${
|
|
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
|
-
` ${
|
|
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)}${
|
|
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)}${
|
|
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
|
|
1612
|
-
const spinner = options.json ? null :
|
|
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(
|
|
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
|
|
1643
|
-
const spinner = options.json ? null :
|
|
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
|
|
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(
|
|
2300
|
+
console.log(chalk6.dim(provisioningNote));
|
|
1674
2301
|
}
|
|
1675
|
-
spinner =
|
|
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
|
-
|
|
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 ? ` ${
|
|
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(
|
|
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
|
|
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
|
|
1754
|
-
selectedOptions.primaryPort = (await
|
|
1755
|
-
selectedOptions.primaryPath = (await
|
|
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
|
|
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
|
|
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 =
|
|
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 ${
|
|
2414
|
+
message: `Delete computer ${chalk6.bold(computer.handle)}?`,
|
|
1788
2415
|
default: false
|
|
1789
2416
|
});
|
|
1790
2417
|
if (!confirmed) {
|
|
1791
|
-
console.log(
|
|
2418
|
+
console.log(chalk6.dim(" Cancelled."));
|
|
1792
2419
|
return;
|
|
1793
2420
|
}
|
|
1794
2421
|
}
|
|
1795
|
-
const deleteSpinner =
|
|
2422
|
+
const deleteSpinner = ora4("Deleting computer...").start();
|
|
1796
2423
|
await api(`/v1/computers/${computer.id}`, {
|
|
1797
2424
|
method: "DELETE"
|
|
1798
2425
|
});
|
|
1799
|
-
deleteSpinner.succeed(
|
|
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 =
|
|
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} ${
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
2117
|
-
import
|
|
2118
|
-
import
|
|
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 =
|
|
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
|
|
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(
|
|
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(
|
|
2359
|
-
console.log(
|
|
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(
|
|
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 =
|
|
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 ${
|
|
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 =
|
|
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(
|
|
2397
|
-
console.log(
|
|
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 ${
|
|
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
|
|
2430
|
-
import
|
|
2431
|
-
var logoutCommand = new
|
|
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(
|
|
3484
|
+
console.log(chalk9.dim(" Not logged in."));
|
|
2435
3485
|
if (hasEnvAPIKey()) {
|
|
2436
|
-
console.log(
|
|
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(
|
|
3493
|
+
console.log(chalk9.green(" Logged out."));
|
|
2444
3494
|
if (hasEnvAPIKey()) {
|
|
2445
|
-
console.log(
|
|
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
|
|
2452
|
-
import
|
|
2453
|
-
import
|
|
2454
|
-
var whoamiCommand = new
|
|
2455
|
-
const spinner = options.json ? null :
|
|
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(` ${
|
|
3514
|
+
console.log(` ${chalk10.bold.white(me.user.display_name || me.user.email)}`);
|
|
2465
3515
|
if (me.user.display_name) {
|
|
2466
|
-
console.log(` ${
|
|
3516
|
+
console.log(` ${chalk10.dim(me.user.email)}`);
|
|
2467
3517
|
}
|
|
2468
3518
|
if (me.api_key.name) {
|
|
2469
|
-
console.log(` ${
|
|
3519
|
+
console.log(` ${chalk10.dim("Key:")} ${me.api_key.name}`);
|
|
2470
3520
|
}
|
|
2471
|
-
console.log(` ${
|
|
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
|
|
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(` ${
|
|
3543
|
+
lines.push(` ${chalk11.dim(title)}`);
|
|
2494
3544
|
lines.push("");
|
|
2495
3545
|
for (const value of values) {
|
|
2496
|
-
lines.push(` ${
|
|
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(` ${
|
|
3555
|
+
lines.push(` ${chalk11.dim(title)}`);
|
|
2506
3556
|
lines.push("");
|
|
2507
3557
|
for (const entry of entries) {
|
|
2508
|
-
lines.push(` ${
|
|
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(`${
|
|
3583
|
+
lines.push(`${chalk11.bold(cliName)} ${chalk11.dim(`v${version}`)}`);
|
|
2533
3584
|
lines.push("");
|
|
2534
3585
|
if (cmd.description()) {
|
|
2535
|
-
lines.push(` ${
|
|
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 (
|
|
3597
|
+
} else if (name === "image") {
|
|
2547
3598
|
groups[2][1].push(entry);
|
|
2548
|
-
} else if (["
|
|
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(
|
|
3637
|
+
lines.push(chalk11.bold(commandPath(cmd)));
|
|
2585
3638
|
lines.push("");
|
|
2586
3639
|
if (description) {
|
|
2587
|
-
lines.push(` ${
|
|
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);
|