aicomputer 0.1.5 → 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/README.md +7 -0
- package/dist/index.js +1436 -305
- 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
|
|
@@ -857,8 +869,28 @@ function emitError(id, code, message) {
|
|
|
857
869
|
}
|
|
858
870
|
});
|
|
859
871
|
}
|
|
860
|
-
|
|
861
|
-
|
|
872
|
+
function forwardBridgePayload(payload, publicSessionID, backendSessionID) {
|
|
873
|
+
let message;
|
|
874
|
+
try {
|
|
875
|
+
message = JSON.parse(payload);
|
|
876
|
+
} catch {
|
|
877
|
+
return;
|
|
878
|
+
}
|
|
879
|
+
const method = typeof message.method === "string" ? message.method : "";
|
|
880
|
+
if (method !== "session/update" && method !== "session/request_permission") {
|
|
881
|
+
return;
|
|
882
|
+
}
|
|
883
|
+
const params = typeof message.params === "object" && message.params !== null ? { ...message.params } : {};
|
|
884
|
+
const payloadSessionID = typeof params.sessionId === "string" ? params.sessionId.trim() : "";
|
|
885
|
+
if (backendSessionID?.trim() && payloadSessionID && payloadSessionID !== backendSessionID.trim()) {
|
|
886
|
+
return;
|
|
887
|
+
}
|
|
888
|
+
params.sessionId = publicSessionID;
|
|
889
|
+
process.stdout.write(`${JSON.stringify({ ...message, params })}
|
|
890
|
+
`);
|
|
891
|
+
}
|
|
892
|
+
async function forwardRawEventStream(computerID, publicSessionID, backendSessionID, signal) {
|
|
893
|
+
const response = await openAgentSessionEventsStream(computerID, publicSessionID, { signal });
|
|
862
894
|
if (!response.body) {
|
|
863
895
|
return;
|
|
864
896
|
}
|
|
@@ -882,8 +914,7 @@ async function forwardRawEventStream(computerID, sessionID, signal) {
|
|
|
882
914
|
if (!payload || payload === "heartbeat") {
|
|
883
915
|
continue;
|
|
884
916
|
}
|
|
885
|
-
|
|
886
|
-
`);
|
|
917
|
+
forwardBridgePayload(payload, publicSessionID, backendSessionID);
|
|
887
918
|
}
|
|
888
919
|
}
|
|
889
920
|
}
|
|
@@ -904,15 +935,21 @@ async function handlePromptRequest(computerID, sessionID, id, params) {
|
|
|
904
935
|
if (prompt.length === 0) {
|
|
905
936
|
throw new Error("session/prompt requires prompt content");
|
|
906
937
|
}
|
|
938
|
+
const accepted = await promptAgentSession(computerID, sessionID, { prompt });
|
|
939
|
+
const current = await getAgentSession(computerID, sessionID);
|
|
907
940
|
const controller = new AbortController();
|
|
908
|
-
const streamPromise = forwardRawEventStream(
|
|
941
|
+
const streamPromise = forwardRawEventStream(
|
|
942
|
+
computerID,
|
|
943
|
+
sessionID,
|
|
944
|
+
current.backend_session_id,
|
|
945
|
+
controller.signal
|
|
946
|
+
).catch((error) => {
|
|
909
947
|
if (error instanceof Error && error.name === "AbortError") {
|
|
910
948
|
return;
|
|
911
949
|
}
|
|
912
|
-
|
|
950
|
+
return;
|
|
913
951
|
});
|
|
914
952
|
try {
|
|
915
|
-
const accepted = await promptAgentSession(computerID, sessionID, { prompt });
|
|
916
953
|
const settled = await waitForSessionToSettle(computerID, sessionID, accepted.prompt_id);
|
|
917
954
|
emitResult(id, {
|
|
918
955
|
stopReason: settled.last_stop_reason || "end_turn"
|
|
@@ -1037,22 +1074,22 @@ acpCommand.command("serve").description("Serve a local stdio ACP bridge backed b
|
|
|
1037
1074
|
|
|
1038
1075
|
// src/commands/agent.ts
|
|
1039
1076
|
import { Command as Command3 } from "commander";
|
|
1040
|
-
import
|
|
1077
|
+
import chalk4 from "chalk";
|
|
1041
1078
|
import ora2 from "ora";
|
|
1042
1079
|
function formatAgentSessionStatus(status) {
|
|
1043
1080
|
switch (status) {
|
|
1044
1081
|
case "idle":
|
|
1045
|
-
return
|
|
1082
|
+
return chalk4.green(status);
|
|
1046
1083
|
case "running":
|
|
1047
|
-
return
|
|
1084
|
+
return chalk4.blue(status);
|
|
1048
1085
|
case "cancelling":
|
|
1049
|
-
return
|
|
1086
|
+
return chalk4.yellow(status);
|
|
1050
1087
|
case "interrupted":
|
|
1051
|
-
return
|
|
1088
|
+
return chalk4.yellow(status);
|
|
1052
1089
|
case "failed":
|
|
1053
|
-
return
|
|
1090
|
+
return chalk4.red(status);
|
|
1054
1091
|
case "closed":
|
|
1055
|
-
return
|
|
1092
|
+
return chalk4.gray(status);
|
|
1056
1093
|
default:
|
|
1057
1094
|
return status;
|
|
1058
1095
|
}
|
|
@@ -1061,14 +1098,14 @@ function printAgents(agents) {
|
|
|
1061
1098
|
const idWidth = Math.max(5, ...agents.map((agent) => agent.id.length));
|
|
1062
1099
|
console.log();
|
|
1063
1100
|
console.log(
|
|
1064
|
-
` ${
|
|
1101
|
+
` ${chalk4.dim(padEnd("Agent", idWidth + 2))}${chalk4.dim(padEnd("Installed", 12))}${chalk4.dim(padEnd("Creds", 8))}${chalk4.dim("Version")}`
|
|
1065
1102
|
);
|
|
1066
1103
|
console.log(
|
|
1067
|
-
` ${
|
|
1104
|
+
` ${chalk4.dim("-".repeat(idWidth + 2))}${chalk4.dim("-".repeat(12))}${chalk4.dim("-".repeat(8))}${chalk4.dim("-".repeat(12))}`
|
|
1068
1105
|
);
|
|
1069
1106
|
for (const agent of agents) {
|
|
1070
1107
|
console.log(
|
|
1071
|
-
` ${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")}`
|
|
1072
1109
|
);
|
|
1073
1110
|
}
|
|
1074
1111
|
console.log();
|
|
@@ -1076,7 +1113,7 @@ function printAgents(agents) {
|
|
|
1076
1113
|
function printSessions(sessions, handleByComputerID = /* @__PURE__ */ new Map()) {
|
|
1077
1114
|
if (sessions.length === 0) {
|
|
1078
1115
|
console.log();
|
|
1079
|
-
console.log(
|
|
1116
|
+
console.log(chalk4.dim(" No agent sessions found."));
|
|
1080
1117
|
console.log();
|
|
1081
1118
|
return;
|
|
1082
1119
|
}
|
|
@@ -1085,19 +1122,19 @@ function printSessions(sessions, handleByComputerID = /* @__PURE__ */ new Map())
|
|
|
1085
1122
|
const statusWidth = 13;
|
|
1086
1123
|
console.log();
|
|
1087
1124
|
console.log(
|
|
1088
|
-
` ${
|
|
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")}`
|
|
1089
1126
|
);
|
|
1090
1127
|
console.log(
|
|
1091
|
-
` ${
|
|
1128
|
+
` ${chalk4.dim("-".repeat(14))}${chalk4.dim("-".repeat(nameWidth + 2))}${chalk4.dim("-".repeat(agentWidth + 2))}${chalk4.dim("-".repeat(statusWidth + 2))}${chalk4.dim("-".repeat(20))}`
|
|
1092
1129
|
);
|
|
1093
1130
|
for (const session of sessions) {
|
|
1094
1131
|
const location = handleByComputerID.get(session.computer_id) ?? session.computer_id;
|
|
1095
1132
|
console.log(
|
|
1096
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}`
|
|
1097
1134
|
);
|
|
1098
|
-
console.log(` ${
|
|
1135
|
+
console.log(` ${chalk4.dim(` cwd=${session.cwd} updated=${timeAgo(session.updated_at)}`)}`);
|
|
1099
1136
|
if (session.last_stop_reason || session.last_error) {
|
|
1100
|
-
console.log(` ${
|
|
1137
|
+
console.log(` ${chalk4.dim(` ${session.last_error ? `error=${session.last_error}` : `stop=${session.last_stop_reason}`}`)}`);
|
|
1101
1138
|
}
|
|
1102
1139
|
}
|
|
1103
1140
|
console.log();
|
|
@@ -1111,7 +1148,7 @@ var StreamPrinter = class {
|
|
|
1111
1148
|
}
|
|
1112
1149
|
writeDim(text) {
|
|
1113
1150
|
this.ensureBreak();
|
|
1114
|
-
process.stdout.write(
|
|
1151
|
+
process.stdout.write(chalk4.dim(text));
|
|
1115
1152
|
this.inlineOpen = !text.endsWith("\n");
|
|
1116
1153
|
}
|
|
1117
1154
|
writeLine(text) {
|
|
@@ -1129,7 +1166,7 @@ var StreamPrinter = class {
|
|
|
1129
1166
|
};
|
|
1130
1167
|
async function streamSessionEvents(computerID, sessionID, options = {}) {
|
|
1131
1168
|
const response = await openAgentSessionEventsStream(computerID, sessionID, {
|
|
1132
|
-
lastEventId: options.
|
|
1169
|
+
lastEventId: options.lastEventId?.trim() || void 0,
|
|
1133
1170
|
signal: options.signal
|
|
1134
1171
|
});
|
|
1135
1172
|
if (!response.body) {
|
|
@@ -1179,7 +1216,7 @@ function renderSSEChunk(chunk, printer, asJson) {
|
|
|
1179
1216
|
try {
|
|
1180
1217
|
envelope = JSON.parse(payload);
|
|
1181
1218
|
} catch {
|
|
1182
|
-
printer.writeLine(
|
|
1219
|
+
printer.writeLine(chalk4.dim(payload));
|
|
1183
1220
|
return;
|
|
1184
1221
|
}
|
|
1185
1222
|
const method = typeof envelope.method === "string" ? envelope.method : "";
|
|
@@ -1206,14 +1243,14 @@ function renderSSEChunk(chunk, printer, asJson) {
|
|
|
1206
1243
|
case "tool_call_update": {
|
|
1207
1244
|
const title = typeof update?.title === "string" && update.title ? update.title : "tool";
|
|
1208
1245
|
const status = typeof update?.status === "string" && update.status ? update.status : "in_progress";
|
|
1209
|
-
printer.writeLine(
|
|
1246
|
+
printer.writeLine(chalk4.dim(`[tool:${status}] ${title}`));
|
|
1210
1247
|
return;
|
|
1211
1248
|
}
|
|
1212
1249
|
case "plan": {
|
|
1213
1250
|
const entries = Array.isArray(update?.entries) ? update.entries : [];
|
|
1214
1251
|
const detail = entries.filter((entry) => typeof entry === "object" && entry !== null).map((entry) => `- [${entry.status ?? "pending"}] ${entry.content ?? ""}`).join("\n");
|
|
1215
1252
|
if (detail) {
|
|
1216
|
-
printer.writeLine(
|
|
1253
|
+
printer.writeLine(chalk4.dim(`Plan
|
|
1217
1254
|
${detail}`));
|
|
1218
1255
|
}
|
|
1219
1256
|
return;
|
|
@@ -1223,7 +1260,7 @@ ${detail}`));
|
|
|
1223
1260
|
}
|
|
1224
1261
|
}
|
|
1225
1262
|
if (method === "session/request_permission") {
|
|
1226
|
-
printer.writeLine(
|
|
1263
|
+
printer.writeLine(chalk4.yellow("Permission requested by remote agent"));
|
|
1227
1264
|
return;
|
|
1228
1265
|
}
|
|
1229
1266
|
}
|
|
@@ -1314,22 +1351,33 @@ agentCommand.command("prompt").description("Send a prompt to a machine agent ses
|
|
|
1314
1351
|
const computer = await resolveComputer(identifier);
|
|
1315
1352
|
const session = await resolvePromptSession(computer.id, options);
|
|
1316
1353
|
spinner?.stop();
|
|
1317
|
-
|
|
1354
|
+
const canStreamImmediately = !options.noStream && Boolean(session.backend_session_id?.trim());
|
|
1355
|
+
if (canStreamImmediately) {
|
|
1318
1356
|
streamPromise = streamSessionEvents(computer.id, session.id, {
|
|
1319
|
-
replay: false,
|
|
1320
1357
|
json: options.json,
|
|
1321
1358
|
signal: controller.signal
|
|
1322
1359
|
}).catch((error) => {
|
|
1323
1360
|
if (error instanceof Error && error.name === "AbortError") {
|
|
1324
1361
|
return;
|
|
1325
1362
|
}
|
|
1326
|
-
|
|
1363
|
+
return;
|
|
1327
1364
|
});
|
|
1328
1365
|
await new Promise((resolve) => setTimeout(resolve, 150));
|
|
1329
1366
|
}
|
|
1330
1367
|
const promptResponse = await promptAgentSession(computer.id, session.id, {
|
|
1331
1368
|
prompt: [{ type: "text", text }]
|
|
1332
1369
|
});
|
|
1370
|
+
if (!options.noStream && !canStreamImmediately) {
|
|
1371
|
+
streamPromise = streamSessionEvents(computer.id, session.id, {
|
|
1372
|
+
json: options.json,
|
|
1373
|
+
signal: controller.signal
|
|
1374
|
+
}).catch((error) => {
|
|
1375
|
+
if (error instanceof Error && error.name === "AbortError") {
|
|
1376
|
+
return;
|
|
1377
|
+
}
|
|
1378
|
+
return;
|
|
1379
|
+
});
|
|
1380
|
+
}
|
|
1333
1381
|
const settled = await waitForSessionToSettle(computer.id, session.id, promptResponse.prompt_id);
|
|
1334
1382
|
controller.abort();
|
|
1335
1383
|
if (streamPromise) {
|
|
@@ -1340,12 +1388,12 @@ agentCommand.command("prompt").description("Send a prompt to a machine agent ses
|
|
|
1340
1388
|
return;
|
|
1341
1389
|
}
|
|
1342
1390
|
console.log();
|
|
1343
|
-
console.log(` ${
|
|
1391
|
+
console.log(` ${chalk4.bold(session.name || "default")} ${formatAgentSessionStatus(settled.status)}`);
|
|
1344
1392
|
if (settled.last_stop_reason) {
|
|
1345
|
-
console.log(
|
|
1393
|
+
console.log(chalk4.dim(` stop_reason=${settled.last_stop_reason}`));
|
|
1346
1394
|
}
|
|
1347
1395
|
if (settled.last_error) {
|
|
1348
|
-
console.log(
|
|
1396
|
+
console.log(chalk4.red(` error=${settled.last_error}`));
|
|
1349
1397
|
}
|
|
1350
1398
|
console.log();
|
|
1351
1399
|
} catch (error) {
|
|
@@ -1358,11 +1406,14 @@ agentCommand.command("prompt").description("Send a prompt to a machine agent ses
|
|
|
1358
1406
|
process.exit(1);
|
|
1359
1407
|
}
|
|
1360
1408
|
});
|
|
1361
|
-
agentCommand.command("watch").description("Watch a machine agent session event stream").argument("<machine>", "Computer id or handle").requiredOption("--session <id>", "Session id").option("--
|
|
1409
|
+
agentCommand.command("watch").description("Watch a machine agent session event stream").argument("<machine>", "Computer id or handle").requiredOption("--session <id>", "Session id").option("--last-event-id <id>", "Replay buffered events after this event id").option("--json", "Print raw event envelopes").action(async (identifier, options) => {
|
|
1362
1410
|
try {
|
|
1363
1411
|
const computer = await resolveComputer(identifier);
|
|
1412
|
+
if (options.lastEventId && !/^[1-9]\d*$/.test(options.lastEventId.trim())) {
|
|
1413
|
+
throw new Error("--last-event-id must be a positive integer");
|
|
1414
|
+
}
|
|
1364
1415
|
await streamSessionEvents(computer.id, options.session.trim(), {
|
|
1365
|
-
|
|
1416
|
+
lastEventId: options.lastEventId,
|
|
1366
1417
|
json: options.json
|
|
1367
1418
|
});
|
|
1368
1419
|
} catch (error) {
|
|
@@ -1472,105 +1523,677 @@ fleetCommand.command("status").description("List open agent sessions across all
|
|
|
1472
1523
|
}
|
|
1473
1524
|
});
|
|
1474
1525
|
|
|
1475
|
-
// 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";
|
|
1476
1530
|
import { Command as Command4 } from "commander";
|
|
1477
|
-
import
|
|
1531
|
+
import chalk5 from "chalk";
|
|
1478
1532
|
import ora3 from "ora";
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
const
|
|
1488
|
-
|
|
1489
|
-
|
|
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;
|
|
1490
1547
|
console.log();
|
|
1491
|
-
console.log(
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
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
|
+
);
|
|
1509
1623
|
} else {
|
|
1510
|
-
|
|
1624
|
+
markTodo(todos, "cleanup", "skipped", "no helper created");
|
|
1511
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;
|
|
1512
1653
|
}
|
|
1654
|
+
item.state = state;
|
|
1655
|
+
item.detail = detail;
|
|
1656
|
+
}
|
|
1657
|
+
function printTodoList(items) {
|
|
1658
|
+
console.log();
|
|
1659
|
+
console.log(chalk5.dim("TODO"));
|
|
1513
1660
|
console.log();
|
|
1514
|
-
|
|
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
|
+
}
|
|
1515
1666
|
console.log();
|
|
1516
1667
|
}
|
|
1517
|
-
function
|
|
1518
|
-
if (
|
|
1519
|
-
|
|
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
|
+
};
|
|
1520
1678
|
}
|
|
1521
|
-
|
|
1522
|
-
|
|
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
|
+
}
|
|
1523
1714
|
}
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
const handleWidth = Math.max(6, ...computers.map((c) => c.handle.length));
|
|
1528
|
-
const statusWidth = 12;
|
|
1529
|
-
const createdWidth = 10;
|
|
1530
|
-
console.log();
|
|
1531
|
-
console.log(
|
|
1532
|
-
` ${chalk4.dim(padEnd("Handle", handleWidth + 2))}${chalk4.dim(padEnd("Status", statusWidth + 2))}${chalk4.dim(padEnd("Created", createdWidth + 2))}${chalk4.dim("URL")}`
|
|
1715
|
+
const computer = await promptForSSHComputer(
|
|
1716
|
+
computers,
|
|
1717
|
+
"Select a computer for Claude auth"
|
|
1533
1718
|
);
|
|
1534
|
-
|
|
1535
|
-
|
|
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()
|
|
1536
1731
|
);
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
);
|
|
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`);
|
|
1543
1737
|
}
|
|
1544
|
-
console.log();
|
|
1545
1738
|
}
|
|
1546
|
-
function
|
|
1547
|
-
|
|
1548
|
-
|
|
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);
|
|
1549
1785
|
console.log();
|
|
1786
|
+
try {
|
|
1787
|
+
await openBrowserURL(url);
|
|
1788
|
+
} catch {
|
|
1789
|
+
console.log(chalk5.yellow("Unable to open the browser automatically."));
|
|
1790
|
+
}
|
|
1550
1791
|
console.log(
|
|
1551
|
-
|
|
1552
|
-
);
|
|
1553
|
-
console.log(
|
|
1554
|
-
` ${chalk4.dim("-".repeat(handleWidth + 2))}${chalk4.dim("-".repeat(statusWidth + 2))}${chalk4.dim("-".repeat(20))}`
|
|
1792
|
+
"After completing authentication, copy the code shown on the success page."
|
|
1555
1793
|
);
|
|
1556
|
-
|
|
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
|
|
2078
|
+
function isInternalCondition(value) {
|
|
2079
|
+
return /^[A-Z][a-zA-Z]+$/.test(value);
|
|
2080
|
+
}
|
|
2081
|
+
function printComputer(computer) {
|
|
2082
|
+
const vnc = vncURL(computer);
|
|
2083
|
+
const terminal = terminalURL(computer);
|
|
2084
|
+
const ssh = computer.ssh_enabled ? formatSSHCommand2(computer.handle, computer.ssh_host, computer.ssh_port) : "disabled";
|
|
2085
|
+
const isCustom = computer.runtime_family === "custom-machine";
|
|
2086
|
+
console.log();
|
|
2087
|
+
console.log(` ${chalk6.bold.white(computer.handle)} ${formatStatus(computer.status)}`);
|
|
2088
|
+
console.log();
|
|
2089
|
+
console.log(` ${chalk6.dim("ID")} ${computer.id}`);
|
|
2090
|
+
console.log(` ${chalk6.dim("Tier")} ${computer.tier}`);
|
|
2091
|
+
if (isCustom) {
|
|
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
|
+
}
|
|
2101
|
+
}
|
|
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})` : ""}`);
|
|
2104
|
+
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)}`);
|
|
2109
|
+
if (computer.last_error) {
|
|
2110
|
+
console.log();
|
|
2111
|
+
if (isInternalCondition(computer.last_error)) {
|
|
2112
|
+
console.log(` ${chalk6.dim("Condition")} ${chalk6.dim(computer.last_error)}`);
|
|
2113
|
+
} else {
|
|
2114
|
+
console.log(` ${chalk6.dim("Error")} ${chalk6.red(computer.last_error)}`);
|
|
2115
|
+
}
|
|
2116
|
+
}
|
|
2117
|
+
console.log();
|
|
2118
|
+
console.log(` ${chalk6.dim("Created")} ${timeAgo(computer.created_at)}`);
|
|
2119
|
+
console.log();
|
|
2120
|
+
}
|
|
2121
|
+
function formatSSHCommand2(user, host, port) {
|
|
2122
|
+
if (!user.trim() || !host.trim()) {
|
|
2123
|
+
return "ssh unavailable";
|
|
2124
|
+
}
|
|
2125
|
+
if (port <= 0 || port === 22) {
|
|
2126
|
+
return `ssh ${user}@${host}`;
|
|
2127
|
+
}
|
|
2128
|
+
return `ssh -p ${port} ${user}@${host}`;
|
|
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
|
+
}
|
|
2149
|
+
function printComputerTable(computers) {
|
|
2150
|
+
const handleWidth = Math.max(6, ...computers.map((c) => c.handle.length));
|
|
2151
|
+
const statusWidth = 12;
|
|
2152
|
+
const createdWidth = 10;
|
|
2153
|
+
console.log();
|
|
2154
|
+
console.log(
|
|
2155
|
+
` ${chalk6.dim(padEnd("Handle", handleWidth + 2))}${chalk6.dim(padEnd("Status", statusWidth + 2))}${chalk6.dim(padEnd("Created", createdWidth + 2))}${chalk6.dim("URL")}`
|
|
2156
|
+
);
|
|
2157
|
+
console.log(
|
|
2158
|
+
` ${chalk6.dim("-".repeat(handleWidth + 2))}${chalk6.dim("-".repeat(statusWidth + 2))}${chalk6.dim("-".repeat(createdWidth + 2))}${chalk6.dim("-".repeat(20))}`
|
|
2159
|
+
);
|
|
2160
|
+
for (const computer of computers) {
|
|
2161
|
+
const status = formatStatus(computer.status);
|
|
2162
|
+
const created = chalk6.dim(timeAgo(computer.created_at));
|
|
2163
|
+
console.log(
|
|
2164
|
+
` ${chalk6.white(padEnd(computer.handle, handleWidth + 2))}${padEnd(status, statusWidth + 2)}${padEnd(created, createdWidth + 2)}${chalk6.cyan(webURL(computer))}`
|
|
2165
|
+
);
|
|
2166
|
+
}
|
|
2167
|
+
console.log();
|
|
2168
|
+
}
|
|
2169
|
+
function printComputerTableVerbose(computers) {
|
|
2170
|
+
const handleWidth = Math.max(6, ...computers.map((c) => c.handle.length));
|
|
2171
|
+
const statusWidth = 10;
|
|
2172
|
+
console.log();
|
|
2173
|
+
console.log(
|
|
2174
|
+
` ${chalk6.dim(padEnd("Handle", handleWidth + 2))}${chalk6.dim(padEnd("Status", statusWidth + 2))}${chalk6.dim("URLs")}`
|
|
2175
|
+
);
|
|
2176
|
+
console.log(
|
|
2177
|
+
` ${chalk6.dim("-".repeat(handleWidth + 2))}${chalk6.dim("-".repeat(statusWidth + 2))}${chalk6.dim("-".repeat(20))}`
|
|
2178
|
+
);
|
|
2179
|
+
for (const computer of computers) {
|
|
1557
2180
|
const status = formatStatus(computer.status);
|
|
1558
2181
|
const vnc = vncURL(computer);
|
|
1559
2182
|
const terminal = terminalURL(computer);
|
|
1560
2183
|
console.log(
|
|
1561
|
-
` ${
|
|
2184
|
+
` ${chalk6.white(padEnd(computer.handle, handleWidth + 2))}${padEnd(status, statusWidth + 2)}${chalk6.cyan(webURL(computer))}`
|
|
1562
2185
|
);
|
|
1563
2186
|
console.log(
|
|
1564
|
-
` ${padEnd("", handleWidth + 2)}${padEnd("", statusWidth + 2)}${
|
|
2187
|
+
` ${padEnd("", handleWidth + 2)}${padEnd("", statusWidth + 2)}${chalk6.dim(vnc ?? "VNC not available")}`
|
|
1565
2188
|
);
|
|
1566
2189
|
console.log(
|
|
1567
|
-
` ${padEnd("", handleWidth + 2)}${padEnd("", statusWidth + 2)}${
|
|
2190
|
+
` ${padEnd("", handleWidth + 2)}${padEnd("", statusWidth + 2)}${chalk6.dim(terminal ?? "Terminal not available")}`
|
|
1568
2191
|
);
|
|
1569
2192
|
}
|
|
1570
2193
|
console.log();
|
|
1571
2194
|
}
|
|
1572
|
-
var lsCommand = new
|
|
1573
|
-
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();
|
|
1574
2197
|
try {
|
|
1575
2198
|
const computers = await listComputers();
|
|
1576
2199
|
spinner?.stop();
|
|
@@ -1580,7 +2203,7 @@ var lsCommand = new Command4("ls").description("List computers").option("--json"
|
|
|
1580
2203
|
}
|
|
1581
2204
|
if (computers.length === 0) {
|
|
1582
2205
|
console.log();
|
|
1583
|
-
console.log(
|
|
2206
|
+
console.log(chalk6.dim(" No computers found."));
|
|
1584
2207
|
console.log();
|
|
1585
2208
|
return;
|
|
1586
2209
|
}
|
|
@@ -1600,8 +2223,8 @@ var lsCommand = new Command4("ls").description("List computers").option("--json"
|
|
|
1600
2223
|
process.exit(1);
|
|
1601
2224
|
}
|
|
1602
2225
|
});
|
|
1603
|
-
var getCommand = new
|
|
1604
|
-
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();
|
|
1605
2228
|
try {
|
|
1606
2229
|
const computer = await resolveComputer(identifier);
|
|
1607
2230
|
spinner?.stop();
|
|
@@ -1621,19 +2244,28 @@ var getCommand = new Command4("get").description("Show computer details").argume
|
|
|
1621
2244
|
process.exit(1);
|
|
1622
2245
|
}
|
|
1623
2246
|
});
|
|
1624
|
-
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) => {
|
|
1625
2248
|
let spinner;
|
|
1626
2249
|
let timer;
|
|
1627
2250
|
let startTime = 0;
|
|
1628
2251
|
try {
|
|
1629
2252
|
const selectedOptions = await resolveCreateOptions(options);
|
|
1630
2253
|
const runtimeFamily = effectiveRuntimeFamily(selectedOptions.runtimeFamily);
|
|
2254
|
+
const machineSourceSettings = runtimeFamily === "managed-worker" ? await getMachineSourceSettings().catch(() => null) : null;
|
|
1631
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
|
+
}
|
|
1632
2264
|
const provisioningNote = createProvisioningNote(runtimeFamily, filesystemSettings);
|
|
1633
2265
|
if (provisioningNote) {
|
|
1634
|
-
console.log(
|
|
2266
|
+
console.log(chalk6.dim(provisioningNote));
|
|
1635
2267
|
}
|
|
1636
|
-
spinner =
|
|
2268
|
+
spinner = ora4(createSpinnerText(runtimeFamily, filesystemSettings, 0)).start();
|
|
1637
2269
|
startTime = Date.now();
|
|
1638
2270
|
timer = setInterval(() => {
|
|
1639
2271
|
const elapsed2 = (Date.now() - startTime) / 1e3;
|
|
@@ -1649,6 +2281,7 @@ var createCommand = new Command4("create").description("Create a computer").argu
|
|
|
1649
2281
|
source_kind: parseSourceKindOption(selectedOptions.sourceKind),
|
|
1650
2282
|
image_family: selectedOptions.imageFamily,
|
|
1651
2283
|
image_ref: selectedOptions.imageRef,
|
|
2284
|
+
use_platform_default: selectedOptions.usePlatformDefault,
|
|
1652
2285
|
primary_port: parseOptionalPort(selectedOptions.primaryPort),
|
|
1653
2286
|
primary_path: selectedOptions.primaryPath,
|
|
1654
2287
|
healthcheck_type: parseHealthcheckTypeOption(selectedOptions.healthcheckType),
|
|
@@ -1669,7 +2302,7 @@ var createCommand = new Command4("create").description("Create a computer").argu
|
|
|
1669
2302
|
}
|
|
1670
2303
|
const elapsed = ((Date.now() - startTime) / 1e3).toFixed(1);
|
|
1671
2304
|
spinner.succeed(
|
|
1672
|
-
|
|
2305
|
+
chalk6.green(`Created ${chalk6.bold(computer.handle)} ${chalk6.dim(`[${elapsed}s]`)}`)
|
|
1673
2306
|
);
|
|
1674
2307
|
printComputer(computer);
|
|
1675
2308
|
} catch (error) {
|
|
@@ -1678,18 +2311,17 @@ var createCommand = new Command4("create").description("Create a computer").argu
|
|
|
1678
2311
|
}
|
|
1679
2312
|
const message = error instanceof Error ? error.message : "Failed to create computer";
|
|
1680
2313
|
if (spinner) {
|
|
1681
|
-
const suffix = startTime ? ` ${
|
|
2314
|
+
const suffix = startTime ? ` ${chalk6.dim(`[${((Date.now() - startTime) / 1e3).toFixed(1)}s]`)}` : "";
|
|
1682
2315
|
spinner.fail(`${message}${suffix}`);
|
|
1683
2316
|
} else {
|
|
1684
|
-
console.error(
|
|
2317
|
+
console.error(chalk6.red(message));
|
|
1685
2318
|
}
|
|
1686
2319
|
process.exit(1);
|
|
1687
2320
|
}
|
|
1688
2321
|
});
|
|
1689
2322
|
async function resolveCreateOptions(options) {
|
|
1690
2323
|
const selectedOptions = { ...options };
|
|
1691
|
-
const
|
|
1692
|
-
const shouldPrompt = Boolean(selectedOptions.interactive) || !hasExplicitRuntimeSelection;
|
|
2324
|
+
const shouldPrompt = Boolean(selectedOptions.interactive);
|
|
1693
2325
|
if (!process.stdin.isTTY || !process.stdout.isTTY || !shouldPrompt) {
|
|
1694
2326
|
validateCreateOptions(selectedOptions);
|
|
1695
2327
|
return selectedOptions;
|
|
@@ -1711,9 +2343,9 @@ async function resolveCreateOptions(options) {
|
|
|
1711
2343
|
if (runtimeChoice === "custom-machine") {
|
|
1712
2344
|
selectedOptions.runtimeFamily = "custom-machine";
|
|
1713
2345
|
selectedOptions.sourceKind = "oci-image";
|
|
1714
|
-
selectedOptions.imageRef = (await
|
|
1715
|
-
selectedOptions.primaryPort = (await
|
|
1716
|
-
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();
|
|
1717
2349
|
selectedOptions.healthcheckType = await select2({
|
|
1718
2350
|
message: "Healthcheck type",
|
|
1719
2351
|
choices: [
|
|
@@ -1722,7 +2354,7 @@ async function resolveCreateOptions(options) {
|
|
|
1722
2354
|
],
|
|
1723
2355
|
default: "tcp"
|
|
1724
2356
|
});
|
|
1725
|
-
selectedOptions.healthcheckValue = (await
|
|
2357
|
+
selectedOptions.healthcheckValue = (await textInput2({ message: "Healthcheck value (optional):" })).trim();
|
|
1726
2358
|
selectedOptions.sshEnabled = await confirm({
|
|
1727
2359
|
message: "Enable SSH?",
|
|
1728
2360
|
default: false
|
|
@@ -1736,28 +2368,28 @@ async function resolveCreateOptions(options) {
|
|
|
1736
2368
|
validateCreateOptions(selectedOptions);
|
|
1737
2369
|
return selectedOptions;
|
|
1738
2370
|
}
|
|
1739
|
-
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) => {
|
|
1740
2372
|
const globalYes = cmd.parent?.opts()?.yes;
|
|
1741
2373
|
const skipConfirm = Boolean(options.yes || globalYes);
|
|
1742
|
-
const spinner =
|
|
2374
|
+
const spinner = ora4("Resolving computer...").start();
|
|
1743
2375
|
try {
|
|
1744
2376
|
const computer = await resolveComputer(identifier);
|
|
1745
2377
|
spinner.stop();
|
|
1746
2378
|
if (!skipConfirm && process.stdin.isTTY) {
|
|
1747
2379
|
const confirmed = await confirm({
|
|
1748
|
-
message: `Delete computer ${
|
|
2380
|
+
message: `Delete computer ${chalk6.bold(computer.handle)}?`,
|
|
1749
2381
|
default: false
|
|
1750
2382
|
});
|
|
1751
2383
|
if (!confirmed) {
|
|
1752
|
-
console.log(
|
|
2384
|
+
console.log(chalk6.dim(" Cancelled."));
|
|
1753
2385
|
return;
|
|
1754
2386
|
}
|
|
1755
2387
|
}
|
|
1756
|
-
const deleteSpinner =
|
|
2388
|
+
const deleteSpinner = ora4("Deleting computer...").start();
|
|
1757
2389
|
await api(`/v1/computers/${computer.id}`, {
|
|
1758
2390
|
method: "DELETE"
|
|
1759
2391
|
});
|
|
1760
|
-
deleteSpinner.succeed(
|
|
2392
|
+
deleteSpinner.succeed(chalk6.green(`Deleted ${chalk6.bold(computer.handle)}`));
|
|
1761
2393
|
} catch (error) {
|
|
1762
2394
|
spinner.fail(
|
|
1763
2395
|
error instanceof Error ? error.message : "Failed to delete computer"
|
|
@@ -1797,17 +2429,33 @@ function createProvisioningNote(runtimeFamily, filesystemSettings) {
|
|
|
1797
2429
|
}
|
|
1798
2430
|
return "Using isolated storage for this desktop.";
|
|
1799
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
|
+
}
|
|
1800
2447
|
function createSpinnerText(runtimeFamily, filesystemSettings, elapsedSeconds) {
|
|
1801
|
-
const elapsedLabel =
|
|
2448
|
+
const elapsedLabel = chalk6.dim(`${elapsedSeconds.toFixed(1)}s`);
|
|
1802
2449
|
if (runtimeFamily === "managed-worker" && filesystemSettings?.shared_enabled && elapsedSeconds >= 5) {
|
|
1803
|
-
return `Creating computer... ${elapsedLabel} ${
|
|
2450
|
+
return `Creating computer... ${elapsedLabel} ${chalk6.dim("mounting shared home")}`;
|
|
1804
2451
|
}
|
|
1805
2452
|
return `Creating computer... ${elapsedLabel}`;
|
|
1806
2453
|
}
|
|
1807
2454
|
function validateCreateOptions(options) {
|
|
1808
2455
|
const runtimeFamily = parseRuntimeFamilyOption(options.runtimeFamily);
|
|
1809
2456
|
const sourceKind = parseSourceKindOption(options.sourceKind);
|
|
1810
|
-
|
|
2457
|
+
const imageRef = options.imageRef?.trim();
|
|
2458
|
+
if (runtimeFamily === "custom-machine" && !imageRef) {
|
|
1811
2459
|
throw new Error("--image-ref is required for --runtime-family custom-machine");
|
|
1812
2460
|
}
|
|
1813
2461
|
if (runtimeFamily === "custom-machine" && sourceKind === "none") {
|
|
@@ -1816,6 +2464,12 @@ function validateCreateOptions(options) {
|
|
|
1816
2464
|
if (runtimeFamily === "custom-machine" && options.vncEnabled) {
|
|
1817
2465
|
throw new Error("custom-machine does not support platform VNC");
|
|
1818
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
|
+
}
|
|
1819
2473
|
}
|
|
1820
2474
|
function parseSourceKindOption(value) {
|
|
1821
2475
|
switch (value) {
|
|
@@ -1861,7 +2515,7 @@ function resolveOptionalToggle(enabled, disabled, label) {
|
|
|
1861
2515
|
}
|
|
1862
2516
|
|
|
1863
2517
|
// src/commands/completion.ts
|
|
1864
|
-
import { Command as
|
|
2518
|
+
import { Command as Command6 } from "commander";
|
|
1865
2519
|
var ZSH_SCRIPT = `#compdef computer agentcomputer aicomputer
|
|
1866
2520
|
|
|
1867
2521
|
_computer() {
|
|
@@ -1870,12 +2524,18 @@ _computer() {
|
|
|
1870
2524
|
'login:Authenticate the CLI'
|
|
1871
2525
|
'logout:Remove stored API key'
|
|
1872
2526
|
'whoami:Show current user'
|
|
2527
|
+
'claude-auth:Authenticate Claude Code on a computer'
|
|
2528
|
+
'claude-login:Alias for claude-auth'
|
|
1873
2529
|
'create:Create a computer'
|
|
1874
2530
|
'ls:List computers'
|
|
1875
2531
|
'get:Show computer details'
|
|
2532
|
+
'image:Manage machine image sources'
|
|
1876
2533
|
'open:Open in browser'
|
|
1877
2534
|
'ssh:SSH into a computer'
|
|
1878
2535
|
'ports:Manage published ports'
|
|
2536
|
+
'agent:Manage cloud agent sessions'
|
|
2537
|
+
'fleet:View agent activity across your fleet'
|
|
2538
|
+
'acp:Run a local ACP bridge for remote agent sessions'
|
|
1879
2539
|
'rm:Delete a computer'
|
|
1880
2540
|
'completion:Generate shell completions'
|
|
1881
2541
|
'help:Display help'
|
|
@@ -1888,6 +2548,15 @@ _computer() {
|
|
|
1888
2548
|
'rm:Unpublish an app port'
|
|
1889
2549
|
)
|
|
1890
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
|
+
|
|
1891
2560
|
_arguments -C \\
|
|
1892
2561
|
'(-h --help)'{-h,--help}'[Display help]' \\
|
|
1893
2562
|
'(-V --version)'{-V,--version}'[Show version]' \\
|
|
@@ -1909,6 +2578,12 @@ _computer() {
|
|
|
1909
2578
|
whoami)
|
|
1910
2579
|
_arguments '--json[Print raw JSON]'
|
|
1911
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
|
+
;;
|
|
1912
2587
|
create)
|
|
1913
2588
|
_arguments \\
|
|
1914
2589
|
'--name[Display name]:name:' \\
|
|
@@ -1918,6 +2593,7 @@ _computer() {
|
|
|
1918
2593
|
'--source-kind[Source kind]:kind:(none oci-image)' \\
|
|
1919
2594
|
'--image-family[Image family]:family:' \\
|
|
1920
2595
|
'--image-ref[Image reference]:image:' \\
|
|
2596
|
+
'--use-platform-default[Use platform default image]' \\
|
|
1921
2597
|
'--primary-port[Primary app port]:port:' \\
|
|
1922
2598
|
'--primary-path[Primary app path]:path:' \\
|
|
1923
2599
|
'--healthcheck-type[Healthcheck type]:type:(http tcp)' \\
|
|
@@ -1938,6 +2614,50 @@ _computer() {
|
|
|
1938
2614
|
'--json[Print raw JSON]' \\
|
|
1939
2615
|
'1:computer:_computer_handles'
|
|
1940
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
|
+
;;
|
|
1941
2661
|
open)
|
|
1942
2662
|
_arguments \\
|
|
1943
2663
|
'--vnc[Open VNC desktop]' \\
|
|
@@ -1997,13 +2717,22 @@ _computer_handles() {
|
|
|
1997
2717
|
fi
|
|
1998
2718
|
}
|
|
1999
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
|
+
|
|
2000
2728
|
_computer "$@"`;
|
|
2001
2729
|
var BASH_SCRIPT = `_computer() {
|
|
2002
2730
|
local cur prev words cword
|
|
2003
2731
|
_init_completion || return
|
|
2004
2732
|
|
|
2005
|
-
local commands="login logout whoami create ls get open ssh ports 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"
|
|
2006
2734
|
local ports_commands="ls publish rm"
|
|
2735
|
+
local image_commands="ls save default rebuild rm"
|
|
2007
2736
|
|
|
2008
2737
|
if [[ $cword -eq 1 ]]; then
|
|
2009
2738
|
COMPREPLY=($(compgen -W "$commands" -- "$cur"))
|
|
@@ -2019,12 +2748,60 @@ var BASH_SCRIPT = `_computer() {
|
|
|
2019
2748
|
whoami)
|
|
2020
2749
|
COMPREPLY=($(compgen -W "--json" -- "$cur"))
|
|
2021
2750
|
;;
|
|
2751
|
+
claude-auth|claude-login)
|
|
2752
|
+
COMPREPLY=($(compgen -W "--machine --keep-helper --skip-cross-check" -- "$cur"))
|
|
2753
|
+
;;
|
|
2022
2754
|
create)
|
|
2023
|
-
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"))
|
|
2024
2756
|
;;
|
|
2025
2757
|
ls)
|
|
2026
2758
|
COMPREPLY=($(compgen -W "--json --verbose -v" -- "$cur"))
|
|
2027
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
|
+
;;
|
|
2028
2805
|
get|open|ssh|rm)
|
|
2029
2806
|
if [[ $cword -eq 2 ]]; then
|
|
2030
2807
|
local handles cli="\${words[0]:-computer}"
|
|
@@ -2056,7 +2833,7 @@ var BASH_SCRIPT = `_computer() {
|
|
|
2056
2833
|
}
|
|
2057
2834
|
|
|
2058
2835
|
complete -F _computer computer agentcomputer aicomputer`;
|
|
2059
|
-
var completionCommand = new
|
|
2836
|
+
var completionCommand = new Command6("completion").description("Generate shell completions").argument("<shell>", "Shell type (bash or zsh)").action((shell) => {
|
|
2060
2837
|
switch (shell) {
|
|
2061
2838
|
case "zsh":
|
|
2062
2839
|
console.log(ZSH_SCRIPT);
|
|
@@ -2070,19 +2847,299 @@ var completionCommand = new Command5("completion").description("Generate shell c
|
|
|
2070
2847
|
}
|
|
2071
2848
|
});
|
|
2072
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
|
+
|
|
2073
3130
|
// src/commands/login.ts
|
|
2074
|
-
import { Command as
|
|
2075
|
-
import
|
|
2076
|
-
import
|
|
3131
|
+
import { Command as Command8 } from "commander";
|
|
3132
|
+
import chalk8 from "chalk";
|
|
3133
|
+
import ora6 from "ora";
|
|
2077
3134
|
|
|
2078
3135
|
// src/lib/browser-login.ts
|
|
2079
|
-
import { randomBytes } from "crypto";
|
|
3136
|
+
import { randomBytes as randomBytes2 } from "crypto";
|
|
2080
3137
|
import { createServer } from "http";
|
|
2081
3138
|
var CALLBACK_HOST = "127.0.0.1";
|
|
2082
3139
|
var CALLBACK_PATH = "/callback";
|
|
2083
3140
|
var LOGIN_TIMEOUT_MS = 5 * 60 * 1e3;
|
|
2084
3141
|
async function createBrowserLoginAttempt() {
|
|
2085
|
-
const state =
|
|
3142
|
+
const state = randomBytes2(16).toString("hex");
|
|
2086
3143
|
const deferred = createDeferred();
|
|
2087
3144
|
let callbackURL = "";
|
|
2088
3145
|
let closed = false;
|
|
@@ -2301,11 +3358,11 @@ function escapeHTML(value) {
|
|
|
2301
3358
|
}
|
|
2302
3359
|
|
|
2303
3360
|
// src/commands/login.ts
|
|
2304
|
-
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) => {
|
|
2305
3362
|
const existingKey = getStoredAPIKey();
|
|
2306
3363
|
if (existingKey && !options.force) {
|
|
2307
3364
|
console.log();
|
|
2308
|
-
console.log(
|
|
3365
|
+
console.log(chalk8.yellow(" Already logged in. Use --force to overwrite."));
|
|
2309
3366
|
console.log();
|
|
2310
3367
|
return;
|
|
2311
3368
|
}
|
|
@@ -2313,8 +3370,8 @@ var loginCommand = new Command6("login").description("Authenticate the CLI").opt
|
|
|
2313
3370
|
const apiKey = await resolveAPIKeyInput(options.apiKey, options.stdin);
|
|
2314
3371
|
if (!apiKey && wantsManualLogin) {
|
|
2315
3372
|
console.log();
|
|
2316
|
-
console.log(
|
|
2317
|
-
console.log(
|
|
3373
|
+
console.log(chalk8.dim(" Usage: computer login --api-key <ac_live_...>"));
|
|
3374
|
+
console.log(chalk8.dim(` API: ${getBaseURL()}`));
|
|
2318
3375
|
console.log();
|
|
2319
3376
|
process.exit(1);
|
|
2320
3377
|
}
|
|
@@ -2324,15 +3381,15 @@ var loginCommand = new Command6("login").description("Authenticate the CLI").opt
|
|
|
2324
3381
|
}
|
|
2325
3382
|
if (!apiKey.startsWith("ac_live_")) {
|
|
2326
3383
|
console.log();
|
|
2327
|
-
console.log(
|
|
3384
|
+
console.log(chalk8.red(" API key must start with ac_live_"));
|
|
2328
3385
|
console.log();
|
|
2329
3386
|
process.exit(1);
|
|
2330
3387
|
}
|
|
2331
|
-
const spinner =
|
|
3388
|
+
const spinner = ora6("Authenticating...").start();
|
|
2332
3389
|
try {
|
|
2333
3390
|
const me = await apiWithKey(apiKey, "/v1/me");
|
|
2334
3391
|
setAPIKey(apiKey);
|
|
2335
|
-
spinner.succeed(`Logged in as ${
|
|
3392
|
+
spinner.succeed(`Logged in as ${chalk8.bold(me.user.email)}`);
|
|
2336
3393
|
} catch (error) {
|
|
2337
3394
|
spinner.fail(
|
|
2338
3395
|
error instanceof Error ? error.message : "Failed to validate API key"
|
|
@@ -2341,7 +3398,7 @@ var loginCommand = new Command6("login").description("Authenticate the CLI").opt
|
|
|
2341
3398
|
}
|
|
2342
3399
|
});
|
|
2343
3400
|
async function runBrowserLogin() {
|
|
2344
|
-
const spinner =
|
|
3401
|
+
const spinner = ora6("Starting browser login...").start();
|
|
2345
3402
|
let attempt = null;
|
|
2346
3403
|
try {
|
|
2347
3404
|
attempt = await createBrowserLoginAttempt();
|
|
@@ -2351,14 +3408,14 @@ async function runBrowserLogin() {
|
|
|
2351
3408
|
} catch {
|
|
2352
3409
|
spinner.stop();
|
|
2353
3410
|
console.log();
|
|
2354
|
-
console.log(
|
|
2355
|
-
console.log(
|
|
3411
|
+
console.log(chalk8.yellow(" Browser auto-open failed. Open this URL to continue:"));
|
|
3412
|
+
console.log(chalk8.dim(` ${attempt.loginURL}`));
|
|
2356
3413
|
console.log();
|
|
2357
3414
|
spinner.start("Waiting for browser login...");
|
|
2358
3415
|
}
|
|
2359
3416
|
spinner.text = "Waiting for browser login...";
|
|
2360
3417
|
const result = await attempt.waitForResult();
|
|
2361
|
-
spinner.succeed(`Logged in as ${
|
|
3418
|
+
spinner.succeed(`Logged in as ${chalk8.bold(result.me.user.email)}`);
|
|
2362
3419
|
} catch (error) {
|
|
2363
3420
|
spinner.fail(error instanceof Error ? error.message : "Browser login failed");
|
|
2364
3421
|
process.exit(1);
|
|
@@ -2384,33 +3441,33 @@ async function resolveAPIKeyInput(flagValue, readFromStdin) {
|
|
|
2384
3441
|
}
|
|
2385
3442
|
|
|
2386
3443
|
// src/commands/logout.ts
|
|
2387
|
-
import { Command as
|
|
2388
|
-
import
|
|
2389
|
-
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(() => {
|
|
2390
3447
|
if (!getStoredAPIKey()) {
|
|
2391
3448
|
console.log();
|
|
2392
|
-
console.log(
|
|
3449
|
+
console.log(chalk9.dim(" Not logged in."));
|
|
2393
3450
|
if (hasEnvAPIKey()) {
|
|
2394
|
-
console.log(
|
|
3451
|
+
console.log(chalk9.dim(" Environment API key is still active in this shell."));
|
|
2395
3452
|
}
|
|
2396
3453
|
console.log();
|
|
2397
3454
|
return;
|
|
2398
3455
|
}
|
|
2399
3456
|
clearAPIKey();
|
|
2400
3457
|
console.log();
|
|
2401
|
-
console.log(
|
|
3458
|
+
console.log(chalk9.green(" Logged out."));
|
|
2402
3459
|
if (hasEnvAPIKey()) {
|
|
2403
|
-
console.log(
|
|
3460
|
+
console.log(chalk9.dim(" Environment API key is still active in this shell."));
|
|
2404
3461
|
}
|
|
2405
3462
|
console.log();
|
|
2406
3463
|
});
|
|
2407
3464
|
|
|
2408
3465
|
// src/commands/whoami.ts
|
|
2409
|
-
import { Command as
|
|
2410
|
-
import
|
|
2411
|
-
import
|
|
2412
|
-
var whoamiCommand = new
|
|
2413
|
-
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();
|
|
2414
3471
|
try {
|
|
2415
3472
|
const me = await api("/v1/me");
|
|
2416
3473
|
spinner?.stop();
|
|
@@ -2419,14 +3476,14 @@ var whoamiCommand = new Command8("whoami").description("Show current user").opti
|
|
|
2419
3476
|
return;
|
|
2420
3477
|
}
|
|
2421
3478
|
console.log();
|
|
2422
|
-
console.log(` ${
|
|
3479
|
+
console.log(` ${chalk10.bold.white(me.user.display_name || me.user.email)}`);
|
|
2423
3480
|
if (me.user.display_name) {
|
|
2424
|
-
console.log(` ${
|
|
3481
|
+
console.log(` ${chalk10.dim(me.user.email)}`);
|
|
2425
3482
|
}
|
|
2426
3483
|
if (me.api_key.name) {
|
|
2427
|
-
console.log(` ${
|
|
3484
|
+
console.log(` ${chalk10.dim("Key:")} ${me.api_key.name}`);
|
|
2428
3485
|
}
|
|
2429
|
-
console.log(` ${
|
|
3486
|
+
console.log(` ${chalk10.dim("API:")} ${chalk10.dim(getBaseURL())}`);
|
|
2430
3487
|
console.log();
|
|
2431
3488
|
} catch (error) {
|
|
2432
3489
|
if (spinner) {
|
|
@@ -2443,67 +3500,140 @@ var pkg2 = JSON.parse(
|
|
|
2443
3500
|
readFileSync3(new URL("../package.json", import.meta.url), "utf8")
|
|
2444
3501
|
);
|
|
2445
3502
|
var cliName = process.argv[1] ? basename2(process.argv[1]) : "agentcomputer";
|
|
2446
|
-
var program = new
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
|
|
3503
|
+
var program = new Command11();
|
|
3504
|
+
function appendTextSection(lines, title, values) {
|
|
3505
|
+
if (values.length === 0) {
|
|
3506
|
+
return;
|
|
3507
|
+
}
|
|
3508
|
+
lines.push(` ${chalk11.dim(title)}`);
|
|
3509
|
+
lines.push("");
|
|
3510
|
+
for (const value of values) {
|
|
3511
|
+
lines.push(` ${chalk11.white(value)}`);
|
|
3512
|
+
}
|
|
3513
|
+
lines.push("");
|
|
3514
|
+
}
|
|
3515
|
+
function appendTableSection(lines, title, entries) {
|
|
3516
|
+
if (entries.length === 0) {
|
|
3517
|
+
return;
|
|
3518
|
+
}
|
|
3519
|
+
const width = Math.max(...entries.map((entry) => entry.term.length), 0) + 2;
|
|
3520
|
+
lines.push(` ${chalk11.dim(title)}`);
|
|
3521
|
+
lines.push("");
|
|
3522
|
+
for (const entry of entries) {
|
|
3523
|
+
lines.push(` ${chalk11.white(padEnd(entry.term, width))}${chalk11.dim(entry.desc)}`);
|
|
3524
|
+
}
|
|
3525
|
+
lines.push("");
|
|
3526
|
+
}
|
|
3527
|
+
function commandPath(cmd) {
|
|
3528
|
+
const parts = [];
|
|
3529
|
+
let current = cmd;
|
|
3530
|
+
while (current) {
|
|
3531
|
+
parts.unshift(current.name());
|
|
3532
|
+
current = current.parent ?? null;
|
|
3533
|
+
}
|
|
3534
|
+
return parts.join(" ");
|
|
3535
|
+
}
|
|
3536
|
+
function formatRootHelp(cmd) {
|
|
3537
|
+
const version = pkg2.version ?? "0.0.0";
|
|
3538
|
+
const lines = [];
|
|
3539
|
+
const groups = [
|
|
3540
|
+
["Auth", []],
|
|
3541
|
+
["Computers", []],
|
|
3542
|
+
["Images", []],
|
|
3543
|
+
["Access", []],
|
|
3544
|
+
["Agents", []],
|
|
3545
|
+
["Other", []]
|
|
3546
|
+
];
|
|
3547
|
+
const otherGroup = groups.find(([name]) => name === "Other")[1];
|
|
3548
|
+
lines.push(`${chalk11.bold(cliName)} ${chalk11.dim(`v${version}`)}`);
|
|
3549
|
+
lines.push("");
|
|
3550
|
+
if (cmd.description()) {
|
|
3551
|
+
lines.push(` ${chalk11.dim(cmd.description())}`);
|
|
2452
3552
|
lines.push("");
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
-
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
groups.Agents.push(entry);
|
|
2471
|
-
} else if (["open", "ssh", "ports"].includes(name)) {
|
|
2472
|
-
groups.Access.push(entry);
|
|
2473
|
-
} else {
|
|
2474
|
-
groups.Other.push(entry);
|
|
2475
|
-
}
|
|
2476
|
-
}
|
|
2477
|
-
for (const [groupName, entries] of Object.entries(groups)) {
|
|
2478
|
-
if (entries.length === 0) continue;
|
|
2479
|
-
lines.push(` ${chalk8.dim(groupName)}`);
|
|
2480
|
-
for (const entry of entries) {
|
|
2481
|
-
const padded = entry.name.padEnd(14);
|
|
2482
|
-
lines.push(` ${chalk8.white(padded)}${chalk8.dim(entry.desc)}`);
|
|
2483
|
-
}
|
|
2484
|
-
lines.push("");
|
|
2485
|
-
}
|
|
2486
|
-
}
|
|
2487
|
-
const globalOpts = [
|
|
2488
|
-
{ flags: "-y, --yes", desc: "Skip confirmation prompts" },
|
|
2489
|
-
{ flags: "-V, --version", desc: "Show version" },
|
|
2490
|
-
{ flags: "-h, --help", desc: "Show help" }
|
|
2491
|
-
];
|
|
2492
|
-
lines.push(` ${chalk8.dim("Options")}`);
|
|
2493
|
-
for (const opt of globalOpts) {
|
|
2494
|
-
const padded = opt.flags.padEnd(14);
|
|
2495
|
-
lines.push(` ${chalk8.white(padded)}${chalk8.dim(opt.desc)}`);
|
|
3553
|
+
}
|
|
3554
|
+
appendTextSection(lines, "Usage", [`${cliName} <command> [options]`]);
|
|
3555
|
+
for (const sub of cmd.commands) {
|
|
3556
|
+
const name = sub.name();
|
|
3557
|
+
const entry = { term: name, desc: sub.description() };
|
|
3558
|
+
if (["login", "logout", "whoami", "claude-auth"].includes(name)) {
|
|
3559
|
+
groups[0][1].push(entry);
|
|
3560
|
+
} else if (["create", "ls", "get", "rm"].includes(name)) {
|
|
3561
|
+
groups[1][1].push(entry);
|
|
3562
|
+
} else if (name === "image") {
|
|
3563
|
+
groups[2][1].push(entry);
|
|
3564
|
+
} else if (["open", "ssh", "ports"].includes(name)) {
|
|
3565
|
+
groups[3][1].push(entry);
|
|
3566
|
+
} else if (["agent", "fleet", "acp"].includes(name)) {
|
|
3567
|
+
groups[4][1].push(entry);
|
|
3568
|
+
} else {
|
|
3569
|
+
otherGroup.push(entry);
|
|
2496
3570
|
}
|
|
3571
|
+
}
|
|
3572
|
+
for (const [groupName, entries] of groups) {
|
|
3573
|
+
appendTableSection(
|
|
3574
|
+
lines,
|
|
3575
|
+
groupName,
|
|
3576
|
+
entries
|
|
3577
|
+
);
|
|
3578
|
+
}
|
|
3579
|
+
appendTableSection(lines, "Options", [
|
|
3580
|
+
{ term: "-y, --yes", desc: "Skip confirmation prompts" },
|
|
3581
|
+
{ term: "-V, --version", desc: "Show version" },
|
|
3582
|
+
{ term: "-h, --help", desc: "Show help" }
|
|
3583
|
+
]);
|
|
3584
|
+
return `${lines.join("\n").trimEnd()}
|
|
3585
|
+
`;
|
|
3586
|
+
}
|
|
3587
|
+
function formatSubcommandHelp(cmd, helper) {
|
|
3588
|
+
const lines = [];
|
|
3589
|
+
const description = helper.commandDescription(cmd);
|
|
3590
|
+
const argumentsList = helper.visibleArguments(cmd).map((argument) => ({
|
|
3591
|
+
term: helper.argumentTerm(argument),
|
|
3592
|
+
desc: helper.argumentDescription(argument)
|
|
3593
|
+
}));
|
|
3594
|
+
const commandList = helper.visibleCommands(cmd).map((subcommand) => ({
|
|
3595
|
+
term: helper.subcommandTerm(subcommand),
|
|
3596
|
+
desc: helper.subcommandDescription(subcommand)
|
|
3597
|
+
}));
|
|
3598
|
+
const optionList = helper.visibleOptions(cmd).map((option) => ({
|
|
3599
|
+
term: helper.optionTerm(option),
|
|
3600
|
+
desc: helper.optionDescription(option)
|
|
3601
|
+
}));
|
|
3602
|
+
lines.push(chalk11.bold(commandPath(cmd)));
|
|
3603
|
+
lines.push("");
|
|
3604
|
+
if (description) {
|
|
3605
|
+
lines.push(` ${chalk11.dim(description)}`);
|
|
2497
3606
|
lines.push("");
|
|
2498
|
-
return lines.join("\n");
|
|
2499
3607
|
}
|
|
2500
|
-
|
|
3608
|
+
appendTextSection(lines, "Usage", [helper.commandUsage(cmd)]);
|
|
3609
|
+
appendTableSection(lines, "Arguments", argumentsList);
|
|
3610
|
+
appendTableSection(lines, "Commands", commandList);
|
|
3611
|
+
appendTableSection(lines, "Options", optionList);
|
|
3612
|
+
return `${lines.join("\n").trimEnd()}
|
|
3613
|
+
`;
|
|
3614
|
+
}
|
|
3615
|
+
function applyHelpFormatting(cmd) {
|
|
3616
|
+
cmd.configureHelp({
|
|
3617
|
+
formatHelp(current, helper) {
|
|
3618
|
+
if (!current.parent) {
|
|
3619
|
+
return formatRootHelp(current);
|
|
3620
|
+
}
|
|
3621
|
+
return formatSubcommandHelp(current, helper);
|
|
3622
|
+
}
|
|
3623
|
+
});
|
|
3624
|
+
for (const subcommand of cmd.commands) {
|
|
3625
|
+
applyHelpFormatting(subcommand);
|
|
3626
|
+
}
|
|
3627
|
+
}
|
|
3628
|
+
program.name(cliName).description("Agent Computer CLI").version(pkg2.version ?? "0.0.0").option("-y, --yes", "Skip confirmation prompts");
|
|
2501
3629
|
program.addCommand(loginCommand);
|
|
2502
3630
|
program.addCommand(logoutCommand);
|
|
2503
3631
|
program.addCommand(whoamiCommand);
|
|
3632
|
+
program.addCommand(claudeAuthCommand);
|
|
2504
3633
|
program.addCommand(createCommand);
|
|
2505
3634
|
program.addCommand(lsCommand);
|
|
2506
3635
|
program.addCommand(getCommand);
|
|
3636
|
+
program.addCommand(imageCommand);
|
|
2507
3637
|
program.addCommand(agentCommand);
|
|
2508
3638
|
program.addCommand(fleetCommand);
|
|
2509
3639
|
program.addCommand(acpCommand);
|
|
@@ -2512,4 +3642,5 @@ program.addCommand(sshCommand);
|
|
|
2512
3642
|
program.addCommand(portsCommand);
|
|
2513
3643
|
program.addCommand(removeCommand);
|
|
2514
3644
|
program.addCommand(completionCommand);
|
|
3645
|
+
applyHelpFormatting(program);
|
|
2515
3646
|
program.parse();
|