aicomputer 0.1.8 → 0.1.9

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 (2) hide show
  1. package/dist/index.js +720 -344
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/index.ts
4
- import { Command as Command11 } from "commander";
5
- import chalk11 from "chalk";
4
+ import { Command as Command12 } from "commander";
5
+ import chalk12 from "chalk";
6
6
  import { readFileSync as readFileSync3 } from "fs";
7
7
  import { basename as basename2 } from "path";
8
8
 
@@ -1524,20 +1524,218 @@ fleetCommand.command("status").description("List open agent sessions across all
1524
1524
  });
1525
1525
 
1526
1526
  // src/commands/claude-auth.ts
1527
- import { randomBytes, createHash } from "crypto";
1528
- import { spawn as spawn2 } from "child_process";
1527
+ import { randomBytes as randomBytes2, createHash } from "crypto";
1529
1528
  import { input as textInput } from "@inquirer/prompts";
1530
1529
  import { Command as Command4 } from "commander";
1531
1530
  import chalk5 from "chalk";
1531
+ import ora4 from "ora";
1532
+
1533
+ // src/lib/remote-auth.ts
1534
+ import { randomBytes } from "crypto";
1535
+ import { spawn as spawn2 } from "child_process";
1532
1536
  import ora3 from "ora";
1537
+ var readyPollIntervalMs = 2e3;
1538
+ var readyPollTimeoutMs = 18e4;
1539
+ async function prepareAuthTarget(options, config) {
1540
+ if (options.machine?.trim()) {
1541
+ const computer2 = await resolveComputer(options.machine.trim());
1542
+ assertSSHAuthTarget(computer2);
1543
+ return {
1544
+ computer: computer2,
1545
+ helperCreated: false,
1546
+ sharedInstall: isSharedInstallTarget(computer2),
1547
+ detail: describeTarget(computer2, false)
1548
+ };
1549
+ }
1550
+ const computers = await listComputers();
1551
+ const filesystemSettings = await getFilesystemSettings().catch(() => null);
1552
+ if (filesystemSettings?.shared_enabled) {
1553
+ const existing = pickSharedRunningComputer(computers);
1554
+ if (existing) {
1555
+ return {
1556
+ computer: existing,
1557
+ helperCreated: false,
1558
+ sharedInstall: true,
1559
+ detail: describeTarget(existing, false)
1560
+ };
1561
+ }
1562
+ const spinner = ora3(`Creating temporary ${config.helperPrefix} helper...`).start();
1563
+ try {
1564
+ const helper = await createComputer({
1565
+ handle: `${config.helperPrefix}-${randomSuffix(6)}`,
1566
+ display_name: config.helperDisplayName,
1567
+ runtime_family: "managed-worker",
1568
+ use_platform_default: true,
1569
+ ssh_enabled: true,
1570
+ vnc_enabled: false
1571
+ });
1572
+ spinner.succeed(`Created temporary helper ${helper.handle}`);
1573
+ return {
1574
+ computer: helper,
1575
+ helperCreated: true,
1576
+ sharedInstall: true,
1577
+ detail: describeTarget(helper, true)
1578
+ };
1579
+ } catch (error) {
1580
+ spinner.fail(
1581
+ error instanceof Error ? error.message : "Failed to create temporary helper"
1582
+ );
1583
+ throw error;
1584
+ }
1585
+ }
1586
+ const computer = await promptForSSHComputer(computers, config.promptMessage);
1587
+ return {
1588
+ computer,
1589
+ helperCreated: false,
1590
+ sharedInstall: isSharedInstallTarget(computer),
1591
+ detail: describeTarget(computer, false)
1592
+ };
1593
+ }
1594
+ async function waitForRunning(initial) {
1595
+ if (initial.status === "running") {
1596
+ return initial;
1597
+ }
1598
+ const spinner = ora3(`Waiting for ${initial.handle} to be ready...`).start();
1599
+ const deadline = Date.now() + readyPollTimeoutMs;
1600
+ let lastStatus = initial.status;
1601
+ while (Date.now() < deadline) {
1602
+ const current = await getComputerByID(initial.id);
1603
+ if (current.status === "running") {
1604
+ spinner.succeed(`${current.handle} is ready`);
1605
+ return current;
1606
+ }
1607
+ if (current.status !== lastStatus) {
1608
+ lastStatus = current.status;
1609
+ spinner.text = `Waiting for ${current.handle}... ${current.status}`;
1610
+ }
1611
+ if (current.status === "error" || current.status === "deleted" || current.status === "stopped") {
1612
+ spinner.fail(`${current.handle} entered ${current.status}`);
1613
+ throw new Error(current.last_error || `${current.handle} entered ${current.status}`);
1614
+ }
1615
+ await delay(readyPollIntervalMs);
1616
+ }
1617
+ spinner.fail(`Timed out waiting for ${initial.handle}`);
1618
+ throw new Error(`timed out waiting for ${initial.handle} to be ready`);
1619
+ }
1620
+ async function resolveSSHTarget(computer) {
1621
+ assertSSHAuthTarget(computer);
1622
+ const registered = await ensureDefaultSSHKeyRegistered();
1623
+ const info = await getConnectionInfo(computer.id);
1624
+ if (!info.connection.ssh_available) {
1625
+ throw new Error(`SSH is not available for ${computer.handle}`);
1626
+ }
1627
+ return {
1628
+ handle: computer.handle,
1629
+ host: info.connection.ssh_host,
1630
+ port: info.connection.ssh_port,
1631
+ user: info.connection.ssh_user,
1632
+ identityFile: registered.privateKeyPath
1633
+ };
1634
+ }
1635
+ async function verifySecondaryMachine(primaryComputerID, sharedInstall, skip, verify) {
1636
+ if (!sharedInstall) {
1637
+ return { status: "skipped", reason: "target uses isolated filesystem" };
1638
+ }
1639
+ if (skip) {
1640
+ return { status: "skipped", reason: "cross-check skipped by flag" };
1641
+ }
1642
+ const secondary = (await listComputers()).filter(
1643
+ (computer) => computer.id !== primaryComputerID && computer.runtime_family === "managed-worker" && computer.filesystem_mode === "shared" && computer.ssh_enabled && computer.status === "running"
1644
+ ).sort(
1645
+ (left, right) => new Date(right.updated_at).getTime() - new Date(left.updated_at).getTime()
1646
+ )[0];
1647
+ if (!secondary) {
1648
+ return {
1649
+ status: "skipped",
1650
+ reason: "no second running shared managed-worker was available"
1651
+ };
1652
+ }
1653
+ const sshTarget = await resolveSSHTarget(secondary);
1654
+ const verification = await verify(sshTarget);
1655
+ if (verification.status !== "verified") {
1656
+ return { status: "skipped", reason: verification.detail };
1657
+ }
1658
+ return { status: "verified", handle: secondary.handle };
1659
+ }
1660
+ async function runRemoteCommand(target, remoteArgs, script) {
1661
+ const args = [
1662
+ "-T",
1663
+ "-i",
1664
+ target.identityFile,
1665
+ "-p",
1666
+ String(target.port),
1667
+ `${target.user}@${target.host}`,
1668
+ ...remoteArgs
1669
+ ];
1670
+ return new Promise((resolve, reject) => {
1671
+ const child = spawn2("ssh", args, {
1672
+ stdio: ["pipe", "pipe", "pipe"]
1673
+ });
1674
+ let stdout = "";
1675
+ let stderr = "";
1676
+ child.stdout.on("data", (chunk) => {
1677
+ stdout += chunk.toString();
1678
+ });
1679
+ child.stderr.on("data", (chunk) => {
1680
+ stderr += chunk.toString();
1681
+ });
1682
+ child.on("error", reject);
1683
+ child.on("exit", (code) => {
1684
+ if (code === 0) {
1685
+ resolve({ stdout, stderr });
1686
+ return;
1687
+ }
1688
+ const message = stderr.trim() || stdout.trim() || `ssh exited with code ${code ?? 1}`;
1689
+ reject(new Error(message));
1690
+ });
1691
+ if (script !== void 0) {
1692
+ child.stdin.end(script);
1693
+ return;
1694
+ }
1695
+ child.stdin.end();
1696
+ });
1697
+ }
1698
+ function isSharedInstallTarget(computer) {
1699
+ return computer.runtime_family === "managed-worker" && computer.filesystem_mode === "shared";
1700
+ }
1701
+ function assertSSHAuthTarget(computer) {
1702
+ if (!computer.ssh_enabled) {
1703
+ throw new Error(`${computer.handle} does not have SSH enabled`);
1704
+ }
1705
+ }
1706
+ function randomSuffix(length) {
1707
+ return randomBytes(Math.ceil(length / 2)).toString("hex").slice(0, length);
1708
+ }
1709
+ function pickSharedRunningComputer(computers) {
1710
+ const candidates = computers.filter(
1711
+ (computer) => computer.runtime_family === "managed-worker" && computer.filesystem_mode === "shared" && computer.ssh_enabled && computer.status === "running"
1712
+ ).sort(
1713
+ (left, right) => new Date(right.updated_at).getTime() - new Date(left.updated_at).getTime()
1714
+ );
1715
+ return candidates[0] ?? null;
1716
+ }
1717
+ function describeTarget(computer, helperCreated) {
1718
+ if (helperCreated) {
1719
+ return `created temporary helper ${computer.handle}`;
1720
+ }
1721
+ if (isSharedInstallTarget(computer)) {
1722
+ return `using shared machine ${computer.handle}`;
1723
+ }
1724
+ return `using ${computer.handle}`;
1725
+ }
1726
+ function delay(ms) {
1727
+ return new Promise((resolve) => {
1728
+ setTimeout(resolve, ms);
1729
+ });
1730
+ }
1731
+
1732
+ // src/commands/claude-auth.ts
1533
1733
  var CLAUDE_OAUTH_CLIENT_ID = process.env.CLAUDE_OAUTH_CLIENT_ID ?? "9d1c250a-e61b-44d9-88ed-5944d1962f5e";
1534
1734
  var CLAUDE_OAUTH_AUTHORIZE_URL = process.env.CLAUDE_OAUTH_AUTHORIZE_URL ?? "https://claude.ai/oauth/authorize";
1535
1735
  var CLAUDE_OAUTH_TOKEN_URL = process.env.CLAUDE_OAUTH_TOKEN_URL ?? "https://platform.claude.com/v1/oauth/token";
1536
1736
  var CLAUDE_OAUTH_REDIRECT_URL = process.env.CLAUDE_OAUTH_REDIRECT_URL ?? "https://platform.claude.com/oauth/code/callback";
1537
1737
  var CLAUDE_OAUTH_SCOPES = (process.env.CLAUDE_OAUTH_SCOPES ?? "user:profile user:inference user:sessions:claude_code user:mcp_servers user:file_upload").split(/\s+/).filter(Boolean);
1538
- var readyPollIntervalMs = 2e3;
1539
- var readyPollTimeoutMs = 18e4;
1540
- var claudeAuthCommand = new Command4("claude-auth").alias("claude-login").description("Authenticate Claude Code on a computer").option("--machine <id-or-handle>", "Use a specific computer").option("--keep-helper", "Keep a temporary helper machine if one is created").option("--skip-cross-check", "Skip verification on a second shared machine").option("--verbose", "Show step-by-step auth diagnostics").action(async (options) => {
1738
+ 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) => {
1541
1739
  const todos = createTodoList();
1542
1740
  let target = null;
1543
1741
  let helperCreated = false;
@@ -1573,7 +1771,7 @@ var claudeAuthCommand = new Command4("claude-auth").alias("claude-login").descri
1573
1771
  sharedInstall ? `installed Claude login on shared home via ${target.handle}` : `installed Claude login on ${target.handle}`
1574
1772
  );
1575
1773
  activeTodoID = "verify-primary";
1576
- const primaryVerification = await verifyStoredAuth(sshTarget);
1774
+ const primaryVerification = await verifyTargetMachine(target.handle, sshTarget);
1577
1775
  markVerificationTodo(
1578
1776
  todos,
1579
1777
  "verify-primary",
@@ -1581,10 +1779,12 @@ var claudeAuthCommand = new Command4("claude-auth").alias("claude-login").descri
1581
1779
  `${target.handle} fresh login shell sees Claude auth`
1582
1780
  );
1583
1781
  activeTodoID = "verify-shared";
1584
- const sharedCheck = primaryVerification.status === "verified" ? await verifySecondaryMachine(
1782
+ const sharedCheck = primaryVerification.status === "verified" ? await verifySharedInstall(
1783
+ target.handle,
1585
1784
  target.id,
1586
1785
  sharedInstall,
1587
- Boolean(options.skipCrossCheck)
1786
+ Boolean(options.skipCrossCheck),
1787
+ verifyStoredAuth
1588
1788
  ) : {
1589
1789
  status: "skipped",
1590
1790
  reason: "primary verification was inconclusive"
@@ -1680,174 +1880,72 @@ function printTodoList(items) {
1680
1880
  console.log();
1681
1881
  }
1682
1882
  async function prepareTargetMachine(options) {
1683
- if (options.machine?.trim()) {
1684
- const computer2 = await resolveComputer(options.machine.trim());
1685
- assertClaudeAuthTarget(computer2);
1883
+ return prepareAuthTarget(options, {
1884
+ helperPrefix: "claude-auth",
1885
+ helperDisplayName: "Claude Auth Helper",
1886
+ promptMessage: "Select a computer for Claude auth"
1887
+ });
1888
+ }
1889
+ async function runManualOAuthFlow() {
1890
+ const codeVerifier = base64url(randomBytes2(32));
1891
+ const state = randomBytes2(16).toString("hex");
1892
+ const codeChallenge = base64url(createHash("sha256").update(codeVerifier).digest());
1893
+ const url = buildAuthorizationURL(codeChallenge, state);
1894
+ 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");
1896
+ console.log(url);
1897
+ console.log();
1898
+ try {
1899
+ await openBrowserURL(url);
1900
+ } catch {
1901
+ console.log(chalk5.yellow("Unable to open the browser automatically."));
1902
+ }
1903
+ console.log(
1904
+ "After completing authentication, copy the code shown on the success page."
1905
+ );
1906
+ console.log("You can paste either the full URL, or a value formatted as CODE#STATE.\n");
1907
+ const pasted = (await textInput({
1908
+ message: "Paste the authorization code (or URL) here:"
1909
+ })).trim();
1910
+ if (!pasted) {
1911
+ throw new Error("no authorization code provided");
1912
+ }
1913
+ const parsed = parseAuthorizationInput(pasted, state);
1914
+ const spinner = ora4("Exchanging authorization code...").start();
1915
+ try {
1916
+ const response = await fetch(CLAUDE_OAUTH_TOKEN_URL, {
1917
+ method: "POST",
1918
+ headers: {
1919
+ "Content-Type": "application/json"
1920
+ },
1921
+ body: JSON.stringify({
1922
+ grant_type: "authorization_code",
1923
+ code: parsed.code,
1924
+ state: parsed.state,
1925
+ redirect_uri: CLAUDE_OAUTH_REDIRECT_URL,
1926
+ client_id: CLAUDE_OAUTH_CLIENT_ID,
1927
+ code_verifier: codeVerifier
1928
+ })
1929
+ });
1930
+ if (!response.ok) {
1931
+ throw new Error(
1932
+ `token exchange failed: ${response.status} ${await response.text()}`
1933
+ );
1934
+ }
1935
+ const payload = await response.json();
1936
+ if (!payload.refresh_token || !payload.scope) {
1937
+ throw new Error("token exchange returned an incomplete response");
1938
+ }
1939
+ spinner.succeed("Authorization code exchanged");
1686
1940
  return {
1687
- computer: computer2,
1688
- helperCreated: false,
1689
- sharedInstall: isSharedInstallTarget(computer2),
1690
- detail: describeTarget(computer2, false)
1941
+ refreshToken: payload.refresh_token,
1942
+ scope: payload.scope
1691
1943
  };
1692
- }
1693
- const computers = await listComputers();
1694
- const filesystemSettings = await getFilesystemSettings().catch(() => null);
1695
- if (filesystemSettings?.shared_enabled) {
1696
- const existing = pickSharedRunningComputer(computers);
1697
- if (existing) {
1698
- return {
1699
- computer: existing,
1700
- helperCreated: false,
1701
- sharedInstall: true,
1702
- detail: describeTarget(existing, false)
1703
- };
1704
- }
1705
- const spinner = ora3("Creating temporary shared helper...").start();
1706
- try {
1707
- const helper = await createComputer({
1708
- handle: `claude-auth-${randomSuffix(6)}`,
1709
- display_name: "Claude Auth Helper",
1710
- runtime_family: "managed-worker",
1711
- use_platform_default: true,
1712
- ssh_enabled: true,
1713
- vnc_enabled: false
1714
- });
1715
- spinner.succeed(`Created temporary helper ${chalk5.bold(helper.handle)}`);
1716
- return {
1717
- computer: helper,
1718
- helperCreated: true,
1719
- sharedInstall: true,
1720
- detail: describeTarget(helper, true)
1721
- };
1722
- } catch (error) {
1723
- spinner.fail(
1724
- error instanceof Error ? error.message : "Failed to create temporary helper"
1725
- );
1726
- throw error;
1727
- }
1728
- }
1729
- const computer = await promptForSSHComputer(
1730
- computers,
1731
- "Select a computer for Claude auth"
1732
- );
1733
- return {
1734
- computer,
1735
- helperCreated: false,
1736
- sharedInstall: isSharedInstallTarget(computer),
1737
- detail: describeTarget(computer, false)
1738
- };
1739
- }
1740
- function pickSharedRunningComputer(computers) {
1741
- const candidates = computers.filter(
1742
- (computer) => computer.runtime_family === "managed-worker" && computer.filesystem_mode === "shared" && computer.ssh_enabled && computer.status === "running"
1743
- ).sort(
1744
- (left, right) => new Date(right.updated_at).getTime() - new Date(left.updated_at).getTime()
1745
- );
1746
- return candidates[0] ?? null;
1747
- }
1748
- function assertClaudeAuthTarget(computer) {
1749
- if (!computer.ssh_enabled) {
1750
- throw new Error(`${computer.handle} does not have SSH enabled`);
1751
- }
1752
- }
1753
- function isSharedInstallTarget(computer) {
1754
- return computer.runtime_family === "managed-worker" && computer.filesystem_mode === "shared";
1755
- }
1756
- function describeTarget(computer, helperCreated) {
1757
- if (helperCreated) {
1758
- return `created temporary helper ${computer.handle}`;
1759
- }
1760
- if (isSharedInstallTarget(computer)) {
1761
- return `using shared machine ${computer.handle}`;
1762
- }
1763
- return `using ${computer.handle}`;
1764
- }
1765
- async function waitForRunning(initial) {
1766
- if (initial.status === "running") {
1767
- return initial;
1768
- }
1769
- const spinner = ora3(`Waiting for ${chalk5.bold(initial.handle)} to be ready...`).start();
1770
- const deadline = Date.now() + readyPollTimeoutMs;
1771
- let lastStatus = initial.status;
1772
- while (Date.now() < deadline) {
1773
- const current = await getComputerByID(initial.id);
1774
- if (current.status === "running") {
1775
- spinner.succeed(`${chalk5.bold(current.handle)} is ready`);
1776
- return current;
1777
- }
1778
- if (current.status !== lastStatus) {
1779
- lastStatus = current.status;
1780
- spinner.text = `Waiting for ${chalk5.bold(current.handle)}... ${chalk5.dim(current.status)}`;
1781
- }
1782
- if (current.status === "error" || current.status === "deleted" || current.status === "stopped") {
1783
- spinner.fail(`${current.handle} entered ${current.status}`);
1784
- throw new Error(current.last_error || `${current.handle} entered ${current.status}`);
1785
- }
1786
- await delay(readyPollIntervalMs);
1787
- }
1788
- spinner.fail(`Timed out waiting for ${initial.handle}`);
1789
- throw new Error(`timed out waiting for ${initial.handle} to be ready`);
1790
- }
1791
- async function runManualOAuthFlow() {
1792
- const codeVerifier = base64url(randomBytes(32));
1793
- const state = randomBytes(16).toString("hex");
1794
- const codeChallenge = base64url(createHash("sha256").update(codeVerifier).digest());
1795
- const url = buildAuthorizationURL(codeChallenge, state);
1796
- console.log("We will open your browser so you can authenticate with Claude.");
1797
- console.log("If the browser does not open automatically, use the URL below:\n");
1798
- console.log(url);
1799
- console.log();
1800
- try {
1801
- await openBrowserURL(url);
1802
- } catch {
1803
- console.log(chalk5.yellow("Unable to open the browser automatically."));
1804
- }
1805
- console.log(
1806
- "After completing authentication, copy the code shown on the success page."
1807
- );
1808
- console.log("You can paste either the full URL, or a value formatted as CODE#STATE.\n");
1809
- const pasted = (await textInput({
1810
- message: "Paste the authorization code (or URL) here:"
1811
- })).trim();
1812
- if (!pasted) {
1813
- throw new Error("no authorization code provided");
1814
- }
1815
- const parsed = parseAuthorizationInput(pasted, state);
1816
- const spinner = ora3("Exchanging authorization code...").start();
1817
- try {
1818
- const response = await fetch(CLAUDE_OAUTH_TOKEN_URL, {
1819
- method: "POST",
1820
- headers: {
1821
- "Content-Type": "application/json"
1822
- },
1823
- body: JSON.stringify({
1824
- grant_type: "authorization_code",
1825
- code: parsed.code,
1826
- state: parsed.state,
1827
- redirect_uri: CLAUDE_OAUTH_REDIRECT_URL,
1828
- client_id: CLAUDE_OAUTH_CLIENT_ID,
1829
- code_verifier: codeVerifier
1830
- })
1831
- });
1832
- if (!response.ok) {
1833
- throw new Error(
1834
- `token exchange failed: ${response.status} ${await response.text()}`
1835
- );
1836
- }
1837
- const payload = await response.json();
1838
- if (!payload.refresh_token || !payload.scope) {
1839
- throw new Error("token exchange returned an incomplete response");
1840
- }
1841
- spinner.succeed("Authorization code exchanged");
1842
- return {
1843
- refreshToken: payload.refresh_token,
1844
- scope: payload.scope
1845
- };
1846
- } catch (error) {
1847
- spinner.fail(
1848
- error instanceof Error ? error.message : "Failed to exchange authorization code"
1849
- );
1850
- throw error;
1944
+ } catch (error) {
1945
+ spinner.fail(
1946
+ error instanceof Error ? error.message : "Failed to exchange authorization code"
1947
+ );
1948
+ throw error;
1851
1949
  }
1852
1950
  }
1853
1951
  function buildAuthorizationURL(codeChallenge, state) {
@@ -1885,22 +1983,8 @@ function parseAuthorizationInput(value, expectedState) {
1885
1983
  }
1886
1984
  return { code, state };
1887
1985
  }
1888
- async function resolveSSHTarget(computer) {
1889
- const registered = await ensureDefaultSSHKeyRegistered();
1890
- const info = await getConnectionInfo(computer.id);
1891
- if (!info.connection.ssh_available) {
1892
- throw new Error(`SSH is not available for ${computer.handle}`);
1893
- }
1894
- return {
1895
- handle: computer.handle,
1896
- host: info.connection.ssh_host,
1897
- port: info.connection.ssh_port,
1898
- user: info.connection.ssh_user,
1899
- identityFile: registered.privateKeyPath
1900
- };
1901
- }
1902
1986
  async function installClaudeAuth(target, oauth) {
1903
- const spinner = ora3(`Installing Claude auth on ${chalk5.bold(target.handle)}...`).start();
1987
+ const spinner = ora4(`Installing Claude auth on ${chalk5.bold(target.handle)}...`).start();
1904
1988
  try {
1905
1989
  const installScript = buildInstallScript(oauth.refreshToken, oauth.scope);
1906
1990
  const result = await runRemoteCommand(target, ["bash", "-s"], installScript);
@@ -1916,9 +2000,36 @@ async function installClaudeAuth(target, oauth) {
1916
2000
  throw error;
1917
2001
  }
1918
2002
  }
2003
+ async function verifyTargetMachine(handle, target) {
2004
+ const spinner = ora4(`Verifying Claude login on ${chalk5.bold(handle)}...`).start();
2005
+ const result = await verifyStoredAuth(target);
2006
+ if (result.status === "verified") {
2007
+ spinner.succeed(`Verified Claude login on ${chalk5.bold(handle)}`);
2008
+ return result;
2009
+ }
2010
+ spinner.warn(result.detail);
2011
+ return result;
2012
+ }
2013
+ async function verifySharedInstall(primaryHandle, primaryComputerID, sharedInstall, skip, verify) {
2014
+ const spinner = ora4(
2015
+ `Verifying shared-home Claude login from ${chalk5.bold(primaryHandle)}...`
2016
+ ).start();
2017
+ const result = await verifySecondaryMachine(
2018
+ primaryComputerID,
2019
+ sharedInstall,
2020
+ skip,
2021
+ verify
2022
+ );
2023
+ if (result.status === "verified") {
2024
+ spinner.succeed(`Verified shared-home Claude login on ${chalk5.bold(result.handle)}`);
2025
+ return result;
2026
+ }
2027
+ spinner.info(result.reason);
2028
+ return result;
2029
+ }
1919
2030
  function buildInstallScript(refreshToken, scopes) {
1920
- const tokenMarker = `TOKEN_${randomSuffix(12)}`;
1921
- const scopeMarker = `SCOPES_${randomSuffix(12)}`;
2031
+ const tokenMarker = `TOKEN_${randomSuffix2(12)}`;
2032
+ const scopeMarker = `SCOPES_${randomSuffix2(12)}`;
1922
2033
  return [
1923
2034
  "set -euo pipefail",
1924
2035
  'command -v claude >/dev/null 2>&1 || { echo "claude is not installed on this computer" >&2; exit 1; }',
@@ -1965,12 +2076,12 @@ function parseStatusOutput(stdout, stderr) {
1965
2076
  const end = combined.lastIndexOf("}");
1966
2077
  if (start === -1 || end === -1 || end <= start) {
1967
2078
  const normalized = combined.toLowerCase();
1968
- if (normalized.includes("logged in")) {
1969
- return { loggedIn: true };
1970
- }
1971
2079
  if (normalized.includes("not logged in") || normalized.includes("logged out")) {
1972
2080
  return { loggedIn: false, detail: firstStatusLine(combined) };
1973
2081
  }
2082
+ if (normalized.includes("logged in")) {
2083
+ return { loggedIn: true };
2084
+ }
1974
2085
  throw new Error(
1975
2086
  combined ? `could not verify Claude auth from status output: ${firstStatusLine(combined)}` : "could not verify Claude auth from empty status output"
1976
2087
  );
@@ -1984,85 +2095,17 @@ function parseStatusOutput(stdout, stderr) {
1984
2095
  function firstStatusLine(value) {
1985
2096
  return value.split(/\r?\n/).map((line) => line.trim()).find(Boolean) ?? "unknown output";
1986
2097
  }
1987
- async function verifySecondaryMachine(primaryComputerID, sharedInstall, skip) {
1988
- if (!sharedInstall) {
1989
- return { status: "skipped", reason: "target uses isolated filesystem" };
1990
- }
1991
- if (skip) {
1992
- return { status: "skipped", reason: "cross-check skipped by flag" };
1993
- }
1994
- const secondary = (await listComputers()).filter(
1995
- (computer) => computer.id !== primaryComputerID && computer.runtime_family === "managed-worker" && computer.filesystem_mode === "shared" && computer.ssh_enabled && computer.status === "running"
1996
- ).sort(
1997
- (left, right) => new Date(right.updated_at).getTime() - new Date(left.updated_at).getTime()
1998
- )[0];
1999
- if (!secondary) {
2000
- return {
2001
- status: "skipped",
2002
- reason: "no second running shared managed-worker was available"
2003
- };
2004
- }
2005
- const sshTarget = await resolveSSHTarget(secondary);
2006
- const verification = await verifyStoredAuth(sshTarget);
2007
- if (verification.status !== "verified") {
2008
- return { status: "skipped", reason: verification.detail };
2009
- }
2010
- return { status: "verified", handle: secondary.handle };
2011
- }
2012
- async function runRemoteCommand(target, remoteArgs, script) {
2013
- const args = [
2014
- "-T",
2015
- "-i",
2016
- target.identityFile,
2017
- "-p",
2018
- String(target.port),
2019
- `${target.user}@${target.host}`,
2020
- ...remoteArgs
2021
- ];
2022
- return new Promise((resolve, reject) => {
2023
- const child = spawn2("ssh", args, {
2024
- stdio: ["pipe", "pipe", "pipe"]
2025
- });
2026
- let stdout = "";
2027
- let stderr = "";
2028
- child.stdout.on("data", (chunk) => {
2029
- stdout += chunk.toString();
2030
- });
2031
- child.stderr.on("data", (chunk) => {
2032
- stderr += chunk.toString();
2033
- });
2034
- child.on("error", reject);
2035
- child.on("exit", (code) => {
2036
- if (code === 0) {
2037
- resolve({ stdout, stderr });
2038
- return;
2039
- }
2040
- const message = stderr.trim() || stdout.trim() || `ssh exited with code ${code ?? 1}`;
2041
- reject(new Error(message));
2042
- });
2043
- if (script !== void 0) {
2044
- child.stdin.end(script);
2045
- } else {
2046
- child.stdin.end();
2047
- }
2048
- });
2049
- }
2050
2098
  function base64url(buffer) {
2051
2099
  return buffer.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
2052
2100
  }
2053
- function randomSuffix(length) {
2054
- return randomBytes(Math.ceil(length / 2)).toString("hex").slice(0, length);
2055
- }
2056
- function delay(ms) {
2057
- return new Promise((resolve) => {
2058
- setTimeout(resolve, ms);
2059
- });
2101
+ function randomSuffix2(length) {
2102
+ return randomBytes2(Math.ceil(length / 2)).toString("hex").slice(0, length);
2060
2103
  }
2061
2104
 
2062
2105
  // src/commands/computers.ts
2063
2106
  import { Command as Command5 } from "commander";
2064
2107
  import chalk6 from "chalk";
2065
- import ora4 from "ora";
2108
+ import ora5 from "ora";
2066
2109
  import { select as select2, input as textInput2, confirm } from "@inquirer/prompts";
2067
2110
 
2068
2111
  // src/lib/machine-sources.ts
@@ -2227,7 +2270,7 @@ function printComputerTableVerbose(computers) {
2227
2270
  console.log();
2228
2271
  }
2229
2272
  var lsCommand = new Command5("ls").description("List computers").option("--json", "Print raw JSON").option("-v, --verbose", "Show all URLs for each computer").action(async (options) => {
2230
- const spinner = options.json ? null : ora4("Fetching computers...").start();
2273
+ const spinner = options.json ? null : ora5("Fetching computers...").start();
2231
2274
  try {
2232
2275
  const computers = await listComputers();
2233
2276
  spinner?.stop();
@@ -2258,7 +2301,7 @@ var lsCommand = new Command5("ls").description("List computers").option("--json"
2258
2301
  }
2259
2302
  });
2260
2303
  var getCommand = new Command5("get").description("Show computer details").argument("<id-or-handle>", "Computer id or handle").option("--json", "Print raw JSON").action(async (identifier, options) => {
2261
- const spinner = options.json ? null : ora4("Fetching computer...").start();
2304
+ const spinner = options.json ? null : ora5("Fetching computer...").start();
2262
2305
  try {
2263
2306
  const computer = await resolveComputer(identifier);
2264
2307
  spinner?.stop();
@@ -2299,7 +2342,7 @@ var createCommand = new Command5("create").description("Create a computer").argu
2299
2342
  if (provisioningNote) {
2300
2343
  console.log(chalk6.dim(provisioningNote));
2301
2344
  }
2302
- spinner = ora4(createSpinnerText(runtimeFamily, filesystemSettings, 0)).start();
2345
+ spinner = ora5(createSpinnerText(runtimeFamily, filesystemSettings, 0)).start();
2303
2346
  startTime = Date.now();
2304
2347
  timer = setInterval(() => {
2305
2348
  const elapsed2 = (Date.now() - startTime) / 1e3;
@@ -2405,7 +2448,7 @@ async function resolveCreateOptions(options) {
2405
2448
  var removeCommand = new Command5("rm").description("Delete a computer").argument("<id-or-handle>", "Computer id or handle").option("-y, --yes", "Skip confirmation prompt").action(async (identifier, options, cmd) => {
2406
2449
  const globalYes = cmd.parent?.opts()?.yes;
2407
2450
  const skipConfirm = Boolean(options.yes || globalYes);
2408
- const spinner = ora4("Resolving computer...").start();
2451
+ const spinner = ora5("Resolving computer...").start();
2409
2452
  try {
2410
2453
  const computer = await resolveComputer(identifier);
2411
2454
  spinner.stop();
@@ -2419,7 +2462,7 @@ var removeCommand = new Command5("rm").description("Delete a computer").argument
2419
2462
  return;
2420
2463
  }
2421
2464
  }
2422
- const deleteSpinner = ora4("Deleting computer...").start();
2465
+ const deleteSpinner = ora5("Deleting computer...").start();
2423
2466
  await api(`/v1/computers/${computer.id}`, {
2424
2467
  method: "DELETE"
2425
2468
  });
@@ -2558,8 +2601,10 @@ _computer() {
2558
2601
  'login:Authenticate the CLI'
2559
2602
  'logout:Remove stored API key'
2560
2603
  'whoami:Show current user'
2561
- 'claude-auth:Authenticate Claude Code on a computer'
2562
- 'claude-login:Alias for claude-auth'
2604
+ 'claude-login:Authenticate Claude Code on a computer'
2605
+ 'claude-auth:Alias for claude-login'
2606
+ 'codex-login:Authenticate Codex on a computer'
2607
+ 'codex-auth:Alias for codex-login'
2563
2608
  'create:Create a computer'
2564
2609
  'ls:List computers'
2565
2610
  'get:Show computer details'
@@ -2612,7 +2657,7 @@ _computer() {
2612
2657
  whoami)
2613
2658
  _arguments '--json[Print raw JSON]'
2614
2659
  ;;
2615
- claude-auth|claude-login)
2660
+ claude-auth|claude-login|codex-auth|codex-login)
2616
2661
  _arguments \\
2617
2662
  '--machine[Use a specific computer]:computer:_computer_handles' \\
2618
2663
  '--keep-helper[Keep a temporary helper machine]' \\
@@ -2765,7 +2810,7 @@ var BASH_SCRIPT = `_computer() {
2765
2810
  local cur prev words cword
2766
2811
  _init_completion || return
2767
2812
 
2768
- local commands="login logout whoami claude-auth claude-login create ls get image open ssh ports agent fleet acp rm completion help"
2813
+ 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"
2769
2814
  local ports_commands="ls publish rm"
2770
2815
  local image_commands="ls save default rebuild rm"
2771
2816
 
@@ -2783,7 +2828,7 @@ var BASH_SCRIPT = `_computer() {
2783
2828
  whoami)
2784
2829
  COMPREPLY=($(compgen -W "--json" -- "$cur"))
2785
2830
  ;;
2786
- claude-auth|claude-login)
2831
+ claude-auth|claude-login|codex-auth|codex-login)
2787
2832
  COMPREPLY=($(compgen -W "--machine --keep-helper --skip-cross-check --verbose" -- "$cur"))
2788
2833
  ;;
2789
2834
  create)
@@ -2882,14 +2927,344 @@ var completionCommand = new Command6("completion").description("Generate shell c
2882
2927
  }
2883
2928
  });
2884
2929
 
2885
- // src/commands/images.ts
2886
- import { confirm as confirm2, input as textInput3, select as select3 } from "@inquirer/prompts";
2930
+ // src/commands/codex-login.ts
2931
+ import { spawn as spawn3 } from "child_process";
2932
+ import { readFile as readFile3 } from "fs/promises";
2933
+ import { homedir as homedir4 } from "os";
2934
+ import { join as join3 } from "path";
2887
2935
  import { Command as Command7 } from "commander";
2888
2936
  import chalk7 from "chalk";
2889
- import ora5 from "ora";
2890
- var imageCommand = new Command7("image").description("Manage machine image sources");
2937
+ import ora6 from "ora";
2938
+ 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) => {
2939
+ const todos = createTodoList2();
2940
+ let target = null;
2941
+ let helperCreated = false;
2942
+ let sharedInstall = false;
2943
+ let activeTodoID = "target";
2944
+ let failureMessage = null;
2945
+ console.log();
2946
+ console.log(chalk7.cyan("Authenticating with Codex...\n"));
2947
+ try {
2948
+ const prepared = await prepareTargetMachine2(options);
2949
+ target = prepared.computer;
2950
+ helperCreated = prepared.helperCreated;
2951
+ sharedInstall = prepared.sharedInstall;
2952
+ markTodo2(todos, "target", "done", prepared.detail);
2953
+ activeTodoID = "ready";
2954
+ target = await waitForRunning(target);
2955
+ markTodo2(todos, "ready", "done", `${target.handle} is running`);
2956
+ activeTodoID = "local-auth";
2957
+ const localAuth = await ensureLocalCodexAuth();
2958
+ markTodo2(todos, "local-auth", "done", localAuth.detail);
2959
+ activeTodoID = "install";
2960
+ const sshTarget = await resolveSSHTarget(target);
2961
+ await installCodexAuth(sshTarget, localAuth.authJSON);
2962
+ markTodo2(
2963
+ todos,
2964
+ "install",
2965
+ "done",
2966
+ sharedInstall ? `installed Codex login on shared home via ${target.handle}` : `installed Codex login on ${target.handle}`
2967
+ );
2968
+ activeTodoID = "verify-primary";
2969
+ const primaryVerification = await verifyTargetMachine2(target.handle, sshTarget);
2970
+ markVerificationTodo2(
2971
+ todos,
2972
+ "verify-primary",
2973
+ primaryVerification,
2974
+ `${target.handle} fresh login shell sees Codex auth`
2975
+ );
2976
+ activeTodoID = "verify-shared";
2977
+ const sharedCheck = primaryVerification.status === "verified" ? await verifySharedInstall2(
2978
+ target.handle,
2979
+ target.id,
2980
+ sharedInstall,
2981
+ Boolean(options.skipCrossCheck),
2982
+ verifyStoredCodexAuth
2983
+ ) : {
2984
+ status: "skipped",
2985
+ reason: "primary verification was inconclusive"
2986
+ };
2987
+ if (sharedCheck.status === "verified") {
2988
+ markTodo2(
2989
+ todos,
2990
+ "verify-shared",
2991
+ "done",
2992
+ `${sharedCheck.handle} also sees stored Codex auth`
2993
+ );
2994
+ } else {
2995
+ markTodo2(todos, "verify-shared", "skipped", sharedCheck.reason);
2996
+ }
2997
+ } catch (error) {
2998
+ failureMessage = error instanceof Error ? error.message : "Failed to authenticate Codex";
2999
+ markTodo2(todos, activeTodoID, "failed", failureMessage);
3000
+ } finally {
3001
+ if (helperCreated && target && !options.keepHelper) {
3002
+ try {
3003
+ await deleteComputer(target.id);
3004
+ markTodo2(
3005
+ todos,
3006
+ "cleanup",
3007
+ "done",
3008
+ `removed temporary helper ${target.handle}`
3009
+ );
3010
+ } catch (error) {
3011
+ const message = error instanceof Error ? error.message : "failed to remove helper";
3012
+ markTodo2(todos, "cleanup", "failed", message);
3013
+ }
3014
+ } else if (helperCreated && target && options.keepHelper) {
3015
+ markTodo2(todos, "cleanup", "skipped", `kept helper ${target.handle}`);
3016
+ } else {
3017
+ markTodo2(todos, "cleanup", "skipped", "no helper created");
3018
+ }
3019
+ if (options.verbose) {
3020
+ printTodoList2(todos);
3021
+ }
3022
+ }
3023
+ if (failureMessage) {
3024
+ console.error(chalk7.red(`
3025
+ ${failureMessage}`));
3026
+ process.exit(1);
3027
+ }
3028
+ if (target) {
3029
+ console.log(
3030
+ chalk7.green(`Codex login installed on ${chalk7.bold(target.handle)}.`)
3031
+ );
3032
+ console.log();
3033
+ }
3034
+ });
3035
+ function createTodoList2() {
3036
+ return [
3037
+ { id: "target", label: "Pick target computer", state: "pending" },
3038
+ { id: "ready", label: "Wait for machine readiness", state: "pending" },
3039
+ { id: "local-auth", label: "Complete local Codex auth", state: "pending" },
3040
+ { id: "install", label: "Install stored Codex login", state: "pending" },
3041
+ { id: "verify-primary", label: "Verify on target machine", state: "pending" },
3042
+ { id: "verify-shared", label: "Verify shared-home propagation", state: "pending" },
3043
+ { id: "cleanup", label: "Clean up temporary helper", state: "pending" }
3044
+ ];
3045
+ }
3046
+ function markTodo2(items, id, state, detail) {
3047
+ const item = items.find((entry) => entry.id === id);
3048
+ if (!item) {
3049
+ return;
3050
+ }
3051
+ item.state = state;
3052
+ item.detail = detail;
3053
+ }
3054
+ function markVerificationTodo2(items, id, result, successDetail) {
3055
+ if (result.status === "verified") {
3056
+ markTodo2(items, id, "done", successDetail);
3057
+ return;
3058
+ }
3059
+ markTodo2(items, id, "skipped", result.detail);
3060
+ }
3061
+ function printTodoList2(items) {
3062
+ console.log();
3063
+ console.log(chalk7.dim("TODO"));
3064
+ console.log();
3065
+ for (const item of items) {
3066
+ const marker = item.state === "done" ? chalk7.green("[x]") : item.state === "skipped" ? chalk7.yellow("[-]") : item.state === "failed" ? chalk7.red("[!]") : chalk7.dim("[ ]");
3067
+ const detail = item.detail ? chalk7.dim(` ${item.detail}`) : "";
3068
+ console.log(` ${marker} ${item.label}${detail ? ` ${detail}` : ""}`);
3069
+ }
3070
+ console.log();
3071
+ }
3072
+ async function prepareTargetMachine2(options) {
3073
+ return prepareAuthTarget(options, {
3074
+ helperPrefix: "codex-login",
3075
+ helperDisplayName: "Codex Login Helper",
3076
+ promptMessage: "Select a computer for Codex login"
3077
+ });
3078
+ }
3079
+ async function ensureLocalCodexAuth() {
3080
+ const localStatus = await getLocalCodexStatus();
3081
+ if (localStatus.loggedIn) {
3082
+ return {
3083
+ authJSON: await readLocalCodexAuthFile(),
3084
+ detail: "reused existing local Codex login"
3085
+ };
3086
+ }
3087
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
3088
+ throw new Error("local Codex login is required when not running interactively");
3089
+ }
3090
+ console.log("We will open your browser so you can authenticate Codex locally.");
3091
+ console.log("If Codex falls back to device auth, complete that flow and return here.\n");
3092
+ await runInteractiveCodexLogin();
3093
+ const refreshedStatus = await getLocalCodexStatus();
3094
+ if (!refreshedStatus.loggedIn) {
3095
+ throw new Error(
3096
+ refreshedStatus.detail || "codex login did not complete successfully"
3097
+ );
3098
+ }
3099
+ return {
3100
+ authJSON: await readLocalCodexAuthFile(),
3101
+ detail: "local Codex login completed"
3102
+ };
3103
+ }
3104
+ async function getLocalCodexStatus() {
3105
+ const result = await captureLocalCommand("codex", ["login", "status"]);
3106
+ return parseCodexStatusOutput(result.stdout, result.stderr);
3107
+ }
3108
+ async function readLocalCodexAuthFile() {
3109
+ const authPath = join3(homedir4(), ".codex", "auth.json");
3110
+ let raw;
3111
+ try {
3112
+ raw = await readFile3(authPath, "utf8");
3113
+ } catch (error) {
3114
+ throw new Error(
3115
+ error instanceof Error ? `failed to read ${authPath}: ${error.message}` : `failed to read ${authPath}`
3116
+ );
3117
+ }
3118
+ try {
3119
+ JSON.parse(raw);
3120
+ } catch (error) {
3121
+ throw new Error(
3122
+ error instanceof Error ? `local Codex auth file is invalid JSON: ${error.message}` : "local Codex auth file is invalid JSON"
3123
+ );
3124
+ }
3125
+ return `${raw.trimEnd()}
3126
+ `;
3127
+ }
3128
+ async function runInteractiveCodexLogin() {
3129
+ await new Promise((resolve, reject) => {
3130
+ const child = spawn3("codex", ["login"], {
3131
+ stdio: "inherit"
3132
+ });
3133
+ child.on("error", (error) => {
3134
+ reject(
3135
+ error instanceof Error ? new Error(`failed to start local codex login: ${error.message}`) : new Error("failed to start local codex login")
3136
+ );
3137
+ });
3138
+ child.on("exit", (code, signal) => {
3139
+ if (code === 0) {
3140
+ resolve();
3141
+ return;
3142
+ }
3143
+ if (signal) {
3144
+ reject(new Error(`codex login was interrupted by ${signal}`));
3145
+ return;
3146
+ }
3147
+ reject(new Error(`codex login exited with code ${code ?? 1}`));
3148
+ });
3149
+ });
3150
+ }
3151
+ async function captureLocalCommand(command, args) {
3152
+ return new Promise((resolve, reject) => {
3153
+ const child = spawn3(command, args, {
3154
+ stdio: ["ignore", "pipe", "pipe"]
3155
+ });
3156
+ let stdout = "";
3157
+ let stderr = "";
3158
+ child.stdout.on("data", (chunk) => {
3159
+ stdout += chunk.toString();
3160
+ });
3161
+ child.stderr.on("data", (chunk) => {
3162
+ stderr += chunk.toString();
3163
+ });
3164
+ child.on("error", reject);
3165
+ child.on("exit", (code) => {
3166
+ resolve({ stdout, stderr, exitCode: code });
3167
+ });
3168
+ });
3169
+ }
3170
+ async function installCodexAuth(target, authJSON) {
3171
+ const spinner = ora6(`Installing Codex login on ${chalk7.bold(target.handle)}...`).start();
3172
+ try {
3173
+ const installScript = buildInstallScript2(authJSON);
3174
+ await runRemoteCommand(target, ["bash", "-s"], installScript);
3175
+ spinner.succeed(`Installed Codex login on ${chalk7.bold(target.handle)}`);
3176
+ } catch (error) {
3177
+ spinner.fail(
3178
+ error instanceof Error ? error.message : `Failed to install Codex login on ${target.handle}`
3179
+ );
3180
+ throw error;
3181
+ }
3182
+ }
3183
+ async function verifyTargetMachine2(handle, target) {
3184
+ const spinner = ora6(`Verifying Codex login on ${chalk7.bold(handle)}...`).start();
3185
+ const result = await verifyStoredCodexAuth(target);
3186
+ if (result.status === "verified") {
3187
+ spinner.succeed(`Verified Codex login on ${chalk7.bold(handle)}`);
3188
+ return result;
3189
+ }
3190
+ spinner.warn(result.detail);
3191
+ return result;
3192
+ }
3193
+ async function verifySharedInstall2(primaryHandle, primaryComputerID, sharedInstall, skip, verify) {
3194
+ const spinner = ora6(
3195
+ `Verifying shared-home Codex login from ${chalk7.bold(primaryHandle)}...`
3196
+ ).start();
3197
+ const result = await verifySecondaryMachine(
3198
+ primaryComputerID,
3199
+ sharedInstall,
3200
+ skip,
3201
+ verify
3202
+ );
3203
+ if (result.status === "verified") {
3204
+ spinner.succeed(`Verified shared-home Codex login on ${chalk7.bold(result.handle)}`);
3205
+ return result;
3206
+ }
3207
+ spinner.info(result.reason);
3208
+ return result;
3209
+ }
3210
+ function buildInstallScript2(authJSON) {
3211
+ const authMarker = `AUTH_${randomSuffix(12)}`;
3212
+ return [
3213
+ "set -euo pipefail",
3214
+ 'command -v codex >/dev/null 2>&1 || { echo "codex is not installed on this computer" >&2; exit 1; }',
3215
+ 'mkdir -p "$HOME/.codex"',
3216
+ 'chmod 700 "$HOME/.codex"',
3217
+ `cat > "$HOME/.codex/auth.json" <<'${authMarker}'`,
3218
+ authJSON.trimEnd(),
3219
+ authMarker,
3220
+ 'chmod 600 "$HOME/.codex/auth.json"'
3221
+ ].join("\n");
3222
+ }
3223
+ async function verifyStoredCodexAuth(target) {
3224
+ try {
3225
+ const result = await runRemoteCommand(target, [
3226
+ 'PATH="$HOME/.local/bin:$PATH" codex login status 2>&1 || true'
3227
+ ]);
3228
+ const payload = parseCodexStatusOutput(result.stdout, result.stderr);
3229
+ if (payload.loggedIn) {
3230
+ return { status: "verified", detail: "verified" };
3231
+ }
3232
+ return {
3233
+ status: "failed",
3234
+ detail: payload.detail ? `verification failed: ${payload.detail}` : "verification failed"
3235
+ };
3236
+ } catch (error) {
3237
+ return {
3238
+ status: "inconclusive",
3239
+ detail: error instanceof Error ? error.message : "verification command did not complete cleanly"
3240
+ };
3241
+ }
3242
+ }
3243
+ function parseCodexStatusOutput(stdout, stderr) {
3244
+ const combined = [stdout, stderr].map((value) => value.trim()).filter(Boolean).join("\n");
3245
+ const normalized = combined.toLowerCase();
3246
+ if (normalized.includes("not logged in") || normalized.includes("logged out")) {
3247
+ return { loggedIn: false, detail: firstStatusLine2(combined) };
3248
+ }
3249
+ if (normalized.includes("logged in")) {
3250
+ return { loggedIn: true };
3251
+ }
3252
+ throw new Error(
3253
+ combined ? `could not verify Codex auth from status output: ${firstStatusLine2(combined)}` : "could not verify Codex auth from empty status output"
3254
+ );
3255
+ }
3256
+ function firstStatusLine2(value) {
3257
+ return value.split(/\r?\n/).map((line) => line.trim()).find(Boolean) ?? "unknown output";
3258
+ }
3259
+
3260
+ // src/commands/images.ts
3261
+ import { confirm as confirm2, input as textInput3, select as select3 } from "@inquirer/prompts";
3262
+ import { Command as Command8 } from "commander";
3263
+ import chalk8 from "chalk";
3264
+ import ora7 from "ora";
3265
+ var imageCommand = new Command8("image").description("Manage machine image sources");
2891
3266
  imageCommand.command("ls").description("List machine image sources").option("--json", "Print raw JSON").action(async (options) => {
2892
- const spinner = options.json ? null : ora5("Fetching machine images...").start();
3267
+ const spinner = options.json ? null : ora7("Fetching machine images...").start();
2893
3268
  try {
2894
3269
  const settings = await getMachineSourceSettings();
2895
3270
  spinner?.stop();
@@ -2908,7 +3283,7 @@ imageCommand.command("ls").description("List machine image sources").option("--j
2908
3283
  }
2909
3284
  });
2910
3285
  imageCommand.command("save").description("Create or update a machine image source").option("--id <id>", "Existing source id to update").option("--kind <kind>", "oci-image or nix-git").option("--requested-ref <ref>", "OCI image ref or explicit resolved ref").option("--git-url <url>", "Repository URL for a Nix git source").option("--git-ref <ref>", "Git ref for a Nix git source").option("--git-subpath <path>", "Subdirectory for a Nix git source").option("--set-as-default", "Select this source as the default after saving").option("--json", "Print raw JSON").action(async (options) => {
2911
- const spinner = options.json ? null : ora5("Saving machine image source...").start();
3286
+ const spinner = options.json ? null : ora7("Saving machine image source...").start();
2912
3287
  try {
2913
3288
  const input = await resolveSaveInput(options);
2914
3289
  const settings = await upsertMachineSource(input);
@@ -2918,7 +3293,7 @@ imageCommand.command("save").description("Create or update a machine image sourc
2918
3293
  return;
2919
3294
  }
2920
3295
  console.log();
2921
- console.log(chalk7.green("Saved machine image source."));
3296
+ console.log(chalk8.green("Saved machine image source."));
2922
3297
  printMachineSourceSettings(settings);
2923
3298
  } catch (error) {
2924
3299
  if (spinner) {
@@ -2931,7 +3306,7 @@ imageCommand.command("save").description("Create or update a machine image sourc
2931
3306
  });
2932
3307
  imageCommand.command("default").description("Set the default machine image source").argument("[source-id]", "Source id to select; use 'platform' or omit for the platform default").option("--json", "Print raw JSON").action(async (sourceID, options) => {
2933
3308
  const usePlatformDefault = !sourceID || sourceID === "platform";
2934
- const spinner = options.json ? null : ora5("Updating machine image default...").start();
3309
+ const spinner = options.json ? null : ora7("Updating machine image default...").start();
2935
3310
  try {
2936
3311
  let settings;
2937
3312
  if (usePlatformDefault) {
@@ -2957,9 +3332,9 @@ imageCommand.command("default").description("Set the default machine image sourc
2957
3332
  if (!usePlatformDefault) {
2958
3333
  const selected = settings.default_machine_source ?? void 0;
2959
3334
  const label = selected ? summarizeMachineSource(selected) : sourceID;
2960
- console.log(chalk7.green(`Selected ${chalk7.bold(label)} as the default machine image.`));
3335
+ console.log(chalk8.green(`Selected ${chalk8.bold(label)} as the default machine image.`));
2961
3336
  } else {
2962
- console.log(chalk7.green("Using the AgentComputer platform default image."));
3337
+ console.log(chalk8.green("Using the AgentComputer platform default image."));
2963
3338
  }
2964
3339
  printMachineSourceSettings(settings);
2965
3340
  } catch (error) {
@@ -2972,7 +3347,7 @@ imageCommand.command("default").description("Set the default machine image sourc
2972
3347
  }
2973
3348
  });
2974
3349
  imageCommand.command("rebuild").description("Rebuild a machine image source").argument("<source-id>", "Source id to rebuild").option("--json", "Print raw JSON").action(async (sourceID, options) => {
2975
- const spinner = options.json ? null : ora5("Queueing machine image rebuild...").start();
3350
+ const spinner = options.json ? null : ora7("Queueing machine image rebuild...").start();
2976
3351
  try {
2977
3352
  const settings = await rebuildMachineSource(sourceID);
2978
3353
  spinner?.stop();
@@ -2981,7 +3356,7 @@ imageCommand.command("rebuild").description("Rebuild a machine image source").ar
2981
3356
  return;
2982
3357
  }
2983
3358
  console.log();
2984
- console.log(chalk7.green(`Queued rebuild for ${chalk7.bold(sourceID)}.`));
3359
+ console.log(chalk8.green(`Queued rebuild for ${chalk8.bold(sourceID)}.`));
2985
3360
  printMachineSourceSettings(settings);
2986
3361
  } catch (error) {
2987
3362
  if (spinner) {
@@ -3000,11 +3375,11 @@ imageCommand.command("rm").description("Delete a machine image source").argument
3000
3375
  if (!skipConfirm && process.stdin.isTTY) {
3001
3376
  const confirmed = await confirmDeletion(sourceID);
3002
3377
  if (!confirmed) {
3003
- console.log(chalk7.dim(" Cancelled."));
3378
+ console.log(chalk8.dim(" Cancelled."));
3004
3379
  return;
3005
3380
  }
3006
3381
  }
3007
- spinner = options.json ? null : ora5("Deleting machine image source...").start();
3382
+ spinner = options.json ? null : ora7("Deleting machine image source...").start();
3008
3383
  const settings = await deleteMachineSource(sourceID);
3009
3384
  spinner?.stop();
3010
3385
  if (options.json) {
@@ -3012,7 +3387,7 @@ imageCommand.command("rm").description("Delete a machine image source").argument
3012
3387
  return;
3013
3388
  }
3014
3389
  console.log();
3015
- console.log(chalk7.green(`Deleted machine image source ${chalk7.bold(sourceID)}.`));
3390
+ console.log(chalk8.green(`Deleted machine image source ${chalk8.bold(sourceID)}.`));
3016
3391
  printMachineSourceSettings(settings);
3017
3392
  } catch (error) {
3018
3393
  if (spinner) {
@@ -3024,10 +3399,10 @@ imageCommand.command("rm").description("Delete a machine image source").argument
3024
3399
  }
3025
3400
  });
3026
3401
  function printMachineSourceSettings(settings) {
3027
- console.log(` ${chalk7.dim("Default")} ${chalk7.white(summarizeMachineSourceSelection(settings))}`);
3402
+ console.log(` ${chalk8.dim("Default")} ${chalk8.white(summarizeMachineSourceSelection(settings))}`);
3028
3403
  console.log();
3029
3404
  if (settings.sources.length === 0) {
3030
- console.log(chalk7.dim(" No custom machine images configured yet."));
3405
+ console.log(chalk8.dim(" No custom machine images configured yet."));
3031
3406
  console.log();
3032
3407
  return;
3033
3408
  }
@@ -3036,17 +3411,17 @@ function printMachineSourceSettings(settings) {
3036
3411
  }
3037
3412
  }
3038
3413
  function printMachineSourceCard(source, isDefault) {
3039
- console.log(` ${chalk7.bold(machineSourceTitle(source))}${isDefault ? chalk7.green(" (default)") : ""}`);
3040
- console.log(` ${chalk7.dim(" ID")} ${source.id}`);
3041
- console.log(` ${chalk7.dim(" Kind")} ${source.kind}`);
3042
- console.log(` ${chalk7.dim(" Status")} ${source.status}${source.latest_build ? ` | latest build ${source.latest_build.status}` : ""}`);
3414
+ console.log(` ${chalk8.bold(machineSourceTitle(source))}${isDefault ? chalk8.green(" (default)") : ""}`);
3415
+ console.log(` ${chalk8.dim(" ID")} ${source.id}`);
3416
+ console.log(` ${chalk8.dim(" Kind")} ${source.kind}`);
3417
+ console.log(` ${chalk8.dim(" Status")} ${source.status}${source.latest_build ? ` | latest build ${source.latest_build.status}` : ""}`);
3043
3418
  if (source.resolved_image_ref) {
3044
- console.log(` ${chalk7.dim(" Resolved")} ${source.resolved_image_ref}`);
3419
+ console.log(` ${chalk8.dim(" Resolved")} ${source.resolved_image_ref}`);
3045
3420
  }
3046
3421
  if (source.error) {
3047
- console.log(` ${chalk7.dim(" Error")} ${chalk7.red(source.error)}`);
3422
+ console.log(` ${chalk8.dim(" Error")} ${chalk8.red(source.error)}`);
3048
3423
  }
3049
- console.log(` ${chalk7.dim(" Source")} ${summarizeMachineSource(source)}`);
3424
+ console.log(` ${chalk8.dim(" Source")} ${summarizeMachineSource(source)}`);
3050
3425
  console.log();
3051
3426
  }
3052
3427
  function machineSourceTitle(source) {
@@ -3163,18 +3538,18 @@ async function confirmDeletion(sourceID) {
3163
3538
  }
3164
3539
 
3165
3540
  // src/commands/login.ts
3166
- import { Command as Command8 } from "commander";
3167
- import chalk8 from "chalk";
3168
- import ora6 from "ora";
3541
+ import { Command as Command9 } from "commander";
3542
+ import chalk9 from "chalk";
3543
+ import ora8 from "ora";
3169
3544
 
3170
3545
  // src/lib/browser-login.ts
3171
- import { randomBytes as randomBytes2 } from "crypto";
3546
+ import { randomBytes as randomBytes3 } from "crypto";
3172
3547
  import { createServer } from "http";
3173
3548
  var CALLBACK_HOST = "127.0.0.1";
3174
3549
  var CALLBACK_PATH = "/callback";
3175
3550
  var LOGIN_TIMEOUT_MS = 5 * 60 * 1e3;
3176
3551
  async function createBrowserLoginAttempt() {
3177
- const state = randomBytes2(16).toString("hex");
3552
+ const state = randomBytes3(16).toString("hex");
3178
3553
  const deferred = createDeferred();
3179
3554
  let callbackURL = "";
3180
3555
  let closed = false;
@@ -3393,11 +3768,11 @@ function escapeHTML(value) {
3393
3768
  }
3394
3769
 
3395
3770
  // src/commands/login.ts
3396
- var loginCommand = new Command8("login").description("Authenticate the CLI").option("--api-key <key>", "API key starting with ac_live_").option("--stdin", "Read the API key from stdin").option("-f, --force", "Overwrite an existing stored API key").action(async (options) => {
3771
+ var loginCommand = new Command9("login").description("Authenticate the CLI").option("--api-key <key>", "API key starting with ac_live_").option("--stdin", "Read the API key from stdin").option("-f, --force", "Overwrite an existing stored API key").action(async (options) => {
3397
3772
  const existingKey = getStoredAPIKey();
3398
3773
  if (existingKey && !options.force) {
3399
3774
  console.log();
3400
- console.log(chalk8.yellow(" Already logged in. Use --force to overwrite."));
3775
+ console.log(chalk9.yellow(" Already logged in. Use --force to overwrite."));
3401
3776
  console.log();
3402
3777
  return;
3403
3778
  }
@@ -3405,8 +3780,8 @@ var loginCommand = new Command8("login").description("Authenticate the CLI").opt
3405
3780
  const apiKey = await resolveAPIKeyInput(options.apiKey, options.stdin);
3406
3781
  if (!apiKey && wantsManualLogin) {
3407
3782
  console.log();
3408
- console.log(chalk8.dim(" Usage: computer login --api-key <ac_live_...>"));
3409
- console.log(chalk8.dim(` API: ${getBaseURL()}`));
3783
+ console.log(chalk9.dim(" Usage: computer login --api-key <ac_live_...>"));
3784
+ console.log(chalk9.dim(` API: ${getBaseURL()}`));
3410
3785
  console.log();
3411
3786
  process.exit(1);
3412
3787
  }
@@ -3416,15 +3791,15 @@ var loginCommand = new Command8("login").description("Authenticate the CLI").opt
3416
3791
  }
3417
3792
  if (!apiKey.startsWith("ac_live_")) {
3418
3793
  console.log();
3419
- console.log(chalk8.red(" API key must start with ac_live_"));
3794
+ console.log(chalk9.red(" API key must start with ac_live_"));
3420
3795
  console.log();
3421
3796
  process.exit(1);
3422
3797
  }
3423
- const spinner = ora6("Authenticating...").start();
3798
+ const spinner = ora8("Authenticating...").start();
3424
3799
  try {
3425
3800
  const me = await apiWithKey(apiKey, "/v1/me");
3426
3801
  setAPIKey(apiKey);
3427
- spinner.succeed(`Logged in as ${chalk8.bold(me.user.email)}`);
3802
+ spinner.succeed(`Logged in as ${chalk9.bold(me.user.email)}`);
3428
3803
  } catch (error) {
3429
3804
  spinner.fail(
3430
3805
  error instanceof Error ? error.message : "Failed to validate API key"
@@ -3433,7 +3808,7 @@ var loginCommand = new Command8("login").description("Authenticate the CLI").opt
3433
3808
  }
3434
3809
  });
3435
3810
  async function runBrowserLogin() {
3436
- const spinner = ora6("Starting browser login...").start();
3811
+ const spinner = ora8("Starting browser login...").start();
3437
3812
  let attempt = null;
3438
3813
  try {
3439
3814
  attempt = await createBrowserLoginAttempt();
@@ -3443,14 +3818,14 @@ async function runBrowserLogin() {
3443
3818
  } catch {
3444
3819
  spinner.stop();
3445
3820
  console.log();
3446
- console.log(chalk8.yellow(" Browser auto-open failed. Open this URL to continue:"));
3447
- console.log(chalk8.dim(` ${attempt.loginURL}`));
3821
+ console.log(chalk9.yellow(" Browser auto-open failed. Open this URL to continue:"));
3822
+ console.log(chalk9.dim(` ${attempt.loginURL}`));
3448
3823
  console.log();
3449
3824
  spinner.start("Waiting for browser login...");
3450
3825
  }
3451
3826
  spinner.text = "Waiting for browser login...";
3452
3827
  const result = await attempt.waitForResult();
3453
- spinner.succeed(`Logged in as ${chalk8.bold(result.me.user.email)}`);
3828
+ spinner.succeed(`Logged in as ${chalk9.bold(result.me.user.email)}`);
3454
3829
  } catch (error) {
3455
3830
  spinner.fail(error instanceof Error ? error.message : "Browser login failed");
3456
3831
  process.exit(1);
@@ -3476,33 +3851,33 @@ async function resolveAPIKeyInput(flagValue, readFromStdin) {
3476
3851
  }
3477
3852
 
3478
3853
  // src/commands/logout.ts
3479
- import { Command as Command9 } from "commander";
3480
- import chalk9 from "chalk";
3481
- var logoutCommand = new Command9("logout").description("Remove stored API key").action(() => {
3854
+ import { Command as Command10 } from "commander";
3855
+ import chalk10 from "chalk";
3856
+ var logoutCommand = new Command10("logout").description("Remove stored API key").action(() => {
3482
3857
  if (!getStoredAPIKey()) {
3483
3858
  console.log();
3484
- console.log(chalk9.dim(" Not logged in."));
3859
+ console.log(chalk10.dim(" Not logged in."));
3485
3860
  if (hasEnvAPIKey()) {
3486
- console.log(chalk9.dim(" Environment API key is still active in this shell."));
3861
+ console.log(chalk10.dim(" Environment API key is still active in this shell."));
3487
3862
  }
3488
3863
  console.log();
3489
3864
  return;
3490
3865
  }
3491
3866
  clearAPIKey();
3492
3867
  console.log();
3493
- console.log(chalk9.green(" Logged out."));
3868
+ console.log(chalk10.green(" Logged out."));
3494
3869
  if (hasEnvAPIKey()) {
3495
- console.log(chalk9.dim(" Environment API key is still active in this shell."));
3870
+ console.log(chalk10.dim(" Environment API key is still active in this shell."));
3496
3871
  }
3497
3872
  console.log();
3498
3873
  });
3499
3874
 
3500
3875
  // src/commands/whoami.ts
3501
- import { Command as Command10 } from "commander";
3502
- import chalk10 from "chalk";
3503
- import ora7 from "ora";
3504
- var whoamiCommand = new Command10("whoami").description("Show current user").option("--json", "Print raw JSON").action(async (options) => {
3505
- const spinner = options.json ? null : ora7("Loading user...").start();
3876
+ import { Command as Command11 } from "commander";
3877
+ import chalk11 from "chalk";
3878
+ import ora9 from "ora";
3879
+ var whoamiCommand = new Command11("whoami").description("Show current user").option("--json", "Print raw JSON").action(async (options) => {
3880
+ const spinner = options.json ? null : ora9("Loading user...").start();
3506
3881
  try {
3507
3882
  const me = await api("/v1/me");
3508
3883
  spinner?.stop();
@@ -3511,14 +3886,14 @@ var whoamiCommand = new Command10("whoami").description("Show current user").opt
3511
3886
  return;
3512
3887
  }
3513
3888
  console.log();
3514
- console.log(` ${chalk10.bold.white(me.user.display_name || me.user.email)}`);
3889
+ console.log(` ${chalk11.bold.white(me.user.display_name || me.user.email)}`);
3515
3890
  if (me.user.display_name) {
3516
- console.log(` ${chalk10.dim(me.user.email)}`);
3891
+ console.log(` ${chalk11.dim(me.user.email)}`);
3517
3892
  }
3518
3893
  if (me.api_key.name) {
3519
- console.log(` ${chalk10.dim("Key:")} ${me.api_key.name}`);
3894
+ console.log(` ${chalk11.dim("Key:")} ${me.api_key.name}`);
3520
3895
  }
3521
- console.log(` ${chalk10.dim("API:")} ${chalk10.dim(getBaseURL())}`);
3896
+ console.log(` ${chalk11.dim("API:")} ${chalk11.dim(getBaseURL())}`);
3522
3897
  console.log();
3523
3898
  } catch (error) {
3524
3899
  if (spinner) {
@@ -3535,15 +3910,15 @@ var pkg2 = JSON.parse(
3535
3910
  readFileSync3(new URL("../package.json", import.meta.url), "utf8")
3536
3911
  );
3537
3912
  var cliName = process.argv[1] ? basename2(process.argv[1]) : "agentcomputer";
3538
- var program = new Command11();
3913
+ var program = new Command12();
3539
3914
  function appendTextSection(lines, title, values) {
3540
3915
  if (values.length === 0) {
3541
3916
  return;
3542
3917
  }
3543
- lines.push(` ${chalk11.dim(title)}`);
3918
+ lines.push(` ${chalk12.dim(title)}`);
3544
3919
  lines.push("");
3545
3920
  for (const value of values) {
3546
- lines.push(` ${chalk11.white(value)}`);
3921
+ lines.push(` ${chalk12.white(value)}`);
3547
3922
  }
3548
3923
  lines.push("");
3549
3924
  }
@@ -3552,10 +3927,10 @@ function appendTableSection(lines, title, entries) {
3552
3927
  return;
3553
3928
  }
3554
3929
  const width = Math.max(...entries.map((entry) => entry.term.length), 0) + 2;
3555
- lines.push(` ${chalk11.dim(title)}`);
3930
+ lines.push(` ${chalk12.dim(title)}`);
3556
3931
  lines.push("");
3557
3932
  for (const entry of entries) {
3558
- lines.push(` ${chalk11.white(padEnd(entry.term, width))}${chalk11.dim(entry.desc)}`);
3933
+ lines.push(` ${chalk12.white(padEnd(entry.term, width))}${chalk12.dim(entry.desc)}`);
3559
3934
  }
3560
3935
  lines.push("");
3561
3936
  }
@@ -3580,17 +3955,17 @@ function formatRootHelp(cmd) {
3580
3955
  ["Other", []]
3581
3956
  ];
3582
3957
  const otherGroup = groups.find(([name]) => name === "Other")[1];
3583
- lines.push(`${chalk11.bold(cliName)} ${chalk11.dim(`v${version}`)}`);
3958
+ lines.push(`${chalk12.bold(cliName)} ${chalk12.dim(`v${version}`)}`);
3584
3959
  lines.push("");
3585
3960
  if (cmd.description()) {
3586
- lines.push(` ${chalk11.dim(cmd.description())}`);
3961
+ lines.push(` ${chalk12.dim(cmd.description())}`);
3587
3962
  lines.push("");
3588
3963
  }
3589
3964
  appendTextSection(lines, "Usage", [`${cliName} <command> [options]`]);
3590
3965
  for (const sub of cmd.commands) {
3591
3966
  const name = sub.name();
3592
3967
  const entry = { term: name, desc: sub.description() };
3593
- if (["login", "logout", "whoami", "claude-auth"].includes(name)) {
3968
+ if (["login", "logout", "whoami", "claude-login", "codex-login"].includes(name)) {
3594
3969
  groups[0][1].push(entry);
3595
3970
  } else if (["create", "ls", "get", "rm"].includes(name)) {
3596
3971
  groups[1][1].push(entry);
@@ -3634,10 +4009,10 @@ function formatSubcommandHelp(cmd, helper) {
3634
4009
  term: helper.optionTerm(option),
3635
4010
  desc: helper.optionDescription(option)
3636
4011
  }));
3637
- lines.push(chalk11.bold(commandPath(cmd)));
4012
+ lines.push(chalk12.bold(commandPath(cmd)));
3638
4013
  lines.push("");
3639
4014
  if (description) {
3640
- lines.push(` ${chalk11.dim(description)}`);
4015
+ lines.push(` ${chalk12.dim(description)}`);
3641
4016
  lines.push("");
3642
4017
  }
3643
4018
  appendTextSection(lines, "Usage", [helper.commandUsage(cmd)]);
@@ -3664,7 +4039,8 @@ program.name(cliName).description("Agent Computer CLI").version(pkg2.version ??
3664
4039
  program.addCommand(loginCommand);
3665
4040
  program.addCommand(logoutCommand);
3666
4041
  program.addCommand(whoamiCommand);
3667
- program.addCommand(claudeAuthCommand);
4042
+ program.addCommand(claudeLoginCommand);
4043
+ program.addCommand(codexLoginCommand);
3668
4044
  program.addCommand(createCommand);
3669
4045
  program.addCommand(lsCommand);
3670
4046
  program.addCommand(getCommand);