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.
Files changed (3) hide show
  1. package/README.md +12 -4
  2. package/dist/index.js +525 -171
  3. 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 fleet status` to see active agent work across every machine in your fleet.
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 Command12 } from "commander";
5
- import chalk12 from "chalk";
6
- import { readFileSync as readFileSync3 } from "fs";
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(error instanceof Error ? error.message : "Failed to open computer");
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", "22").action(async (identifier, options) => {
536
- if (options.setup) {
537
- await setupSSHAlias(options);
538
- return;
539
- }
540
- const spinner = ora(
541
- identifier ? "Preparing SSH access..." : "Fetching computers..."
542
- ).start();
543
- try {
544
- const computer = await resolveSSHComputer(identifier, spinner);
545
- const info = await getConnectionInfo(computer.id);
546
- if (!info.connection.ssh_available) {
547
- throw new Error("SSH is not available for this computer");
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("Manage published app ports");
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(error instanceof Error ? error.message : "Failed to fetch ports");
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("--subdomain <value>", "Custom left-hand host label before --<handle>").option("--protocol <value>", "http or https", "http").action(async (identifier, port, options) => {
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(`Published port ${chalk3.bold(String(published.target_port))}`);
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(error instanceof Error ? error.message : "Failed to publish port");
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(error instanceof Error ? error.message : "Failed to remove port");
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 ?? "22");
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(error instanceof Error ? error.message : "Failed to configure SSH alias");
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("ssh alias may contain only letters, numbers, dot, dash, or underscore");
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(computers, "Select a computer to SSH into");
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(target.handle, sshTarget);
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
- console.error(chalk5.red(`
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
- { id: "verify-primary", label: "Verify on target machine", state: "pending" },
1852
- { id: "verify-shared", label: "Verify shared-home propagation", state: "pending" },
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(createHash("sha256").update(codeVerifier).digest());
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("If the browser does not open automatically, use the URL below:\n");
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("You can paste either the full URL, or a value formatted as CODE#STATE.\n");
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("state mismatch detected; restart the authentication flow");
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(`Installing Claude auth on ${chalk5.bold(target.handle)}...`).start();
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(target, ["bash", "-s"], installScript);
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(`Verifying Claude login on ${chalk5.bold(handle)}...`).start();
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(`Verified shared-home Claude login on ${chalk5.bold(result.handle)}`);
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 fleet acp rm completion help"
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(target.handle, sshTarget);
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
- console.error(chalk7.red(`
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
- { id: "verify-primary", label: "Verify on target machine", state: "pending" },
3040
- { id: "verify-shared", label: "Verify shared-home propagation", state: "pending" },
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("local Codex login is required when not running interactively");
3147
+ throw new Error(
3148
+ "local Codex login is required when not running interactively"
3149
+ );
3087
3150
  }
3088
- console.log("We will open your browser so you can authenticate Codex locally.");
3089
- console.log("If Codex falls back to device auth, complete that flow and return here.\n");
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(`Installing Codex login on ${chalk7.bold(target.handle)}...`).start();
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(`Verifying Codex login on ${chalk7.bold(handle)}...`).start();
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(`Verified shared-home Codex login on ${chalk7.bold(result.handle)}`);
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 { createServer } from "http";
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 = 5 * 60 * 1e3;
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(response, 409, renderErrorPage("This login link has already been used."));
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(response, 200, renderSuccessPage(me.user.email));
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(email) {
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>. You can close this tab.</p>
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(chalk9.yellow(" Already logged in. Use --force to overwrite."));
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(chalk9.yellow(" Browser auto-open failed. Open this URL to continue:"));
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(error instanceof Error ? error.message : "Browser login failed");
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/whoami.ts
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 whoamiCommand = new Command11("whoami").description("Show current user").option("--json", "Print raw JSON").action(async (options) => {
3989
- const spinner = options.json ? null : ora9("Loading user...").start();
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(` ${chalk11.bold.white(me.user.display_name || me.user.email)}`);
4352
+ console.log(` ${chalk12.bold.white(me.user.display_name || me.user.email)}`);
3999
4353
  if (me.user.display_name) {
4000
- console.log(` ${chalk11.dim(me.user.email)}`);
4354
+ console.log(` ${chalk12.dim(me.user.email)}`);
4001
4355
  }
4002
4356
  if (me.api_key.name) {
4003
- console.log(` ${chalk11.dim("Key:")} ${me.api_key.name}`);
4357
+ console.log(` ${chalk12.dim("Key:")} ${me.api_key.name}`);
4004
4358
  }
4005
- console.log(` ${chalk11.dim("API:")} ${chalk11.dim(getBaseURL())}`);
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 pkg2 = JSON.parse(
4019
- readFileSync3(new URL("../package.json", import.meta.url), "utf8")
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 Command12();
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(` ${chalk12.dim(title)}`);
4381
+ lines.push(` ${chalk13.dim(title)}`);
4028
4382
  lines.push("");
4029
4383
  for (const value of values) {
4030
- lines.push(` ${chalk12.white(value)}`);
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(` ${chalk12.dim(title)}`);
4393
+ lines.push(` ${chalk13.dim(title)}`);
4040
4394
  lines.push("");
4041
4395
  for (const entry of entries) {
4042
- lines.push(` ${chalk12.white(padEnd(entry.term, width))}${chalk12.dim(entry.desc)}`);
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 = pkg2.version ?? "0.0.0";
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(`${chalk12.bold(cliName)} ${chalk12.dim(`v${version}`)}`);
4421
+ lines.push(`${chalk13.bold(cliName)} ${chalk13.dim(`v${version}`)}`);
4068
4422
  lines.push("");
4069
4423
  if (cmd.description()) {
4070
- lines.push(` ${chalk12.dim(cmd.description())}`);
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", "fleet", "acp"].includes(name)) {
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(chalk12.bold(commandPath(cmd)));
4475
+ lines.push(chalk13.bold(commandPath(cmd)));
4122
4476
  lines.push("");
4123
4477
  if (description) {
4124
- lines.push(` ${chalk12.dim(description)}`);
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(pkg2.version ?? "0.0.0").option("-y, --yes", "Skip confirmation prompts");
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.12",
4
- "description": "Computer CLI - manage your Agent Computer fleet from the terminal",
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",