aicomputer 0.1.6 → 0.1.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +1233 -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,558 @@ 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").action(async (options) => {
|
|
1541
|
+
const todos = createTodoList();
|
|
1542
|
+
let target = null;
|
|
1543
|
+
let helperCreated = false;
|
|
1544
|
+
let sharedInstall = false;
|
|
1545
|
+
let activeTodoID = "target";
|
|
1546
|
+
let failureMessage = null;
|
|
1547
|
+
console.log();
|
|
1548
|
+
console.log(chalk5.cyan("Authenticating with Claude Code...\n"));
|
|
1549
|
+
try {
|
|
1550
|
+
const prepared = await prepareTargetMachine(options);
|
|
1551
|
+
target = prepared.computer;
|
|
1552
|
+
helperCreated = prepared.helperCreated;
|
|
1553
|
+
sharedInstall = prepared.sharedInstall;
|
|
1554
|
+
markTodo(
|
|
1555
|
+
todos,
|
|
1556
|
+
"target",
|
|
1557
|
+
"done",
|
|
1558
|
+
prepared.detail
|
|
1559
|
+
);
|
|
1560
|
+
activeTodoID = "ready";
|
|
1561
|
+
target = await waitForRunning(target);
|
|
1562
|
+
markTodo(todos, "ready", "done", `${target.handle} is running`);
|
|
1563
|
+
activeTodoID = "oauth";
|
|
1564
|
+
const oauth = await runManualOAuthFlow();
|
|
1565
|
+
markTodo(todos, "oauth", "done", "browser flow completed");
|
|
1566
|
+
activeTodoID = "install";
|
|
1567
|
+
const sshTarget = await resolveSSHTarget(target);
|
|
1568
|
+
await installClaudeAuth(sshTarget, oauth);
|
|
1569
|
+
markTodo(
|
|
1570
|
+
todos,
|
|
1571
|
+
"install",
|
|
1572
|
+
"done",
|
|
1573
|
+
sharedInstall ? `installed Claude login on shared home via ${target.handle}` : `installed Claude login on ${target.handle}`
|
|
1574
|
+
);
|
|
1575
|
+
activeTodoID = "verify-primary";
|
|
1576
|
+
await verifyStoredAuth(sshTarget);
|
|
1577
|
+
markTodo(
|
|
1578
|
+
todos,
|
|
1579
|
+
"verify-primary",
|
|
1580
|
+
"done",
|
|
1581
|
+
`${target.handle} fresh login shell sees Claude auth`
|
|
1582
|
+
);
|
|
1583
|
+
activeTodoID = "verify-shared";
|
|
1584
|
+
const sharedCheck = await verifySecondaryMachine(
|
|
1585
|
+
target.id,
|
|
1586
|
+
sharedInstall,
|
|
1587
|
+
Boolean(options.skipCrossCheck)
|
|
1588
|
+
);
|
|
1589
|
+
if (sharedCheck.status === "verified") {
|
|
1590
|
+
markTodo(
|
|
1591
|
+
todos,
|
|
1592
|
+
"verify-shared",
|
|
1593
|
+
"done",
|
|
1594
|
+
`${sharedCheck.handle} also sees stored Claude auth`
|
|
1595
|
+
);
|
|
1596
|
+
} else {
|
|
1597
|
+
markTodo(todos, "verify-shared", "skipped", sharedCheck.reason);
|
|
1598
|
+
}
|
|
1599
|
+
} catch (error) {
|
|
1600
|
+
failureMessage = error instanceof Error ? error.message : "Failed to authenticate Claude";
|
|
1601
|
+
markTodo(todos, activeTodoID, "failed", failureMessage);
|
|
1602
|
+
} finally {
|
|
1603
|
+
if (helperCreated && target && !options.keepHelper) {
|
|
1604
|
+
try {
|
|
1605
|
+
await deleteComputer(target.id);
|
|
1606
|
+
markTodo(
|
|
1607
|
+
todos,
|
|
1608
|
+
"cleanup",
|
|
1609
|
+
"done",
|
|
1610
|
+
`removed temporary helper ${target.handle}`
|
|
1611
|
+
);
|
|
1612
|
+
} catch (error) {
|
|
1613
|
+
const message = error instanceof Error ? error.message : "failed to remove helper";
|
|
1614
|
+
markTodo(todos, "cleanup", "failed", message);
|
|
1615
|
+
}
|
|
1616
|
+
} else if (helperCreated && target && options.keepHelper) {
|
|
1617
|
+
markTodo(
|
|
1618
|
+
todos,
|
|
1619
|
+
"cleanup",
|
|
1620
|
+
"skipped",
|
|
1621
|
+
`kept helper ${target.handle}`
|
|
1622
|
+
);
|
|
1623
|
+
} else {
|
|
1624
|
+
markTodo(todos, "cleanup", "skipped", "no helper created");
|
|
1625
|
+
}
|
|
1626
|
+
printTodoList(todos);
|
|
1627
|
+
}
|
|
1628
|
+
if (failureMessage) {
|
|
1629
|
+
console.error(chalk5.red(`
|
|
1630
|
+
${failureMessage}`));
|
|
1631
|
+
process.exit(1);
|
|
1632
|
+
}
|
|
1633
|
+
if (target) {
|
|
1634
|
+
console.log(chalk5.green(`Claude is ready on ${chalk5.bold(target.handle)}.`));
|
|
1635
|
+
console.log();
|
|
1636
|
+
}
|
|
1637
|
+
});
|
|
1638
|
+
function createTodoList() {
|
|
1639
|
+
return [
|
|
1640
|
+
{ id: "target", label: "Pick target computer", state: "pending" },
|
|
1641
|
+
{ id: "ready", label: "Wait for machine readiness", state: "pending" },
|
|
1642
|
+
{ id: "oauth", label: "Complete Claude browser auth", state: "pending" },
|
|
1643
|
+
{ id: "install", label: "Install stored Claude login", state: "pending" },
|
|
1644
|
+
{ id: "verify-primary", label: "Verify on target machine", state: "pending" },
|
|
1645
|
+
{ id: "verify-shared", label: "Verify shared-home propagation", state: "pending" },
|
|
1646
|
+
{ id: "cleanup", label: "Clean up temporary helper", state: "pending" }
|
|
1647
|
+
];
|
|
1648
|
+
}
|
|
1649
|
+
function markTodo(items, id, state, detail) {
|
|
1650
|
+
const item = items.find((entry) => entry.id === id);
|
|
1651
|
+
if (!item) {
|
|
1652
|
+
return;
|
|
1653
|
+
}
|
|
1654
|
+
item.state = state;
|
|
1655
|
+
item.detail = detail;
|
|
1656
|
+
}
|
|
1657
|
+
function printTodoList(items) {
|
|
1658
|
+
console.log();
|
|
1659
|
+
console.log(chalk5.dim("TODO"));
|
|
1660
|
+
console.log();
|
|
1661
|
+
for (const item of items) {
|
|
1662
|
+
const marker = item.state === "done" ? chalk5.green("[x]") : item.state === "skipped" ? chalk5.yellow("[-]") : item.state === "failed" ? chalk5.red("[!]") : chalk5.dim("[ ]");
|
|
1663
|
+
const detail = item.detail ? chalk5.dim(` ${item.detail}`) : "";
|
|
1664
|
+
console.log(` ${marker} ${item.label}${detail ? ` ${detail}` : ""}`);
|
|
1665
|
+
}
|
|
1666
|
+
console.log();
|
|
1667
|
+
}
|
|
1668
|
+
async function prepareTargetMachine(options) {
|
|
1669
|
+
if (options.machine?.trim()) {
|
|
1670
|
+
const computer2 = await resolveComputer(options.machine.trim());
|
|
1671
|
+
assertClaudeAuthTarget(computer2);
|
|
1672
|
+
return {
|
|
1673
|
+
computer: computer2,
|
|
1674
|
+
helperCreated: false,
|
|
1675
|
+
sharedInstall: isSharedInstallTarget(computer2),
|
|
1676
|
+
detail: describeTarget(computer2, false)
|
|
1677
|
+
};
|
|
1678
|
+
}
|
|
1679
|
+
const computers = await listComputers();
|
|
1680
|
+
const filesystemSettings = await getFilesystemSettings().catch(() => null);
|
|
1681
|
+
if (filesystemSettings?.shared_enabled) {
|
|
1682
|
+
const existing = pickSharedRunningComputer(computers);
|
|
1683
|
+
if (existing) {
|
|
1684
|
+
return {
|
|
1685
|
+
computer: existing,
|
|
1686
|
+
helperCreated: false,
|
|
1687
|
+
sharedInstall: true,
|
|
1688
|
+
detail: describeTarget(existing, false)
|
|
1689
|
+
};
|
|
1690
|
+
}
|
|
1691
|
+
const spinner = ora3("Creating temporary shared helper...").start();
|
|
1692
|
+
try {
|
|
1693
|
+
const helper = await createComputer({
|
|
1694
|
+
handle: `claude-auth-${randomSuffix(6)}`,
|
|
1695
|
+
display_name: "Claude Auth Helper",
|
|
1696
|
+
runtime_family: "managed-worker",
|
|
1697
|
+
use_platform_default: true,
|
|
1698
|
+
ssh_enabled: true,
|
|
1699
|
+
vnc_enabled: false
|
|
1700
|
+
});
|
|
1701
|
+
spinner.succeed(`Created temporary helper ${chalk5.bold(helper.handle)}`);
|
|
1702
|
+
return {
|
|
1703
|
+
computer: helper,
|
|
1704
|
+
helperCreated: true,
|
|
1705
|
+
sharedInstall: true,
|
|
1706
|
+
detail: describeTarget(helper, true)
|
|
1707
|
+
};
|
|
1708
|
+
} catch (error) {
|
|
1709
|
+
spinner.fail(
|
|
1710
|
+
error instanceof Error ? error.message : "Failed to create temporary helper"
|
|
1711
|
+
);
|
|
1712
|
+
throw error;
|
|
1713
|
+
}
|
|
1714
|
+
}
|
|
1715
|
+
const computer = await promptForSSHComputer(
|
|
1716
|
+
computers,
|
|
1717
|
+
"Select a computer for Claude auth"
|
|
1718
|
+
);
|
|
1719
|
+
return {
|
|
1720
|
+
computer,
|
|
1721
|
+
helperCreated: false,
|
|
1722
|
+
sharedInstall: isSharedInstallTarget(computer),
|
|
1723
|
+
detail: describeTarget(computer, false)
|
|
1724
|
+
};
|
|
1725
|
+
}
|
|
1726
|
+
function pickSharedRunningComputer(computers) {
|
|
1727
|
+
const candidates = computers.filter(
|
|
1728
|
+
(computer) => computer.runtime_family === "managed-worker" && computer.filesystem_mode === "shared" && computer.ssh_enabled && computer.status === "running"
|
|
1729
|
+
).sort(
|
|
1730
|
+
(left, right) => new Date(right.updated_at).getTime() - new Date(left.updated_at).getTime()
|
|
1731
|
+
);
|
|
1732
|
+
return candidates[0] ?? null;
|
|
1733
|
+
}
|
|
1734
|
+
function assertClaudeAuthTarget(computer) {
|
|
1735
|
+
if (!computer.ssh_enabled) {
|
|
1736
|
+
throw new Error(`${computer.handle} does not have SSH enabled`);
|
|
1737
|
+
}
|
|
1738
|
+
}
|
|
1739
|
+
function isSharedInstallTarget(computer) {
|
|
1740
|
+
return computer.runtime_family === "managed-worker" && computer.filesystem_mode === "shared";
|
|
1741
|
+
}
|
|
1742
|
+
function describeTarget(computer, helperCreated) {
|
|
1743
|
+
if (helperCreated) {
|
|
1744
|
+
return `created temporary helper ${computer.handle}`;
|
|
1745
|
+
}
|
|
1746
|
+
if (isSharedInstallTarget(computer)) {
|
|
1747
|
+
return `using shared machine ${computer.handle}`;
|
|
1748
|
+
}
|
|
1749
|
+
return `using ${computer.handle}`;
|
|
1750
|
+
}
|
|
1751
|
+
async function waitForRunning(initial) {
|
|
1752
|
+
if (initial.status === "running") {
|
|
1753
|
+
return initial;
|
|
1754
|
+
}
|
|
1755
|
+
const spinner = ora3(`Waiting for ${chalk5.bold(initial.handle)} to be ready...`).start();
|
|
1756
|
+
const deadline = Date.now() + readyPollTimeoutMs;
|
|
1757
|
+
let lastStatus = initial.status;
|
|
1758
|
+
while (Date.now() < deadline) {
|
|
1759
|
+
const current = await getComputerByID(initial.id);
|
|
1760
|
+
if (current.status === "running") {
|
|
1761
|
+
spinner.succeed(`${chalk5.bold(current.handle)} is ready`);
|
|
1762
|
+
return current;
|
|
1763
|
+
}
|
|
1764
|
+
if (current.status !== lastStatus) {
|
|
1765
|
+
lastStatus = current.status;
|
|
1766
|
+
spinner.text = `Waiting for ${chalk5.bold(current.handle)}... ${chalk5.dim(current.status)}`;
|
|
1767
|
+
}
|
|
1768
|
+
if (current.status === "error" || current.status === "deleted" || current.status === "stopped") {
|
|
1769
|
+
spinner.fail(`${current.handle} entered ${current.status}`);
|
|
1770
|
+
throw new Error(current.last_error || `${current.handle} entered ${current.status}`);
|
|
1771
|
+
}
|
|
1772
|
+
await delay(readyPollIntervalMs);
|
|
1773
|
+
}
|
|
1774
|
+
spinner.fail(`Timed out waiting for ${initial.handle}`);
|
|
1775
|
+
throw new Error(`timed out waiting for ${initial.handle} to be ready`);
|
|
1776
|
+
}
|
|
1777
|
+
async function runManualOAuthFlow() {
|
|
1778
|
+
const codeVerifier = base64url(randomBytes(32));
|
|
1779
|
+
const state = randomBytes(16).toString("hex");
|
|
1780
|
+
const codeChallenge = base64url(createHash("sha256").update(codeVerifier).digest());
|
|
1781
|
+
const url = buildAuthorizationURL(codeChallenge, state);
|
|
1782
|
+
console.log("We will open your browser so you can authenticate with Claude.");
|
|
1783
|
+
console.log("If the browser does not open automatically, use the URL below:\n");
|
|
1784
|
+
console.log(url);
|
|
1785
|
+
console.log();
|
|
1786
|
+
try {
|
|
1787
|
+
await openBrowserURL(url);
|
|
1788
|
+
} catch {
|
|
1789
|
+
console.log(chalk5.yellow("Unable to open the browser automatically."));
|
|
1790
|
+
}
|
|
1791
|
+
console.log(
|
|
1792
|
+
"After completing authentication, copy the code shown on the success page."
|
|
1793
|
+
);
|
|
1794
|
+
console.log("You can paste either the full URL, or a value formatted as CODE#STATE.\n");
|
|
1795
|
+
const pasted = (await textInput({
|
|
1796
|
+
message: "Paste the authorization code (or URL) here:"
|
|
1797
|
+
})).trim();
|
|
1798
|
+
if (!pasted) {
|
|
1799
|
+
throw new Error("no authorization code provided");
|
|
1800
|
+
}
|
|
1801
|
+
const parsed = parseAuthorizationInput(pasted, state);
|
|
1802
|
+
const spinner = ora3("Exchanging authorization code...").start();
|
|
1803
|
+
try {
|
|
1804
|
+
const response = await fetch(CLAUDE_OAUTH_TOKEN_URL, {
|
|
1805
|
+
method: "POST",
|
|
1806
|
+
headers: {
|
|
1807
|
+
"Content-Type": "application/json"
|
|
1808
|
+
},
|
|
1809
|
+
body: JSON.stringify({
|
|
1810
|
+
grant_type: "authorization_code",
|
|
1811
|
+
code: parsed.code,
|
|
1812
|
+
state: parsed.state,
|
|
1813
|
+
redirect_uri: CLAUDE_OAUTH_REDIRECT_URL,
|
|
1814
|
+
client_id: CLAUDE_OAUTH_CLIENT_ID,
|
|
1815
|
+
code_verifier: codeVerifier
|
|
1816
|
+
})
|
|
1817
|
+
});
|
|
1818
|
+
if (!response.ok) {
|
|
1819
|
+
throw new Error(
|
|
1820
|
+
`token exchange failed: ${response.status} ${await response.text()}`
|
|
1821
|
+
);
|
|
1822
|
+
}
|
|
1823
|
+
const payload = await response.json();
|
|
1824
|
+
if (!payload.refresh_token || !payload.scope) {
|
|
1825
|
+
throw new Error("token exchange returned an incomplete response");
|
|
1826
|
+
}
|
|
1827
|
+
spinner.succeed("Authorization code exchanged");
|
|
1828
|
+
return {
|
|
1829
|
+
refreshToken: payload.refresh_token,
|
|
1830
|
+
scope: payload.scope
|
|
1831
|
+
};
|
|
1832
|
+
} catch (error) {
|
|
1833
|
+
spinner.fail(
|
|
1834
|
+
error instanceof Error ? error.message : "Failed to exchange authorization code"
|
|
1835
|
+
);
|
|
1836
|
+
throw error;
|
|
1837
|
+
}
|
|
1838
|
+
}
|
|
1839
|
+
function buildAuthorizationURL(codeChallenge, state) {
|
|
1840
|
+
const params = new URLSearchParams({
|
|
1841
|
+
code: "true",
|
|
1842
|
+
client_id: CLAUDE_OAUTH_CLIENT_ID,
|
|
1843
|
+
response_type: "code",
|
|
1844
|
+
redirect_uri: CLAUDE_OAUTH_REDIRECT_URL,
|
|
1845
|
+
scope: CLAUDE_OAUTH_SCOPES.join(" "),
|
|
1846
|
+
code_challenge: codeChallenge,
|
|
1847
|
+
code_challenge_method: "S256",
|
|
1848
|
+
state
|
|
1849
|
+
});
|
|
1850
|
+
return `${CLAUDE_OAUTH_AUTHORIZE_URL}?${params.toString()}`;
|
|
1851
|
+
}
|
|
1852
|
+
function parseAuthorizationInput(value, expectedState) {
|
|
1853
|
+
if (value.startsWith("http://") || value.startsWith("https://")) {
|
|
1854
|
+
const parsed = new URL(value);
|
|
1855
|
+
const code2 = parsed.searchParams.get("code");
|
|
1856
|
+
const state2 = parsed.searchParams.get("state");
|
|
1857
|
+
if (!code2 || !state2) {
|
|
1858
|
+
throw new Error("pasted URL is missing code or state");
|
|
1859
|
+
}
|
|
1860
|
+
if (state2 !== expectedState) {
|
|
1861
|
+
throw new Error("state mismatch detected; restart the authentication flow");
|
|
1862
|
+
}
|
|
1863
|
+
return { code: code2, state: state2 };
|
|
1864
|
+
}
|
|
1865
|
+
const [code, state] = value.split("#", 2).map((part) => part?.trim() ?? "");
|
|
1866
|
+
if (!code || !state) {
|
|
1867
|
+
throw new Error("expected a full URL or a CODE#STATE value");
|
|
1868
|
+
}
|
|
1869
|
+
if (state !== expectedState) {
|
|
1870
|
+
throw new Error("state mismatch detected; restart the authentication flow");
|
|
1871
|
+
}
|
|
1872
|
+
return { code, state };
|
|
1873
|
+
}
|
|
1874
|
+
async function resolveSSHTarget(computer) {
|
|
1875
|
+
const registered = await ensureDefaultSSHKeyRegistered();
|
|
1876
|
+
const info = await getConnectionInfo(computer.id);
|
|
1877
|
+
if (!info.connection.ssh_available) {
|
|
1878
|
+
throw new Error(`SSH is not available for ${computer.handle}`);
|
|
1879
|
+
}
|
|
1880
|
+
return {
|
|
1881
|
+
handle: computer.handle,
|
|
1882
|
+
host: info.connection.ssh_host,
|
|
1883
|
+
port: info.connection.ssh_port,
|
|
1884
|
+
user: info.connection.ssh_user,
|
|
1885
|
+
identityFile: registered.privateKeyPath
|
|
1886
|
+
};
|
|
1887
|
+
}
|
|
1888
|
+
async function installClaudeAuth(target, oauth) {
|
|
1889
|
+
const spinner = ora3(`Installing Claude auth on ${chalk5.bold(target.handle)}...`).start();
|
|
1890
|
+
try {
|
|
1891
|
+
const installScript = buildInstallScript(oauth.refreshToken, oauth.scope);
|
|
1892
|
+
const result = await runRemoteBash(target, ["-s"], installScript);
|
|
1893
|
+
if (result.stdout.trim()) {
|
|
1894
|
+
spinner.succeed(`Installed Claude auth on ${chalk5.bold(target.handle)}`);
|
|
1895
|
+
return;
|
|
1896
|
+
}
|
|
1897
|
+
spinner.succeed(`Installed Claude auth on ${chalk5.bold(target.handle)}`);
|
|
1898
|
+
} catch (error) {
|
|
1899
|
+
spinner.fail(
|
|
1900
|
+
error instanceof Error ? error.message : `Failed to install Claude auth on ${target.handle}`
|
|
1901
|
+
);
|
|
1902
|
+
throw error;
|
|
1903
|
+
}
|
|
1904
|
+
}
|
|
1905
|
+
function buildInstallScript(refreshToken, scopes) {
|
|
1906
|
+
const tokenMarker = `TOKEN_${randomSuffix(12)}`;
|
|
1907
|
+
const scopeMarker = `SCOPES_${randomSuffix(12)}`;
|
|
1908
|
+
return [
|
|
1909
|
+
"set -euo pipefail",
|
|
1910
|
+
'command -v claude >/dev/null 2>&1 || { echo "claude is not installed on this computer" >&2; exit 1; }',
|
|
1911
|
+
`export CLAUDE_CODE_OAUTH_REFRESH_TOKEN="$(cat <<'` + tokenMarker + "'",
|
|
1912
|
+
refreshToken,
|
|
1913
|
+
tokenMarker,
|
|
1914
|
+
')"',
|
|
1915
|
+
`export CLAUDE_CODE_OAUTH_SCOPES="$(cat <<'` + scopeMarker + "'",
|
|
1916
|
+
scopes,
|
|
1917
|
+
scopeMarker,
|
|
1918
|
+
')"',
|
|
1919
|
+
"claude auth login",
|
|
1920
|
+
"unset CLAUDE_CODE_OAUTH_REFRESH_TOKEN",
|
|
1921
|
+
"unset CLAUDE_CODE_OAUTH_SCOPES"
|
|
1922
|
+
].join("\n");
|
|
1923
|
+
}
|
|
1924
|
+
async function verifyStoredAuth(target) {
|
|
1925
|
+
const command = freshShellCommand("claude auth status --json");
|
|
1926
|
+
const result = await runRemoteBash(target, ["-lc", command]);
|
|
1927
|
+
const payload = parseStatusOutput(result.stdout);
|
|
1928
|
+
if (!payload.loggedIn) {
|
|
1929
|
+
throw new Error(`Claude auth is not visible on ${target.handle}`);
|
|
1930
|
+
}
|
|
1931
|
+
}
|
|
1932
|
+
function freshShellCommand(inner) {
|
|
1933
|
+
return [
|
|
1934
|
+
"env -i",
|
|
1935
|
+
'HOME="$HOME"',
|
|
1936
|
+
'USER="${USER:-node}"',
|
|
1937
|
+
'SHELL="/bin/bash"',
|
|
1938
|
+
'PATH="$HOME/.local/bin:/usr/local/bin:/usr/bin:/bin"',
|
|
1939
|
+
"bash -lc",
|
|
1940
|
+
shellQuote(inner)
|
|
1941
|
+
].join(" ");
|
|
1942
|
+
}
|
|
1943
|
+
function parseStatusOutput(stdout) {
|
|
1944
|
+
const start = stdout.indexOf("{");
|
|
1945
|
+
const end = stdout.lastIndexOf("}");
|
|
1946
|
+
if (start === -1 || end === -1 || end <= start) {
|
|
1947
|
+
throw new Error("could not parse claude auth status output");
|
|
1948
|
+
}
|
|
1949
|
+
const parsed = JSON.parse(stdout.slice(start, end + 1));
|
|
1950
|
+
return { loggedIn: parsed.loggedIn === true };
|
|
1951
|
+
}
|
|
1952
|
+
async function verifySecondaryMachine(primaryComputerID, sharedInstall, skip) {
|
|
1953
|
+
if (!sharedInstall) {
|
|
1954
|
+
return { status: "skipped", reason: "target uses isolated filesystem" };
|
|
1955
|
+
}
|
|
1956
|
+
if (skip) {
|
|
1957
|
+
return { status: "skipped", reason: "cross-check skipped by flag" };
|
|
1958
|
+
}
|
|
1959
|
+
const secondary = (await listComputers()).filter(
|
|
1960
|
+
(computer) => computer.id !== primaryComputerID && computer.runtime_family === "managed-worker" && computer.filesystem_mode === "shared" && computer.ssh_enabled && computer.status === "running"
|
|
1961
|
+
).sort(
|
|
1962
|
+
(left, right) => new Date(right.updated_at).getTime() - new Date(left.updated_at).getTime()
|
|
1963
|
+
)[0];
|
|
1964
|
+
if (!secondary) {
|
|
1965
|
+
return {
|
|
1966
|
+
status: "skipped",
|
|
1967
|
+
reason: "no second running shared managed-worker was available"
|
|
1968
|
+
};
|
|
1969
|
+
}
|
|
1970
|
+
const sshTarget = await resolveSSHTarget(secondary);
|
|
1971
|
+
await verifyStoredAuth(sshTarget);
|
|
1972
|
+
return { status: "verified", handle: secondary.handle };
|
|
1973
|
+
}
|
|
1974
|
+
async function runRemoteBash(target, bashArgs, script) {
|
|
1975
|
+
const args = [
|
|
1976
|
+
"-T",
|
|
1977
|
+
"-i",
|
|
1978
|
+
target.identityFile,
|
|
1979
|
+
"-p",
|
|
1980
|
+
String(target.port),
|
|
1981
|
+
`${target.user}@${target.host}`,
|
|
1982
|
+
"bash",
|
|
1983
|
+
...bashArgs
|
|
1984
|
+
];
|
|
1985
|
+
return new Promise((resolve, reject) => {
|
|
1986
|
+
const child = spawn2("ssh", args, {
|
|
1987
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
1988
|
+
});
|
|
1989
|
+
let stdout = "";
|
|
1990
|
+
let stderr = "";
|
|
1991
|
+
child.stdout.on("data", (chunk) => {
|
|
1992
|
+
stdout += chunk.toString();
|
|
1993
|
+
});
|
|
1994
|
+
child.stderr.on("data", (chunk) => {
|
|
1995
|
+
stderr += chunk.toString();
|
|
1996
|
+
});
|
|
1997
|
+
child.on("error", reject);
|
|
1998
|
+
child.on("exit", (code) => {
|
|
1999
|
+
if (code === 0) {
|
|
2000
|
+
resolve({ stdout, stderr });
|
|
2001
|
+
return;
|
|
2002
|
+
}
|
|
2003
|
+
const message = stderr.trim() || stdout.trim() || `ssh exited with code ${code ?? 1}`;
|
|
2004
|
+
reject(new Error(message));
|
|
2005
|
+
});
|
|
2006
|
+
if (script !== void 0) {
|
|
2007
|
+
child.stdin.end(script);
|
|
2008
|
+
} else {
|
|
2009
|
+
child.stdin.end();
|
|
2010
|
+
}
|
|
2011
|
+
});
|
|
2012
|
+
}
|
|
2013
|
+
function shellQuote(value) {
|
|
2014
|
+
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
2015
|
+
}
|
|
2016
|
+
function base64url(buffer) {
|
|
2017
|
+
return buffer.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
|
|
2018
|
+
}
|
|
2019
|
+
function randomSuffix(length) {
|
|
2020
|
+
return randomBytes(Math.ceil(length / 2)).toString("hex").slice(0, length);
|
|
2021
|
+
}
|
|
2022
|
+
function delay(ms) {
|
|
2023
|
+
return new Promise((resolve) => {
|
|
2024
|
+
setTimeout(resolve, ms);
|
|
2025
|
+
});
|
|
2026
|
+
}
|
|
2027
|
+
|
|
2028
|
+
// src/commands/computers.ts
|
|
2029
|
+
import { Command as Command5 } from "commander";
|
|
2030
|
+
import chalk6 from "chalk";
|
|
2031
|
+
import ora4 from "ora";
|
|
2032
|
+
import { select as select2, input as textInput2, confirm } from "@inquirer/prompts";
|
|
2033
|
+
|
|
2034
|
+
// src/lib/machine-sources.ts
|
|
2035
|
+
async function getMachineSourceSettings() {
|
|
2036
|
+
return api("/v1/me/machine-source");
|
|
2037
|
+
}
|
|
2038
|
+
async function upsertMachineSource(input) {
|
|
2039
|
+
return api("/v1/me/machine-source", {
|
|
2040
|
+
method: "PUT",
|
|
2041
|
+
body: JSON.stringify(input)
|
|
2042
|
+
});
|
|
2043
|
+
}
|
|
2044
|
+
async function clearMachineSourceDefault() {
|
|
2045
|
+
return api("/v1/me/machine-source", {
|
|
2046
|
+
method: "DELETE"
|
|
2047
|
+
});
|
|
2048
|
+
}
|
|
2049
|
+
async function rebuildMachineSource(sourceID) {
|
|
2050
|
+
return api(`/v1/me/machine-source/${encodeURIComponent(sourceID)}/rebuild`, {
|
|
2051
|
+
method: "POST"
|
|
2052
|
+
});
|
|
2053
|
+
}
|
|
2054
|
+
async function deleteMachineSource(sourceID) {
|
|
2055
|
+
return api(`/v1/me/machine-source/${encodeURIComponent(sourceID)}`, {
|
|
2056
|
+
method: "DELETE"
|
|
2057
|
+
});
|
|
2058
|
+
}
|
|
2059
|
+
function summarizeMachineSourceSelection(settings) {
|
|
2060
|
+
if (settings.platform_default || !settings.default_machine_source) {
|
|
2061
|
+
return "AgentComputer platform default image";
|
|
2062
|
+
}
|
|
2063
|
+
return summarizeMachineSource(settings.default_machine_source);
|
|
2064
|
+
}
|
|
2065
|
+
function summarizeMachineSource(source) {
|
|
2066
|
+
if (source.kind === "oci-image") {
|
|
2067
|
+
return source.requested_ref || "OCI image";
|
|
2068
|
+
}
|
|
2069
|
+
const parts = [
|
|
2070
|
+
source.git_url || "Nix git source",
|
|
2071
|
+
source.git_ref ? `ref ${source.git_ref}` : "",
|
|
2072
|
+
source.git_subpath ? `subpath ${source.git_subpath}` : ""
|
|
2073
|
+
].filter(Boolean);
|
|
2074
|
+
return parts.join(" | ");
|
|
2075
|
+
}
|
|
2076
|
+
|
|
2077
|
+
// src/commands/computers.ts
|
|
1519
2078
|
function isInternalCondition(value) {
|
|
1520
2079
|
return /^[A-Z][a-zA-Z]+$/.test(value);
|
|
1521
2080
|
}
|
|
@@ -1525,32 +2084,38 @@ function printComputer(computer) {
|
|
|
1525
2084
|
const ssh = computer.ssh_enabled ? formatSSHCommand2(computer.handle, computer.ssh_host, computer.ssh_port) : "disabled";
|
|
1526
2085
|
const isCustom = computer.runtime_family === "custom-machine";
|
|
1527
2086
|
console.log();
|
|
1528
|
-
console.log(` ${
|
|
2087
|
+
console.log(` ${chalk6.bold.white(computer.handle)} ${formatStatus(computer.status)}`);
|
|
1529
2088
|
console.log();
|
|
1530
|
-
console.log(` ${
|
|
1531
|
-
console.log(` ${
|
|
2089
|
+
console.log(` ${chalk6.dim("ID")} ${computer.id}`);
|
|
2090
|
+
console.log(` ${chalk6.dim("Tier")} ${computer.tier}`);
|
|
1532
2091
|
if (isCustom) {
|
|
1533
|
-
console.log(` ${
|
|
1534
|
-
console.log(` ${
|
|
1535
|
-
console.log(` ${
|
|
2092
|
+
console.log(` ${chalk6.dim("Runtime")} ${computer.runtime_family}`);
|
|
2093
|
+
console.log(` ${chalk6.dim("Source")} ${computer.source_kind}`);
|
|
2094
|
+
console.log(` ${chalk6.dim("Image")} ${computer.image_family}`);
|
|
2095
|
+
} else {
|
|
2096
|
+
console.log(` ${chalk6.dim("Runtime")} ${computer.runtime_family}`);
|
|
2097
|
+
console.log(` ${chalk6.dim("Launch")} ${formatManagedWorkerLaunchSource(computer)}`);
|
|
2098
|
+
if (computer.user_source_id && computer.resolved_image_ref) {
|
|
2099
|
+
console.log(` ${chalk6.dim("Image")} ${computer.resolved_image_ref}`);
|
|
2100
|
+
}
|
|
1536
2101
|
}
|
|
1537
|
-
console.log(` ${
|
|
1538
|
-
console.log(` ${
|
|
2102
|
+
console.log(` ${chalk6.dim("Primary")} :${computer.primary_port}${computer.primary_path}`);
|
|
2103
|
+
console.log(` ${chalk6.dim("Health")} ${computer.healthcheck_type}${computer.healthcheck_value ? ` (${computer.healthcheck_value})` : ""}`);
|
|
1539
2104
|
console.log();
|
|
1540
|
-
console.log(` ${
|
|
1541
|
-
console.log(` ${
|
|
1542
|
-
console.log(` ${
|
|
1543
|
-
console.log(` ${
|
|
2105
|
+
console.log(` ${chalk6.dim("Gateway")} ${chalk6.cyan(webURL(computer))}`);
|
|
2106
|
+
console.log(` ${chalk6.dim("VNC")} ${vnc ? chalk6.cyan(vnc) : chalk6.dim("not available")}`);
|
|
2107
|
+
console.log(` ${chalk6.dim("Terminal")} ${terminal ? chalk6.cyan(terminal) : chalk6.dim("not available")}`);
|
|
2108
|
+
console.log(` ${chalk6.dim("SSH")} ${computer.ssh_enabled ? chalk6.white(ssh) : chalk6.dim(ssh)}`);
|
|
1544
2109
|
if (computer.last_error) {
|
|
1545
2110
|
console.log();
|
|
1546
2111
|
if (isInternalCondition(computer.last_error)) {
|
|
1547
|
-
console.log(` ${
|
|
2112
|
+
console.log(` ${chalk6.dim("Condition")} ${chalk6.dim(computer.last_error)}`);
|
|
1548
2113
|
} else {
|
|
1549
|
-
console.log(` ${
|
|
2114
|
+
console.log(` ${chalk6.dim("Error")} ${chalk6.red(computer.last_error)}`);
|
|
1550
2115
|
}
|
|
1551
2116
|
}
|
|
1552
2117
|
console.log();
|
|
1553
|
-
console.log(` ${
|
|
2118
|
+
console.log(` ${chalk6.dim("Created")} ${timeAgo(computer.created_at)}`);
|
|
1554
2119
|
console.log();
|
|
1555
2120
|
}
|
|
1556
2121
|
function formatSSHCommand2(user, host, port) {
|
|
@@ -1562,22 +2127,41 @@ function formatSSHCommand2(user, host, port) {
|
|
|
1562
2127
|
}
|
|
1563
2128
|
return `ssh -p ${port} ${user}@${host}`;
|
|
1564
2129
|
}
|
|
2130
|
+
function formatManagedWorkerLaunchSource(computer) {
|
|
2131
|
+
if (!computer.user_source_id) {
|
|
2132
|
+
return "AgentComputer managed-worker platform default";
|
|
2133
|
+
}
|
|
2134
|
+
if (computer.source_repo_url) {
|
|
2135
|
+
let value = computer.source_repo_url;
|
|
2136
|
+
if (computer.source_ref) {
|
|
2137
|
+
value += ` @ ${computer.source_ref}`;
|
|
2138
|
+
}
|
|
2139
|
+
if (computer.source_subpath) {
|
|
2140
|
+
value += ` # ${computer.source_subpath}`;
|
|
2141
|
+
}
|
|
2142
|
+
return `saved nix-git source ${computer.user_source_id} (${value})`;
|
|
2143
|
+
}
|
|
2144
|
+
if (computer.source_ref) {
|
|
2145
|
+
return `saved oci-image source ${computer.user_source_id} (${computer.source_ref})`;
|
|
2146
|
+
}
|
|
2147
|
+
return `saved custom source ${computer.user_source_id}`;
|
|
2148
|
+
}
|
|
1565
2149
|
function printComputerTable(computers) {
|
|
1566
2150
|
const handleWidth = Math.max(6, ...computers.map((c) => c.handle.length));
|
|
1567
2151
|
const statusWidth = 12;
|
|
1568
2152
|
const createdWidth = 10;
|
|
1569
2153
|
console.log();
|
|
1570
2154
|
console.log(
|
|
1571
|
-
` ${
|
|
2155
|
+
` ${chalk6.dim(padEnd("Handle", handleWidth + 2))}${chalk6.dim(padEnd("Status", statusWidth + 2))}${chalk6.dim(padEnd("Created", createdWidth + 2))}${chalk6.dim("URL")}`
|
|
1572
2156
|
);
|
|
1573
2157
|
console.log(
|
|
1574
|
-
` ${
|
|
2158
|
+
` ${chalk6.dim("-".repeat(handleWidth + 2))}${chalk6.dim("-".repeat(statusWidth + 2))}${chalk6.dim("-".repeat(createdWidth + 2))}${chalk6.dim("-".repeat(20))}`
|
|
1575
2159
|
);
|
|
1576
2160
|
for (const computer of computers) {
|
|
1577
2161
|
const status = formatStatus(computer.status);
|
|
1578
|
-
const created =
|
|
2162
|
+
const created = chalk6.dim(timeAgo(computer.created_at));
|
|
1579
2163
|
console.log(
|
|
1580
|
-
` ${
|
|
2164
|
+
` ${chalk6.white(padEnd(computer.handle, handleWidth + 2))}${padEnd(status, statusWidth + 2)}${padEnd(created, createdWidth + 2)}${chalk6.cyan(webURL(computer))}`
|
|
1581
2165
|
);
|
|
1582
2166
|
}
|
|
1583
2167
|
console.log();
|
|
@@ -1587,29 +2171,29 @@ function printComputerTableVerbose(computers) {
|
|
|
1587
2171
|
const statusWidth = 10;
|
|
1588
2172
|
console.log();
|
|
1589
2173
|
console.log(
|
|
1590
|
-
` ${
|
|
2174
|
+
` ${chalk6.dim(padEnd("Handle", handleWidth + 2))}${chalk6.dim(padEnd("Status", statusWidth + 2))}${chalk6.dim("URLs")}`
|
|
1591
2175
|
);
|
|
1592
2176
|
console.log(
|
|
1593
|
-
` ${
|
|
2177
|
+
` ${chalk6.dim("-".repeat(handleWidth + 2))}${chalk6.dim("-".repeat(statusWidth + 2))}${chalk6.dim("-".repeat(20))}`
|
|
1594
2178
|
);
|
|
1595
2179
|
for (const computer of computers) {
|
|
1596
2180
|
const status = formatStatus(computer.status);
|
|
1597
2181
|
const vnc = vncURL(computer);
|
|
1598
2182
|
const terminal = terminalURL(computer);
|
|
1599
2183
|
console.log(
|
|
1600
|
-
` ${
|
|
2184
|
+
` ${chalk6.white(padEnd(computer.handle, handleWidth + 2))}${padEnd(status, statusWidth + 2)}${chalk6.cyan(webURL(computer))}`
|
|
1601
2185
|
);
|
|
1602
2186
|
console.log(
|
|
1603
|
-
` ${padEnd("", handleWidth + 2)}${padEnd("", statusWidth + 2)}${
|
|
2187
|
+
` ${padEnd("", handleWidth + 2)}${padEnd("", statusWidth + 2)}${chalk6.dim(vnc ?? "VNC not available")}`
|
|
1604
2188
|
);
|
|
1605
2189
|
console.log(
|
|
1606
|
-
` ${padEnd("", handleWidth + 2)}${padEnd("", statusWidth + 2)}${
|
|
2190
|
+
` ${padEnd("", handleWidth + 2)}${padEnd("", statusWidth + 2)}${chalk6.dim(terminal ?? "Terminal not available")}`
|
|
1607
2191
|
);
|
|
1608
2192
|
}
|
|
1609
2193
|
console.log();
|
|
1610
2194
|
}
|
|
1611
|
-
var lsCommand = new
|
|
1612
|
-
const spinner = options.json ? null :
|
|
2195
|
+
var lsCommand = new Command5("ls").description("List computers").option("--json", "Print raw JSON").option("-v, --verbose", "Show all URLs for each computer").action(async (options) => {
|
|
2196
|
+
const spinner = options.json ? null : ora4("Fetching computers...").start();
|
|
1613
2197
|
try {
|
|
1614
2198
|
const computers = await listComputers();
|
|
1615
2199
|
spinner?.stop();
|
|
@@ -1619,7 +2203,7 @@ var lsCommand = new Command4("ls").description("List computers").option("--json"
|
|
|
1619
2203
|
}
|
|
1620
2204
|
if (computers.length === 0) {
|
|
1621
2205
|
console.log();
|
|
1622
|
-
console.log(
|
|
2206
|
+
console.log(chalk6.dim(" No computers found."));
|
|
1623
2207
|
console.log();
|
|
1624
2208
|
return;
|
|
1625
2209
|
}
|
|
@@ -1639,8 +2223,8 @@ var lsCommand = new Command4("ls").description("List computers").option("--json"
|
|
|
1639
2223
|
process.exit(1);
|
|
1640
2224
|
}
|
|
1641
2225
|
});
|
|
1642
|
-
var getCommand = new
|
|
1643
|
-
const spinner = options.json ? null :
|
|
2226
|
+
var getCommand = new Command5("get").description("Show computer details").argument("<id-or-handle>", "Computer id or handle").option("--json", "Print raw JSON").action(async (identifier, options) => {
|
|
2227
|
+
const spinner = options.json ? null : ora4("Fetching computer...").start();
|
|
1644
2228
|
try {
|
|
1645
2229
|
const computer = await resolveComputer(identifier);
|
|
1646
2230
|
spinner?.stop();
|
|
@@ -1660,19 +2244,28 @@ var getCommand = new Command4("get").description("Show computer details").argume
|
|
|
1660
2244
|
process.exit(1);
|
|
1661
2245
|
}
|
|
1662
2246
|
});
|
|
1663
|
-
var createCommand = new
|
|
2247
|
+
var createCommand = new Command5("create").description("Create a computer").argument("[handle]", "Optional computer handle").option("--name <display-name>", "Display name").option("--tier <tier>", "Tier override").option("--interactive", "Prompt for runtime choices").option("--runtime-family <runtime-family>", "managed-worker or custom-machine").option("--source-kind <source-kind>", "none or oci-image").option("--image-family <family>", "Image family override").option("--image-ref <image>", "Resolved image override").option("--use-platform-default", "Use the AgentComputer platform default image").option("--primary-port <port>", "Primary app port").option("--primary-path <path>", "Primary app path").option("--healthcheck-type <type>", "http or tcp").option("--healthcheck-value <value>", "Health check path or port").option("--ssh-enabled", "Enable SSH access").option("--ssh-disabled", "Disable SSH access").option("--vnc-enabled", "Enable VNC access").option("--vnc-disabled", "Disable VNC access").action(async (handle, options) => {
|
|
1664
2248
|
let spinner;
|
|
1665
2249
|
let timer;
|
|
1666
2250
|
let startTime = 0;
|
|
1667
2251
|
try {
|
|
1668
2252
|
const selectedOptions = await resolveCreateOptions(options);
|
|
1669
2253
|
const runtimeFamily = effectiveRuntimeFamily(selectedOptions.runtimeFamily);
|
|
2254
|
+
const machineSourceSettings = runtimeFamily === "managed-worker" ? await getMachineSourceSettings().catch(() => null) : null;
|
|
1670
2255
|
const filesystemSettings = await loadFilesystemSettingsForCreate(runtimeFamily);
|
|
2256
|
+
const machineSourceNote = createMachineSourceNote(
|
|
2257
|
+
runtimeFamily,
|
|
2258
|
+
machineSourceSettings,
|
|
2259
|
+
Boolean(selectedOptions.usePlatformDefault)
|
|
2260
|
+
);
|
|
2261
|
+
if (machineSourceNote) {
|
|
2262
|
+
console.log(chalk6.dim(machineSourceNote));
|
|
2263
|
+
}
|
|
1671
2264
|
const provisioningNote = createProvisioningNote(runtimeFamily, filesystemSettings);
|
|
1672
2265
|
if (provisioningNote) {
|
|
1673
|
-
console.log(
|
|
2266
|
+
console.log(chalk6.dim(provisioningNote));
|
|
1674
2267
|
}
|
|
1675
|
-
spinner =
|
|
2268
|
+
spinner = ora4(createSpinnerText(runtimeFamily, filesystemSettings, 0)).start();
|
|
1676
2269
|
startTime = Date.now();
|
|
1677
2270
|
timer = setInterval(() => {
|
|
1678
2271
|
const elapsed2 = (Date.now() - startTime) / 1e3;
|
|
@@ -1688,6 +2281,7 @@ var createCommand = new Command4("create").description("Create a computer").argu
|
|
|
1688
2281
|
source_kind: parseSourceKindOption(selectedOptions.sourceKind),
|
|
1689
2282
|
image_family: selectedOptions.imageFamily,
|
|
1690
2283
|
image_ref: selectedOptions.imageRef,
|
|
2284
|
+
use_platform_default: selectedOptions.usePlatformDefault,
|
|
1691
2285
|
primary_port: parseOptionalPort(selectedOptions.primaryPort),
|
|
1692
2286
|
primary_path: selectedOptions.primaryPath,
|
|
1693
2287
|
healthcheck_type: parseHealthcheckTypeOption(selectedOptions.healthcheckType),
|
|
@@ -1708,7 +2302,7 @@ var createCommand = new Command4("create").description("Create a computer").argu
|
|
|
1708
2302
|
}
|
|
1709
2303
|
const elapsed = ((Date.now() - startTime) / 1e3).toFixed(1);
|
|
1710
2304
|
spinner.succeed(
|
|
1711
|
-
|
|
2305
|
+
chalk6.green(`Created ${chalk6.bold(computer.handle)} ${chalk6.dim(`[${elapsed}s]`)}`)
|
|
1712
2306
|
);
|
|
1713
2307
|
printComputer(computer);
|
|
1714
2308
|
} catch (error) {
|
|
@@ -1717,18 +2311,17 @@ var createCommand = new Command4("create").description("Create a computer").argu
|
|
|
1717
2311
|
}
|
|
1718
2312
|
const message = error instanceof Error ? error.message : "Failed to create computer";
|
|
1719
2313
|
if (spinner) {
|
|
1720
|
-
const suffix = startTime ? ` ${
|
|
2314
|
+
const suffix = startTime ? ` ${chalk6.dim(`[${((Date.now() - startTime) / 1e3).toFixed(1)}s]`)}` : "";
|
|
1721
2315
|
spinner.fail(`${message}${suffix}`);
|
|
1722
2316
|
} else {
|
|
1723
|
-
console.error(
|
|
2317
|
+
console.error(chalk6.red(message));
|
|
1724
2318
|
}
|
|
1725
2319
|
process.exit(1);
|
|
1726
2320
|
}
|
|
1727
2321
|
});
|
|
1728
2322
|
async function resolveCreateOptions(options) {
|
|
1729
2323
|
const selectedOptions = { ...options };
|
|
1730
|
-
const
|
|
1731
|
-
const shouldPrompt = Boolean(selectedOptions.interactive) || !hasExplicitRuntimeSelection;
|
|
2324
|
+
const shouldPrompt = Boolean(selectedOptions.interactive);
|
|
1732
2325
|
if (!process.stdin.isTTY || !process.stdout.isTTY || !shouldPrompt) {
|
|
1733
2326
|
validateCreateOptions(selectedOptions);
|
|
1734
2327
|
return selectedOptions;
|
|
@@ -1750,9 +2343,9 @@ async function resolveCreateOptions(options) {
|
|
|
1750
2343
|
if (runtimeChoice === "custom-machine") {
|
|
1751
2344
|
selectedOptions.runtimeFamily = "custom-machine";
|
|
1752
2345
|
selectedOptions.sourceKind = "oci-image";
|
|
1753
|
-
selectedOptions.imageRef = (await
|
|
1754
|
-
selectedOptions.primaryPort = (await
|
|
1755
|
-
selectedOptions.primaryPath = (await
|
|
2346
|
+
selectedOptions.imageRef = (await textInput2({ message: "OCI image ref (required):" })).trim();
|
|
2347
|
+
selectedOptions.primaryPort = (await textInput2({ message: "Primary port:", default: "3000" })).trim();
|
|
2348
|
+
selectedOptions.primaryPath = (await textInput2({ message: "Primary path:", default: "/" })).trim();
|
|
1756
2349
|
selectedOptions.healthcheckType = await select2({
|
|
1757
2350
|
message: "Healthcheck type",
|
|
1758
2351
|
choices: [
|
|
@@ -1761,7 +2354,7 @@ async function resolveCreateOptions(options) {
|
|
|
1761
2354
|
],
|
|
1762
2355
|
default: "tcp"
|
|
1763
2356
|
});
|
|
1764
|
-
selectedOptions.healthcheckValue = (await
|
|
2357
|
+
selectedOptions.healthcheckValue = (await textInput2({ message: "Healthcheck value (optional):" })).trim();
|
|
1765
2358
|
selectedOptions.sshEnabled = await confirm({
|
|
1766
2359
|
message: "Enable SSH?",
|
|
1767
2360
|
default: false
|
|
@@ -1775,28 +2368,28 @@ async function resolveCreateOptions(options) {
|
|
|
1775
2368
|
validateCreateOptions(selectedOptions);
|
|
1776
2369
|
return selectedOptions;
|
|
1777
2370
|
}
|
|
1778
|
-
var removeCommand = new
|
|
2371
|
+
var removeCommand = new Command5("rm").description("Delete a computer").argument("<id-or-handle>", "Computer id or handle").option("-y, --yes", "Skip confirmation prompt").action(async (identifier, options, cmd) => {
|
|
1779
2372
|
const globalYes = cmd.parent?.opts()?.yes;
|
|
1780
2373
|
const skipConfirm = Boolean(options.yes || globalYes);
|
|
1781
|
-
const spinner =
|
|
2374
|
+
const spinner = ora4("Resolving computer...").start();
|
|
1782
2375
|
try {
|
|
1783
2376
|
const computer = await resolveComputer(identifier);
|
|
1784
2377
|
spinner.stop();
|
|
1785
2378
|
if (!skipConfirm && process.stdin.isTTY) {
|
|
1786
2379
|
const confirmed = await confirm({
|
|
1787
|
-
message: `Delete computer ${
|
|
2380
|
+
message: `Delete computer ${chalk6.bold(computer.handle)}?`,
|
|
1788
2381
|
default: false
|
|
1789
2382
|
});
|
|
1790
2383
|
if (!confirmed) {
|
|
1791
|
-
console.log(
|
|
2384
|
+
console.log(chalk6.dim(" Cancelled."));
|
|
1792
2385
|
return;
|
|
1793
2386
|
}
|
|
1794
2387
|
}
|
|
1795
|
-
const deleteSpinner =
|
|
2388
|
+
const deleteSpinner = ora4("Deleting computer...").start();
|
|
1796
2389
|
await api(`/v1/computers/${computer.id}`, {
|
|
1797
2390
|
method: "DELETE"
|
|
1798
2391
|
});
|
|
1799
|
-
deleteSpinner.succeed(
|
|
2392
|
+
deleteSpinner.succeed(chalk6.green(`Deleted ${chalk6.bold(computer.handle)}`));
|
|
1800
2393
|
} catch (error) {
|
|
1801
2394
|
spinner.fail(
|
|
1802
2395
|
error instanceof Error ? error.message : "Failed to delete computer"
|
|
@@ -1836,17 +2429,33 @@ function createProvisioningNote(runtimeFamily, filesystemSettings) {
|
|
|
1836
2429
|
}
|
|
1837
2430
|
return "Using isolated storage for this desktop.";
|
|
1838
2431
|
}
|
|
2432
|
+
function createMachineSourceNote(runtimeFamily, machineSourceSettings, usePlatformDefault) {
|
|
2433
|
+
if (runtimeFamily !== "managed-worker") {
|
|
2434
|
+
return null;
|
|
2435
|
+
}
|
|
2436
|
+
if (usePlatformDefault) {
|
|
2437
|
+
return "Using the AgentComputer platform default image for this machine.";
|
|
2438
|
+
}
|
|
2439
|
+
if (!machineSourceSettings) {
|
|
2440
|
+
return null;
|
|
2441
|
+
}
|
|
2442
|
+
if (machineSourceSettings.platform_default || !machineSourceSettings.default_machine_source) {
|
|
2443
|
+
return "Using the AgentComputer platform default image.";
|
|
2444
|
+
}
|
|
2445
|
+
return `Using managed-worker image source: ${summarizeMachineSourceSelection(machineSourceSettings)}.`;
|
|
2446
|
+
}
|
|
1839
2447
|
function createSpinnerText(runtimeFamily, filesystemSettings, elapsedSeconds) {
|
|
1840
|
-
const elapsedLabel =
|
|
2448
|
+
const elapsedLabel = chalk6.dim(`${elapsedSeconds.toFixed(1)}s`);
|
|
1841
2449
|
if (runtimeFamily === "managed-worker" && filesystemSettings?.shared_enabled && elapsedSeconds >= 5) {
|
|
1842
|
-
return `Creating computer... ${elapsedLabel} ${
|
|
2450
|
+
return `Creating computer... ${elapsedLabel} ${chalk6.dim("mounting shared home")}`;
|
|
1843
2451
|
}
|
|
1844
2452
|
return `Creating computer... ${elapsedLabel}`;
|
|
1845
2453
|
}
|
|
1846
2454
|
function validateCreateOptions(options) {
|
|
1847
2455
|
const runtimeFamily = parseRuntimeFamilyOption(options.runtimeFamily);
|
|
1848
2456
|
const sourceKind = parseSourceKindOption(options.sourceKind);
|
|
1849
|
-
|
|
2457
|
+
const imageRef = options.imageRef?.trim();
|
|
2458
|
+
if (runtimeFamily === "custom-machine" && !imageRef) {
|
|
1850
2459
|
throw new Error("--image-ref is required for --runtime-family custom-machine");
|
|
1851
2460
|
}
|
|
1852
2461
|
if (runtimeFamily === "custom-machine" && sourceKind === "none") {
|
|
@@ -1855,6 +2464,12 @@ function validateCreateOptions(options) {
|
|
|
1855
2464
|
if (runtimeFamily === "custom-machine" && options.vncEnabled) {
|
|
1856
2465
|
throw new Error("custom-machine does not support platform VNC");
|
|
1857
2466
|
}
|
|
2467
|
+
if (runtimeFamily === "custom-machine" && options.usePlatformDefault) {
|
|
2468
|
+
throw new Error("--use-platform-default cannot be used with --runtime-family custom-machine");
|
|
2469
|
+
}
|
|
2470
|
+
if (imageRef && options.usePlatformDefault) {
|
|
2471
|
+
throw new Error("choose either --image-ref or --use-platform-default");
|
|
2472
|
+
}
|
|
1858
2473
|
}
|
|
1859
2474
|
function parseSourceKindOption(value) {
|
|
1860
2475
|
switch (value) {
|
|
@@ -1900,7 +2515,7 @@ function resolveOptionalToggle(enabled, disabled, label) {
|
|
|
1900
2515
|
}
|
|
1901
2516
|
|
|
1902
2517
|
// src/commands/completion.ts
|
|
1903
|
-
import { Command as
|
|
2518
|
+
import { Command as Command6 } from "commander";
|
|
1904
2519
|
var ZSH_SCRIPT = `#compdef computer agentcomputer aicomputer
|
|
1905
2520
|
|
|
1906
2521
|
_computer() {
|
|
@@ -1909,9 +2524,12 @@ _computer() {
|
|
|
1909
2524
|
'login:Authenticate the CLI'
|
|
1910
2525
|
'logout:Remove stored API key'
|
|
1911
2526
|
'whoami:Show current user'
|
|
2527
|
+
'claude-auth:Authenticate Claude Code on a computer'
|
|
2528
|
+
'claude-login:Alias for claude-auth'
|
|
1912
2529
|
'create:Create a computer'
|
|
1913
2530
|
'ls:List computers'
|
|
1914
2531
|
'get:Show computer details'
|
|
2532
|
+
'image:Manage machine image sources'
|
|
1915
2533
|
'open:Open in browser'
|
|
1916
2534
|
'ssh:SSH into a computer'
|
|
1917
2535
|
'ports:Manage published ports'
|
|
@@ -1930,6 +2548,15 @@ _computer() {
|
|
|
1930
2548
|
'rm:Unpublish an app port'
|
|
1931
2549
|
)
|
|
1932
2550
|
|
|
2551
|
+
local -a image_commands
|
|
2552
|
+
image_commands=(
|
|
2553
|
+
'ls:List machine image sources'
|
|
2554
|
+
'save:Create or update a machine image source'
|
|
2555
|
+
'default:Set the default machine image source'
|
|
2556
|
+
'rebuild:Rebuild a machine image source'
|
|
2557
|
+
'rm:Delete a machine image source'
|
|
2558
|
+
)
|
|
2559
|
+
|
|
1933
2560
|
_arguments -C \\
|
|
1934
2561
|
'(-h --help)'{-h,--help}'[Display help]' \\
|
|
1935
2562
|
'(-V --version)'{-V,--version}'[Show version]' \\
|
|
@@ -1951,6 +2578,12 @@ _computer() {
|
|
|
1951
2578
|
whoami)
|
|
1952
2579
|
_arguments '--json[Print raw JSON]'
|
|
1953
2580
|
;;
|
|
2581
|
+
claude-auth|claude-login)
|
|
2582
|
+
_arguments \\
|
|
2583
|
+
'--machine[Use a specific computer]:computer:_computer_handles' \\
|
|
2584
|
+
'--keep-helper[Keep a temporary helper machine]' \\
|
|
2585
|
+
'--skip-cross-check[Skip second-machine verification]'
|
|
2586
|
+
;;
|
|
1954
2587
|
create)
|
|
1955
2588
|
_arguments \\
|
|
1956
2589
|
'--name[Display name]:name:' \\
|
|
@@ -1960,6 +2593,7 @@ _computer() {
|
|
|
1960
2593
|
'--source-kind[Source kind]:kind:(none oci-image)' \\
|
|
1961
2594
|
'--image-family[Image family]:family:' \\
|
|
1962
2595
|
'--image-ref[Image reference]:image:' \\
|
|
2596
|
+
'--use-platform-default[Use platform default image]' \\
|
|
1963
2597
|
'--primary-port[Primary app port]:port:' \\
|
|
1964
2598
|
'--primary-path[Primary app path]:path:' \\
|
|
1965
2599
|
'--healthcheck-type[Healthcheck type]:type:(http tcp)' \\
|
|
@@ -1980,6 +2614,50 @@ _computer() {
|
|
|
1980
2614
|
'--json[Print raw JSON]' \\
|
|
1981
2615
|
'1:computer:_computer_handles'
|
|
1982
2616
|
;;
|
|
2617
|
+
image)
|
|
2618
|
+
_arguments -C \\
|
|
2619
|
+
'1:command:->image_command' \\
|
|
2620
|
+
'*::arg:->image_args'
|
|
2621
|
+
case "$state" in
|
|
2622
|
+
image_command)
|
|
2623
|
+
_describe -t commands 'image command' image_commands
|
|
2624
|
+
;;
|
|
2625
|
+
image_args)
|
|
2626
|
+
case "$words[2]" in
|
|
2627
|
+
ls)
|
|
2628
|
+
_arguments '--json[Print raw JSON]'
|
|
2629
|
+
;;
|
|
2630
|
+
save)
|
|
2631
|
+
_arguments \\
|
|
2632
|
+
'--id[source id]:id:' \\
|
|
2633
|
+
'--kind[source kind]:kind:(oci-image nix-git)' \\
|
|
2634
|
+
'--requested-ref[OCI image ref or resolved ref]:ref:' \\
|
|
2635
|
+
'--git-url[Git URL]:url:' \\
|
|
2636
|
+
'--git-ref[Git ref]:ref:' \\
|
|
2637
|
+
'--git-subpath[Git subpath]:path:' \\
|
|
2638
|
+
'--set-as-default[Select as default after saving]' \\
|
|
2639
|
+
'--json[Print raw JSON]'
|
|
2640
|
+
;;
|
|
2641
|
+
default)
|
|
2642
|
+
_arguments \\
|
|
2643
|
+
'--json[Print raw JSON]' \\
|
|
2644
|
+
'1:source id:_machine_source_ids'
|
|
2645
|
+
;;
|
|
2646
|
+
rebuild)
|
|
2647
|
+
_arguments \\
|
|
2648
|
+
'--json[Print raw JSON]' \\
|
|
2649
|
+
'1:source id:_machine_source_ids'
|
|
2650
|
+
;;
|
|
2651
|
+
rm)
|
|
2652
|
+
_arguments \\
|
|
2653
|
+
'--json[Print raw JSON]' \\
|
|
2654
|
+
'(-y --yes)'{-y,--yes}'[Skip confirmation]' \\
|
|
2655
|
+
'1:source id:_machine_source_ids'
|
|
2656
|
+
;;
|
|
2657
|
+
esac
|
|
2658
|
+
;;
|
|
2659
|
+
esac
|
|
2660
|
+
;;
|
|
1983
2661
|
open)
|
|
1984
2662
|
_arguments \\
|
|
1985
2663
|
'--vnc[Open VNC desktop]' \\
|
|
@@ -2039,13 +2717,22 @@ _computer_handles() {
|
|
|
2039
2717
|
fi
|
|
2040
2718
|
}
|
|
2041
2719
|
|
|
2720
|
+
_machine_source_ids() {
|
|
2721
|
+
local -a ids
|
|
2722
|
+
local cli="\${words[1]:-computer}"
|
|
2723
|
+
if ids=(\${(f)"$(\${cli} image ls --json 2>/dev/null | grep '"id"' | sed 's/.*"id": "\\([^"]*\\)".*/\\1/')"}); then
|
|
2724
|
+
_describe -t ids 'machine image source' ids
|
|
2725
|
+
fi
|
|
2726
|
+
}
|
|
2727
|
+
|
|
2042
2728
|
_computer "$@"`;
|
|
2043
2729
|
var BASH_SCRIPT = `_computer() {
|
|
2044
2730
|
local cur prev words cword
|
|
2045
2731
|
_init_completion || return
|
|
2046
2732
|
|
|
2047
|
-
local commands="login logout whoami create ls get open ssh ports agent fleet acp rm completion help"
|
|
2733
|
+
local commands="login logout whoami claude-auth claude-login create ls get image open ssh ports agent fleet acp rm completion help"
|
|
2048
2734
|
local ports_commands="ls publish rm"
|
|
2735
|
+
local image_commands="ls save default rebuild rm"
|
|
2049
2736
|
|
|
2050
2737
|
if [[ $cword -eq 1 ]]; then
|
|
2051
2738
|
COMPREPLY=($(compgen -W "$commands" -- "$cur"))
|
|
@@ -2061,12 +2748,60 @@ var BASH_SCRIPT = `_computer() {
|
|
|
2061
2748
|
whoami)
|
|
2062
2749
|
COMPREPLY=($(compgen -W "--json" -- "$cur"))
|
|
2063
2750
|
;;
|
|
2751
|
+
claude-auth|claude-login)
|
|
2752
|
+
COMPREPLY=($(compgen -W "--machine --keep-helper --skip-cross-check" -- "$cur"))
|
|
2753
|
+
;;
|
|
2064
2754
|
create)
|
|
2065
|
-
COMPREPLY=($(compgen -W "--name --tier --interactive --runtime-family --source-kind --image-family --image-ref --primary-port --primary-path --healthcheck-type --healthcheck-value --ssh-enabled --ssh-disabled --vnc-enabled --vnc-disabled" -- "$cur"))
|
|
2755
|
+
COMPREPLY=($(compgen -W "--name --tier --interactive --runtime-family --source-kind --image-family --image-ref --use-platform-default --primary-port --primary-path --healthcheck-type --healthcheck-value --ssh-enabled --ssh-disabled --vnc-enabled --vnc-disabled" -- "$cur"))
|
|
2066
2756
|
;;
|
|
2067
2757
|
ls)
|
|
2068
2758
|
COMPREPLY=($(compgen -W "--json --verbose -v" -- "$cur"))
|
|
2069
2759
|
;;
|
|
2760
|
+
image)
|
|
2761
|
+
if [[ $cword -eq 2 ]]; then
|
|
2762
|
+
COMPREPLY=($(compgen -W "$image_commands" -- "$cur"))
|
|
2763
|
+
else
|
|
2764
|
+
case "\${words[2]}" in
|
|
2765
|
+
ls)
|
|
2766
|
+
COMPREPLY=($(compgen -W "--json" -- "$cur"))
|
|
2767
|
+
;;
|
|
2768
|
+
save)
|
|
2769
|
+
COMPREPLY=($(compgen -W "--id --kind --requested-ref --git-url --git-ref --git-subpath --set-as-default --json" -- "$cur"))
|
|
2770
|
+
;;
|
|
2771
|
+
default|rebuild|rm)
|
|
2772
|
+
case "\${words[2]}" in
|
|
2773
|
+
default)
|
|
2774
|
+
if [[ "$cur" == -* ]]; then
|
|
2775
|
+
COMPREPLY=($(compgen -W "--json" -- "$cur"))
|
|
2776
|
+
else
|
|
2777
|
+
local source_ids cli="\${words[0]:-computer}"
|
|
2778
|
+
source_ids=$(\${cli} image ls --json 2>/dev/null | grep '"id"' | sed 's/.*"id": "([^"]*)".*/\\1/')
|
|
2779
|
+
COMPREPLY=($(compgen -W "$source_ids" -- "$cur"))
|
|
2780
|
+
fi
|
|
2781
|
+
;;
|
|
2782
|
+
rebuild)
|
|
2783
|
+
if [[ "$cur" == -* ]]; then
|
|
2784
|
+
COMPREPLY=($(compgen -W "--json" -- "$cur"))
|
|
2785
|
+
else
|
|
2786
|
+
local source_ids cli="\${words[0]:-computer}"
|
|
2787
|
+
source_ids=$(\${cli} image ls --json 2>/dev/null | grep '"id"' | sed 's/.*"id": "([^"]*)".*/\\1/')
|
|
2788
|
+
COMPREPLY=($(compgen -W "$source_ids" -- "$cur"))
|
|
2789
|
+
fi
|
|
2790
|
+
;;
|
|
2791
|
+
rm)
|
|
2792
|
+
if [[ "$cur" == -* ]]; then
|
|
2793
|
+
COMPREPLY=($(compgen -W "--json --yes -y" -- "$cur"))
|
|
2794
|
+
else
|
|
2795
|
+
local source_ids cli="\${words[0]:-computer}"
|
|
2796
|
+
source_ids=$(\${cli} image ls --json 2>/dev/null | grep '"id"' | sed 's/.*"id": "([^"]*)".*/\\1/')
|
|
2797
|
+
COMPREPLY=($(compgen -W "$source_ids" -- "$cur"))
|
|
2798
|
+
fi
|
|
2799
|
+
;;
|
|
2800
|
+
esac
|
|
2801
|
+
;;
|
|
2802
|
+
esac
|
|
2803
|
+
fi
|
|
2804
|
+
;;
|
|
2070
2805
|
get|open|ssh|rm)
|
|
2071
2806
|
if [[ $cword -eq 2 ]]; then
|
|
2072
2807
|
local handles cli="\${words[0]:-computer}"
|
|
@@ -2098,7 +2833,7 @@ var BASH_SCRIPT = `_computer() {
|
|
|
2098
2833
|
}
|
|
2099
2834
|
|
|
2100
2835
|
complete -F _computer computer agentcomputer aicomputer`;
|
|
2101
|
-
var completionCommand = new
|
|
2836
|
+
var completionCommand = new Command6("completion").description("Generate shell completions").argument("<shell>", "Shell type (bash or zsh)").action((shell) => {
|
|
2102
2837
|
switch (shell) {
|
|
2103
2838
|
case "zsh":
|
|
2104
2839
|
console.log(ZSH_SCRIPT);
|
|
@@ -2112,19 +2847,299 @@ var completionCommand = new Command5("completion").description("Generate shell c
|
|
|
2112
2847
|
}
|
|
2113
2848
|
});
|
|
2114
2849
|
|
|
2850
|
+
// src/commands/images.ts
|
|
2851
|
+
import { confirm as confirm2, input as textInput3, select as select3 } from "@inquirer/prompts";
|
|
2852
|
+
import { Command as Command7 } from "commander";
|
|
2853
|
+
import chalk7 from "chalk";
|
|
2854
|
+
import ora5 from "ora";
|
|
2855
|
+
var imageCommand = new Command7("image").description("Manage machine image sources");
|
|
2856
|
+
imageCommand.command("ls").description("List machine image sources").option("--json", "Print raw JSON").action(async (options) => {
|
|
2857
|
+
const spinner = options.json ? null : ora5("Fetching machine images...").start();
|
|
2858
|
+
try {
|
|
2859
|
+
const settings = await getMachineSourceSettings();
|
|
2860
|
+
spinner?.stop();
|
|
2861
|
+
if (options.json) {
|
|
2862
|
+
console.log(JSON.stringify(settings, null, 2));
|
|
2863
|
+
return;
|
|
2864
|
+
}
|
|
2865
|
+
printMachineSourceSettings(settings);
|
|
2866
|
+
} catch (error) {
|
|
2867
|
+
if (spinner) {
|
|
2868
|
+
spinner.fail(error instanceof Error ? error.message : "Failed to fetch machine images");
|
|
2869
|
+
} else {
|
|
2870
|
+
console.error(error instanceof Error ? error.message : "Failed to fetch machine images");
|
|
2871
|
+
}
|
|
2872
|
+
process.exit(1);
|
|
2873
|
+
}
|
|
2874
|
+
});
|
|
2875
|
+
imageCommand.command("save").description("Create or update a machine image source").option("--id <id>", "Existing source id to update").option("--kind <kind>", "oci-image or nix-git").option("--requested-ref <ref>", "OCI image ref or explicit resolved ref").option("--git-url <url>", "Repository URL for a Nix git source").option("--git-ref <ref>", "Git ref for a Nix git source").option("--git-subpath <path>", "Subdirectory for a Nix git source").option("--set-as-default", "Select this source as the default after saving").option("--json", "Print raw JSON").action(async (options) => {
|
|
2876
|
+
const spinner = options.json ? null : ora5("Saving machine image source...").start();
|
|
2877
|
+
try {
|
|
2878
|
+
const input = await resolveSaveInput(options);
|
|
2879
|
+
const settings = await upsertMachineSource(input);
|
|
2880
|
+
spinner?.stop();
|
|
2881
|
+
if (options.json) {
|
|
2882
|
+
console.log(JSON.stringify(settings, null, 2));
|
|
2883
|
+
return;
|
|
2884
|
+
}
|
|
2885
|
+
console.log();
|
|
2886
|
+
console.log(chalk7.green("Saved machine image source."));
|
|
2887
|
+
printMachineSourceSettings(settings);
|
|
2888
|
+
} catch (error) {
|
|
2889
|
+
if (spinner) {
|
|
2890
|
+
spinner.fail(error instanceof Error ? error.message : "Failed to save machine image source");
|
|
2891
|
+
} else {
|
|
2892
|
+
console.error(error instanceof Error ? error.message : "Failed to save machine image source");
|
|
2893
|
+
}
|
|
2894
|
+
process.exit(1);
|
|
2895
|
+
}
|
|
2896
|
+
});
|
|
2897
|
+
imageCommand.command("default").description("Set the default machine image source").argument("[source-id]", "Source id to select; use 'platform' or omit for the platform default").option("--json", "Print raw JSON").action(async (sourceID, options) => {
|
|
2898
|
+
const usePlatformDefault = !sourceID || sourceID === "platform";
|
|
2899
|
+
const spinner = options.json ? null : ora5("Updating machine image default...").start();
|
|
2900
|
+
try {
|
|
2901
|
+
let settings;
|
|
2902
|
+
if (usePlatformDefault) {
|
|
2903
|
+
settings = await clearMachineSourceDefault();
|
|
2904
|
+
} else {
|
|
2905
|
+
const current = await getMachineSourceSettings();
|
|
2906
|
+
const source = current.sources.find((entry) => entry.id === sourceID);
|
|
2907
|
+
if (!source) {
|
|
2908
|
+
throw new Error(`machine image source '${sourceID}' not found`);
|
|
2909
|
+
}
|
|
2910
|
+
settings = await upsertMachineSource({
|
|
2911
|
+
id: source.id,
|
|
2912
|
+
kind: source.kind,
|
|
2913
|
+
set_as_default: true
|
|
2914
|
+
});
|
|
2915
|
+
}
|
|
2916
|
+
spinner?.stop();
|
|
2917
|
+
if (options.json) {
|
|
2918
|
+
console.log(JSON.stringify(settings, null, 2));
|
|
2919
|
+
return;
|
|
2920
|
+
}
|
|
2921
|
+
console.log();
|
|
2922
|
+
if (!usePlatformDefault) {
|
|
2923
|
+
const selected = settings.default_machine_source ?? void 0;
|
|
2924
|
+
const label = selected ? summarizeMachineSource(selected) : sourceID;
|
|
2925
|
+
console.log(chalk7.green(`Selected ${chalk7.bold(label)} as the default machine image.`));
|
|
2926
|
+
} else {
|
|
2927
|
+
console.log(chalk7.green("Using the AgentComputer platform default image."));
|
|
2928
|
+
}
|
|
2929
|
+
printMachineSourceSettings(settings);
|
|
2930
|
+
} catch (error) {
|
|
2931
|
+
if (spinner) {
|
|
2932
|
+
spinner.fail(error instanceof Error ? error.message : "Failed to update machine image default");
|
|
2933
|
+
} else {
|
|
2934
|
+
console.error(error instanceof Error ? error.message : "Failed to update machine image default");
|
|
2935
|
+
}
|
|
2936
|
+
process.exit(1);
|
|
2937
|
+
}
|
|
2938
|
+
});
|
|
2939
|
+
imageCommand.command("rebuild").description("Rebuild a machine image source").argument("<source-id>", "Source id to rebuild").option("--json", "Print raw JSON").action(async (sourceID, options) => {
|
|
2940
|
+
const spinner = options.json ? null : ora5("Queueing machine image rebuild...").start();
|
|
2941
|
+
try {
|
|
2942
|
+
const settings = await rebuildMachineSource(sourceID);
|
|
2943
|
+
spinner?.stop();
|
|
2944
|
+
if (options.json) {
|
|
2945
|
+
console.log(JSON.stringify(settings, null, 2));
|
|
2946
|
+
return;
|
|
2947
|
+
}
|
|
2948
|
+
console.log();
|
|
2949
|
+
console.log(chalk7.green(`Queued rebuild for ${chalk7.bold(sourceID)}.`));
|
|
2950
|
+
printMachineSourceSettings(settings);
|
|
2951
|
+
} catch (error) {
|
|
2952
|
+
if (spinner) {
|
|
2953
|
+
spinner.fail(error instanceof Error ? error.message : "Failed to rebuild machine image source");
|
|
2954
|
+
} else {
|
|
2955
|
+
console.error(error instanceof Error ? error.message : "Failed to rebuild machine image source");
|
|
2956
|
+
}
|
|
2957
|
+
process.exit(1);
|
|
2958
|
+
}
|
|
2959
|
+
});
|
|
2960
|
+
imageCommand.command("rm").description("Delete a machine image source").argument("<source-id>", "Source id to delete").option("-y, --yes", "Skip confirmation prompt").option("--json", "Print raw JSON").action(async (sourceID, options, cmd) => {
|
|
2961
|
+
const globalYes = cmd.parent?.parent?.opts()?.yes;
|
|
2962
|
+
const skipConfirm = Boolean(options.yes || globalYes);
|
|
2963
|
+
let spinner = null;
|
|
2964
|
+
try {
|
|
2965
|
+
if (!skipConfirm && process.stdin.isTTY) {
|
|
2966
|
+
const confirmed = await confirmDeletion(sourceID);
|
|
2967
|
+
if (!confirmed) {
|
|
2968
|
+
console.log(chalk7.dim(" Cancelled."));
|
|
2969
|
+
return;
|
|
2970
|
+
}
|
|
2971
|
+
}
|
|
2972
|
+
spinner = options.json ? null : ora5("Deleting machine image source...").start();
|
|
2973
|
+
const settings = await deleteMachineSource(sourceID);
|
|
2974
|
+
spinner?.stop();
|
|
2975
|
+
if (options.json) {
|
|
2976
|
+
console.log(JSON.stringify(settings, null, 2));
|
|
2977
|
+
return;
|
|
2978
|
+
}
|
|
2979
|
+
console.log();
|
|
2980
|
+
console.log(chalk7.green(`Deleted machine image source ${chalk7.bold(sourceID)}.`));
|
|
2981
|
+
printMachineSourceSettings(settings);
|
|
2982
|
+
} catch (error) {
|
|
2983
|
+
if (spinner) {
|
|
2984
|
+
spinner.fail(error instanceof Error ? error.message : "Failed to delete machine image source");
|
|
2985
|
+
} else {
|
|
2986
|
+
console.error(error instanceof Error ? error.message : "Failed to delete machine image source");
|
|
2987
|
+
}
|
|
2988
|
+
process.exit(1);
|
|
2989
|
+
}
|
|
2990
|
+
});
|
|
2991
|
+
function printMachineSourceSettings(settings) {
|
|
2992
|
+
console.log(` ${chalk7.dim("Default")} ${chalk7.white(summarizeMachineSourceSelection(settings))}`);
|
|
2993
|
+
console.log();
|
|
2994
|
+
if (settings.sources.length === 0) {
|
|
2995
|
+
console.log(chalk7.dim(" No custom machine images configured yet."));
|
|
2996
|
+
console.log();
|
|
2997
|
+
return;
|
|
2998
|
+
}
|
|
2999
|
+
for (const source of settings.sources) {
|
|
3000
|
+
printMachineSourceCard(source, settings.default_machine_source_id === source.id);
|
|
3001
|
+
}
|
|
3002
|
+
}
|
|
3003
|
+
function printMachineSourceCard(source, isDefault) {
|
|
3004
|
+
console.log(` ${chalk7.bold(machineSourceTitle(source))}${isDefault ? chalk7.green(" (default)") : ""}`);
|
|
3005
|
+
console.log(` ${chalk7.dim(" ID")} ${source.id}`);
|
|
3006
|
+
console.log(` ${chalk7.dim(" Kind")} ${source.kind}`);
|
|
3007
|
+
console.log(` ${chalk7.dim(" Status")} ${source.status}${source.latest_build ? ` | latest build ${source.latest_build.status}` : ""}`);
|
|
3008
|
+
if (source.resolved_image_ref) {
|
|
3009
|
+
console.log(` ${chalk7.dim(" Resolved")} ${source.resolved_image_ref}`);
|
|
3010
|
+
}
|
|
3011
|
+
if (source.error) {
|
|
3012
|
+
console.log(` ${chalk7.dim(" Error")} ${chalk7.red(source.error)}`);
|
|
3013
|
+
}
|
|
3014
|
+
console.log(` ${chalk7.dim(" Source")} ${summarizeMachineSource(source)}`);
|
|
3015
|
+
console.log();
|
|
3016
|
+
}
|
|
3017
|
+
function machineSourceTitle(source) {
|
|
3018
|
+
if (source.kind === "oci-image") {
|
|
3019
|
+
return source.requested_ref || "OCI image";
|
|
3020
|
+
}
|
|
3021
|
+
return source.git_url || "Nix git source";
|
|
3022
|
+
}
|
|
3023
|
+
function parseMachineSourceKind(value) {
|
|
3024
|
+
switch (value) {
|
|
3025
|
+
case void 0:
|
|
3026
|
+
case "oci-image":
|
|
3027
|
+
case "nix-git":
|
|
3028
|
+
return value;
|
|
3029
|
+
default:
|
|
3030
|
+
throw new Error("--kind must be oci-image or nix-git");
|
|
3031
|
+
}
|
|
3032
|
+
}
|
|
3033
|
+
function hasTTY() {
|
|
3034
|
+
return process.stdin.isTTY && process.stdout.isTTY;
|
|
3035
|
+
}
|
|
3036
|
+
async function resolveSaveInput(options) {
|
|
3037
|
+
const existing = await loadExistingSource(options.id);
|
|
3038
|
+
const selectedKind = parseMachineSourceKind(options.kind) ?? existing?.kind;
|
|
3039
|
+
let kind = selectedKind;
|
|
3040
|
+
if (!kind) {
|
|
3041
|
+
if (!hasTTY()) {
|
|
3042
|
+
throw new Error("--kind is required");
|
|
3043
|
+
}
|
|
3044
|
+
kind = await selectMachineSourceKind();
|
|
3045
|
+
}
|
|
3046
|
+
const input = {
|
|
3047
|
+
id: options.id,
|
|
3048
|
+
kind,
|
|
3049
|
+
set_as_default: options.setAsDefault
|
|
3050
|
+
};
|
|
3051
|
+
switch (kind) {
|
|
3052
|
+
case "oci-image": {
|
|
3053
|
+
const existingSameKind = existing?.kind === kind;
|
|
3054
|
+
const requestedRef = normalizeValue(options.requestedRef) ?? (existingSameKind ? existing?.requested_ref : void 0);
|
|
3055
|
+
if (!requestedRef) {
|
|
3056
|
+
if (!hasTTY()) {
|
|
3057
|
+
throw new Error("--requested-ref is required for oci-image sources");
|
|
3058
|
+
}
|
|
3059
|
+
const promptedRef = normalizeValue(await textInput3({ message: "OCI image ref:" }));
|
|
3060
|
+
if (!promptedRef) {
|
|
3061
|
+
throw new Error("OCI image ref is required");
|
|
3062
|
+
}
|
|
3063
|
+
input.requested_ref = promptedRef;
|
|
3064
|
+
break;
|
|
3065
|
+
}
|
|
3066
|
+
input.requested_ref = requestedRef;
|
|
3067
|
+
break;
|
|
3068
|
+
}
|
|
3069
|
+
case "nix-git": {
|
|
3070
|
+
const existingSameKind = existing?.kind === kind;
|
|
3071
|
+
const gitUrl = normalizeValue(options.gitUrl) ?? (existingSameKind ? existing?.git_url : void 0);
|
|
3072
|
+
const gitRef = normalizeValue(options.gitRef) ?? (existingSameKind ? existing?.git_ref : void 0);
|
|
3073
|
+
const gitSubpath = normalizeValue(options.gitSubpath) ?? (existingSameKind ? existing?.git_subpath : void 0);
|
|
3074
|
+
if (!gitUrl) {
|
|
3075
|
+
if (!hasTTY()) {
|
|
3076
|
+
throw new Error("--git-url is required for nix-git sources");
|
|
3077
|
+
}
|
|
3078
|
+
const promptedGitURL = normalizeValue(await textInput3({ message: "Git URL:" }));
|
|
3079
|
+
if (!promptedGitURL) {
|
|
3080
|
+
throw new Error("Git URL is required");
|
|
3081
|
+
}
|
|
3082
|
+
input.git_url = promptedGitURL;
|
|
3083
|
+
break;
|
|
3084
|
+
}
|
|
3085
|
+
input.git_url = gitUrl;
|
|
3086
|
+
if (gitRef) {
|
|
3087
|
+
input.git_ref = gitRef;
|
|
3088
|
+
}
|
|
3089
|
+
if (gitSubpath) {
|
|
3090
|
+
input.git_subpath = gitSubpath;
|
|
3091
|
+
}
|
|
3092
|
+
break;
|
|
3093
|
+
}
|
|
3094
|
+
}
|
|
3095
|
+
return input;
|
|
3096
|
+
}
|
|
3097
|
+
async function selectMachineSourceKind() {
|
|
3098
|
+
const kind = await select3({
|
|
3099
|
+
message: "Select machine image kind",
|
|
3100
|
+
choices: [
|
|
3101
|
+
{ name: "oci-image - resolve an OCI image digest", value: "oci-image" },
|
|
3102
|
+
{ name: "nix-git - build a Nix source into OCI", value: "nix-git" }
|
|
3103
|
+
],
|
|
3104
|
+
default: "oci-image"
|
|
3105
|
+
});
|
|
3106
|
+
return kind;
|
|
3107
|
+
}
|
|
3108
|
+
async function loadExistingSource(sourceID) {
|
|
3109
|
+
if (!sourceID) {
|
|
3110
|
+
return void 0;
|
|
3111
|
+
}
|
|
3112
|
+
const settings = await getMachineSourceSettings();
|
|
3113
|
+
const source = settings.sources.find((entry) => entry.id === sourceID);
|
|
3114
|
+
if (!source) {
|
|
3115
|
+
throw new Error(`machine image source '${sourceID}' not found`);
|
|
3116
|
+
}
|
|
3117
|
+
return source;
|
|
3118
|
+
}
|
|
3119
|
+
function normalizeValue(value) {
|
|
3120
|
+
const trimmed = value?.trim();
|
|
3121
|
+
return trimmed ? trimmed : void 0;
|
|
3122
|
+
}
|
|
3123
|
+
async function confirmDeletion(sourceID) {
|
|
3124
|
+
return confirm2({
|
|
3125
|
+
message: `Delete machine image source ${sourceID}?`,
|
|
3126
|
+
default: false
|
|
3127
|
+
});
|
|
3128
|
+
}
|
|
3129
|
+
|
|
2115
3130
|
// src/commands/login.ts
|
|
2116
|
-
import { Command as
|
|
2117
|
-
import
|
|
2118
|
-
import
|
|
3131
|
+
import { Command as Command8 } from "commander";
|
|
3132
|
+
import chalk8 from "chalk";
|
|
3133
|
+
import ora6 from "ora";
|
|
2119
3134
|
|
|
2120
3135
|
// src/lib/browser-login.ts
|
|
2121
|
-
import { randomBytes } from "crypto";
|
|
3136
|
+
import { randomBytes as randomBytes2 } from "crypto";
|
|
2122
3137
|
import { createServer } from "http";
|
|
2123
3138
|
var CALLBACK_HOST = "127.0.0.1";
|
|
2124
3139
|
var CALLBACK_PATH = "/callback";
|
|
2125
3140
|
var LOGIN_TIMEOUT_MS = 5 * 60 * 1e3;
|
|
2126
3141
|
async function createBrowserLoginAttempt() {
|
|
2127
|
-
const state =
|
|
3142
|
+
const state = randomBytes2(16).toString("hex");
|
|
2128
3143
|
const deferred = createDeferred();
|
|
2129
3144
|
let callbackURL = "";
|
|
2130
3145
|
let closed = false;
|
|
@@ -2343,11 +3358,11 @@ function escapeHTML(value) {
|
|
|
2343
3358
|
}
|
|
2344
3359
|
|
|
2345
3360
|
// src/commands/login.ts
|
|
2346
|
-
var loginCommand = new
|
|
3361
|
+
var loginCommand = new Command8("login").description("Authenticate the CLI").option("--api-key <key>", "API key starting with ac_live_").option("--stdin", "Read the API key from stdin").option("-f, --force", "Overwrite an existing stored API key").action(async (options) => {
|
|
2347
3362
|
const existingKey = getStoredAPIKey();
|
|
2348
3363
|
if (existingKey && !options.force) {
|
|
2349
3364
|
console.log();
|
|
2350
|
-
console.log(
|
|
3365
|
+
console.log(chalk8.yellow(" Already logged in. Use --force to overwrite."));
|
|
2351
3366
|
console.log();
|
|
2352
3367
|
return;
|
|
2353
3368
|
}
|
|
@@ -2355,8 +3370,8 @@ var loginCommand = new Command6("login").description("Authenticate the CLI").opt
|
|
|
2355
3370
|
const apiKey = await resolveAPIKeyInput(options.apiKey, options.stdin);
|
|
2356
3371
|
if (!apiKey && wantsManualLogin) {
|
|
2357
3372
|
console.log();
|
|
2358
|
-
console.log(
|
|
2359
|
-
console.log(
|
|
3373
|
+
console.log(chalk8.dim(" Usage: computer login --api-key <ac_live_...>"));
|
|
3374
|
+
console.log(chalk8.dim(` API: ${getBaseURL()}`));
|
|
2360
3375
|
console.log();
|
|
2361
3376
|
process.exit(1);
|
|
2362
3377
|
}
|
|
@@ -2366,15 +3381,15 @@ var loginCommand = new Command6("login").description("Authenticate the CLI").opt
|
|
|
2366
3381
|
}
|
|
2367
3382
|
if (!apiKey.startsWith("ac_live_")) {
|
|
2368
3383
|
console.log();
|
|
2369
|
-
console.log(
|
|
3384
|
+
console.log(chalk8.red(" API key must start with ac_live_"));
|
|
2370
3385
|
console.log();
|
|
2371
3386
|
process.exit(1);
|
|
2372
3387
|
}
|
|
2373
|
-
const spinner =
|
|
3388
|
+
const spinner = ora6("Authenticating...").start();
|
|
2374
3389
|
try {
|
|
2375
3390
|
const me = await apiWithKey(apiKey, "/v1/me");
|
|
2376
3391
|
setAPIKey(apiKey);
|
|
2377
|
-
spinner.succeed(`Logged in as ${
|
|
3392
|
+
spinner.succeed(`Logged in as ${chalk8.bold(me.user.email)}`);
|
|
2378
3393
|
} catch (error) {
|
|
2379
3394
|
spinner.fail(
|
|
2380
3395
|
error instanceof Error ? error.message : "Failed to validate API key"
|
|
@@ -2383,7 +3398,7 @@ var loginCommand = new Command6("login").description("Authenticate the CLI").opt
|
|
|
2383
3398
|
}
|
|
2384
3399
|
});
|
|
2385
3400
|
async function runBrowserLogin() {
|
|
2386
|
-
const spinner =
|
|
3401
|
+
const spinner = ora6("Starting browser login...").start();
|
|
2387
3402
|
let attempt = null;
|
|
2388
3403
|
try {
|
|
2389
3404
|
attempt = await createBrowserLoginAttempt();
|
|
@@ -2393,14 +3408,14 @@ async function runBrowserLogin() {
|
|
|
2393
3408
|
} catch {
|
|
2394
3409
|
spinner.stop();
|
|
2395
3410
|
console.log();
|
|
2396
|
-
console.log(
|
|
2397
|
-
console.log(
|
|
3411
|
+
console.log(chalk8.yellow(" Browser auto-open failed. Open this URL to continue:"));
|
|
3412
|
+
console.log(chalk8.dim(` ${attempt.loginURL}`));
|
|
2398
3413
|
console.log();
|
|
2399
3414
|
spinner.start("Waiting for browser login...");
|
|
2400
3415
|
}
|
|
2401
3416
|
spinner.text = "Waiting for browser login...";
|
|
2402
3417
|
const result = await attempt.waitForResult();
|
|
2403
|
-
spinner.succeed(`Logged in as ${
|
|
3418
|
+
spinner.succeed(`Logged in as ${chalk8.bold(result.me.user.email)}`);
|
|
2404
3419
|
} catch (error) {
|
|
2405
3420
|
spinner.fail(error instanceof Error ? error.message : "Browser login failed");
|
|
2406
3421
|
process.exit(1);
|
|
@@ -2426,33 +3441,33 @@ async function resolveAPIKeyInput(flagValue, readFromStdin) {
|
|
|
2426
3441
|
}
|
|
2427
3442
|
|
|
2428
3443
|
// src/commands/logout.ts
|
|
2429
|
-
import { Command as
|
|
2430
|
-
import
|
|
2431
|
-
var logoutCommand = new
|
|
3444
|
+
import { Command as Command9 } from "commander";
|
|
3445
|
+
import chalk9 from "chalk";
|
|
3446
|
+
var logoutCommand = new Command9("logout").description("Remove stored API key").action(() => {
|
|
2432
3447
|
if (!getStoredAPIKey()) {
|
|
2433
3448
|
console.log();
|
|
2434
|
-
console.log(
|
|
3449
|
+
console.log(chalk9.dim(" Not logged in."));
|
|
2435
3450
|
if (hasEnvAPIKey()) {
|
|
2436
|
-
console.log(
|
|
3451
|
+
console.log(chalk9.dim(" Environment API key is still active in this shell."));
|
|
2437
3452
|
}
|
|
2438
3453
|
console.log();
|
|
2439
3454
|
return;
|
|
2440
3455
|
}
|
|
2441
3456
|
clearAPIKey();
|
|
2442
3457
|
console.log();
|
|
2443
|
-
console.log(
|
|
3458
|
+
console.log(chalk9.green(" Logged out."));
|
|
2444
3459
|
if (hasEnvAPIKey()) {
|
|
2445
|
-
console.log(
|
|
3460
|
+
console.log(chalk9.dim(" Environment API key is still active in this shell."));
|
|
2446
3461
|
}
|
|
2447
3462
|
console.log();
|
|
2448
3463
|
});
|
|
2449
3464
|
|
|
2450
3465
|
// src/commands/whoami.ts
|
|
2451
|
-
import { Command as
|
|
2452
|
-
import
|
|
2453
|
-
import
|
|
2454
|
-
var whoamiCommand = new
|
|
2455
|
-
const spinner = options.json ? null :
|
|
3466
|
+
import { Command as Command10 } from "commander";
|
|
3467
|
+
import chalk10 from "chalk";
|
|
3468
|
+
import ora7 from "ora";
|
|
3469
|
+
var whoamiCommand = new Command10("whoami").description("Show current user").option("--json", "Print raw JSON").action(async (options) => {
|
|
3470
|
+
const spinner = options.json ? null : ora7("Loading user...").start();
|
|
2456
3471
|
try {
|
|
2457
3472
|
const me = await api("/v1/me");
|
|
2458
3473
|
spinner?.stop();
|
|
@@ -2461,14 +3476,14 @@ var whoamiCommand = new Command8("whoami").description("Show current user").opti
|
|
|
2461
3476
|
return;
|
|
2462
3477
|
}
|
|
2463
3478
|
console.log();
|
|
2464
|
-
console.log(` ${
|
|
3479
|
+
console.log(` ${chalk10.bold.white(me.user.display_name || me.user.email)}`);
|
|
2465
3480
|
if (me.user.display_name) {
|
|
2466
|
-
console.log(` ${
|
|
3481
|
+
console.log(` ${chalk10.dim(me.user.email)}`);
|
|
2467
3482
|
}
|
|
2468
3483
|
if (me.api_key.name) {
|
|
2469
|
-
console.log(` ${
|
|
3484
|
+
console.log(` ${chalk10.dim("Key:")} ${me.api_key.name}`);
|
|
2470
3485
|
}
|
|
2471
|
-
console.log(` ${
|
|
3486
|
+
console.log(` ${chalk10.dim("API:")} ${chalk10.dim(getBaseURL())}`);
|
|
2472
3487
|
console.log();
|
|
2473
3488
|
} catch (error) {
|
|
2474
3489
|
if (spinner) {
|
|
@@ -2485,15 +3500,15 @@ var pkg2 = JSON.parse(
|
|
|
2485
3500
|
readFileSync3(new URL("../package.json", import.meta.url), "utf8")
|
|
2486
3501
|
);
|
|
2487
3502
|
var cliName = process.argv[1] ? basename2(process.argv[1]) : "agentcomputer";
|
|
2488
|
-
var program = new
|
|
3503
|
+
var program = new Command11();
|
|
2489
3504
|
function appendTextSection(lines, title, values) {
|
|
2490
3505
|
if (values.length === 0) {
|
|
2491
3506
|
return;
|
|
2492
3507
|
}
|
|
2493
|
-
lines.push(` ${
|
|
3508
|
+
lines.push(` ${chalk11.dim(title)}`);
|
|
2494
3509
|
lines.push("");
|
|
2495
3510
|
for (const value of values) {
|
|
2496
|
-
lines.push(` ${
|
|
3511
|
+
lines.push(` ${chalk11.white(value)}`);
|
|
2497
3512
|
}
|
|
2498
3513
|
lines.push("");
|
|
2499
3514
|
}
|
|
@@ -2502,10 +3517,10 @@ function appendTableSection(lines, title, entries) {
|
|
|
2502
3517
|
return;
|
|
2503
3518
|
}
|
|
2504
3519
|
const width = Math.max(...entries.map((entry) => entry.term.length), 0) + 2;
|
|
2505
|
-
lines.push(` ${
|
|
3520
|
+
lines.push(` ${chalk11.dim(title)}`);
|
|
2506
3521
|
lines.push("");
|
|
2507
3522
|
for (const entry of entries) {
|
|
2508
|
-
lines.push(` ${
|
|
3523
|
+
lines.push(` ${chalk11.white(padEnd(entry.term, width))}${chalk11.dim(entry.desc)}`);
|
|
2509
3524
|
}
|
|
2510
3525
|
lines.push("");
|
|
2511
3526
|
}
|
|
@@ -2524,29 +3539,32 @@ function formatRootHelp(cmd) {
|
|
|
2524
3539
|
const groups = [
|
|
2525
3540
|
["Auth", []],
|
|
2526
3541
|
["Computers", []],
|
|
3542
|
+
["Images", []],
|
|
2527
3543
|
["Access", []],
|
|
2528
3544
|
["Agents", []],
|
|
2529
3545
|
["Other", []]
|
|
2530
3546
|
];
|
|
2531
3547
|
const otherGroup = groups.find(([name]) => name === "Other")[1];
|
|
2532
|
-
lines.push(`${
|
|
3548
|
+
lines.push(`${chalk11.bold(cliName)} ${chalk11.dim(`v${version}`)}`);
|
|
2533
3549
|
lines.push("");
|
|
2534
3550
|
if (cmd.description()) {
|
|
2535
|
-
lines.push(` ${
|
|
3551
|
+
lines.push(` ${chalk11.dim(cmd.description())}`);
|
|
2536
3552
|
lines.push("");
|
|
2537
3553
|
}
|
|
2538
3554
|
appendTextSection(lines, "Usage", [`${cliName} <command> [options]`]);
|
|
2539
3555
|
for (const sub of cmd.commands) {
|
|
2540
3556
|
const name = sub.name();
|
|
2541
3557
|
const entry = { term: name, desc: sub.description() };
|
|
2542
|
-
if (["login", "logout", "whoami"].includes(name)) {
|
|
3558
|
+
if (["login", "logout", "whoami", "claude-auth"].includes(name)) {
|
|
2543
3559
|
groups[0][1].push(entry);
|
|
2544
3560
|
} else if (["create", "ls", "get", "rm"].includes(name)) {
|
|
2545
3561
|
groups[1][1].push(entry);
|
|
2546
|
-
} else if (
|
|
3562
|
+
} else if (name === "image") {
|
|
2547
3563
|
groups[2][1].push(entry);
|
|
2548
|
-
} else if (["
|
|
3564
|
+
} else if (["open", "ssh", "ports"].includes(name)) {
|
|
2549
3565
|
groups[3][1].push(entry);
|
|
3566
|
+
} else if (["agent", "fleet", "acp"].includes(name)) {
|
|
3567
|
+
groups[4][1].push(entry);
|
|
2550
3568
|
} else {
|
|
2551
3569
|
otherGroup.push(entry);
|
|
2552
3570
|
}
|
|
@@ -2581,10 +3599,10 @@ function formatSubcommandHelp(cmd, helper) {
|
|
|
2581
3599
|
term: helper.optionTerm(option),
|
|
2582
3600
|
desc: helper.optionDescription(option)
|
|
2583
3601
|
}));
|
|
2584
|
-
lines.push(
|
|
3602
|
+
lines.push(chalk11.bold(commandPath(cmd)));
|
|
2585
3603
|
lines.push("");
|
|
2586
3604
|
if (description) {
|
|
2587
|
-
lines.push(` ${
|
|
3605
|
+
lines.push(` ${chalk11.dim(description)}`);
|
|
2588
3606
|
lines.push("");
|
|
2589
3607
|
}
|
|
2590
3608
|
appendTextSection(lines, "Usage", [helper.commandUsage(cmd)]);
|
|
@@ -2611,9 +3629,11 @@ program.name(cliName).description("Agent Computer CLI").version(pkg2.version ??
|
|
|
2611
3629
|
program.addCommand(loginCommand);
|
|
2612
3630
|
program.addCommand(logoutCommand);
|
|
2613
3631
|
program.addCommand(whoamiCommand);
|
|
3632
|
+
program.addCommand(claudeAuthCommand);
|
|
2614
3633
|
program.addCommand(createCommand);
|
|
2615
3634
|
program.addCommand(lsCommand);
|
|
2616
3635
|
program.addCommand(getCommand);
|
|
3636
|
+
program.addCommand(imageCommand);
|
|
2617
3637
|
program.addCommand(agentCommand);
|
|
2618
3638
|
program.addCommand(fleetCommand);
|
|
2619
3639
|
program.addCommand(acpCommand);
|