aicomputer 0.1.7 → 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.
- package/dist/index.js +739 -328
- 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
|
|
5
|
-
import
|
|
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
|
|
1539
|
-
var readyPollTimeoutMs = 18e4;
|
|
1540
|
-
var claudeAuthCommand = new Command4("claude-auth").alias("claude-login").description("Authenticate Claude Code on a computer").option("--machine <id-or-handle>", "Use a specific computer").option("--keep-helper", "Keep a temporary helper machine if one is created").option("--skip-cross-check", "Skip verification on a second shared machine").action(async (options) => {
|
|
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,19 +1771,24 @@ 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
|
-
await
|
|
1577
|
-
|
|
1774
|
+
const primaryVerification = await verifyTargetMachine(target.handle, sshTarget);
|
|
1775
|
+
markVerificationTodo(
|
|
1578
1776
|
todos,
|
|
1579
1777
|
"verify-primary",
|
|
1580
|
-
|
|
1778
|
+
primaryVerification,
|
|
1581
1779
|
`${target.handle} fresh login shell sees Claude auth`
|
|
1582
1780
|
);
|
|
1583
1781
|
activeTodoID = "verify-shared";
|
|
1584
|
-
const sharedCheck = await
|
|
1782
|
+
const sharedCheck = primaryVerification.status === "verified" ? await verifySharedInstall(
|
|
1783
|
+
target.handle,
|
|
1585
1784
|
target.id,
|
|
1586
1785
|
sharedInstall,
|
|
1587
|
-
Boolean(options.skipCrossCheck)
|
|
1588
|
-
|
|
1786
|
+
Boolean(options.skipCrossCheck),
|
|
1787
|
+
verifyStoredAuth
|
|
1788
|
+
) : {
|
|
1789
|
+
status: "skipped",
|
|
1790
|
+
reason: "primary verification was inconclusive"
|
|
1791
|
+
};
|
|
1589
1792
|
if (sharedCheck.status === "verified") {
|
|
1590
1793
|
markTodo(
|
|
1591
1794
|
todos,
|
|
@@ -1623,7 +1826,9 @@ var claudeAuthCommand = new Command4("claude-auth").alias("claude-login").descri
|
|
|
1623
1826
|
} else {
|
|
1624
1827
|
markTodo(todos, "cleanup", "skipped", "no helper created");
|
|
1625
1828
|
}
|
|
1626
|
-
|
|
1829
|
+
if (options.verbose) {
|
|
1830
|
+
printTodoList(todos);
|
|
1831
|
+
}
|
|
1627
1832
|
}
|
|
1628
1833
|
if (failureMessage) {
|
|
1629
1834
|
console.error(chalk5.red(`
|
|
@@ -1631,7 +1836,9 @@ ${failureMessage}`));
|
|
|
1631
1836
|
process.exit(1);
|
|
1632
1837
|
}
|
|
1633
1838
|
if (target) {
|
|
1634
|
-
console.log(
|
|
1839
|
+
console.log(
|
|
1840
|
+
chalk5.green(`Claude login installed on ${chalk5.bold(target.handle)}.`)
|
|
1841
|
+
);
|
|
1635
1842
|
console.log();
|
|
1636
1843
|
}
|
|
1637
1844
|
});
|
|
@@ -1654,6 +1861,13 @@ function markTodo(items, id, state, detail) {
|
|
|
1654
1861
|
item.state = state;
|
|
1655
1862
|
item.detail = detail;
|
|
1656
1863
|
}
|
|
1864
|
+
function markVerificationTodo(items, id, result, successDetail) {
|
|
1865
|
+
if (result.status === "verified") {
|
|
1866
|
+
markTodo(items, id, "done", successDetail);
|
|
1867
|
+
return;
|
|
1868
|
+
}
|
|
1869
|
+
markTodo(items, id, "skipped", result.detail);
|
|
1870
|
+
}
|
|
1657
1871
|
function printTodoList(items) {
|
|
1658
1872
|
console.log();
|
|
1659
1873
|
console.log(chalk5.dim("TODO"));
|
|
@@ -1666,130 +1880,28 @@ function printTodoList(items) {
|
|
|
1666
1880
|
console.log();
|
|
1667
1881
|
}
|
|
1668
1882
|
async function prepareTargetMachine(options) {
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
const
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
detail: describeTarget(existing, false)
|
|
1689
|
-
};
|
|
1690
|
-
}
|
|
1691
|
-
const spinner = ora3("Creating temporary shared helper...").start();
|
|
1692
|
-
try {
|
|
1693
|
-
const helper = await createComputer({
|
|
1694
|
-
handle: `claude-auth-${randomSuffix(6)}`,
|
|
1695
|
-
display_name: "Claude Auth Helper",
|
|
1696
|
-
runtime_family: "managed-worker",
|
|
1697
|
-
use_platform_default: true,
|
|
1698
|
-
ssh_enabled: true,
|
|
1699
|
-
vnc_enabled: false
|
|
1700
|
-
});
|
|
1701
|
-
spinner.succeed(`Created temporary helper ${chalk5.bold(helper.handle)}`);
|
|
1702
|
-
return {
|
|
1703
|
-
computer: helper,
|
|
1704
|
-
helperCreated: true,
|
|
1705
|
-
sharedInstall: true,
|
|
1706
|
-
detail: describeTarget(helper, true)
|
|
1707
|
-
};
|
|
1708
|
-
} catch (error) {
|
|
1709
|
-
spinner.fail(
|
|
1710
|
-
error instanceof Error ? error.message : "Failed to create temporary helper"
|
|
1711
|
-
);
|
|
1712
|
-
throw error;
|
|
1713
|
-
}
|
|
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."));
|
|
1714
1902
|
}
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
"Select a computer for Claude auth"
|
|
1718
|
-
);
|
|
1719
|
-
return {
|
|
1720
|
-
computer,
|
|
1721
|
-
helperCreated: false,
|
|
1722
|
-
sharedInstall: isSharedInstallTarget(computer),
|
|
1723
|
-
detail: describeTarget(computer, false)
|
|
1724
|
-
};
|
|
1725
|
-
}
|
|
1726
|
-
function pickSharedRunningComputer(computers) {
|
|
1727
|
-
const candidates = computers.filter(
|
|
1728
|
-
(computer) => computer.runtime_family === "managed-worker" && computer.filesystem_mode === "shared" && computer.ssh_enabled && computer.status === "running"
|
|
1729
|
-
).sort(
|
|
1730
|
-
(left, right) => new Date(right.updated_at).getTime() - new Date(left.updated_at).getTime()
|
|
1731
|
-
);
|
|
1732
|
-
return candidates[0] ?? null;
|
|
1733
|
-
}
|
|
1734
|
-
function assertClaudeAuthTarget(computer) {
|
|
1735
|
-
if (!computer.ssh_enabled) {
|
|
1736
|
-
throw new Error(`${computer.handle} does not have SSH enabled`);
|
|
1737
|
-
}
|
|
1738
|
-
}
|
|
1739
|
-
function isSharedInstallTarget(computer) {
|
|
1740
|
-
return computer.runtime_family === "managed-worker" && computer.filesystem_mode === "shared";
|
|
1741
|
-
}
|
|
1742
|
-
function describeTarget(computer, helperCreated) {
|
|
1743
|
-
if (helperCreated) {
|
|
1744
|
-
return `created temporary helper ${computer.handle}`;
|
|
1745
|
-
}
|
|
1746
|
-
if (isSharedInstallTarget(computer)) {
|
|
1747
|
-
return `using shared machine ${computer.handle}`;
|
|
1748
|
-
}
|
|
1749
|
-
return `using ${computer.handle}`;
|
|
1750
|
-
}
|
|
1751
|
-
async function waitForRunning(initial) {
|
|
1752
|
-
if (initial.status === "running") {
|
|
1753
|
-
return initial;
|
|
1754
|
-
}
|
|
1755
|
-
const spinner = ora3(`Waiting for ${chalk5.bold(initial.handle)} to be ready...`).start();
|
|
1756
|
-
const deadline = Date.now() + readyPollTimeoutMs;
|
|
1757
|
-
let lastStatus = initial.status;
|
|
1758
|
-
while (Date.now() < deadline) {
|
|
1759
|
-
const current = await getComputerByID(initial.id);
|
|
1760
|
-
if (current.status === "running") {
|
|
1761
|
-
spinner.succeed(`${chalk5.bold(current.handle)} is ready`);
|
|
1762
|
-
return current;
|
|
1763
|
-
}
|
|
1764
|
-
if (current.status !== lastStatus) {
|
|
1765
|
-
lastStatus = current.status;
|
|
1766
|
-
spinner.text = `Waiting for ${chalk5.bold(current.handle)}... ${chalk5.dim(current.status)}`;
|
|
1767
|
-
}
|
|
1768
|
-
if (current.status === "error" || current.status === "deleted" || current.status === "stopped") {
|
|
1769
|
-
spinner.fail(`${current.handle} entered ${current.status}`);
|
|
1770
|
-
throw new Error(current.last_error || `${current.handle} entered ${current.status}`);
|
|
1771
|
-
}
|
|
1772
|
-
await delay(readyPollIntervalMs);
|
|
1773
|
-
}
|
|
1774
|
-
spinner.fail(`Timed out waiting for ${initial.handle}`);
|
|
1775
|
-
throw new Error(`timed out waiting for ${initial.handle} to be ready`);
|
|
1776
|
-
}
|
|
1777
|
-
async function runManualOAuthFlow() {
|
|
1778
|
-
const codeVerifier = base64url(randomBytes(32));
|
|
1779
|
-
const state = randomBytes(16).toString("hex");
|
|
1780
|
-
const codeChallenge = base64url(createHash("sha256").update(codeVerifier).digest());
|
|
1781
|
-
const url = buildAuthorizationURL(codeChallenge, state);
|
|
1782
|
-
console.log("We will open your browser so you can authenticate with Claude.");
|
|
1783
|
-
console.log("If the browser does not open automatically, use the URL below:\n");
|
|
1784
|
-
console.log(url);
|
|
1785
|
-
console.log();
|
|
1786
|
-
try {
|
|
1787
|
-
await openBrowserURL(url);
|
|
1788
|
-
} catch {
|
|
1789
|
-
console.log(chalk5.yellow("Unable to open the browser automatically."));
|
|
1790
|
-
}
|
|
1791
|
-
console.log(
|
|
1792
|
-
"After completing authentication, copy the code shown on the success page."
|
|
1903
|
+
console.log(
|
|
1904
|
+
"After completing authentication, copy the code shown on the success page."
|
|
1793
1905
|
);
|
|
1794
1906
|
console.log("You can paste either the full URL, or a value formatted as CODE#STATE.\n");
|
|
1795
1907
|
const pasted = (await textInput({
|
|
@@ -1799,7 +1911,7 @@ async function runManualOAuthFlow() {
|
|
|
1799
1911
|
throw new Error("no authorization code provided");
|
|
1800
1912
|
}
|
|
1801
1913
|
const parsed = parseAuthorizationInput(pasted, state);
|
|
1802
|
-
const spinner =
|
|
1914
|
+
const spinner = ora4("Exchanging authorization code...").start();
|
|
1803
1915
|
try {
|
|
1804
1916
|
const response = await fetch(CLAUDE_OAUTH_TOKEN_URL, {
|
|
1805
1917
|
method: "POST",
|
|
@@ -1871,25 +1983,11 @@ function parseAuthorizationInput(value, expectedState) {
|
|
|
1871
1983
|
}
|
|
1872
1984
|
return { code, state };
|
|
1873
1985
|
}
|
|
1874
|
-
async function resolveSSHTarget(computer) {
|
|
1875
|
-
const registered = await ensureDefaultSSHKeyRegistered();
|
|
1876
|
-
const info = await getConnectionInfo(computer.id);
|
|
1877
|
-
if (!info.connection.ssh_available) {
|
|
1878
|
-
throw new Error(`SSH is not available for ${computer.handle}`);
|
|
1879
|
-
}
|
|
1880
|
-
return {
|
|
1881
|
-
handle: computer.handle,
|
|
1882
|
-
host: info.connection.ssh_host,
|
|
1883
|
-
port: info.connection.ssh_port,
|
|
1884
|
-
user: info.connection.ssh_user,
|
|
1885
|
-
identityFile: registered.privateKeyPath
|
|
1886
|
-
};
|
|
1887
|
-
}
|
|
1888
1986
|
async function installClaudeAuth(target, oauth) {
|
|
1889
|
-
const spinner =
|
|
1987
|
+
const spinner = ora4(`Installing Claude auth on ${chalk5.bold(target.handle)}...`).start();
|
|
1890
1988
|
try {
|
|
1891
1989
|
const installScript = buildInstallScript(oauth.refreshToken, oauth.scope);
|
|
1892
|
-
const result = await
|
|
1990
|
+
const result = await runRemoteCommand(target, ["bash", "-s"], installScript);
|
|
1893
1991
|
if (result.stdout.trim()) {
|
|
1894
1992
|
spinner.succeed(`Installed Claude auth on ${chalk5.bold(target.handle)}`);
|
|
1895
1993
|
return;
|
|
@@ -1902,9 +2000,36 @@ async function installClaudeAuth(target, oauth) {
|
|
|
1902
2000
|
throw error;
|
|
1903
2001
|
}
|
|
1904
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
|
+
}
|
|
1905
2030
|
function buildInstallScript(refreshToken, scopes) {
|
|
1906
|
-
const tokenMarker = `TOKEN_${
|
|
1907
|
-
const scopeMarker = `SCOPES_${
|
|
2031
|
+
const tokenMarker = `TOKEN_${randomSuffix2(12)}`;
|
|
2032
|
+
const scopeMarker = `SCOPES_${randomSuffix2(12)}`;
|
|
1908
2033
|
return [
|
|
1909
2034
|
"set -euo pipefail",
|
|
1910
2035
|
'command -v claude >/dev/null 2>&1 || { echo "claude is not installed on this computer" >&2; exit 1; }',
|
|
@@ -1922,113 +2047,65 @@ function buildInstallScript(refreshToken, scopes) {
|
|
|
1922
2047
|
].join("\n");
|
|
1923
2048
|
}
|
|
1924
2049
|
async function verifyStoredAuth(target) {
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
'SHELL="/bin/bash"',
|
|
1938
|
-
'PATH="$HOME/.local/bin:/usr/local/bin:/usr/bin:/bin"',
|
|
1939
|
-
"bash -lc",
|
|
1940
|
-
shellQuote(inner)
|
|
1941
|
-
].join(" ");
|
|
1942
|
-
}
|
|
1943
|
-
function parseStatusOutput(stdout) {
|
|
1944
|
-
const start = stdout.indexOf("{");
|
|
1945
|
-
const end = stdout.lastIndexOf("}");
|
|
1946
|
-
if (start === -1 || end === -1 || end <= start) {
|
|
1947
|
-
throw new Error("could not parse claude auth status output");
|
|
1948
|
-
}
|
|
1949
|
-
const parsed = JSON.parse(stdout.slice(start, end + 1));
|
|
1950
|
-
return { loggedIn: parsed.loggedIn === true };
|
|
1951
|
-
}
|
|
1952
|
-
async function verifySecondaryMachine(primaryComputerID, sharedInstall, skip) {
|
|
1953
|
-
if (!sharedInstall) {
|
|
1954
|
-
return { status: "skipped", reason: "target uses isolated filesystem" };
|
|
1955
|
-
}
|
|
1956
|
-
if (skip) {
|
|
1957
|
-
return { status: "skipped", reason: "cross-check skipped by flag" };
|
|
1958
|
-
}
|
|
1959
|
-
const secondary = (await listComputers()).filter(
|
|
1960
|
-
(computer) => computer.id !== primaryComputerID && computer.runtime_family === "managed-worker" && computer.filesystem_mode === "shared" && computer.ssh_enabled && computer.status === "running"
|
|
1961
|
-
).sort(
|
|
1962
|
-
(left, right) => new Date(right.updated_at).getTime() - new Date(left.updated_at).getTime()
|
|
1963
|
-
)[0];
|
|
1964
|
-
if (!secondary) {
|
|
2050
|
+
try {
|
|
2051
|
+
const result = await runRemoteCommand(target, [
|
|
2052
|
+
"bash",
|
|
2053
|
+
"--noprofile",
|
|
2054
|
+
"--norc",
|
|
2055
|
+
"-lc",
|
|
2056
|
+
"claude auth status --json 2>/dev/null || claude auth status"
|
|
2057
|
+
]);
|
|
2058
|
+
const payload = parseStatusOutput(result.stdout, result.stderr);
|
|
2059
|
+
if (payload.loggedIn) {
|
|
2060
|
+
return { status: "verified", detail: "verified" };
|
|
2061
|
+
}
|
|
1965
2062
|
return {
|
|
1966
|
-
status: "
|
|
1967
|
-
|
|
2063
|
+
status: "failed",
|
|
2064
|
+
detail: payload.detail ? `verification failed: ${payload.detail}` : "verification failed"
|
|
2065
|
+
};
|
|
2066
|
+
} catch (error) {
|
|
2067
|
+
return {
|
|
2068
|
+
status: "inconclusive",
|
|
2069
|
+
detail: error instanceof Error ? error.message : "verification command did not complete cleanly"
|
|
1968
2070
|
};
|
|
1969
2071
|
}
|
|
1970
|
-
const sshTarget = await resolveSSHTarget(secondary);
|
|
1971
|
-
await verifyStoredAuth(sshTarget);
|
|
1972
|
-
return { status: "verified", handle: secondary.handle };
|
|
1973
2072
|
}
|
|
1974
|
-
|
|
1975
|
-
const
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
"bash",
|
|
1983
|
-
...bashArgs
|
|
1984
|
-
];
|
|
1985
|
-
return new Promise((resolve, reject) => {
|
|
1986
|
-
const child = spawn2("ssh", args, {
|
|
1987
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
1988
|
-
});
|
|
1989
|
-
let stdout = "";
|
|
1990
|
-
let stderr = "";
|
|
1991
|
-
child.stdout.on("data", (chunk) => {
|
|
1992
|
-
stdout += chunk.toString();
|
|
1993
|
-
});
|
|
1994
|
-
child.stderr.on("data", (chunk) => {
|
|
1995
|
-
stderr += chunk.toString();
|
|
1996
|
-
});
|
|
1997
|
-
child.on("error", reject);
|
|
1998
|
-
child.on("exit", (code) => {
|
|
1999
|
-
if (code === 0) {
|
|
2000
|
-
resolve({ stdout, stderr });
|
|
2001
|
-
return;
|
|
2002
|
-
}
|
|
2003
|
-
const message = stderr.trim() || stdout.trim() || `ssh exited with code ${code ?? 1}`;
|
|
2004
|
-
reject(new Error(message));
|
|
2005
|
-
});
|
|
2006
|
-
if (script !== void 0) {
|
|
2007
|
-
child.stdin.end(script);
|
|
2008
|
-
} else {
|
|
2009
|
-
child.stdin.end();
|
|
2073
|
+
function parseStatusOutput(stdout, stderr) {
|
|
2074
|
+
const combined = [stdout, stderr].map((value) => value.trim()).filter(Boolean).join("\n");
|
|
2075
|
+
const start = combined.indexOf("{");
|
|
2076
|
+
const end = combined.lastIndexOf("}");
|
|
2077
|
+
if (start === -1 || end === -1 || end <= start) {
|
|
2078
|
+
const normalized = combined.toLowerCase();
|
|
2079
|
+
if (normalized.includes("not logged in") || normalized.includes("logged out")) {
|
|
2080
|
+
return { loggedIn: false, detail: firstStatusLine(combined) };
|
|
2010
2081
|
}
|
|
2011
|
-
|
|
2082
|
+
if (normalized.includes("logged in")) {
|
|
2083
|
+
return { loggedIn: true };
|
|
2084
|
+
}
|
|
2085
|
+
throw new Error(
|
|
2086
|
+
combined ? `could not verify Claude auth from status output: ${firstStatusLine(combined)}` : "could not verify Claude auth from empty status output"
|
|
2087
|
+
);
|
|
2088
|
+
}
|
|
2089
|
+
const parsed = JSON.parse(combined.slice(start, end + 1));
|
|
2090
|
+
return {
|
|
2091
|
+
loggedIn: parsed.loggedIn === true,
|
|
2092
|
+
detail: parsed.loggedIn === true ? void 0 : parsed.error
|
|
2093
|
+
};
|
|
2012
2094
|
}
|
|
2013
|
-
function
|
|
2014
|
-
return
|
|
2095
|
+
function firstStatusLine(value) {
|
|
2096
|
+
return value.split(/\r?\n/).map((line) => line.trim()).find(Boolean) ?? "unknown output";
|
|
2015
2097
|
}
|
|
2016
2098
|
function base64url(buffer) {
|
|
2017
2099
|
return buffer.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
|
|
2018
2100
|
}
|
|
2019
|
-
function
|
|
2020
|
-
return
|
|
2021
|
-
}
|
|
2022
|
-
function delay(ms) {
|
|
2023
|
-
return new Promise((resolve) => {
|
|
2024
|
-
setTimeout(resolve, ms);
|
|
2025
|
-
});
|
|
2101
|
+
function randomSuffix2(length) {
|
|
2102
|
+
return randomBytes2(Math.ceil(length / 2)).toString("hex").slice(0, length);
|
|
2026
2103
|
}
|
|
2027
2104
|
|
|
2028
2105
|
// src/commands/computers.ts
|
|
2029
2106
|
import { Command as Command5 } from "commander";
|
|
2030
2107
|
import chalk6 from "chalk";
|
|
2031
|
-
import
|
|
2108
|
+
import ora5 from "ora";
|
|
2032
2109
|
import { select as select2, input as textInput2, confirm } from "@inquirer/prompts";
|
|
2033
2110
|
|
|
2034
2111
|
// src/lib/machine-sources.ts
|
|
@@ -2193,7 +2270,7 @@ function printComputerTableVerbose(computers) {
|
|
|
2193
2270
|
console.log();
|
|
2194
2271
|
}
|
|
2195
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) => {
|
|
2196
|
-
const spinner = options.json ? null :
|
|
2273
|
+
const spinner = options.json ? null : ora5("Fetching computers...").start();
|
|
2197
2274
|
try {
|
|
2198
2275
|
const computers = await listComputers();
|
|
2199
2276
|
spinner?.stop();
|
|
@@ -2224,7 +2301,7 @@ var lsCommand = new Command5("ls").description("List computers").option("--json"
|
|
|
2224
2301
|
}
|
|
2225
2302
|
});
|
|
2226
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) => {
|
|
2227
|
-
const spinner = options.json ? null :
|
|
2304
|
+
const spinner = options.json ? null : ora5("Fetching computer...").start();
|
|
2228
2305
|
try {
|
|
2229
2306
|
const computer = await resolveComputer(identifier);
|
|
2230
2307
|
spinner?.stop();
|
|
@@ -2265,7 +2342,7 @@ var createCommand = new Command5("create").description("Create a computer").argu
|
|
|
2265
2342
|
if (provisioningNote) {
|
|
2266
2343
|
console.log(chalk6.dim(provisioningNote));
|
|
2267
2344
|
}
|
|
2268
|
-
spinner =
|
|
2345
|
+
spinner = ora5(createSpinnerText(runtimeFamily, filesystemSettings, 0)).start();
|
|
2269
2346
|
startTime = Date.now();
|
|
2270
2347
|
timer = setInterval(() => {
|
|
2271
2348
|
const elapsed2 = (Date.now() - startTime) / 1e3;
|
|
@@ -2371,7 +2448,7 @@ async function resolveCreateOptions(options) {
|
|
|
2371
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) => {
|
|
2372
2449
|
const globalYes = cmd.parent?.opts()?.yes;
|
|
2373
2450
|
const skipConfirm = Boolean(options.yes || globalYes);
|
|
2374
|
-
const spinner =
|
|
2451
|
+
const spinner = ora5("Resolving computer...").start();
|
|
2375
2452
|
try {
|
|
2376
2453
|
const computer = await resolveComputer(identifier);
|
|
2377
2454
|
spinner.stop();
|
|
@@ -2385,7 +2462,7 @@ var removeCommand = new Command5("rm").description("Delete a computer").argument
|
|
|
2385
2462
|
return;
|
|
2386
2463
|
}
|
|
2387
2464
|
}
|
|
2388
|
-
const deleteSpinner =
|
|
2465
|
+
const deleteSpinner = ora5("Deleting computer...").start();
|
|
2389
2466
|
await api(`/v1/computers/${computer.id}`, {
|
|
2390
2467
|
method: "DELETE"
|
|
2391
2468
|
});
|
|
@@ -2524,8 +2601,10 @@ _computer() {
|
|
|
2524
2601
|
'login:Authenticate the CLI'
|
|
2525
2602
|
'logout:Remove stored API key'
|
|
2526
2603
|
'whoami:Show current user'
|
|
2527
|
-
'claude-
|
|
2528
|
-
'claude-
|
|
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'
|
|
2529
2608
|
'create:Create a computer'
|
|
2530
2609
|
'ls:List computers'
|
|
2531
2610
|
'get:Show computer details'
|
|
@@ -2578,11 +2657,12 @@ _computer() {
|
|
|
2578
2657
|
whoami)
|
|
2579
2658
|
_arguments '--json[Print raw JSON]'
|
|
2580
2659
|
;;
|
|
2581
|
-
claude-auth|claude-login)
|
|
2660
|
+
claude-auth|claude-login|codex-auth|codex-login)
|
|
2582
2661
|
_arguments \\
|
|
2583
2662
|
'--machine[Use a specific computer]:computer:_computer_handles' \\
|
|
2584
2663
|
'--keep-helper[Keep a temporary helper machine]' \\
|
|
2585
|
-
'--skip-cross-check[Skip second-machine verification]'
|
|
2664
|
+
'--skip-cross-check[Skip second-machine verification]' \\
|
|
2665
|
+
'--verbose[Show step-by-step auth diagnostics]'
|
|
2586
2666
|
;;
|
|
2587
2667
|
create)
|
|
2588
2668
|
_arguments \\
|
|
@@ -2730,7 +2810,7 @@ var BASH_SCRIPT = `_computer() {
|
|
|
2730
2810
|
local cur prev words cword
|
|
2731
2811
|
_init_completion || return
|
|
2732
2812
|
|
|
2733
|
-
local commands="login logout whoami claude-
|
|
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"
|
|
2734
2814
|
local ports_commands="ls publish rm"
|
|
2735
2815
|
local image_commands="ls save default rebuild rm"
|
|
2736
2816
|
|
|
@@ -2748,8 +2828,8 @@ var BASH_SCRIPT = `_computer() {
|
|
|
2748
2828
|
whoami)
|
|
2749
2829
|
COMPREPLY=($(compgen -W "--json" -- "$cur"))
|
|
2750
2830
|
;;
|
|
2751
|
-
claude-auth|claude-login)
|
|
2752
|
-
COMPREPLY=($(compgen -W "--machine --keep-helper --skip-cross-check" -- "$cur"))
|
|
2831
|
+
claude-auth|claude-login|codex-auth|codex-login)
|
|
2832
|
+
COMPREPLY=($(compgen -W "--machine --keep-helper --skip-cross-check --verbose" -- "$cur"))
|
|
2753
2833
|
;;
|
|
2754
2834
|
create)
|
|
2755
2835
|
COMPREPLY=($(compgen -W "--name --tier --interactive --runtime-family --source-kind --image-family --image-ref --use-platform-default --primary-port --primary-path --healthcheck-type --healthcheck-value --ssh-enabled --ssh-disabled --vnc-enabled --vnc-disabled" -- "$cur"))
|
|
@@ -2847,14 +2927,344 @@ var completionCommand = new Command6("completion").description("Generate shell c
|
|
|
2847
2927
|
}
|
|
2848
2928
|
});
|
|
2849
2929
|
|
|
2850
|
-
// src/commands/
|
|
2851
|
-
import {
|
|
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";
|
|
2852
2935
|
import { Command as Command7 } from "commander";
|
|
2853
2936
|
import chalk7 from "chalk";
|
|
2854
|
-
import
|
|
2855
|
-
var
|
|
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");
|
|
2856
3266
|
imageCommand.command("ls").description("List machine image sources").option("--json", "Print raw JSON").action(async (options) => {
|
|
2857
|
-
const spinner = options.json ? null :
|
|
3267
|
+
const spinner = options.json ? null : ora7("Fetching machine images...").start();
|
|
2858
3268
|
try {
|
|
2859
3269
|
const settings = await getMachineSourceSettings();
|
|
2860
3270
|
spinner?.stop();
|
|
@@ -2873,7 +3283,7 @@ imageCommand.command("ls").description("List machine image sources").option("--j
|
|
|
2873
3283
|
}
|
|
2874
3284
|
});
|
|
2875
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) => {
|
|
2876
|
-
const spinner = options.json ? null :
|
|
3286
|
+
const spinner = options.json ? null : ora7("Saving machine image source...").start();
|
|
2877
3287
|
try {
|
|
2878
3288
|
const input = await resolveSaveInput(options);
|
|
2879
3289
|
const settings = await upsertMachineSource(input);
|
|
@@ -2883,7 +3293,7 @@ imageCommand.command("save").description("Create or update a machine image sourc
|
|
|
2883
3293
|
return;
|
|
2884
3294
|
}
|
|
2885
3295
|
console.log();
|
|
2886
|
-
console.log(
|
|
3296
|
+
console.log(chalk8.green("Saved machine image source."));
|
|
2887
3297
|
printMachineSourceSettings(settings);
|
|
2888
3298
|
} catch (error) {
|
|
2889
3299
|
if (spinner) {
|
|
@@ -2896,7 +3306,7 @@ imageCommand.command("save").description("Create or update a machine image sourc
|
|
|
2896
3306
|
});
|
|
2897
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) => {
|
|
2898
3308
|
const usePlatformDefault = !sourceID || sourceID === "platform";
|
|
2899
|
-
const spinner = options.json ? null :
|
|
3309
|
+
const spinner = options.json ? null : ora7("Updating machine image default...").start();
|
|
2900
3310
|
try {
|
|
2901
3311
|
let settings;
|
|
2902
3312
|
if (usePlatformDefault) {
|
|
@@ -2922,9 +3332,9 @@ imageCommand.command("default").description("Set the default machine image sourc
|
|
|
2922
3332
|
if (!usePlatformDefault) {
|
|
2923
3333
|
const selected = settings.default_machine_source ?? void 0;
|
|
2924
3334
|
const label = selected ? summarizeMachineSource(selected) : sourceID;
|
|
2925
|
-
console.log(
|
|
3335
|
+
console.log(chalk8.green(`Selected ${chalk8.bold(label)} as the default machine image.`));
|
|
2926
3336
|
} else {
|
|
2927
|
-
console.log(
|
|
3337
|
+
console.log(chalk8.green("Using the AgentComputer platform default image."));
|
|
2928
3338
|
}
|
|
2929
3339
|
printMachineSourceSettings(settings);
|
|
2930
3340
|
} catch (error) {
|
|
@@ -2937,7 +3347,7 @@ imageCommand.command("default").description("Set the default machine image sourc
|
|
|
2937
3347
|
}
|
|
2938
3348
|
});
|
|
2939
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) => {
|
|
2940
|
-
const spinner = options.json ? null :
|
|
3350
|
+
const spinner = options.json ? null : ora7("Queueing machine image rebuild...").start();
|
|
2941
3351
|
try {
|
|
2942
3352
|
const settings = await rebuildMachineSource(sourceID);
|
|
2943
3353
|
spinner?.stop();
|
|
@@ -2946,7 +3356,7 @@ imageCommand.command("rebuild").description("Rebuild a machine image source").ar
|
|
|
2946
3356
|
return;
|
|
2947
3357
|
}
|
|
2948
3358
|
console.log();
|
|
2949
|
-
console.log(
|
|
3359
|
+
console.log(chalk8.green(`Queued rebuild for ${chalk8.bold(sourceID)}.`));
|
|
2950
3360
|
printMachineSourceSettings(settings);
|
|
2951
3361
|
} catch (error) {
|
|
2952
3362
|
if (spinner) {
|
|
@@ -2965,11 +3375,11 @@ imageCommand.command("rm").description("Delete a machine image source").argument
|
|
|
2965
3375
|
if (!skipConfirm && process.stdin.isTTY) {
|
|
2966
3376
|
const confirmed = await confirmDeletion(sourceID);
|
|
2967
3377
|
if (!confirmed) {
|
|
2968
|
-
console.log(
|
|
3378
|
+
console.log(chalk8.dim(" Cancelled."));
|
|
2969
3379
|
return;
|
|
2970
3380
|
}
|
|
2971
3381
|
}
|
|
2972
|
-
spinner = options.json ? null :
|
|
3382
|
+
spinner = options.json ? null : ora7("Deleting machine image source...").start();
|
|
2973
3383
|
const settings = await deleteMachineSource(sourceID);
|
|
2974
3384
|
spinner?.stop();
|
|
2975
3385
|
if (options.json) {
|
|
@@ -2977,7 +3387,7 @@ imageCommand.command("rm").description("Delete a machine image source").argument
|
|
|
2977
3387
|
return;
|
|
2978
3388
|
}
|
|
2979
3389
|
console.log();
|
|
2980
|
-
console.log(
|
|
3390
|
+
console.log(chalk8.green(`Deleted machine image source ${chalk8.bold(sourceID)}.`));
|
|
2981
3391
|
printMachineSourceSettings(settings);
|
|
2982
3392
|
} catch (error) {
|
|
2983
3393
|
if (spinner) {
|
|
@@ -2989,10 +3399,10 @@ imageCommand.command("rm").description("Delete a machine image source").argument
|
|
|
2989
3399
|
}
|
|
2990
3400
|
});
|
|
2991
3401
|
function printMachineSourceSettings(settings) {
|
|
2992
|
-
console.log(` ${
|
|
3402
|
+
console.log(` ${chalk8.dim("Default")} ${chalk8.white(summarizeMachineSourceSelection(settings))}`);
|
|
2993
3403
|
console.log();
|
|
2994
3404
|
if (settings.sources.length === 0) {
|
|
2995
|
-
console.log(
|
|
3405
|
+
console.log(chalk8.dim(" No custom machine images configured yet."));
|
|
2996
3406
|
console.log();
|
|
2997
3407
|
return;
|
|
2998
3408
|
}
|
|
@@ -3001,17 +3411,17 @@ function printMachineSourceSettings(settings) {
|
|
|
3001
3411
|
}
|
|
3002
3412
|
}
|
|
3003
3413
|
function printMachineSourceCard(source, isDefault) {
|
|
3004
|
-
console.log(` ${
|
|
3005
|
-
console.log(` ${
|
|
3006
|
-
console.log(` ${
|
|
3007
|
-
console.log(` ${
|
|
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}` : ""}`);
|
|
3008
3418
|
if (source.resolved_image_ref) {
|
|
3009
|
-
console.log(` ${
|
|
3419
|
+
console.log(` ${chalk8.dim(" Resolved")} ${source.resolved_image_ref}`);
|
|
3010
3420
|
}
|
|
3011
3421
|
if (source.error) {
|
|
3012
|
-
console.log(` ${
|
|
3422
|
+
console.log(` ${chalk8.dim(" Error")} ${chalk8.red(source.error)}`);
|
|
3013
3423
|
}
|
|
3014
|
-
console.log(` ${
|
|
3424
|
+
console.log(` ${chalk8.dim(" Source")} ${summarizeMachineSource(source)}`);
|
|
3015
3425
|
console.log();
|
|
3016
3426
|
}
|
|
3017
3427
|
function machineSourceTitle(source) {
|
|
@@ -3128,18 +3538,18 @@ async function confirmDeletion(sourceID) {
|
|
|
3128
3538
|
}
|
|
3129
3539
|
|
|
3130
3540
|
// src/commands/login.ts
|
|
3131
|
-
import { Command as
|
|
3132
|
-
import
|
|
3133
|
-
import
|
|
3541
|
+
import { Command as Command9 } from "commander";
|
|
3542
|
+
import chalk9 from "chalk";
|
|
3543
|
+
import ora8 from "ora";
|
|
3134
3544
|
|
|
3135
3545
|
// src/lib/browser-login.ts
|
|
3136
|
-
import { randomBytes as
|
|
3546
|
+
import { randomBytes as randomBytes3 } from "crypto";
|
|
3137
3547
|
import { createServer } from "http";
|
|
3138
3548
|
var CALLBACK_HOST = "127.0.0.1";
|
|
3139
3549
|
var CALLBACK_PATH = "/callback";
|
|
3140
3550
|
var LOGIN_TIMEOUT_MS = 5 * 60 * 1e3;
|
|
3141
3551
|
async function createBrowserLoginAttempt() {
|
|
3142
|
-
const state =
|
|
3552
|
+
const state = randomBytes3(16).toString("hex");
|
|
3143
3553
|
const deferred = createDeferred();
|
|
3144
3554
|
let callbackURL = "";
|
|
3145
3555
|
let closed = false;
|
|
@@ -3358,11 +3768,11 @@ function escapeHTML(value) {
|
|
|
3358
3768
|
}
|
|
3359
3769
|
|
|
3360
3770
|
// src/commands/login.ts
|
|
3361
|
-
var loginCommand = new
|
|
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) => {
|
|
3362
3772
|
const existingKey = getStoredAPIKey();
|
|
3363
3773
|
if (existingKey && !options.force) {
|
|
3364
3774
|
console.log();
|
|
3365
|
-
console.log(
|
|
3775
|
+
console.log(chalk9.yellow(" Already logged in. Use --force to overwrite."));
|
|
3366
3776
|
console.log();
|
|
3367
3777
|
return;
|
|
3368
3778
|
}
|
|
@@ -3370,8 +3780,8 @@ var loginCommand = new Command8("login").description("Authenticate the CLI").opt
|
|
|
3370
3780
|
const apiKey = await resolveAPIKeyInput(options.apiKey, options.stdin);
|
|
3371
3781
|
if (!apiKey && wantsManualLogin) {
|
|
3372
3782
|
console.log();
|
|
3373
|
-
console.log(
|
|
3374
|
-
console.log(
|
|
3783
|
+
console.log(chalk9.dim(" Usage: computer login --api-key <ac_live_...>"));
|
|
3784
|
+
console.log(chalk9.dim(` API: ${getBaseURL()}`));
|
|
3375
3785
|
console.log();
|
|
3376
3786
|
process.exit(1);
|
|
3377
3787
|
}
|
|
@@ -3381,15 +3791,15 @@ var loginCommand = new Command8("login").description("Authenticate the CLI").opt
|
|
|
3381
3791
|
}
|
|
3382
3792
|
if (!apiKey.startsWith("ac_live_")) {
|
|
3383
3793
|
console.log();
|
|
3384
|
-
console.log(
|
|
3794
|
+
console.log(chalk9.red(" API key must start with ac_live_"));
|
|
3385
3795
|
console.log();
|
|
3386
3796
|
process.exit(1);
|
|
3387
3797
|
}
|
|
3388
|
-
const spinner =
|
|
3798
|
+
const spinner = ora8("Authenticating...").start();
|
|
3389
3799
|
try {
|
|
3390
3800
|
const me = await apiWithKey(apiKey, "/v1/me");
|
|
3391
3801
|
setAPIKey(apiKey);
|
|
3392
|
-
spinner.succeed(`Logged in as ${
|
|
3802
|
+
spinner.succeed(`Logged in as ${chalk9.bold(me.user.email)}`);
|
|
3393
3803
|
} catch (error) {
|
|
3394
3804
|
spinner.fail(
|
|
3395
3805
|
error instanceof Error ? error.message : "Failed to validate API key"
|
|
@@ -3398,7 +3808,7 @@ var loginCommand = new Command8("login").description("Authenticate the CLI").opt
|
|
|
3398
3808
|
}
|
|
3399
3809
|
});
|
|
3400
3810
|
async function runBrowserLogin() {
|
|
3401
|
-
const spinner =
|
|
3811
|
+
const spinner = ora8("Starting browser login...").start();
|
|
3402
3812
|
let attempt = null;
|
|
3403
3813
|
try {
|
|
3404
3814
|
attempt = await createBrowserLoginAttempt();
|
|
@@ -3408,14 +3818,14 @@ async function runBrowserLogin() {
|
|
|
3408
3818
|
} catch {
|
|
3409
3819
|
spinner.stop();
|
|
3410
3820
|
console.log();
|
|
3411
|
-
console.log(
|
|
3412
|
-
console.log(
|
|
3821
|
+
console.log(chalk9.yellow(" Browser auto-open failed. Open this URL to continue:"));
|
|
3822
|
+
console.log(chalk9.dim(` ${attempt.loginURL}`));
|
|
3413
3823
|
console.log();
|
|
3414
3824
|
spinner.start("Waiting for browser login...");
|
|
3415
3825
|
}
|
|
3416
3826
|
spinner.text = "Waiting for browser login...";
|
|
3417
3827
|
const result = await attempt.waitForResult();
|
|
3418
|
-
spinner.succeed(`Logged in as ${
|
|
3828
|
+
spinner.succeed(`Logged in as ${chalk9.bold(result.me.user.email)}`);
|
|
3419
3829
|
} catch (error) {
|
|
3420
3830
|
spinner.fail(error instanceof Error ? error.message : "Browser login failed");
|
|
3421
3831
|
process.exit(1);
|
|
@@ -3441,33 +3851,33 @@ async function resolveAPIKeyInput(flagValue, readFromStdin) {
|
|
|
3441
3851
|
}
|
|
3442
3852
|
|
|
3443
3853
|
// src/commands/logout.ts
|
|
3444
|
-
import { Command as
|
|
3445
|
-
import
|
|
3446
|
-
var logoutCommand = new
|
|
3854
|
+
import { Command as Command10 } from "commander";
|
|
3855
|
+
import chalk10 from "chalk";
|
|
3856
|
+
var logoutCommand = new Command10("logout").description("Remove stored API key").action(() => {
|
|
3447
3857
|
if (!getStoredAPIKey()) {
|
|
3448
3858
|
console.log();
|
|
3449
|
-
console.log(
|
|
3859
|
+
console.log(chalk10.dim(" Not logged in."));
|
|
3450
3860
|
if (hasEnvAPIKey()) {
|
|
3451
|
-
console.log(
|
|
3861
|
+
console.log(chalk10.dim(" Environment API key is still active in this shell."));
|
|
3452
3862
|
}
|
|
3453
3863
|
console.log();
|
|
3454
3864
|
return;
|
|
3455
3865
|
}
|
|
3456
3866
|
clearAPIKey();
|
|
3457
3867
|
console.log();
|
|
3458
|
-
console.log(
|
|
3868
|
+
console.log(chalk10.green(" Logged out."));
|
|
3459
3869
|
if (hasEnvAPIKey()) {
|
|
3460
|
-
console.log(
|
|
3870
|
+
console.log(chalk10.dim(" Environment API key is still active in this shell."));
|
|
3461
3871
|
}
|
|
3462
3872
|
console.log();
|
|
3463
3873
|
});
|
|
3464
3874
|
|
|
3465
3875
|
// src/commands/whoami.ts
|
|
3466
|
-
import { Command as
|
|
3467
|
-
import
|
|
3468
|
-
import
|
|
3469
|
-
var whoamiCommand = new
|
|
3470
|
-
const spinner = options.json ? null :
|
|
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();
|
|
3471
3881
|
try {
|
|
3472
3882
|
const me = await api("/v1/me");
|
|
3473
3883
|
spinner?.stop();
|
|
@@ -3476,14 +3886,14 @@ var whoamiCommand = new Command10("whoami").description("Show current user").opt
|
|
|
3476
3886
|
return;
|
|
3477
3887
|
}
|
|
3478
3888
|
console.log();
|
|
3479
|
-
console.log(` ${
|
|
3889
|
+
console.log(` ${chalk11.bold.white(me.user.display_name || me.user.email)}`);
|
|
3480
3890
|
if (me.user.display_name) {
|
|
3481
|
-
console.log(` ${
|
|
3891
|
+
console.log(` ${chalk11.dim(me.user.email)}`);
|
|
3482
3892
|
}
|
|
3483
3893
|
if (me.api_key.name) {
|
|
3484
|
-
console.log(` ${
|
|
3894
|
+
console.log(` ${chalk11.dim("Key:")} ${me.api_key.name}`);
|
|
3485
3895
|
}
|
|
3486
|
-
console.log(` ${
|
|
3896
|
+
console.log(` ${chalk11.dim("API:")} ${chalk11.dim(getBaseURL())}`);
|
|
3487
3897
|
console.log();
|
|
3488
3898
|
} catch (error) {
|
|
3489
3899
|
if (spinner) {
|
|
@@ -3500,15 +3910,15 @@ var pkg2 = JSON.parse(
|
|
|
3500
3910
|
readFileSync3(new URL("../package.json", import.meta.url), "utf8")
|
|
3501
3911
|
);
|
|
3502
3912
|
var cliName = process.argv[1] ? basename2(process.argv[1]) : "agentcomputer";
|
|
3503
|
-
var program = new
|
|
3913
|
+
var program = new Command12();
|
|
3504
3914
|
function appendTextSection(lines, title, values) {
|
|
3505
3915
|
if (values.length === 0) {
|
|
3506
3916
|
return;
|
|
3507
3917
|
}
|
|
3508
|
-
lines.push(` ${
|
|
3918
|
+
lines.push(` ${chalk12.dim(title)}`);
|
|
3509
3919
|
lines.push("");
|
|
3510
3920
|
for (const value of values) {
|
|
3511
|
-
lines.push(` ${
|
|
3921
|
+
lines.push(` ${chalk12.white(value)}`);
|
|
3512
3922
|
}
|
|
3513
3923
|
lines.push("");
|
|
3514
3924
|
}
|
|
@@ -3517,10 +3927,10 @@ function appendTableSection(lines, title, entries) {
|
|
|
3517
3927
|
return;
|
|
3518
3928
|
}
|
|
3519
3929
|
const width = Math.max(...entries.map((entry) => entry.term.length), 0) + 2;
|
|
3520
|
-
lines.push(` ${
|
|
3930
|
+
lines.push(` ${chalk12.dim(title)}`);
|
|
3521
3931
|
lines.push("");
|
|
3522
3932
|
for (const entry of entries) {
|
|
3523
|
-
lines.push(` ${
|
|
3933
|
+
lines.push(` ${chalk12.white(padEnd(entry.term, width))}${chalk12.dim(entry.desc)}`);
|
|
3524
3934
|
}
|
|
3525
3935
|
lines.push("");
|
|
3526
3936
|
}
|
|
@@ -3545,17 +3955,17 @@ function formatRootHelp(cmd) {
|
|
|
3545
3955
|
["Other", []]
|
|
3546
3956
|
];
|
|
3547
3957
|
const otherGroup = groups.find(([name]) => name === "Other")[1];
|
|
3548
|
-
lines.push(`${
|
|
3958
|
+
lines.push(`${chalk12.bold(cliName)} ${chalk12.dim(`v${version}`)}`);
|
|
3549
3959
|
lines.push("");
|
|
3550
3960
|
if (cmd.description()) {
|
|
3551
|
-
lines.push(` ${
|
|
3961
|
+
lines.push(` ${chalk12.dim(cmd.description())}`);
|
|
3552
3962
|
lines.push("");
|
|
3553
3963
|
}
|
|
3554
3964
|
appendTextSection(lines, "Usage", [`${cliName} <command> [options]`]);
|
|
3555
3965
|
for (const sub of cmd.commands) {
|
|
3556
3966
|
const name = sub.name();
|
|
3557
3967
|
const entry = { term: name, desc: sub.description() };
|
|
3558
|
-
if (["login", "logout", "whoami", "claude-
|
|
3968
|
+
if (["login", "logout", "whoami", "claude-login", "codex-login"].includes(name)) {
|
|
3559
3969
|
groups[0][1].push(entry);
|
|
3560
3970
|
} else if (["create", "ls", "get", "rm"].includes(name)) {
|
|
3561
3971
|
groups[1][1].push(entry);
|
|
@@ -3599,10 +4009,10 @@ function formatSubcommandHelp(cmd, helper) {
|
|
|
3599
4009
|
term: helper.optionTerm(option),
|
|
3600
4010
|
desc: helper.optionDescription(option)
|
|
3601
4011
|
}));
|
|
3602
|
-
lines.push(
|
|
4012
|
+
lines.push(chalk12.bold(commandPath(cmd)));
|
|
3603
4013
|
lines.push("");
|
|
3604
4014
|
if (description) {
|
|
3605
|
-
lines.push(` ${
|
|
4015
|
+
lines.push(` ${chalk12.dim(description)}`);
|
|
3606
4016
|
lines.push("");
|
|
3607
4017
|
}
|
|
3608
4018
|
appendTextSection(lines, "Usage", [helper.commandUsage(cmd)]);
|
|
@@ -3629,7 +4039,8 @@ program.name(cliName).description("Agent Computer CLI").version(pkg2.version ??
|
|
|
3629
4039
|
program.addCommand(loginCommand);
|
|
3630
4040
|
program.addCommand(logoutCommand);
|
|
3631
4041
|
program.addCommand(whoamiCommand);
|
|
3632
|
-
program.addCommand(
|
|
4042
|
+
program.addCommand(claudeLoginCommand);
|
|
4043
|
+
program.addCommand(codexLoginCommand);
|
|
3633
4044
|
program.addCommand(createCommand);
|
|
3634
4045
|
program.addCommand(lsCommand);
|
|
3635
4046
|
program.addCommand(getCommand);
|