aicomputer 0.1.12 → 0.1.14
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 +12 -4
- package/dist/index.js +525 -171
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -8,6 +8,12 @@ Agent Computer CLI for creating, opening, and managing computers from the termin
|
|
|
8
8
|
npm install -g aicomputer
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
+
Upgrade the installed CLI later with:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
computer upgrade
|
|
15
|
+
```
|
|
16
|
+
|
|
11
17
|
Or run it directly with Nix:
|
|
12
18
|
|
|
13
19
|
```bash
|
|
@@ -78,6 +84,7 @@ After installing, use the `computer` command:
|
|
|
78
84
|
|
|
79
85
|
```bash
|
|
80
86
|
computer login
|
|
87
|
+
computer upgrade
|
|
81
88
|
computer login --api-key <ac_live_...>
|
|
82
89
|
computer claude-login
|
|
83
90
|
computer codex-login
|
|
@@ -90,14 +97,15 @@ computer ssh my-box
|
|
|
90
97
|
computer ssh --setup
|
|
91
98
|
computer agent agents my-box
|
|
92
99
|
computer agent sessions list my-box
|
|
93
|
-
computer agent prompt my-box "inspect /workspace" --agent codex
|
|
94
|
-
computer fleet status
|
|
100
|
+
computer agent prompt my-box "inspect /home/node/workspace-my-box" --agent codex
|
|
95
101
|
computer acp serve my-box --agent codex
|
|
96
102
|
```
|
|
97
103
|
|
|
98
104
|
`computer login` authenticates the CLI against Agent Computer. Use
|
|
99
105
|
`computer claude-login` and `computer codex-login` to install Claude Code or
|
|
100
|
-
Codex credentials onto a machine after the CLI is already logged in.
|
|
106
|
+
Codex credentials onto a machine after the CLI is already logged in. Use
|
|
107
|
+
`computer upgrade` to update a global npm install or the matching Nix profile
|
|
108
|
+
entry.
|
|
101
109
|
|
|
102
110
|
Run `computer ssh` without a handle in an interactive terminal to pick from your available machines.
|
|
103
111
|
|
|
@@ -108,6 +116,6 @@ ssh agentcomputer.ai
|
|
|
108
116
|
ssh my-box@agentcomputer.ai
|
|
109
117
|
```
|
|
110
118
|
|
|
111
|
-
Use `computer agent` to inspect agents on one machine and manage remote sessions. Use `computer
|
|
119
|
+
Use `computer agent` to inspect agents on one machine and manage remote sessions. Use `computer acp serve` when you want to expose one remote session through a local ACP bridge.
|
|
112
120
|
|
|
113
121
|
You can also run without a global install via `npx aicomputer <command>`.
|
package/dist/index.js
CHANGED
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
|
-
import { Command as
|
|
5
|
-
import
|
|
6
|
-
import { readFileSync as
|
|
4
|
+
import { Command as Command13 } from "commander";
|
|
5
|
+
import chalk13 from "chalk";
|
|
6
|
+
import { readFileSync as readFileSync4 } from "fs";
|
|
7
7
|
import { basename as basename2 } from "path";
|
|
8
8
|
|
|
9
9
|
// src/commands/access.ts
|
|
10
|
-
import { spawn } from "child_process";
|
|
11
10
|
import { Command } from "commander";
|
|
12
11
|
import chalk3 from "chalk";
|
|
13
12
|
import ora from "ora";
|
|
@@ -447,6 +446,59 @@ async function generateSSHKey() {
|
|
|
447
446
|
return { publicKeyPath, privateKeyPath };
|
|
448
447
|
}
|
|
449
448
|
|
|
449
|
+
// src/lib/ssh-access.ts
|
|
450
|
+
import { spawn } from "child_process";
|
|
451
|
+
async function prepareSSHConnection(computer) {
|
|
452
|
+
const registered = await ensureDefaultSSHKeyRegistered();
|
|
453
|
+
const info = await getConnectionInfo(computer.id);
|
|
454
|
+
if (!info.connection.ssh_available) {
|
|
455
|
+
throw new Error("SSH is not available for this computer");
|
|
456
|
+
}
|
|
457
|
+
return {
|
|
458
|
+
computer,
|
|
459
|
+
command: formatSSHCommand(
|
|
460
|
+
info.connection.ssh_user,
|
|
461
|
+
info.connection.ssh_host,
|
|
462
|
+
info.connection.ssh_port
|
|
463
|
+
),
|
|
464
|
+
args: [
|
|
465
|
+
"-i",
|
|
466
|
+
registered.privateKeyPath,
|
|
467
|
+
"-p",
|
|
468
|
+
String(info.connection.ssh_port),
|
|
469
|
+
`${info.connection.ssh_user}@${info.connection.ssh_host}`
|
|
470
|
+
]
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
async function prepareSSHConnectionByIdentifier(identifier) {
|
|
474
|
+
const computer = await resolveComputer(identifier);
|
|
475
|
+
return prepareSSHConnection(computer);
|
|
476
|
+
}
|
|
477
|
+
async function openSSHConnection(connection) {
|
|
478
|
+
await new Promise((resolve, reject) => {
|
|
479
|
+
const child = spawn("ssh", connection.args, {
|
|
480
|
+
stdio: "inherit"
|
|
481
|
+
});
|
|
482
|
+
child.on("error", reject);
|
|
483
|
+
child.on("exit", (code) => {
|
|
484
|
+
if (code === 0) {
|
|
485
|
+
resolve();
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
488
|
+
reject(new Error(`ssh exited with code ${code ?? 1}`));
|
|
489
|
+
});
|
|
490
|
+
});
|
|
491
|
+
}
|
|
492
|
+
function formatSSHCommand(user, host, port) {
|
|
493
|
+
if (!user.trim() || !host.trim()) {
|
|
494
|
+
return "ssh unavailable";
|
|
495
|
+
}
|
|
496
|
+
if (port <= 0 || port === 22) {
|
|
497
|
+
return `ssh ${user}@${host}`;
|
|
498
|
+
}
|
|
499
|
+
return `ssh -p ${port} ${user}@${host}`;
|
|
500
|
+
}
|
|
501
|
+
|
|
450
502
|
// src/lib/open-browser.ts
|
|
451
503
|
import { constants } from "fs";
|
|
452
504
|
import { access } from "fs/promises";
|
|
@@ -528,41 +580,39 @@ var openCommand = new Command("open").description("Open a computer in your brows
|
|
|
528
580
|
await openBrowserURL(access2.access_url);
|
|
529
581
|
console.log(chalk3.dim(` ${access2.access_url}`));
|
|
530
582
|
} catch (error) {
|
|
531
|
-
spinner.fail(
|
|
583
|
+
spinner.fail(
|
|
584
|
+
error instanceof Error ? error.message : "Failed to open computer"
|
|
585
|
+
);
|
|
532
586
|
process.exit(1);
|
|
533
587
|
}
|
|
534
588
|
});
|
|
535
|
-
var sshCommand = new Command("ssh").description("Open an SSH session to a computer").argument("[id-or-handle]", "Computer id or handle").option("--setup", "Register key and configure a global SSH alias").option("--alias <alias>", "SSH host alias", "agentcomputer.ai").option("--host <host>", "SSH gateway host", "ssh.agentcomputer.ai").option("--port <port>", "SSH gateway port", "
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
589
|
+
var sshCommand = new Command("ssh").description("Open an SSH session to a computer").argument("[id-or-handle]", "Computer id or handle").option("--setup", "Register key and configure a global SSH alias").option("--alias <alias>", "SSH host alias", "agentcomputer.ai").option("--host <host>", "SSH gateway host", "ssh.agentcomputer.ai").option("--port <port>", "SSH gateway port", "443").action(
|
|
590
|
+
async (identifier, options) => {
|
|
591
|
+
if (options.setup) {
|
|
592
|
+
await setupSSHAlias(options);
|
|
593
|
+
return;
|
|
594
|
+
}
|
|
595
|
+
const spinner = ora(
|
|
596
|
+
identifier ? "Preparing SSH access..." : "Fetching computers..."
|
|
597
|
+
).start();
|
|
598
|
+
try {
|
|
599
|
+
const computer = await resolveSSHComputer(identifier, spinner);
|
|
600
|
+
const connection = await prepareSSHConnection(computer);
|
|
601
|
+
spinner.succeed(`Connecting to ${chalk3.bold(computer.handle)}`);
|
|
602
|
+
console.log(chalk3.dim(` ${connection.command}`));
|
|
603
|
+
console.log();
|
|
604
|
+
await openSSHConnection(connection);
|
|
605
|
+
} catch (error) {
|
|
606
|
+
spinner.fail(
|
|
607
|
+
error instanceof Error ? error.message : "Failed to prepare SSH access"
|
|
608
|
+
);
|
|
609
|
+
process.exit(1);
|
|
548
610
|
}
|
|
549
|
-
const registered = await ensureDefaultSSHKeyRegistered();
|
|
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)}`));
|
|
552
|
-
console.log();
|
|
553
|
-
await runSSH([
|
|
554
|
-
"-i",
|
|
555
|
-
registered.privateKeyPath,
|
|
556
|
-
"-p",
|
|
557
|
-
String(info.connection.ssh_port),
|
|
558
|
-
`${info.connection.ssh_user}@${info.connection.ssh_host}`
|
|
559
|
-
]);
|
|
560
|
-
} catch (error) {
|
|
561
|
-
spinner.fail(error instanceof Error ? error.message : "Failed to prepare SSH access");
|
|
562
|
-
process.exit(1);
|
|
563
611
|
}
|
|
564
|
-
|
|
565
|
-
var portsCommand = new Command("ports").description(
|
|
612
|
+
);
|
|
613
|
+
var portsCommand = new Command("ports").description(
|
|
614
|
+
"Manage published app ports"
|
|
615
|
+
);
|
|
566
616
|
portsCommand.command("ls").description("List published ports for a computer").argument("<id-or-handle>", "Computer id or handle").action(async (identifier) => {
|
|
567
617
|
const spinner = ora("Fetching ports...").start();
|
|
568
618
|
try {
|
|
@@ -588,17 +638,20 @@ portsCommand.command("ls").description("List published ports for a computer").ar
|
|
|
588
638
|
console.log(
|
|
589
639
|
` ${padEnd(port.subdomain, subWidth + 2)}${padEnd(String(port.target_port), 8)}${port.protocol}`
|
|
590
640
|
);
|
|
591
|
-
console.log(
|
|
592
|
-
` ${chalk3.dim(url)}`
|
|
593
|
-
);
|
|
641
|
+
console.log(` ${chalk3.dim(url)}`);
|
|
594
642
|
}
|
|
595
643
|
console.log();
|
|
596
644
|
} catch (error) {
|
|
597
|
-
spinner.fail(
|
|
645
|
+
spinner.fail(
|
|
646
|
+
error instanceof Error ? error.message : "Failed to fetch ports"
|
|
647
|
+
);
|
|
598
648
|
process.exit(1);
|
|
599
649
|
}
|
|
600
650
|
});
|
|
601
|
-
portsCommand.command("publish").description("Publish an HTTP app port").argument("<id-or-handle>", "Computer id or handle").argument("<port>", "Target port").option(
|
|
651
|
+
portsCommand.command("publish").description("Publish an HTTP app port").argument("<id-or-handle>", "Computer id or handle").argument("<port>", "Target port").option(
|
|
652
|
+
"--subdomain <value>",
|
|
653
|
+
"Custom left-hand host label before --<handle>"
|
|
654
|
+
).option("--protocol <value>", "http or https", "http").action(async (identifier, port, options) => {
|
|
602
655
|
const spinner = ora("Publishing port...").start();
|
|
603
656
|
try {
|
|
604
657
|
const targetPort = Number.parseInt(port, 10);
|
|
@@ -612,10 +665,14 @@ portsCommand.command("publish").description("Publish an HTTP app port").argument
|
|
|
612
665
|
protocol: options.protocol
|
|
613
666
|
});
|
|
614
667
|
const url = `https://${published.subdomain}--${computer.handle}.computer.agentcomputer.ai`;
|
|
615
|
-
spinner.succeed(
|
|
668
|
+
spinner.succeed(
|
|
669
|
+
`Published port ${chalk3.bold(String(published.target_port))}`
|
|
670
|
+
);
|
|
616
671
|
console.log(chalk3.dim(` ${url}`));
|
|
617
672
|
} catch (error) {
|
|
618
|
-
spinner.fail(
|
|
673
|
+
spinner.fail(
|
|
674
|
+
error instanceof Error ? error.message : "Failed to publish port"
|
|
675
|
+
);
|
|
619
676
|
process.exit(1);
|
|
620
677
|
}
|
|
621
678
|
});
|
|
@@ -630,31 +687,18 @@ portsCommand.command("rm").description("Unpublish an app port").argument("<id-or
|
|
|
630
687
|
await deletePublishedPort(computer.id, targetPort);
|
|
631
688
|
spinner.succeed(`Removed port ${chalk3.bold(String(targetPort))}`);
|
|
632
689
|
} catch (error) {
|
|
633
|
-
spinner.fail(
|
|
690
|
+
spinner.fail(
|
|
691
|
+
error instanceof Error ? error.message : "Failed to remove port"
|
|
692
|
+
);
|
|
634
693
|
process.exit(1);
|
|
635
694
|
}
|
|
636
695
|
});
|
|
637
|
-
async function runSSH(args) {
|
|
638
|
-
await new Promise((resolve, reject) => {
|
|
639
|
-
const child = spawn("ssh", args, {
|
|
640
|
-
stdio: "inherit"
|
|
641
|
-
});
|
|
642
|
-
child.on("error", reject);
|
|
643
|
-
child.on("exit", (code) => {
|
|
644
|
-
if (code === 0) {
|
|
645
|
-
resolve();
|
|
646
|
-
return;
|
|
647
|
-
}
|
|
648
|
-
reject(new Error(`ssh exited with code ${code ?? 1}`));
|
|
649
|
-
});
|
|
650
|
-
});
|
|
651
|
-
}
|
|
652
696
|
async function setupSSHAlias(options) {
|
|
653
697
|
const spinner = ora("Configuring global SSH access...").start();
|
|
654
698
|
try {
|
|
655
699
|
const alias = normalizeSSHAlias(options.alias ?? "agentcomputer.ai");
|
|
656
700
|
const host = normalizeSSHHost(options.host ?? "ssh.agentcomputer.ai");
|
|
657
|
-
const port = parseSSHPort(options.port ?? "
|
|
701
|
+
const port = parseSSHPort(options.port ?? "443");
|
|
658
702
|
const registered = await ensureDefaultSSHKeyRegistered();
|
|
659
703
|
const configResult = await ensureSSHAliasConfig({
|
|
660
704
|
alias,
|
|
@@ -672,7 +716,9 @@ async function setupSSHAlias(options) {
|
|
|
672
716
|
console.log(` ${chalk3.bold("Direct:")} ssh <handle>@${alias}`);
|
|
673
717
|
console.log();
|
|
674
718
|
} catch (error) {
|
|
675
|
-
spinner.fail(
|
|
719
|
+
spinner.fail(
|
|
720
|
+
error instanceof Error ? error.message : "Failed to configure SSH alias"
|
|
721
|
+
);
|
|
676
722
|
process.exit(1);
|
|
677
723
|
}
|
|
678
724
|
}
|
|
@@ -682,7 +728,9 @@ function normalizeSSHAlias(value) {
|
|
|
682
728
|
throw new Error("ssh alias cannot be empty");
|
|
683
729
|
}
|
|
684
730
|
if (!/^[A-Za-z0-9._-]+$/.test(alias)) {
|
|
685
|
-
throw new Error(
|
|
731
|
+
throw new Error(
|
|
732
|
+
"ssh alias may contain only letters, numbers, dot, dash, or underscore"
|
|
733
|
+
);
|
|
686
734
|
}
|
|
687
735
|
return alias;
|
|
688
736
|
}
|
|
@@ -703,15 +751,6 @@ function parseSSHPort(value) {
|
|
|
703
751
|
}
|
|
704
752
|
return parsed;
|
|
705
753
|
}
|
|
706
|
-
function formatSSHCommand(user, host, port) {
|
|
707
|
-
if (!user.trim() || !host.trim()) {
|
|
708
|
-
return "ssh unavailable";
|
|
709
|
-
}
|
|
710
|
-
if (port <= 0 || port === 22) {
|
|
711
|
-
return `ssh ${user}@${host}`;
|
|
712
|
-
}
|
|
713
|
-
return `ssh -p ${port} ${user}@${host}`;
|
|
714
|
-
}
|
|
715
754
|
async function resolveSSHComputer(identifier, spinner) {
|
|
716
755
|
const trimmed = identifier?.trim();
|
|
717
756
|
if (trimmed) {
|
|
@@ -720,7 +759,10 @@ async function resolveSSHComputer(identifier, spinner) {
|
|
|
720
759
|
const computers = await listComputers();
|
|
721
760
|
spinner.stop();
|
|
722
761
|
try {
|
|
723
|
-
return await promptForSSHComputer(
|
|
762
|
+
return await promptForSSHComputer(
|
|
763
|
+
computers,
|
|
764
|
+
"Select a computer to SSH into"
|
|
765
|
+
);
|
|
724
766
|
} finally {
|
|
725
767
|
spinner.start("Preparing SSH access...");
|
|
726
768
|
}
|
|
@@ -740,10 +782,6 @@ async function listAgentSessions(computerID) {
|
|
|
740
782
|
const response = await api(`/v1/computers/${computerID}/agent-sessions`);
|
|
741
783
|
return response.sessions;
|
|
742
784
|
}
|
|
743
|
-
async function listFleetAgentSessions() {
|
|
744
|
-
const response = await api("/v1/fleet/agent-sessions");
|
|
745
|
-
return response.sessions;
|
|
746
|
-
}
|
|
747
785
|
async function createAgentSession(computerID, input) {
|
|
748
786
|
const response = await api(`/v1/computers/${computerID}/agent-sessions`, {
|
|
749
787
|
method: "POST",
|
|
@@ -1499,29 +1537,6 @@ agentCommand.command("close").description("Close and delete a machine agent sess
|
|
|
1499
1537
|
process.exit(1);
|
|
1500
1538
|
}
|
|
1501
1539
|
});
|
|
1502
|
-
var fleetCommand = new Command3("fleet").description("View agent activity across your fleet");
|
|
1503
|
-
fleetCommand.command("status").description("List open agent sessions across all machines").option("--json", "Print raw JSON").action(async (options) => {
|
|
1504
|
-
const spinner = options.json ? null : ora2("Fetching fleet status...").start();
|
|
1505
|
-
try {
|
|
1506
|
-
const [sessions, computers] = await Promise.all([
|
|
1507
|
-
listFleetAgentSessions(),
|
|
1508
|
-
listComputers()
|
|
1509
|
-
]);
|
|
1510
|
-
spinner?.stop();
|
|
1511
|
-
if (options.json) {
|
|
1512
|
-
console.log(JSON.stringify({ sessions }, null, 2));
|
|
1513
|
-
return;
|
|
1514
|
-
}
|
|
1515
|
-
const handleByComputerID = new Map(computers.map((computer) => [computer.id, computer.handle]));
|
|
1516
|
-
printSessions(sessions, handleByComputerID);
|
|
1517
|
-
} catch (error) {
|
|
1518
|
-
spinner?.fail(error instanceof Error ? error.message : "Failed to fetch fleet status");
|
|
1519
|
-
if (!spinner) {
|
|
1520
|
-
console.error(error instanceof Error ? error.message : "Failed to fetch fleet status");
|
|
1521
|
-
}
|
|
1522
|
-
process.exit(1);
|
|
1523
|
-
}
|
|
1524
|
-
});
|
|
1525
1540
|
|
|
1526
1541
|
// src/commands/claude-auth.ts
|
|
1527
1542
|
import { randomBytes as randomBytes2, createHash } from "crypto";
|
|
@@ -1736,6 +1751,16 @@ var CLAUDE_OAUTH_TOKEN_URL = process.env.CLAUDE_OAUTH_TOKEN_URL ?? "https://plat
|
|
|
1736
1751
|
var CLAUDE_OAUTH_REDIRECT_URL = process.env.CLAUDE_OAUTH_REDIRECT_URL ?? "https://platform.claude.com/oauth/code/callback";
|
|
1737
1752
|
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);
|
|
1738
1753
|
var claudeLoginCommand = new Command4("claude-login").alias("claude-auth").description("Authenticate Claude Code on a computer").option("--machine <id-or-handle>", "Use a specific computer").option("--keep-helper", "Keep a temporary helper machine if one is created").option("--skip-cross-check", "Skip verification on a second shared machine").option("--verbose", "Show step-by-step auth diagnostics").action(async (options) => {
|
|
1754
|
+
try {
|
|
1755
|
+
await runClaudeLogin(options);
|
|
1756
|
+
} catch (error) {
|
|
1757
|
+
const message = error instanceof Error ? error.message : "Failed to authenticate Claude";
|
|
1758
|
+
console.error(chalk5.red(`
|
|
1759
|
+
${message}`));
|
|
1760
|
+
process.exit(1);
|
|
1761
|
+
}
|
|
1762
|
+
});
|
|
1763
|
+
async function runClaudeLogin(options) {
|
|
1739
1764
|
const todos = createTodoList();
|
|
1740
1765
|
let target = null;
|
|
1741
1766
|
let helperCreated = false;
|
|
@@ -1749,12 +1774,7 @@ var claudeLoginCommand = new Command4("claude-login").alias("claude-auth").descr
|
|
|
1749
1774
|
target = prepared.computer;
|
|
1750
1775
|
helperCreated = prepared.helperCreated;
|
|
1751
1776
|
sharedInstall = prepared.sharedInstall;
|
|
1752
|
-
markTodo(
|
|
1753
|
-
todos,
|
|
1754
|
-
"target",
|
|
1755
|
-
"done",
|
|
1756
|
-
prepared.detail
|
|
1757
|
-
);
|
|
1777
|
+
markTodo(todos, "target", "done", prepared.detail);
|
|
1758
1778
|
activeTodoID = "ready";
|
|
1759
1779
|
target = await waitForRunning(target);
|
|
1760
1780
|
markTodo(todos, "ready", "done", `${target.handle} is running`);
|
|
@@ -1771,7 +1791,10 @@ var claudeLoginCommand = new Command4("claude-login").alias("claude-auth").descr
|
|
|
1771
1791
|
sharedInstall ? `installed Claude login on shared home via ${target.handle}` : `installed Claude login on ${target.handle}`
|
|
1772
1792
|
);
|
|
1773
1793
|
activeTodoID = "verify-primary";
|
|
1774
|
-
const primaryVerification = await verifyTargetMachine(
|
|
1794
|
+
const primaryVerification = await verifyTargetMachine(
|
|
1795
|
+
target.handle,
|
|
1796
|
+
sshTarget
|
|
1797
|
+
);
|
|
1775
1798
|
markVerificationTodo(
|
|
1776
1799
|
todos,
|
|
1777
1800
|
"verify-primary",
|
|
@@ -1817,12 +1840,7 @@ var claudeLoginCommand = new Command4("claude-login").alias("claude-auth").descr
|
|
|
1817
1840
|
markTodo(todos, "cleanup", "failed", message);
|
|
1818
1841
|
}
|
|
1819
1842
|
} else if (helperCreated && target && options.keepHelper) {
|
|
1820
|
-
markTodo(
|
|
1821
|
-
todos,
|
|
1822
|
-
"cleanup",
|
|
1823
|
-
"skipped",
|
|
1824
|
-
`kept helper ${target.handle}`
|
|
1825
|
-
);
|
|
1843
|
+
markTodo(todos, "cleanup", "skipped", `kept helper ${target.handle}`);
|
|
1826
1844
|
} else {
|
|
1827
1845
|
markTodo(todos, "cleanup", "skipped", "no helper created");
|
|
1828
1846
|
}
|
|
@@ -1831,9 +1849,7 @@ var claudeLoginCommand = new Command4("claude-login").alias("claude-auth").descr
|
|
|
1831
1849
|
}
|
|
1832
1850
|
}
|
|
1833
1851
|
if (failureMessage) {
|
|
1834
|
-
|
|
1835
|
-
${failureMessage}`));
|
|
1836
|
-
process.exit(1);
|
|
1852
|
+
throw new Error(failureMessage);
|
|
1837
1853
|
}
|
|
1838
1854
|
if (target) {
|
|
1839
1855
|
console.log(
|
|
@@ -1841,15 +1857,23 @@ ${failureMessage}`));
|
|
|
1841
1857
|
);
|
|
1842
1858
|
console.log();
|
|
1843
1859
|
}
|
|
1844
|
-
}
|
|
1860
|
+
}
|
|
1845
1861
|
function createTodoList() {
|
|
1846
1862
|
return [
|
|
1847
1863
|
{ id: "target", label: "Pick target computer", state: "pending" },
|
|
1848
1864
|
{ id: "ready", label: "Wait for machine readiness", state: "pending" },
|
|
1849
1865
|
{ id: "oauth", label: "Complete Claude browser auth", state: "pending" },
|
|
1850
1866
|
{ id: "install", label: "Install stored Claude login", state: "pending" },
|
|
1851
|
-
{
|
|
1852
|
-
|
|
1867
|
+
{
|
|
1868
|
+
id: "verify-primary",
|
|
1869
|
+
label: "Verify on target machine",
|
|
1870
|
+
state: "pending"
|
|
1871
|
+
},
|
|
1872
|
+
{
|
|
1873
|
+
id: "verify-shared",
|
|
1874
|
+
label: "Verify shared-home propagation",
|
|
1875
|
+
state: "pending"
|
|
1876
|
+
},
|
|
1853
1877
|
{ id: "cleanup", label: "Clean up temporary helper", state: "pending" }
|
|
1854
1878
|
];
|
|
1855
1879
|
}
|
|
@@ -1889,10 +1913,14 @@ async function prepareTargetMachine(options) {
|
|
|
1889
1913
|
async function runManualOAuthFlow() {
|
|
1890
1914
|
const codeVerifier = base64url(randomBytes2(32));
|
|
1891
1915
|
const state = randomBytes2(16).toString("hex");
|
|
1892
|
-
const codeChallenge = base64url(
|
|
1916
|
+
const codeChallenge = base64url(
|
|
1917
|
+
createHash("sha256").update(codeVerifier).digest()
|
|
1918
|
+
);
|
|
1893
1919
|
const url = buildAuthorizationURL(codeChallenge, state);
|
|
1894
1920
|
console.log("We will open your browser so you can authenticate with Claude.");
|
|
1895
|
-
console.log(
|
|
1921
|
+
console.log(
|
|
1922
|
+
"If the browser does not open automatically, use the URL below:\n"
|
|
1923
|
+
);
|
|
1896
1924
|
console.log(url);
|
|
1897
1925
|
console.log();
|
|
1898
1926
|
try {
|
|
@@ -1903,7 +1931,9 @@ async function runManualOAuthFlow() {
|
|
|
1903
1931
|
console.log(
|
|
1904
1932
|
"After completing authentication, copy the code shown on the success page."
|
|
1905
1933
|
);
|
|
1906
|
-
console.log(
|
|
1934
|
+
console.log(
|
|
1935
|
+
"You can paste either the full URL, or a value formatted as CODE#STATE.\n"
|
|
1936
|
+
);
|
|
1907
1937
|
const pasted = (await textInput({
|
|
1908
1938
|
message: "Paste the authorization code (or URL) here:"
|
|
1909
1939
|
})).trim();
|
|
@@ -1970,7 +2000,9 @@ function parseAuthorizationInput(value, expectedState) {
|
|
|
1970
2000
|
throw new Error("pasted URL is missing code or state");
|
|
1971
2001
|
}
|
|
1972
2002
|
if (state2 !== expectedState) {
|
|
1973
|
-
throw new Error(
|
|
2003
|
+
throw new Error(
|
|
2004
|
+
"state mismatch detected; restart the authentication flow"
|
|
2005
|
+
);
|
|
1974
2006
|
}
|
|
1975
2007
|
return { code: code2, state: state2 };
|
|
1976
2008
|
}
|
|
@@ -1984,10 +2016,16 @@ function parseAuthorizationInput(value, expectedState) {
|
|
|
1984
2016
|
return { code, state };
|
|
1985
2017
|
}
|
|
1986
2018
|
async function installClaudeAuth(target, oauth) {
|
|
1987
|
-
const spinner = ora4(
|
|
2019
|
+
const spinner = ora4(
|
|
2020
|
+
`Installing Claude auth on ${chalk5.bold(target.handle)}...`
|
|
2021
|
+
).start();
|
|
1988
2022
|
try {
|
|
1989
2023
|
const installScript = buildInstallScript(oauth.refreshToken, oauth.scope);
|
|
1990
|
-
const result = await runRemoteCommand(
|
|
2024
|
+
const result = await runRemoteCommand(
|
|
2025
|
+
target,
|
|
2026
|
+
["bash", "-s"],
|
|
2027
|
+
installScript
|
|
2028
|
+
);
|
|
1991
2029
|
if (result.stdout.trim()) {
|
|
1992
2030
|
spinner.succeed(`Installed Claude auth on ${chalk5.bold(target.handle)}`);
|
|
1993
2031
|
return;
|
|
@@ -2001,7 +2039,9 @@ async function installClaudeAuth(target, oauth) {
|
|
|
2001
2039
|
}
|
|
2002
2040
|
}
|
|
2003
2041
|
async function verifyTargetMachine(handle, target) {
|
|
2004
|
-
const spinner = ora4(
|
|
2042
|
+
const spinner = ora4(
|
|
2043
|
+
`Verifying Claude login on ${chalk5.bold(handle)}...`
|
|
2044
|
+
).start();
|
|
2005
2045
|
const result = await verifyStoredAuth(target);
|
|
2006
2046
|
if (result.status === "verified") {
|
|
2007
2047
|
spinner.succeed(`Verified Claude login on ${chalk5.bold(handle)}`);
|
|
@@ -2021,7 +2061,9 @@ async function verifySharedInstall(primaryHandle, primaryComputerID, sharedInsta
|
|
|
2021
2061
|
verify
|
|
2022
2062
|
);
|
|
2023
2063
|
if (result.status === "verified") {
|
|
2024
|
-
spinner.succeed(
|
|
2064
|
+
spinner.succeed(
|
|
2065
|
+
`Verified shared-home Claude login on ${chalk5.bold(result.handle)}`
|
|
2066
|
+
);
|
|
2025
2067
|
return result;
|
|
2026
2068
|
}
|
|
2027
2069
|
spinner.info(result.reason);
|
|
@@ -2598,6 +2640,7 @@ _computer() {
|
|
|
2598
2640
|
local -a commands
|
|
2599
2641
|
commands=(
|
|
2600
2642
|
'login:Authenticate the CLI'
|
|
2643
|
+
'upgrade:Update the CLI to the latest version'
|
|
2601
2644
|
'logout:Remove stored API key'
|
|
2602
2645
|
'whoami:Show current user'
|
|
2603
2646
|
'claude-login:Authenticate Claude Code on a computer'
|
|
@@ -2612,7 +2655,6 @@ _computer() {
|
|
|
2612
2655
|
'ssh:SSH into a computer'
|
|
2613
2656
|
'ports:Manage published ports'
|
|
2614
2657
|
'agent:Manage cloud agent sessions'
|
|
2615
|
-
'fleet:View agent activity across your fleet'
|
|
2616
2658
|
'acp:Run a local ACP bridge for remote agent sessions'
|
|
2617
2659
|
'rm:Delete a computer'
|
|
2618
2660
|
'completion:Generate shell completions'
|
|
@@ -2808,7 +2850,7 @@ var BASH_SCRIPT = `_computer() {
|
|
|
2808
2850
|
local cur prev words cword
|
|
2809
2851
|
_init_completion || return
|
|
2810
2852
|
|
|
2811
|
-
local commands="login logout whoami claude-login claude-auth codex-login codex-auth create ls get image open ssh ports agent
|
|
2853
|
+
local commands="login upgrade logout whoami claude-login claude-auth codex-login codex-auth create ls get image open ssh ports agent acp rm completion help"
|
|
2812
2854
|
local ports_commands="ls publish rm"
|
|
2813
2855
|
local image_commands="ls save default rebuild rm"
|
|
2814
2856
|
|
|
@@ -2934,6 +2976,16 @@ import { Command as Command7 } from "commander";
|
|
|
2934
2976
|
import chalk7 from "chalk";
|
|
2935
2977
|
import ora6 from "ora";
|
|
2936
2978
|
var codexLoginCommand = new Command7("codex-login").alias("codex-auth").description("Authenticate Codex on a computer").option("--machine <id-or-handle>", "Use a specific computer").option("--keep-helper", "Keep a temporary helper machine if one is created").option("--skip-cross-check", "Skip verification on a second shared machine").option("--verbose", "Show step-by-step auth diagnostics").action(async (options) => {
|
|
2979
|
+
try {
|
|
2980
|
+
await runCodexLogin(options);
|
|
2981
|
+
} catch (error) {
|
|
2982
|
+
const message = error instanceof Error ? error.message : "Failed to authenticate Codex";
|
|
2983
|
+
console.error(chalk7.red(`
|
|
2984
|
+
${message}`));
|
|
2985
|
+
process.exit(1);
|
|
2986
|
+
}
|
|
2987
|
+
});
|
|
2988
|
+
async function runCodexLogin(options) {
|
|
2937
2989
|
const todos = createTodoList2();
|
|
2938
2990
|
let target = null;
|
|
2939
2991
|
let helperCreated = false;
|
|
@@ -2964,7 +3016,10 @@ var codexLoginCommand = new Command7("codex-login").alias("codex-auth").descript
|
|
|
2964
3016
|
sharedInstall ? `installed Codex login on shared home via ${target.handle}` : `installed Codex login on ${target.handle}`
|
|
2965
3017
|
);
|
|
2966
3018
|
activeTodoID = "verify-primary";
|
|
2967
|
-
const primaryVerification = await verifyTargetMachine2(
|
|
3019
|
+
const primaryVerification = await verifyTargetMachine2(
|
|
3020
|
+
target.handle,
|
|
3021
|
+
sshTarget
|
|
3022
|
+
);
|
|
2968
3023
|
markVerificationTodo2(
|
|
2969
3024
|
todos,
|
|
2970
3025
|
"verify-primary",
|
|
@@ -3019,9 +3074,7 @@ var codexLoginCommand = new Command7("codex-login").alias("codex-auth").descript
|
|
|
3019
3074
|
}
|
|
3020
3075
|
}
|
|
3021
3076
|
if (failureMessage) {
|
|
3022
|
-
|
|
3023
|
-
${failureMessage}`));
|
|
3024
|
-
process.exit(1);
|
|
3077
|
+
throw new Error(failureMessage);
|
|
3025
3078
|
}
|
|
3026
3079
|
if (target) {
|
|
3027
3080
|
console.log(
|
|
@@ -3029,15 +3082,23 @@ ${failureMessage}`));
|
|
|
3029
3082
|
);
|
|
3030
3083
|
console.log();
|
|
3031
3084
|
}
|
|
3032
|
-
}
|
|
3085
|
+
}
|
|
3033
3086
|
function createTodoList2() {
|
|
3034
3087
|
return [
|
|
3035
3088
|
{ id: "target", label: "Pick target computer", state: "pending" },
|
|
3036
3089
|
{ id: "ready", label: "Wait for machine readiness", state: "pending" },
|
|
3037
3090
|
{ id: "local-auth", label: "Complete local Codex auth", state: "pending" },
|
|
3038
3091
|
{ id: "install", label: "Install stored Codex login", state: "pending" },
|
|
3039
|
-
{
|
|
3040
|
-
|
|
3092
|
+
{
|
|
3093
|
+
id: "verify-primary",
|
|
3094
|
+
label: "Verify on target machine",
|
|
3095
|
+
state: "pending"
|
|
3096
|
+
},
|
|
3097
|
+
{
|
|
3098
|
+
id: "verify-shared",
|
|
3099
|
+
label: "Verify shared-home propagation",
|
|
3100
|
+
state: "pending"
|
|
3101
|
+
},
|
|
3041
3102
|
{ id: "cleanup", label: "Clean up temporary helper", state: "pending" }
|
|
3042
3103
|
];
|
|
3043
3104
|
}
|
|
@@ -3083,10 +3144,16 @@ async function ensureLocalCodexAuth() {
|
|
|
3083
3144
|
};
|
|
3084
3145
|
}
|
|
3085
3146
|
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
3086
|
-
throw new Error(
|
|
3147
|
+
throw new Error(
|
|
3148
|
+
"local Codex login is required when not running interactively"
|
|
3149
|
+
);
|
|
3087
3150
|
}
|
|
3088
|
-
console.log(
|
|
3089
|
-
|
|
3151
|
+
console.log(
|
|
3152
|
+
"We will open your browser so you can authenticate Codex locally."
|
|
3153
|
+
);
|
|
3154
|
+
console.log(
|
|
3155
|
+
"If Codex falls back to device auth, complete that flow and return here.\n"
|
|
3156
|
+
);
|
|
3090
3157
|
await runInteractiveCodexLogin();
|
|
3091
3158
|
const refreshedStatus = await getLocalCodexStatus();
|
|
3092
3159
|
if (!refreshedStatus.loggedIn) {
|
|
@@ -3166,7 +3233,9 @@ async function captureLocalCommand(command, args) {
|
|
|
3166
3233
|
});
|
|
3167
3234
|
}
|
|
3168
3235
|
async function installCodexAuth(target, authJSON) {
|
|
3169
|
-
const spinner = ora6(
|
|
3236
|
+
const spinner = ora6(
|
|
3237
|
+
`Installing Codex login on ${chalk7.bold(target.handle)}...`
|
|
3238
|
+
).start();
|
|
3170
3239
|
try {
|
|
3171
3240
|
const installScript = buildInstallScript2(authJSON);
|
|
3172
3241
|
await runRemoteCommand(target, ["bash", "-s"], installScript);
|
|
@@ -3179,7 +3248,9 @@ async function installCodexAuth(target, authJSON) {
|
|
|
3179
3248
|
}
|
|
3180
3249
|
}
|
|
3181
3250
|
async function verifyTargetMachine2(handle, target) {
|
|
3182
|
-
const spinner = ora6(
|
|
3251
|
+
const spinner = ora6(
|
|
3252
|
+
`Verifying Codex login on ${chalk7.bold(handle)}...`
|
|
3253
|
+
).start();
|
|
3183
3254
|
const result = await verifyStoredCodexAuth(target);
|
|
3184
3255
|
if (result.status === "verified") {
|
|
3185
3256
|
spinner.succeed(`Verified Codex login on ${chalk7.bold(handle)}`);
|
|
@@ -3199,7 +3270,9 @@ async function verifySharedInstall2(primaryHandle, primaryComputerID, sharedInst
|
|
|
3199
3270
|
verify
|
|
3200
3271
|
);
|
|
3201
3272
|
if (result.status === "verified") {
|
|
3202
|
-
spinner.succeed(
|
|
3273
|
+
spinner.succeed(
|
|
3274
|
+
`Verified shared-home Codex login on ${chalk7.bold(result.handle)}`
|
|
3275
|
+
);
|
|
3203
3276
|
return result;
|
|
3204
3277
|
}
|
|
3205
3278
|
spinner.info(result.reason);
|
|
@@ -3653,10 +3726,12 @@ import ora8 from "ora";
|
|
|
3653
3726
|
|
|
3654
3727
|
// src/lib/browser-login.ts
|
|
3655
3728
|
import { randomBytes as randomBytes3 } from "crypto";
|
|
3656
|
-
import {
|
|
3729
|
+
import {
|
|
3730
|
+
createServer
|
|
3731
|
+
} from "http";
|
|
3657
3732
|
var CALLBACK_HOST = "127.0.0.1";
|
|
3658
3733
|
var CALLBACK_PATH = "/callback";
|
|
3659
|
-
var LOGIN_TIMEOUT_MS =
|
|
3734
|
+
var LOGIN_TIMEOUT_MS = 15 * 60 * 1e3;
|
|
3660
3735
|
async function createBrowserLoginAttempt() {
|
|
3661
3736
|
const state = randomBytes3(16).toString("hex");
|
|
3662
3737
|
const deferred = createDeferred();
|
|
@@ -3736,7 +3811,11 @@ async function handleRequest(input) {
|
|
|
3736
3811
|
return;
|
|
3737
3812
|
}
|
|
3738
3813
|
if (settledRef.current) {
|
|
3739
|
-
writeHTML(
|
|
3814
|
+
writeHTML(
|
|
3815
|
+
response,
|
|
3816
|
+
409,
|
|
3817
|
+
renderErrorPage("This login link has already been used.")
|
|
3818
|
+
);
|
|
3740
3819
|
return;
|
|
3741
3820
|
}
|
|
3742
3821
|
const returnedState = url.searchParams.get("state")?.trim();
|
|
@@ -3763,6 +3842,9 @@ async function handleRequest(input) {
|
|
|
3763
3842
|
writeHTML(response, 400, renderErrorPage(error.message));
|
|
3764
3843
|
return;
|
|
3765
3844
|
}
|
|
3845
|
+
const machineHandle = url.searchParams.get("machine_handle")?.trim() || void 0;
|
|
3846
|
+
const provider = parseProvider(url.searchParams.get("provider"));
|
|
3847
|
+
const autoSSH = parseAutoSSH(url.searchParams.get("auto_ssh"));
|
|
3766
3848
|
try {
|
|
3767
3849
|
const me = await apiWithKey(apiKey, "/v1/me");
|
|
3768
3850
|
setAPIKey(apiKey);
|
|
@@ -3771,9 +3853,21 @@ async function handleRequest(input) {
|
|
|
3771
3853
|
apiKey,
|
|
3772
3854
|
callbackURL,
|
|
3773
3855
|
loginURL: buildBrowserLoginURL(callbackURL, state),
|
|
3774
|
-
me
|
|
3856
|
+
me,
|
|
3857
|
+
machineHandle,
|
|
3858
|
+
provider,
|
|
3859
|
+
autoSSH
|
|
3775
3860
|
});
|
|
3776
|
-
writeHTML(
|
|
3861
|
+
writeHTML(
|
|
3862
|
+
response,
|
|
3863
|
+
200,
|
|
3864
|
+
renderSuccessPage({
|
|
3865
|
+
autoSSH,
|
|
3866
|
+
email: me.user.email,
|
|
3867
|
+
machineHandle,
|
|
3868
|
+
provider
|
|
3869
|
+
})
|
|
3870
|
+
);
|
|
3777
3871
|
} catch (error) {
|
|
3778
3872
|
settledRef.current = true;
|
|
3779
3873
|
const message = error instanceof Error ? error.message : "Failed to validate browser login";
|
|
@@ -3781,6 +3875,22 @@ async function handleRequest(input) {
|
|
|
3781
3875
|
writeHTML(response, 401, renderErrorPage(message));
|
|
3782
3876
|
}
|
|
3783
3877
|
}
|
|
3878
|
+
function parseProvider(rawValue) {
|
|
3879
|
+
switch (rawValue?.trim()) {
|
|
3880
|
+
case "claude":
|
|
3881
|
+
case "codex":
|
|
3882
|
+
case "skip":
|
|
3883
|
+
return rawValue.trim();
|
|
3884
|
+
default:
|
|
3885
|
+
return void 0;
|
|
3886
|
+
}
|
|
3887
|
+
}
|
|
3888
|
+
function parseAutoSSH(rawValue) {
|
|
3889
|
+
if (rawValue === null) {
|
|
3890
|
+
return void 0;
|
|
3891
|
+
}
|
|
3892
|
+
return !(rawValue === "0" || rawValue === "false");
|
|
3893
|
+
}
|
|
3784
3894
|
function createDeferred() {
|
|
3785
3895
|
let resolve;
|
|
3786
3896
|
let reject;
|
|
@@ -3824,7 +3934,9 @@ function writeHTML(response, statusCode, body) {
|
|
|
3824
3934
|
response.setHeader("content-type", "text/html; charset=utf-8");
|
|
3825
3935
|
response.end(body);
|
|
3826
3936
|
}
|
|
3827
|
-
function renderSuccessPage(
|
|
3937
|
+
function renderSuccessPage(input) {
|
|
3938
|
+
const providerMessage = input.provider && input.provider !== "skip" ? ` The CLI will continue with ${escapeHTML(input.provider)} setup.` : "";
|
|
3939
|
+
const machineMessage = input.machineHandle ? ` Your new sandbox <code>${escapeHTML(input.machineHandle)}</code> is ready.${providerMessage} Return to the terminal to finish setup.` : " You can close this tab.";
|
|
3828
3940
|
return `<!doctype html>
|
|
3829
3941
|
<html lang="en">
|
|
3830
3942
|
<head>
|
|
@@ -3842,7 +3954,7 @@ function renderSuccessPage(email) {
|
|
|
3842
3954
|
<body>
|
|
3843
3955
|
<main>
|
|
3844
3956
|
<h1>Computer CLI login complete</h1>
|
|
3845
|
-
<p>Signed in as <code>${escapeHTML(email)}</code
|
|
3957
|
+
<p>Signed in as <code>${escapeHTML(input.email)}</code>.${machineMessage}</p>
|
|
3846
3958
|
</main>
|
|
3847
3959
|
<script>
|
|
3848
3960
|
window.setTimeout(() => window.close(), 150);
|
|
@@ -3881,7 +3993,9 @@ var loginCommand = new Command9("login").description("Authenticate the CLI").opt
|
|
|
3881
3993
|
const existingKey = getStoredAPIKey();
|
|
3882
3994
|
if (existingKey && !options.force) {
|
|
3883
3995
|
console.log();
|
|
3884
|
-
console.log(
|
|
3996
|
+
console.log(
|
|
3997
|
+
chalk9.yellow(" Already logged in. Use --force to overwrite.")
|
|
3998
|
+
);
|
|
3885
3999
|
console.log();
|
|
3886
4000
|
return;
|
|
3887
4001
|
}
|
|
@@ -3927,7 +4041,9 @@ async function runBrowserLogin() {
|
|
|
3927
4041
|
} catch {
|
|
3928
4042
|
spinner.stop();
|
|
3929
4043
|
console.log();
|
|
3930
|
-
console.log(
|
|
4044
|
+
console.log(
|
|
4045
|
+
chalk9.yellow(" Browser auto-open failed. Open this URL to continue:")
|
|
4046
|
+
);
|
|
3931
4047
|
console.log(chalk9.dim(` ${attempt.loginURL}`));
|
|
3932
4048
|
console.log();
|
|
3933
4049
|
spinner.start("Waiting for browser login...");
|
|
@@ -3935,8 +4051,11 @@ async function runBrowserLogin() {
|
|
|
3935
4051
|
spinner.text = "Waiting for browser login...";
|
|
3936
4052
|
const result = await attempt.waitForResult();
|
|
3937
4053
|
spinner.succeed(`Logged in as ${chalk9.bold(result.me.user.email)}`);
|
|
4054
|
+
await continueFirstLoginFlow(result);
|
|
3938
4055
|
} catch (error) {
|
|
3939
|
-
spinner.fail(
|
|
4056
|
+
spinner.fail(
|
|
4057
|
+
error instanceof Error ? error.message : "Browser login failed"
|
|
4058
|
+
);
|
|
3940
4059
|
process.exit(1);
|
|
3941
4060
|
} finally {
|
|
3942
4061
|
await attempt?.close();
|
|
@@ -3958,6 +4077,73 @@ async function resolveAPIKeyInput(flagValue, readFromStdin) {
|
|
|
3958
4077
|
}
|
|
3959
4078
|
return Buffer.concat(chunks).toString("utf8").trim();
|
|
3960
4079
|
}
|
|
4080
|
+
async function continueFirstLoginFlow(result) {
|
|
4081
|
+
const machineHandle = result.machineHandle?.trim();
|
|
4082
|
+
if (!machineHandle) {
|
|
4083
|
+
return;
|
|
4084
|
+
}
|
|
4085
|
+
console.log();
|
|
4086
|
+
console.log(
|
|
4087
|
+
chalk9.cyan(
|
|
4088
|
+
`Continuing first-time setup for ${chalk9.bold(machineHandle)}...
|
|
4089
|
+
`
|
|
4090
|
+
)
|
|
4091
|
+
);
|
|
4092
|
+
try {
|
|
4093
|
+
await runSelectedProvider(result.provider, machineHandle);
|
|
4094
|
+
if (result.autoSSH === false) {
|
|
4095
|
+
printNextStep(machineHandle);
|
|
4096
|
+
return;
|
|
4097
|
+
}
|
|
4098
|
+
const spinner = ora8(`Preparing SSH access for ${machineHandle}...`).start();
|
|
4099
|
+
try {
|
|
4100
|
+
const connection = await prepareSSHConnectionByIdentifier(machineHandle);
|
|
4101
|
+
spinner.succeed(`Connecting to ${chalk9.bold(machineHandle)}`);
|
|
4102
|
+
console.log(chalk9.dim(` ${connection.command}`));
|
|
4103
|
+
console.log();
|
|
4104
|
+
await openSSHConnection(connection);
|
|
4105
|
+
} catch (error) {
|
|
4106
|
+
spinner.fail(
|
|
4107
|
+
error instanceof Error ? error.message : "Failed to prepare SSH access"
|
|
4108
|
+
);
|
|
4109
|
+
throw error;
|
|
4110
|
+
}
|
|
4111
|
+
} catch (error) {
|
|
4112
|
+
const message = error instanceof Error ? error.message : "Failed to finish first-time setup";
|
|
4113
|
+
console.error(chalk9.red(`
|
|
4114
|
+
${message}`));
|
|
4115
|
+
console.log();
|
|
4116
|
+
if (result.provider === "claude") {
|
|
4117
|
+
console.log(
|
|
4118
|
+
chalk9.dim(` computer claude-login --machine ${machineHandle}`)
|
|
4119
|
+
);
|
|
4120
|
+
} else if (result.provider === "codex") {
|
|
4121
|
+
console.log(
|
|
4122
|
+
chalk9.dim(` computer codex-login --machine ${machineHandle}`)
|
|
4123
|
+
);
|
|
4124
|
+
}
|
|
4125
|
+
console.log(chalk9.dim(` computer ssh ${machineHandle}`));
|
|
4126
|
+
console.log();
|
|
4127
|
+
process.exit(1);
|
|
4128
|
+
}
|
|
4129
|
+
}
|
|
4130
|
+
async function runSelectedProvider(provider, machineHandle) {
|
|
4131
|
+
if (provider === "claude") {
|
|
4132
|
+
await runClaudeLogin({ machine: machineHandle });
|
|
4133
|
+
return;
|
|
4134
|
+
}
|
|
4135
|
+
if (provider === "codex") {
|
|
4136
|
+
await runCodexLogin({ machine: machineHandle });
|
|
4137
|
+
return;
|
|
4138
|
+
}
|
|
4139
|
+
console.log(chalk9.green(`Sandbox ${chalk9.bold(machineHandle)} is ready.`));
|
|
4140
|
+
console.log();
|
|
4141
|
+
}
|
|
4142
|
+
function printNextStep(machineHandle) {
|
|
4143
|
+
console.log(chalk9.green(`Sandbox ${chalk9.bold(machineHandle)} is ready.`));
|
|
4144
|
+
console.log(chalk9.dim(` computer ssh ${machineHandle}`));
|
|
4145
|
+
console.log();
|
|
4146
|
+
}
|
|
3961
4147
|
|
|
3962
4148
|
// src/commands/logout.ts
|
|
3963
4149
|
import { Command as Command10 } from "commander";
|
|
@@ -3981,12 +4167,180 @@ var logoutCommand = new Command10("logout").description("Remove stored API key")
|
|
|
3981
4167
|
console.log();
|
|
3982
4168
|
});
|
|
3983
4169
|
|
|
3984
|
-
// src/commands/
|
|
4170
|
+
// src/commands/upgrade.ts
|
|
4171
|
+
import { spawnSync } from "child_process";
|
|
4172
|
+
import { readFileSync as readFileSync3, realpathSync } from "fs";
|
|
3985
4173
|
import { Command as Command11 } from "commander";
|
|
3986
4174
|
import chalk11 from "chalk";
|
|
3987
4175
|
import ora9 from "ora";
|
|
3988
|
-
var
|
|
3989
|
-
|
|
4176
|
+
var pkg2 = JSON.parse(
|
|
4177
|
+
readFileSync3(new URL("../package.json", import.meta.url), "utf8")
|
|
4178
|
+
);
|
|
4179
|
+
function normalizeVersion(version) {
|
|
4180
|
+
return version.split("-")[0].split(".").map((part) => Number.parseInt(part, 10)).map((part) => Number.isNaN(part) ? 0 : part);
|
|
4181
|
+
}
|
|
4182
|
+
function compareVersions(a, b) {
|
|
4183
|
+
const left = normalizeVersion(a);
|
|
4184
|
+
const right = normalizeVersion(b);
|
|
4185
|
+
const size = Math.max(left.length, right.length);
|
|
4186
|
+
for (let index = 0; index < size; index += 1) {
|
|
4187
|
+
const diff = (left[index] ?? 0) - (right[index] ?? 0);
|
|
4188
|
+
if (diff !== 0) {
|
|
4189
|
+
return diff;
|
|
4190
|
+
}
|
|
4191
|
+
}
|
|
4192
|
+
return 0;
|
|
4193
|
+
}
|
|
4194
|
+
function resolveExecutablePath() {
|
|
4195
|
+
const candidate = process.argv[1] || process.execPath;
|
|
4196
|
+
try {
|
|
4197
|
+
return realpathSync(candidate);
|
|
4198
|
+
} catch {
|
|
4199
|
+
return candidate;
|
|
4200
|
+
}
|
|
4201
|
+
}
|
|
4202
|
+
function detectInstallMethod(executablePath) {
|
|
4203
|
+
const resolved = `${executablePath}\0${process.execPath}`.toLowerCase();
|
|
4204
|
+
if (resolved.includes("/nix/store/") || resolved.includes("\\nix\\store\\")) {
|
|
4205
|
+
return "nix";
|
|
4206
|
+
}
|
|
4207
|
+
if (resolved.includes("/.pnpm/") || resolved.includes("/pnpm/") || resolved.includes("\\pnpm\\")) {
|
|
4208
|
+
return "pnpm";
|
|
4209
|
+
}
|
|
4210
|
+
if (resolved.includes("/.yarn/") || resolved.includes("/yarn/") || resolved.includes("\\yarn\\")) {
|
|
4211
|
+
return "yarn";
|
|
4212
|
+
}
|
|
4213
|
+
if (resolved.includes("/node_modules/") || resolved.includes("\\node_modules\\")) {
|
|
4214
|
+
return "npm";
|
|
4215
|
+
}
|
|
4216
|
+
return "unknown";
|
|
4217
|
+
}
|
|
4218
|
+
function findNixProfileElement(executablePath) {
|
|
4219
|
+
const result = spawnSync("nix", ["profile", "list", "--json"], {
|
|
4220
|
+
encoding: "utf8"
|
|
4221
|
+
});
|
|
4222
|
+
if (result.status !== 0 || !result.stdout.trim()) {
|
|
4223
|
+
return null;
|
|
4224
|
+
}
|
|
4225
|
+
const profile = JSON.parse(result.stdout);
|
|
4226
|
+
const resolvedExecutable = executablePath.toLowerCase();
|
|
4227
|
+
for (const [name, element] of Object.entries(profile.elements ?? {})) {
|
|
4228
|
+
const storePaths = Array.isArray(element.storePaths) ? element.storePaths.map((storePath) => storePath.toLowerCase()) : [];
|
|
4229
|
+
if (storePaths.some((storePath) => resolvedExecutable.startsWith(storePath))) {
|
|
4230
|
+
return name;
|
|
4231
|
+
}
|
|
4232
|
+
}
|
|
4233
|
+
for (const [name, element] of Object.entries(profile.elements ?? {})) {
|
|
4234
|
+
const originalUrl = String(element.originalUrl ?? "").toLowerCase();
|
|
4235
|
+
if (originalUrl.includes("agentcomputer") || originalUrl.includes("apps/cli")) {
|
|
4236
|
+
return name;
|
|
4237
|
+
}
|
|
4238
|
+
}
|
|
4239
|
+
return null;
|
|
4240
|
+
}
|
|
4241
|
+
function resolveUpgradeCommand(method, executablePath) {
|
|
4242
|
+
const packageName = pkg2.name ?? "aicomputer";
|
|
4243
|
+
switch (method) {
|
|
4244
|
+
case "nix": {
|
|
4245
|
+
const element = findNixProfileElement(executablePath);
|
|
4246
|
+
if (!element) {
|
|
4247
|
+
throw new Error(
|
|
4248
|
+
"Nix install detected, but no matching Nix profile entry was found. Rerun your original Nix install command or update that profile manually."
|
|
4249
|
+
);
|
|
4250
|
+
}
|
|
4251
|
+
return {
|
|
4252
|
+
command: "nix",
|
|
4253
|
+
args: ["profile", "upgrade", element],
|
|
4254
|
+
label: `nix profile upgrade ${element}`
|
|
4255
|
+
};
|
|
4256
|
+
}
|
|
4257
|
+
case "pnpm":
|
|
4258
|
+
return {
|
|
4259
|
+
command: "pnpm",
|
|
4260
|
+
args: ["add", "-g", `${packageName}@latest`],
|
|
4261
|
+
label: `pnpm add -g ${packageName}@latest`
|
|
4262
|
+
};
|
|
4263
|
+
case "yarn":
|
|
4264
|
+
return {
|
|
4265
|
+
command: "yarn",
|
|
4266
|
+
args: ["global", "add", `${packageName}@latest`],
|
|
4267
|
+
label: `yarn global add ${packageName}@latest`
|
|
4268
|
+
};
|
|
4269
|
+
case "npm":
|
|
4270
|
+
case "unknown":
|
|
4271
|
+
return {
|
|
4272
|
+
command: "npm",
|
|
4273
|
+
args: ["install", "-g", `${packageName}@latest`],
|
|
4274
|
+
label: `npm install -g ${packageName}@latest`
|
|
4275
|
+
};
|
|
4276
|
+
}
|
|
4277
|
+
}
|
|
4278
|
+
async function getLatestVersion(packageName) {
|
|
4279
|
+
const response = await fetch(`https://registry.npmjs.org/${packageName}/latest`);
|
|
4280
|
+
if (!response.ok) {
|
|
4281
|
+
throw new Error(`Failed to check npm registry (${response.status})`);
|
|
4282
|
+
}
|
|
4283
|
+
const payload = await response.json();
|
|
4284
|
+
if (!payload.version) {
|
|
4285
|
+
throw new Error("Registry response missing version");
|
|
4286
|
+
}
|
|
4287
|
+
return payload.version;
|
|
4288
|
+
}
|
|
4289
|
+
var upgradeCommand = new Command11("upgrade").description("Update the CLI to the latest version").action(async () => {
|
|
4290
|
+
const currentVersion = pkg2.version ?? "0.0.0";
|
|
4291
|
+
const packageName = pkg2.name ?? "aicomputer";
|
|
4292
|
+
const spinner = ora9("Checking for updates...").start();
|
|
4293
|
+
let latestVersion;
|
|
4294
|
+
try {
|
|
4295
|
+
latestVersion = await getLatestVersion(packageName);
|
|
4296
|
+
} catch (error) {
|
|
4297
|
+
spinner.fail(
|
|
4298
|
+
error instanceof Error ? error.message : "Failed to check for updates"
|
|
4299
|
+
);
|
|
4300
|
+
process.exit(1);
|
|
4301
|
+
return;
|
|
4302
|
+
}
|
|
4303
|
+
if (compareVersions(latestVersion, currentVersion) <= 0) {
|
|
4304
|
+
spinner.succeed(`You're up to date (v${currentVersion}).`);
|
|
4305
|
+
return;
|
|
4306
|
+
}
|
|
4307
|
+
const executablePath = resolveExecutablePath();
|
|
4308
|
+
const method = detectInstallMethod(executablePath);
|
|
4309
|
+
let upgrade;
|
|
4310
|
+
try {
|
|
4311
|
+
upgrade = resolveUpgradeCommand(method, executablePath);
|
|
4312
|
+
} catch (error) {
|
|
4313
|
+
spinner.fail(
|
|
4314
|
+
error instanceof Error ? error.message : "Failed to prepare upgrade"
|
|
4315
|
+
);
|
|
4316
|
+
process.exit(1);
|
|
4317
|
+
return;
|
|
4318
|
+
}
|
|
4319
|
+
spinner.stop();
|
|
4320
|
+
console.log();
|
|
4321
|
+
console.log(
|
|
4322
|
+
chalk11.dim(` Updating ${chalk11.bold(`v${currentVersion}`)} -> ${chalk11.bold(`v${latestVersion}`)}`)
|
|
4323
|
+
);
|
|
4324
|
+
console.log(chalk11.dim(` ${upgrade.label}`));
|
|
4325
|
+
console.log();
|
|
4326
|
+
const result = spawnSync(upgrade.command, upgrade.args, {
|
|
4327
|
+
stdio: "inherit"
|
|
4328
|
+
});
|
|
4329
|
+
if (result.status === 0) {
|
|
4330
|
+
console.log();
|
|
4331
|
+
console.log(chalk11.green(` Updated to v${latestVersion}.`));
|
|
4332
|
+
console.log();
|
|
4333
|
+
return;
|
|
4334
|
+
}
|
|
4335
|
+
process.exit(result.status ?? 1);
|
|
4336
|
+
});
|
|
4337
|
+
|
|
4338
|
+
// src/commands/whoami.ts
|
|
4339
|
+
import { Command as Command12 } from "commander";
|
|
4340
|
+
import chalk12 from "chalk";
|
|
4341
|
+
import ora10 from "ora";
|
|
4342
|
+
var whoamiCommand = new Command12("whoami").description("Show current user").option("--json", "Print raw JSON").action(async (options) => {
|
|
4343
|
+
const spinner = options.json ? null : ora10("Loading user...").start();
|
|
3990
4344
|
try {
|
|
3991
4345
|
const me = await api("/v1/me");
|
|
3992
4346
|
spinner?.stop();
|
|
@@ -3995,14 +4349,14 @@ var whoamiCommand = new Command11("whoami").description("Show current user").opt
|
|
|
3995
4349
|
return;
|
|
3996
4350
|
}
|
|
3997
4351
|
console.log();
|
|
3998
|
-
console.log(` ${
|
|
4352
|
+
console.log(` ${chalk12.bold.white(me.user.display_name || me.user.email)}`);
|
|
3999
4353
|
if (me.user.display_name) {
|
|
4000
|
-
console.log(` ${
|
|
4354
|
+
console.log(` ${chalk12.dim(me.user.email)}`);
|
|
4001
4355
|
}
|
|
4002
4356
|
if (me.api_key.name) {
|
|
4003
|
-
console.log(` ${
|
|
4357
|
+
console.log(` ${chalk12.dim("Key:")} ${me.api_key.name}`);
|
|
4004
4358
|
}
|
|
4005
|
-
console.log(` ${
|
|
4359
|
+
console.log(` ${chalk12.dim("API:")} ${chalk12.dim(getBaseURL())}`);
|
|
4006
4360
|
console.log();
|
|
4007
4361
|
} catch (error) {
|
|
4008
4362
|
if (spinner) {
|
|
@@ -4015,19 +4369,19 @@ var whoamiCommand = new Command11("whoami").description("Show current user").opt
|
|
|
4015
4369
|
});
|
|
4016
4370
|
|
|
4017
4371
|
// src/index.ts
|
|
4018
|
-
var
|
|
4019
|
-
|
|
4372
|
+
var pkg3 = JSON.parse(
|
|
4373
|
+
readFileSync4(new URL("../package.json", import.meta.url), "utf8")
|
|
4020
4374
|
);
|
|
4021
4375
|
var cliName = process.argv[1] ? basename2(process.argv[1]) : "agentcomputer";
|
|
4022
|
-
var program = new
|
|
4376
|
+
var program = new Command13();
|
|
4023
4377
|
function appendTextSection(lines, title, values) {
|
|
4024
4378
|
if (values.length === 0) {
|
|
4025
4379
|
return;
|
|
4026
4380
|
}
|
|
4027
|
-
lines.push(` ${
|
|
4381
|
+
lines.push(` ${chalk13.dim(title)}`);
|
|
4028
4382
|
lines.push("");
|
|
4029
4383
|
for (const value of values) {
|
|
4030
|
-
lines.push(` ${
|
|
4384
|
+
lines.push(` ${chalk13.white(value)}`);
|
|
4031
4385
|
}
|
|
4032
4386
|
lines.push("");
|
|
4033
4387
|
}
|
|
@@ -4036,10 +4390,10 @@ function appendTableSection(lines, title, entries) {
|
|
|
4036
4390
|
return;
|
|
4037
4391
|
}
|
|
4038
4392
|
const width = Math.max(...entries.map((entry) => entry.term.length), 0) + 2;
|
|
4039
|
-
lines.push(` ${
|
|
4393
|
+
lines.push(` ${chalk13.dim(title)}`);
|
|
4040
4394
|
lines.push("");
|
|
4041
4395
|
for (const entry of entries) {
|
|
4042
|
-
lines.push(` ${
|
|
4396
|
+
lines.push(` ${chalk13.white(padEnd(entry.term, width))}${chalk13.dim(entry.desc)}`);
|
|
4043
4397
|
}
|
|
4044
4398
|
lines.push("");
|
|
4045
4399
|
}
|
|
@@ -4053,7 +4407,7 @@ function commandPath(cmd) {
|
|
|
4053
4407
|
return parts.join(" ");
|
|
4054
4408
|
}
|
|
4055
4409
|
function formatRootHelp(cmd) {
|
|
4056
|
-
const version =
|
|
4410
|
+
const version = pkg3.version ?? "0.0.0";
|
|
4057
4411
|
const lines = [];
|
|
4058
4412
|
const groups = [
|
|
4059
4413
|
["Auth", []],
|
|
@@ -4064,10 +4418,10 @@ function formatRootHelp(cmd) {
|
|
|
4064
4418
|
["Other", []]
|
|
4065
4419
|
];
|
|
4066
4420
|
const otherGroup = groups.find(([name]) => name === "Other")[1];
|
|
4067
|
-
lines.push(`${
|
|
4421
|
+
lines.push(`${chalk13.bold(cliName)} ${chalk13.dim(`v${version}`)}`);
|
|
4068
4422
|
lines.push("");
|
|
4069
4423
|
if (cmd.description()) {
|
|
4070
|
-
lines.push(` ${
|
|
4424
|
+
lines.push(` ${chalk13.dim(cmd.description())}`);
|
|
4071
4425
|
lines.push("");
|
|
4072
4426
|
}
|
|
4073
4427
|
appendTextSection(lines, "Usage", [`${cliName} <command> [options]`]);
|
|
@@ -4082,7 +4436,7 @@ function formatRootHelp(cmd) {
|
|
|
4082
4436
|
groups[2][1].push(entry);
|
|
4083
4437
|
} else if (["open", "ssh", "ports"].includes(name)) {
|
|
4084
4438
|
groups[3][1].push(entry);
|
|
4085
|
-
} else if (["agent", "
|
|
4439
|
+
} else if (["agent", "acp"].includes(name)) {
|
|
4086
4440
|
groups[4][1].push(entry);
|
|
4087
4441
|
} else {
|
|
4088
4442
|
otherGroup.push(entry);
|
|
@@ -4118,10 +4472,10 @@ function formatSubcommandHelp(cmd, helper) {
|
|
|
4118
4472
|
term: helper.optionTerm(option),
|
|
4119
4473
|
desc: helper.optionDescription(option)
|
|
4120
4474
|
}));
|
|
4121
|
-
lines.push(
|
|
4475
|
+
lines.push(chalk13.bold(commandPath(cmd)));
|
|
4122
4476
|
lines.push("");
|
|
4123
4477
|
if (description) {
|
|
4124
|
-
lines.push(` ${
|
|
4478
|
+
lines.push(` ${chalk13.dim(description)}`);
|
|
4125
4479
|
lines.push("");
|
|
4126
4480
|
}
|
|
4127
4481
|
appendTextSection(lines, "Usage", [helper.commandUsage(cmd)]);
|
|
@@ -4144,8 +4498,9 @@ function applyHelpFormatting(cmd) {
|
|
|
4144
4498
|
applyHelpFormatting(subcommand);
|
|
4145
4499
|
}
|
|
4146
4500
|
}
|
|
4147
|
-
program.name(cliName).description("Agent Computer CLI").version(
|
|
4501
|
+
program.name(cliName).description("Agent Computer CLI").version(pkg3.version ?? "0.0.0").option("-y, --yes", "Skip confirmation prompts");
|
|
4148
4502
|
program.addCommand(loginCommand);
|
|
4503
|
+
program.addCommand(upgradeCommand);
|
|
4149
4504
|
program.addCommand(logoutCommand);
|
|
4150
4505
|
program.addCommand(whoamiCommand);
|
|
4151
4506
|
program.addCommand(claudeLoginCommand);
|
|
@@ -4155,7 +4510,6 @@ program.addCommand(lsCommand);
|
|
|
4155
4510
|
program.addCommand(getCommand);
|
|
4156
4511
|
program.addCommand(imageCommand);
|
|
4157
4512
|
program.addCommand(agentCommand);
|
|
4158
|
-
program.addCommand(fleetCommand);
|
|
4159
4513
|
program.addCommand(acpCommand);
|
|
4160
4514
|
program.addCommand(openCommand);
|
|
4161
4515
|
program.addCommand(sshCommand);
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "aicomputer",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "Computer CLI - manage your Agent Computer
|
|
3
|
+
"version": "0.1.14",
|
|
4
|
+
"description": "Computer CLI - manage your Agent Computer machines from the terminal",
|
|
5
5
|
"homepage": "https://agentcomputer.ai",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|