aicomputer 0.1.8 → 0.1.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -0
- package/dist/index.js +834 -349
- 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").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
|
|
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
|
|
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
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
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
|
-
|
|
1688
|
-
|
|
1689
|
-
sharedInstall: isSharedInstallTarget(computer2),
|
|
1690
|
-
detail: describeTarget(computer2, false)
|
|
1941
|
+
refreshToken: payload.refresh_token,
|
|
1942
|
+
scope: payload.scope
|
|
1691
1943
|
};
|
|
1692
|
-
}
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
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 =
|
|
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_${
|
|
1921
|
-
const scopeMarker = `SCOPES_${
|
|
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
|
|
2054
|
-
return
|
|
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
|
|
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 :
|
|
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 :
|
|
2304
|
+
const spinner = options.json ? null : ora5("Fetching computer...").start();
|
|
2262
2305
|
try {
|
|
2263
2306
|
const computer = await resolveComputer(identifier);
|
|
2264
2307
|
spinner?.stop();
|
|
@@ -2278,7 +2321,7 @@ var getCommand = new Command5("get").description("Show computer details").argume
|
|
|
2278
2321
|
process.exit(1);
|
|
2279
2322
|
}
|
|
2280
2323
|
});
|
|
2281
|
-
var createCommand = new Command5("create").description("Create a computer").argument("[handle]", "Optional computer handle").option("--name <display-name>", "Display name").option("--
|
|
2324
|
+
var createCommand = new Command5("create").description("Create a computer").argument("[handle]", "Optional computer handle").option("--name <display-name>", "Display name").option("--interactive", "Prompt for runtime choices").option("--runtime-family <runtime-family>", "managed-worker or custom-machine").option("--source-kind <source-kind>", "none or oci-image").option("--image-family <family>", "Image family override").option("--image-ref <image>", "Resolved image override").option("--use-platform-default", "Use the AgentComputer platform default image").option("--primary-port <port>", "Primary app port").option("--primary-path <path>", "Primary app path").option("--healthcheck-type <type>", "http or tcp").option("--healthcheck-value <value>", "Health check path or port").option("--ssh-enabled", "Enable SSH access").option("--ssh-disabled", "Disable SSH access").option("--vnc-enabled", "Enable VNC access").option("--vnc-disabled", "Disable VNC access").action(async (handle, options) => {
|
|
2282
2325
|
let spinner;
|
|
2283
2326
|
let timer;
|
|
2284
2327
|
let startTime = 0;
|
|
@@ -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 =
|
|
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;
|
|
@@ -2310,7 +2353,6 @@ var createCommand = new Command5("create").description("Create a computer").argu
|
|
|
2310
2353
|
const computer = await createComputer({
|
|
2311
2354
|
handle,
|
|
2312
2355
|
display_name: selectedOptions.name,
|
|
2313
|
-
tier: selectedOptions.tier,
|
|
2314
2356
|
runtime_family: runtimeFamily,
|
|
2315
2357
|
source_kind: parseSourceKindOption(selectedOptions.sourceKind),
|
|
2316
2358
|
image_family: selectedOptions.imageFamily,
|
|
@@ -2405,7 +2447,7 @@ async function resolveCreateOptions(options) {
|
|
|
2405
2447
|
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
2448
|
const globalYes = cmd.parent?.opts()?.yes;
|
|
2407
2449
|
const skipConfirm = Boolean(options.yes || globalYes);
|
|
2408
|
-
const spinner =
|
|
2450
|
+
const spinner = ora5("Resolving computer...").start();
|
|
2409
2451
|
try {
|
|
2410
2452
|
const computer = await resolveComputer(identifier);
|
|
2411
2453
|
spinner.stop();
|
|
@@ -2419,7 +2461,7 @@ var removeCommand = new Command5("rm").description("Delete a computer").argument
|
|
|
2419
2461
|
return;
|
|
2420
2462
|
}
|
|
2421
2463
|
}
|
|
2422
|
-
const deleteSpinner =
|
|
2464
|
+
const deleteSpinner = ora5("Deleting computer...").start();
|
|
2423
2465
|
await api(`/v1/computers/${computer.id}`, {
|
|
2424
2466
|
method: "DELETE"
|
|
2425
2467
|
});
|
|
@@ -2558,8 +2600,10 @@ _computer() {
|
|
|
2558
2600
|
'login:Authenticate the CLI'
|
|
2559
2601
|
'logout:Remove stored API key'
|
|
2560
2602
|
'whoami:Show current user'
|
|
2561
|
-
'claude-
|
|
2562
|
-
'claude-
|
|
2603
|
+
'claude-login:Authenticate Claude Code on a computer'
|
|
2604
|
+
'claude-auth:Alias for claude-login'
|
|
2605
|
+
'codex-login:Authenticate Codex on a computer'
|
|
2606
|
+
'codex-auth:Alias for codex-login'
|
|
2563
2607
|
'create:Create a computer'
|
|
2564
2608
|
'ls:List computers'
|
|
2565
2609
|
'get:Show computer details'
|
|
@@ -2612,7 +2656,7 @@ _computer() {
|
|
|
2612
2656
|
whoami)
|
|
2613
2657
|
_arguments '--json[Print raw JSON]'
|
|
2614
2658
|
;;
|
|
2615
|
-
claude-auth|claude-login)
|
|
2659
|
+
claude-auth|claude-login|codex-auth|codex-login)
|
|
2616
2660
|
_arguments \\
|
|
2617
2661
|
'--machine[Use a specific computer]:computer:_computer_handles' \\
|
|
2618
2662
|
'--keep-helper[Keep a temporary helper machine]' \\
|
|
@@ -2622,7 +2666,6 @@ _computer() {
|
|
|
2622
2666
|
create)
|
|
2623
2667
|
_arguments \\
|
|
2624
2668
|
'--name[Display name]:name:' \\
|
|
2625
|
-
'--tier[Tier override]:tier:' \\
|
|
2626
2669
|
'--interactive[Prompt for runtime choices]' \\
|
|
2627
2670
|
'--runtime-family[Runtime family]:family:(managed-worker custom-machine)' \\
|
|
2628
2671
|
'--source-kind[Source kind]:kind:(none oci-image)' \\
|
|
@@ -2765,7 +2808,7 @@ var BASH_SCRIPT = `_computer() {
|
|
|
2765
2808
|
local cur prev words cword
|
|
2766
2809
|
_init_completion || return
|
|
2767
2810
|
|
|
2768
|
-
local commands="login logout whoami claude-
|
|
2811
|
+
local commands="login logout whoami claude-login claude-auth codex-login codex-auth create ls get image open ssh ports agent fleet acp rm completion help"
|
|
2769
2812
|
local ports_commands="ls publish rm"
|
|
2770
2813
|
local image_commands="ls save default rebuild rm"
|
|
2771
2814
|
|
|
@@ -2783,11 +2826,11 @@ var BASH_SCRIPT = `_computer() {
|
|
|
2783
2826
|
whoami)
|
|
2784
2827
|
COMPREPLY=($(compgen -W "--json" -- "$cur"))
|
|
2785
2828
|
;;
|
|
2786
|
-
claude-auth|claude-login)
|
|
2829
|
+
claude-auth|claude-login|codex-auth|codex-login)
|
|
2787
2830
|
COMPREPLY=($(compgen -W "--machine --keep-helper --skip-cross-check --verbose" -- "$cur"))
|
|
2788
2831
|
;;
|
|
2789
2832
|
create)
|
|
2790
|
-
COMPREPLY=($(compgen -W "--name --
|
|
2833
|
+
COMPREPLY=($(compgen -W "--name --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"))
|
|
2791
2834
|
;;
|
|
2792
2835
|
ls)
|
|
2793
2836
|
COMPREPLY=($(compgen -W "--json --verbose -v" -- "$cur"))
|
|
@@ -2882,14 +2925,344 @@ var completionCommand = new Command6("completion").description("Generate shell c
|
|
|
2882
2925
|
}
|
|
2883
2926
|
});
|
|
2884
2927
|
|
|
2885
|
-
// src/commands/
|
|
2886
|
-
import {
|
|
2928
|
+
// src/commands/codex-login.ts
|
|
2929
|
+
import { spawn as spawn3 } from "child_process";
|
|
2930
|
+
import { readFile as readFile3 } from "fs/promises";
|
|
2931
|
+
import { homedir as homedir4 } from "os";
|
|
2932
|
+
import { join as join3 } from "path";
|
|
2887
2933
|
import { Command as Command7 } from "commander";
|
|
2888
2934
|
import chalk7 from "chalk";
|
|
2889
|
-
import
|
|
2890
|
-
var
|
|
2935
|
+
import ora6 from "ora";
|
|
2936
|
+
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) => {
|
|
2937
|
+
const todos = createTodoList2();
|
|
2938
|
+
let target = null;
|
|
2939
|
+
let helperCreated = false;
|
|
2940
|
+
let sharedInstall = false;
|
|
2941
|
+
let activeTodoID = "target";
|
|
2942
|
+
let failureMessage = null;
|
|
2943
|
+
console.log();
|
|
2944
|
+
console.log(chalk7.cyan("Authenticating with Codex...\n"));
|
|
2945
|
+
try {
|
|
2946
|
+
const prepared = await prepareTargetMachine2(options);
|
|
2947
|
+
target = prepared.computer;
|
|
2948
|
+
helperCreated = prepared.helperCreated;
|
|
2949
|
+
sharedInstall = prepared.sharedInstall;
|
|
2950
|
+
markTodo2(todos, "target", "done", prepared.detail);
|
|
2951
|
+
activeTodoID = "ready";
|
|
2952
|
+
target = await waitForRunning(target);
|
|
2953
|
+
markTodo2(todos, "ready", "done", `${target.handle} is running`);
|
|
2954
|
+
activeTodoID = "local-auth";
|
|
2955
|
+
const localAuth = await ensureLocalCodexAuth();
|
|
2956
|
+
markTodo2(todos, "local-auth", "done", localAuth.detail);
|
|
2957
|
+
activeTodoID = "install";
|
|
2958
|
+
const sshTarget = await resolveSSHTarget(target);
|
|
2959
|
+
await installCodexAuth(sshTarget, localAuth.authJSON);
|
|
2960
|
+
markTodo2(
|
|
2961
|
+
todos,
|
|
2962
|
+
"install",
|
|
2963
|
+
"done",
|
|
2964
|
+
sharedInstall ? `installed Codex login on shared home via ${target.handle}` : `installed Codex login on ${target.handle}`
|
|
2965
|
+
);
|
|
2966
|
+
activeTodoID = "verify-primary";
|
|
2967
|
+
const primaryVerification = await verifyTargetMachine2(target.handle, sshTarget);
|
|
2968
|
+
markVerificationTodo2(
|
|
2969
|
+
todos,
|
|
2970
|
+
"verify-primary",
|
|
2971
|
+
primaryVerification,
|
|
2972
|
+
`${target.handle} fresh login shell sees Codex auth`
|
|
2973
|
+
);
|
|
2974
|
+
activeTodoID = "verify-shared";
|
|
2975
|
+
const sharedCheck = primaryVerification.status === "verified" ? await verifySharedInstall2(
|
|
2976
|
+
target.handle,
|
|
2977
|
+
target.id,
|
|
2978
|
+
sharedInstall,
|
|
2979
|
+
Boolean(options.skipCrossCheck),
|
|
2980
|
+
verifyStoredCodexAuth
|
|
2981
|
+
) : {
|
|
2982
|
+
status: "skipped",
|
|
2983
|
+
reason: "primary verification was inconclusive"
|
|
2984
|
+
};
|
|
2985
|
+
if (sharedCheck.status === "verified") {
|
|
2986
|
+
markTodo2(
|
|
2987
|
+
todos,
|
|
2988
|
+
"verify-shared",
|
|
2989
|
+
"done",
|
|
2990
|
+
`${sharedCheck.handle} also sees stored Codex auth`
|
|
2991
|
+
);
|
|
2992
|
+
} else {
|
|
2993
|
+
markTodo2(todos, "verify-shared", "skipped", sharedCheck.reason);
|
|
2994
|
+
}
|
|
2995
|
+
} catch (error) {
|
|
2996
|
+
failureMessage = error instanceof Error ? error.message : "Failed to authenticate Codex";
|
|
2997
|
+
markTodo2(todos, activeTodoID, "failed", failureMessage);
|
|
2998
|
+
} finally {
|
|
2999
|
+
if (helperCreated && target && !options.keepHelper) {
|
|
3000
|
+
try {
|
|
3001
|
+
await deleteComputer(target.id);
|
|
3002
|
+
markTodo2(
|
|
3003
|
+
todos,
|
|
3004
|
+
"cleanup",
|
|
3005
|
+
"done",
|
|
3006
|
+
`removed temporary helper ${target.handle}`
|
|
3007
|
+
);
|
|
3008
|
+
} catch (error) {
|
|
3009
|
+
const message = error instanceof Error ? error.message : "failed to remove helper";
|
|
3010
|
+
markTodo2(todos, "cleanup", "failed", message);
|
|
3011
|
+
}
|
|
3012
|
+
} else if (helperCreated && target && options.keepHelper) {
|
|
3013
|
+
markTodo2(todos, "cleanup", "skipped", `kept helper ${target.handle}`);
|
|
3014
|
+
} else {
|
|
3015
|
+
markTodo2(todos, "cleanup", "skipped", "no helper created");
|
|
3016
|
+
}
|
|
3017
|
+
if (options.verbose) {
|
|
3018
|
+
printTodoList2(todos);
|
|
3019
|
+
}
|
|
3020
|
+
}
|
|
3021
|
+
if (failureMessage) {
|
|
3022
|
+
console.error(chalk7.red(`
|
|
3023
|
+
${failureMessage}`));
|
|
3024
|
+
process.exit(1);
|
|
3025
|
+
}
|
|
3026
|
+
if (target) {
|
|
3027
|
+
console.log(
|
|
3028
|
+
chalk7.green(`Codex login installed on ${chalk7.bold(target.handle)}.`)
|
|
3029
|
+
);
|
|
3030
|
+
console.log();
|
|
3031
|
+
}
|
|
3032
|
+
});
|
|
3033
|
+
function createTodoList2() {
|
|
3034
|
+
return [
|
|
3035
|
+
{ id: "target", label: "Pick target computer", state: "pending" },
|
|
3036
|
+
{ id: "ready", label: "Wait for machine readiness", state: "pending" },
|
|
3037
|
+
{ id: "local-auth", label: "Complete local Codex auth", state: "pending" },
|
|
3038
|
+
{ id: "install", label: "Install stored Codex login", state: "pending" },
|
|
3039
|
+
{ id: "verify-primary", label: "Verify on target machine", state: "pending" },
|
|
3040
|
+
{ id: "verify-shared", label: "Verify shared-home propagation", state: "pending" },
|
|
3041
|
+
{ id: "cleanup", label: "Clean up temporary helper", state: "pending" }
|
|
3042
|
+
];
|
|
3043
|
+
}
|
|
3044
|
+
function markTodo2(items, id, state, detail) {
|
|
3045
|
+
const item = items.find((entry) => entry.id === id);
|
|
3046
|
+
if (!item) {
|
|
3047
|
+
return;
|
|
3048
|
+
}
|
|
3049
|
+
item.state = state;
|
|
3050
|
+
item.detail = detail;
|
|
3051
|
+
}
|
|
3052
|
+
function markVerificationTodo2(items, id, result, successDetail) {
|
|
3053
|
+
if (result.status === "verified") {
|
|
3054
|
+
markTodo2(items, id, "done", successDetail);
|
|
3055
|
+
return;
|
|
3056
|
+
}
|
|
3057
|
+
markTodo2(items, id, "skipped", result.detail);
|
|
3058
|
+
}
|
|
3059
|
+
function printTodoList2(items) {
|
|
3060
|
+
console.log();
|
|
3061
|
+
console.log(chalk7.dim("TODO"));
|
|
3062
|
+
console.log();
|
|
3063
|
+
for (const item of items) {
|
|
3064
|
+
const marker = item.state === "done" ? chalk7.green("[x]") : item.state === "skipped" ? chalk7.yellow("[-]") : item.state === "failed" ? chalk7.red("[!]") : chalk7.dim("[ ]");
|
|
3065
|
+
const detail = item.detail ? chalk7.dim(` ${item.detail}`) : "";
|
|
3066
|
+
console.log(` ${marker} ${item.label}${detail ? ` ${detail}` : ""}`);
|
|
3067
|
+
}
|
|
3068
|
+
console.log();
|
|
3069
|
+
}
|
|
3070
|
+
async function prepareTargetMachine2(options) {
|
|
3071
|
+
return prepareAuthTarget(options, {
|
|
3072
|
+
helperPrefix: "codex-login",
|
|
3073
|
+
helperDisplayName: "Codex Login Helper",
|
|
3074
|
+
promptMessage: "Select a computer for Codex login"
|
|
3075
|
+
});
|
|
3076
|
+
}
|
|
3077
|
+
async function ensureLocalCodexAuth() {
|
|
3078
|
+
const localStatus = await getLocalCodexStatus();
|
|
3079
|
+
if (localStatus.loggedIn) {
|
|
3080
|
+
return {
|
|
3081
|
+
authJSON: await readLocalCodexAuthFile(),
|
|
3082
|
+
detail: "reused existing local Codex login"
|
|
3083
|
+
};
|
|
3084
|
+
}
|
|
3085
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
3086
|
+
throw new Error("local Codex login is required when not running interactively");
|
|
3087
|
+
}
|
|
3088
|
+
console.log("We will open your browser so you can authenticate Codex locally.");
|
|
3089
|
+
console.log("If Codex falls back to device auth, complete that flow and return here.\n");
|
|
3090
|
+
await runInteractiveCodexLogin();
|
|
3091
|
+
const refreshedStatus = await getLocalCodexStatus();
|
|
3092
|
+
if (!refreshedStatus.loggedIn) {
|
|
3093
|
+
throw new Error(
|
|
3094
|
+
refreshedStatus.detail || "codex login did not complete successfully"
|
|
3095
|
+
);
|
|
3096
|
+
}
|
|
3097
|
+
return {
|
|
3098
|
+
authJSON: await readLocalCodexAuthFile(),
|
|
3099
|
+
detail: "local Codex login completed"
|
|
3100
|
+
};
|
|
3101
|
+
}
|
|
3102
|
+
async function getLocalCodexStatus() {
|
|
3103
|
+
const result = await captureLocalCommand("codex", ["login", "status"]);
|
|
3104
|
+
return parseCodexStatusOutput(result.stdout, result.stderr);
|
|
3105
|
+
}
|
|
3106
|
+
async function readLocalCodexAuthFile() {
|
|
3107
|
+
const authPath = join3(homedir4(), ".codex", "auth.json");
|
|
3108
|
+
let raw;
|
|
3109
|
+
try {
|
|
3110
|
+
raw = await readFile3(authPath, "utf8");
|
|
3111
|
+
} catch (error) {
|
|
3112
|
+
throw new Error(
|
|
3113
|
+
error instanceof Error ? `failed to read ${authPath}: ${error.message}` : `failed to read ${authPath}`
|
|
3114
|
+
);
|
|
3115
|
+
}
|
|
3116
|
+
try {
|
|
3117
|
+
JSON.parse(raw);
|
|
3118
|
+
} catch (error) {
|
|
3119
|
+
throw new Error(
|
|
3120
|
+
error instanceof Error ? `local Codex auth file is invalid JSON: ${error.message}` : "local Codex auth file is invalid JSON"
|
|
3121
|
+
);
|
|
3122
|
+
}
|
|
3123
|
+
return `${raw.trimEnd()}
|
|
3124
|
+
`;
|
|
3125
|
+
}
|
|
3126
|
+
async function runInteractiveCodexLogin() {
|
|
3127
|
+
await new Promise((resolve, reject) => {
|
|
3128
|
+
const child = spawn3("codex", ["login"], {
|
|
3129
|
+
stdio: "inherit"
|
|
3130
|
+
});
|
|
3131
|
+
child.on("error", (error) => {
|
|
3132
|
+
reject(
|
|
3133
|
+
error instanceof Error ? new Error(`failed to start local codex login: ${error.message}`) : new Error("failed to start local codex login")
|
|
3134
|
+
);
|
|
3135
|
+
});
|
|
3136
|
+
child.on("exit", (code, signal) => {
|
|
3137
|
+
if (code === 0) {
|
|
3138
|
+
resolve();
|
|
3139
|
+
return;
|
|
3140
|
+
}
|
|
3141
|
+
if (signal) {
|
|
3142
|
+
reject(new Error(`codex login was interrupted by ${signal}`));
|
|
3143
|
+
return;
|
|
3144
|
+
}
|
|
3145
|
+
reject(new Error(`codex login exited with code ${code ?? 1}`));
|
|
3146
|
+
});
|
|
3147
|
+
});
|
|
3148
|
+
}
|
|
3149
|
+
async function captureLocalCommand(command, args) {
|
|
3150
|
+
return new Promise((resolve, reject) => {
|
|
3151
|
+
const child = spawn3(command, args, {
|
|
3152
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
3153
|
+
});
|
|
3154
|
+
let stdout = "";
|
|
3155
|
+
let stderr = "";
|
|
3156
|
+
child.stdout.on("data", (chunk) => {
|
|
3157
|
+
stdout += chunk.toString();
|
|
3158
|
+
});
|
|
3159
|
+
child.stderr.on("data", (chunk) => {
|
|
3160
|
+
stderr += chunk.toString();
|
|
3161
|
+
});
|
|
3162
|
+
child.on("error", reject);
|
|
3163
|
+
child.on("exit", (code) => {
|
|
3164
|
+
resolve({ stdout, stderr, exitCode: code });
|
|
3165
|
+
});
|
|
3166
|
+
});
|
|
3167
|
+
}
|
|
3168
|
+
async function installCodexAuth(target, authJSON) {
|
|
3169
|
+
const spinner = ora6(`Installing Codex login on ${chalk7.bold(target.handle)}...`).start();
|
|
3170
|
+
try {
|
|
3171
|
+
const installScript = buildInstallScript2(authJSON);
|
|
3172
|
+
await runRemoteCommand(target, ["bash", "-s"], installScript);
|
|
3173
|
+
spinner.succeed(`Installed Codex login on ${chalk7.bold(target.handle)}`);
|
|
3174
|
+
} catch (error) {
|
|
3175
|
+
spinner.fail(
|
|
3176
|
+
error instanceof Error ? error.message : `Failed to install Codex login on ${target.handle}`
|
|
3177
|
+
);
|
|
3178
|
+
throw error;
|
|
3179
|
+
}
|
|
3180
|
+
}
|
|
3181
|
+
async function verifyTargetMachine2(handle, target) {
|
|
3182
|
+
const spinner = ora6(`Verifying Codex login on ${chalk7.bold(handle)}...`).start();
|
|
3183
|
+
const result = await verifyStoredCodexAuth(target);
|
|
3184
|
+
if (result.status === "verified") {
|
|
3185
|
+
spinner.succeed(`Verified Codex login on ${chalk7.bold(handle)}`);
|
|
3186
|
+
return result;
|
|
3187
|
+
}
|
|
3188
|
+
spinner.warn(result.detail);
|
|
3189
|
+
return result;
|
|
3190
|
+
}
|
|
3191
|
+
async function verifySharedInstall2(primaryHandle, primaryComputerID, sharedInstall, skip, verify) {
|
|
3192
|
+
const spinner = ora6(
|
|
3193
|
+
`Verifying shared-home Codex login from ${chalk7.bold(primaryHandle)}...`
|
|
3194
|
+
).start();
|
|
3195
|
+
const result = await verifySecondaryMachine(
|
|
3196
|
+
primaryComputerID,
|
|
3197
|
+
sharedInstall,
|
|
3198
|
+
skip,
|
|
3199
|
+
verify
|
|
3200
|
+
);
|
|
3201
|
+
if (result.status === "verified") {
|
|
3202
|
+
spinner.succeed(`Verified shared-home Codex login on ${chalk7.bold(result.handle)}`);
|
|
3203
|
+
return result;
|
|
3204
|
+
}
|
|
3205
|
+
spinner.info(result.reason);
|
|
3206
|
+
return result;
|
|
3207
|
+
}
|
|
3208
|
+
function buildInstallScript2(authJSON) {
|
|
3209
|
+
const authMarker = `AUTH_${randomSuffix(12)}`;
|
|
3210
|
+
return [
|
|
3211
|
+
"set -euo pipefail",
|
|
3212
|
+
'command -v codex >/dev/null 2>&1 || { echo "codex is not installed on this computer" >&2; exit 1; }',
|
|
3213
|
+
'mkdir -p "$HOME/.codex"',
|
|
3214
|
+
'chmod 700 "$HOME/.codex"',
|
|
3215
|
+
`cat > "$HOME/.codex/auth.json" <<'${authMarker}'`,
|
|
3216
|
+
authJSON.trimEnd(),
|
|
3217
|
+
authMarker,
|
|
3218
|
+
'chmod 600 "$HOME/.codex/auth.json"'
|
|
3219
|
+
].join("\n");
|
|
3220
|
+
}
|
|
3221
|
+
async function verifyStoredCodexAuth(target) {
|
|
3222
|
+
try {
|
|
3223
|
+
const result = await runRemoteCommand(target, [
|
|
3224
|
+
'PATH="$HOME/.local/bin:$PATH" codex login status 2>&1 || true'
|
|
3225
|
+
]);
|
|
3226
|
+
const payload = parseCodexStatusOutput(result.stdout, result.stderr);
|
|
3227
|
+
if (payload.loggedIn) {
|
|
3228
|
+
return { status: "verified", detail: "verified" };
|
|
3229
|
+
}
|
|
3230
|
+
return {
|
|
3231
|
+
status: "failed",
|
|
3232
|
+
detail: payload.detail ? `verification failed: ${payload.detail}` : "verification failed"
|
|
3233
|
+
};
|
|
3234
|
+
} catch (error) {
|
|
3235
|
+
return {
|
|
3236
|
+
status: "inconclusive",
|
|
3237
|
+
detail: error instanceof Error ? error.message : "verification command did not complete cleanly"
|
|
3238
|
+
};
|
|
3239
|
+
}
|
|
3240
|
+
}
|
|
3241
|
+
function parseCodexStatusOutput(stdout, stderr) {
|
|
3242
|
+
const combined = [stdout, stderr].map((value) => value.trim()).filter(Boolean).join("\n");
|
|
3243
|
+
const normalized = combined.toLowerCase();
|
|
3244
|
+
if (normalized.includes("not logged in") || normalized.includes("logged out")) {
|
|
3245
|
+
return { loggedIn: false, detail: firstStatusLine2(combined) };
|
|
3246
|
+
}
|
|
3247
|
+
if (normalized.includes("logged in")) {
|
|
3248
|
+
return { loggedIn: true };
|
|
3249
|
+
}
|
|
3250
|
+
throw new Error(
|
|
3251
|
+
combined ? `could not verify Codex auth from status output: ${firstStatusLine2(combined)}` : "could not verify Codex auth from empty status output"
|
|
3252
|
+
);
|
|
3253
|
+
}
|
|
3254
|
+
function firstStatusLine2(value) {
|
|
3255
|
+
return value.split(/\r?\n/).map((line) => line.trim()).find(Boolean) ?? "unknown output";
|
|
3256
|
+
}
|
|
3257
|
+
|
|
3258
|
+
// src/commands/images.ts
|
|
3259
|
+
import { confirm as confirm2, input as textInput3, select as select3 } from "@inquirer/prompts";
|
|
3260
|
+
import { Command as Command8 } from "commander";
|
|
3261
|
+
import chalk8 from "chalk";
|
|
3262
|
+
import ora7 from "ora";
|
|
3263
|
+
var imageCommand = new Command8("image").description("Manage machine image sources");
|
|
2891
3264
|
imageCommand.command("ls").description("List machine image sources").option("--json", "Print raw JSON").action(async (options) => {
|
|
2892
|
-
const spinner = options.json ? null :
|
|
3265
|
+
const spinner = options.json ? null : ora7("Fetching machine images...").start();
|
|
2893
3266
|
try {
|
|
2894
3267
|
const settings = await getMachineSourceSettings();
|
|
2895
3268
|
spinner?.stop();
|
|
@@ -2908,7 +3281,7 @@ imageCommand.command("ls").description("List machine image sources").option("--j
|
|
|
2908
3281
|
}
|
|
2909
3282
|
});
|
|
2910
3283
|
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 :
|
|
3284
|
+
const spinner = options.json ? null : ora7("Saving machine image source...").start();
|
|
2912
3285
|
try {
|
|
2913
3286
|
const input = await resolveSaveInput(options);
|
|
2914
3287
|
const settings = await upsertMachineSource(input);
|
|
@@ -2918,7 +3291,7 @@ imageCommand.command("save").description("Create or update a machine image sourc
|
|
|
2918
3291
|
return;
|
|
2919
3292
|
}
|
|
2920
3293
|
console.log();
|
|
2921
|
-
console.log(
|
|
3294
|
+
console.log(chalk8.green("Saved machine image source."));
|
|
2922
3295
|
printMachineSourceSettings(settings);
|
|
2923
3296
|
} catch (error) {
|
|
2924
3297
|
if (spinner) {
|
|
@@ -2931,7 +3304,7 @@ imageCommand.command("save").description("Create or update a machine image sourc
|
|
|
2931
3304
|
});
|
|
2932
3305
|
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
3306
|
const usePlatformDefault = !sourceID || sourceID === "platform";
|
|
2934
|
-
const spinner = options.json ? null :
|
|
3307
|
+
const spinner = options.json ? null : ora7("Updating machine image default...").start();
|
|
2935
3308
|
try {
|
|
2936
3309
|
let settings;
|
|
2937
3310
|
if (usePlatformDefault) {
|
|
@@ -2957,9 +3330,9 @@ imageCommand.command("default").description("Set the default machine image sourc
|
|
|
2957
3330
|
if (!usePlatformDefault) {
|
|
2958
3331
|
const selected = settings.default_machine_source ?? void 0;
|
|
2959
3332
|
const label = selected ? summarizeMachineSource(selected) : sourceID;
|
|
2960
|
-
console.log(
|
|
3333
|
+
console.log(chalk8.green(`Selected ${chalk8.bold(label)} as the default machine image.`));
|
|
2961
3334
|
} else {
|
|
2962
|
-
console.log(
|
|
3335
|
+
console.log(chalk8.green("Using the AgentComputer platform default image."));
|
|
2963
3336
|
}
|
|
2964
3337
|
printMachineSourceSettings(settings);
|
|
2965
3338
|
} catch (error) {
|
|
@@ -2972,7 +3345,7 @@ imageCommand.command("default").description("Set the default machine image sourc
|
|
|
2972
3345
|
}
|
|
2973
3346
|
});
|
|
2974
3347
|
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 :
|
|
3348
|
+
const spinner = options.json ? null : ora7("Queueing machine image rebuild...").start();
|
|
2976
3349
|
try {
|
|
2977
3350
|
const settings = await rebuildMachineSource(sourceID);
|
|
2978
3351
|
spinner?.stop();
|
|
@@ -2981,7 +3354,7 @@ imageCommand.command("rebuild").description("Rebuild a machine image source").ar
|
|
|
2981
3354
|
return;
|
|
2982
3355
|
}
|
|
2983
3356
|
console.log();
|
|
2984
|
-
console.log(
|
|
3357
|
+
console.log(chalk8.green(`Queued rebuild for ${chalk8.bold(sourceID)}.`));
|
|
2985
3358
|
printMachineSourceSettings(settings);
|
|
2986
3359
|
} catch (error) {
|
|
2987
3360
|
if (spinner) {
|
|
@@ -3000,11 +3373,11 @@ imageCommand.command("rm").description("Delete a machine image source").argument
|
|
|
3000
3373
|
if (!skipConfirm && process.stdin.isTTY) {
|
|
3001
3374
|
const confirmed = await confirmDeletion(sourceID);
|
|
3002
3375
|
if (!confirmed) {
|
|
3003
|
-
console.log(
|
|
3376
|
+
console.log(chalk8.dim(" Cancelled."));
|
|
3004
3377
|
return;
|
|
3005
3378
|
}
|
|
3006
3379
|
}
|
|
3007
|
-
spinner = options.json ? null :
|
|
3380
|
+
spinner = options.json ? null : ora7("Deleting machine image source...").start();
|
|
3008
3381
|
const settings = await deleteMachineSource(sourceID);
|
|
3009
3382
|
spinner?.stop();
|
|
3010
3383
|
if (options.json) {
|
|
@@ -3012,7 +3385,7 @@ imageCommand.command("rm").description("Delete a machine image source").argument
|
|
|
3012
3385
|
return;
|
|
3013
3386
|
}
|
|
3014
3387
|
console.log();
|
|
3015
|
-
console.log(
|
|
3388
|
+
console.log(chalk8.green(`Deleted machine image source ${chalk8.bold(sourceID)}.`));
|
|
3016
3389
|
printMachineSourceSettings(settings);
|
|
3017
3390
|
} catch (error) {
|
|
3018
3391
|
if (spinner) {
|
|
@@ -3024,29 +3397,40 @@ imageCommand.command("rm").description("Delete a machine image source").argument
|
|
|
3024
3397
|
}
|
|
3025
3398
|
});
|
|
3026
3399
|
function printMachineSourceSettings(settings) {
|
|
3027
|
-
console.log(` ${
|
|
3400
|
+
console.log(` ${chalk8.dim("Default")} ${chalk8.white(summarizeDefaultMachineSource(settings))}`);
|
|
3028
3401
|
console.log();
|
|
3029
3402
|
if (settings.sources.length === 0) {
|
|
3030
|
-
console.log(
|
|
3403
|
+
console.log(chalk8.dim(" No custom machine images configured yet."));
|
|
3031
3404
|
console.log();
|
|
3032
3405
|
return;
|
|
3033
3406
|
}
|
|
3034
|
-
|
|
3407
|
+
console.log(` ${chalk8.dim("Custom")} ${chalk8.white(formatMachineSourceCounts(settings.sources))}`);
|
|
3408
|
+
console.log();
|
|
3409
|
+
for (const source of sortMachineSources(settings.sources, settings.default_machine_source_id)) {
|
|
3035
3410
|
printMachineSourceCard(source, settings.default_machine_source_id === source.id);
|
|
3036
3411
|
}
|
|
3037
3412
|
}
|
|
3038
3413
|
function printMachineSourceCard(source, isDefault) {
|
|
3039
|
-
|
|
3040
|
-
|
|
3041
|
-
|
|
3042
|
-
|
|
3414
|
+
const statusLabel = padEnd(formatMachineSourceStatus(source.status), 12);
|
|
3415
|
+
const meta = [
|
|
3416
|
+
source.kind === "oci-image" ? "oci-image" : "nix-git",
|
|
3417
|
+
`id ${source.id}`,
|
|
3418
|
+
`updated ${timeAgo(source.updated_at)}`
|
|
3419
|
+
];
|
|
3420
|
+
const extra = machineSourceExtraParts(source);
|
|
3421
|
+
console.log(
|
|
3422
|
+
` ${statusLabel}${chalk8.bold(machineSourceTitle(source))}${isDefault ? chalk8.green(" default") : ""}`
|
|
3423
|
+
);
|
|
3424
|
+
console.log(` ${chalk8.dim(meta.concat(extra).join(" | "))}`);
|
|
3425
|
+
console.log(` ${chalk8.dim(machineSourceStatusSummary(source))}`);
|
|
3043
3426
|
if (source.resolved_image_ref) {
|
|
3044
|
-
console.log(`
|
|
3427
|
+
console.log(` ${chalk8.dim("resolved")} ${source.resolved_image_ref}`);
|
|
3428
|
+
} else if (source.last_good_resolved_image_ref) {
|
|
3429
|
+
console.log(` ${chalk8.dim("last good")} ${source.last_good_resolved_image_ref}`);
|
|
3045
3430
|
}
|
|
3046
3431
|
if (source.error) {
|
|
3047
|
-
console.log(`
|
|
3432
|
+
console.log(` ${chalk8.red(source.error)}`);
|
|
3048
3433
|
}
|
|
3049
|
-
console.log(` ${chalk7.dim(" Source")} ${summarizeMachineSource(source)}`);
|
|
3050
3434
|
console.log();
|
|
3051
3435
|
}
|
|
3052
3436
|
function machineSourceTitle(source) {
|
|
@@ -3055,6 +3439,106 @@ function machineSourceTitle(source) {
|
|
|
3055
3439
|
}
|
|
3056
3440
|
return source.git_url || "Nix git source";
|
|
3057
3441
|
}
|
|
3442
|
+
function summarizeDefaultMachineSource(settings) {
|
|
3443
|
+
if (settings.platform_default || !settings.default_machine_source) {
|
|
3444
|
+
return "platform default";
|
|
3445
|
+
}
|
|
3446
|
+
return machineSourceTitle(settings.default_machine_source);
|
|
3447
|
+
}
|
|
3448
|
+
function formatMachineSourceCounts(sources) {
|
|
3449
|
+
const counts = {
|
|
3450
|
+
ready: 0,
|
|
3451
|
+
active: 0,
|
|
3452
|
+
failed: 0
|
|
3453
|
+
};
|
|
3454
|
+
for (const source of sources) {
|
|
3455
|
+
if (source.status === "ready") {
|
|
3456
|
+
counts.ready += 1;
|
|
3457
|
+
} else if (source.status === "failed") {
|
|
3458
|
+
counts.failed += 1;
|
|
3459
|
+
} else {
|
|
3460
|
+
counts.active += 1;
|
|
3461
|
+
}
|
|
3462
|
+
}
|
|
3463
|
+
const parts = [`${sources.length} total`, `${counts.ready} ready`];
|
|
3464
|
+
if (counts.active > 0) {
|
|
3465
|
+
parts.push(`${counts.active} in progress`);
|
|
3466
|
+
}
|
|
3467
|
+
if (counts.failed > 0) {
|
|
3468
|
+
parts.push(`${counts.failed} failed`);
|
|
3469
|
+
}
|
|
3470
|
+
return parts.join(" | ");
|
|
3471
|
+
}
|
|
3472
|
+
function sortMachineSources(sources, defaultMachineSourceID) {
|
|
3473
|
+
return [...sources].sort((left, right) => {
|
|
3474
|
+
const rankDelta = machineSourceSortRank(left, defaultMachineSourceID) - machineSourceSortRank(right, defaultMachineSourceID);
|
|
3475
|
+
if (rankDelta !== 0) {
|
|
3476
|
+
return rankDelta;
|
|
3477
|
+
}
|
|
3478
|
+
return new Date(right.updated_at).getTime() - new Date(left.updated_at).getTime();
|
|
3479
|
+
});
|
|
3480
|
+
}
|
|
3481
|
+
function machineSourceSortRank(source, defaultMachineSourceID) {
|
|
3482
|
+
if (source.id === defaultMachineSourceID) {
|
|
3483
|
+
return 0;
|
|
3484
|
+
}
|
|
3485
|
+
switch (source.status) {
|
|
3486
|
+
case "pending":
|
|
3487
|
+
case "resolving":
|
|
3488
|
+
case "building":
|
|
3489
|
+
return 1;
|
|
3490
|
+
case "failed":
|
|
3491
|
+
return 2;
|
|
3492
|
+
case "ready":
|
|
3493
|
+
return 3;
|
|
3494
|
+
default:
|
|
3495
|
+
return 4;
|
|
3496
|
+
}
|
|
3497
|
+
}
|
|
3498
|
+
function formatMachineSourceStatus(status) {
|
|
3499
|
+
const text = status.toUpperCase();
|
|
3500
|
+
switch (status) {
|
|
3501
|
+
case "ready":
|
|
3502
|
+
return chalk8.green(text);
|
|
3503
|
+
case "failed":
|
|
3504
|
+
return chalk8.red(text);
|
|
3505
|
+
case "pending":
|
|
3506
|
+
case "resolving":
|
|
3507
|
+
case "building":
|
|
3508
|
+
return chalk8.yellow(text);
|
|
3509
|
+
default:
|
|
3510
|
+
return text;
|
|
3511
|
+
}
|
|
3512
|
+
}
|
|
3513
|
+
function machineSourceExtraParts(source) {
|
|
3514
|
+
if (source.kind === "oci-image") {
|
|
3515
|
+
return ["digest-pinned registry image"];
|
|
3516
|
+
}
|
|
3517
|
+
const parts = [];
|
|
3518
|
+
if (source.git_ref) {
|
|
3519
|
+
parts.push(`ref ${source.git_ref}`);
|
|
3520
|
+
}
|
|
3521
|
+
if (source.git_subpath) {
|
|
3522
|
+
parts.push(`subpath ${source.git_subpath}`);
|
|
3523
|
+
}
|
|
3524
|
+
return parts;
|
|
3525
|
+
}
|
|
3526
|
+
function machineSourceStatusSummary(source) {
|
|
3527
|
+
switch (source.status) {
|
|
3528
|
+
case "ready":
|
|
3529
|
+
return "Ready to use for new managed machines.";
|
|
3530
|
+
case "pending":
|
|
3531
|
+
return "Queued to resolve or build.";
|
|
3532
|
+
case "resolving":
|
|
3533
|
+
return source.kind === "oci-image" ? "Resolving the requested image ref." : "Resolving the latest build result.";
|
|
3534
|
+
case "building":
|
|
3535
|
+
return "Build in progress. This source is not selectable yet.";
|
|
3536
|
+
case "failed":
|
|
3537
|
+
return source.last_good_resolved_image_ref ? "Latest build failed. Last good image is shown below." : "Build failed. Fix the source and rebuild.";
|
|
3538
|
+
default:
|
|
3539
|
+
return source.status;
|
|
3540
|
+
}
|
|
3541
|
+
}
|
|
3058
3542
|
function parseMachineSourceKind(value) {
|
|
3059
3543
|
switch (value) {
|
|
3060
3544
|
case void 0:
|
|
@@ -3163,18 +3647,18 @@ async function confirmDeletion(sourceID) {
|
|
|
3163
3647
|
}
|
|
3164
3648
|
|
|
3165
3649
|
// src/commands/login.ts
|
|
3166
|
-
import { Command as
|
|
3167
|
-
import
|
|
3168
|
-
import
|
|
3650
|
+
import { Command as Command9 } from "commander";
|
|
3651
|
+
import chalk9 from "chalk";
|
|
3652
|
+
import ora8 from "ora";
|
|
3169
3653
|
|
|
3170
3654
|
// src/lib/browser-login.ts
|
|
3171
|
-
import { randomBytes as
|
|
3655
|
+
import { randomBytes as randomBytes3 } from "crypto";
|
|
3172
3656
|
import { createServer } from "http";
|
|
3173
3657
|
var CALLBACK_HOST = "127.0.0.1";
|
|
3174
3658
|
var CALLBACK_PATH = "/callback";
|
|
3175
3659
|
var LOGIN_TIMEOUT_MS = 5 * 60 * 1e3;
|
|
3176
3660
|
async function createBrowserLoginAttempt() {
|
|
3177
|
-
const state =
|
|
3661
|
+
const state = randomBytes3(16).toString("hex");
|
|
3178
3662
|
const deferred = createDeferred();
|
|
3179
3663
|
let callbackURL = "";
|
|
3180
3664
|
let closed = false;
|
|
@@ -3393,11 +3877,11 @@ function escapeHTML(value) {
|
|
|
3393
3877
|
}
|
|
3394
3878
|
|
|
3395
3879
|
// src/commands/login.ts
|
|
3396
|
-
var loginCommand = new
|
|
3880
|
+
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
3881
|
const existingKey = getStoredAPIKey();
|
|
3398
3882
|
if (existingKey && !options.force) {
|
|
3399
3883
|
console.log();
|
|
3400
|
-
console.log(
|
|
3884
|
+
console.log(chalk9.yellow(" Already logged in. Use --force to overwrite."));
|
|
3401
3885
|
console.log();
|
|
3402
3886
|
return;
|
|
3403
3887
|
}
|
|
@@ -3405,8 +3889,8 @@ var loginCommand = new Command8("login").description("Authenticate the CLI").opt
|
|
|
3405
3889
|
const apiKey = await resolveAPIKeyInput(options.apiKey, options.stdin);
|
|
3406
3890
|
if (!apiKey && wantsManualLogin) {
|
|
3407
3891
|
console.log();
|
|
3408
|
-
console.log(
|
|
3409
|
-
console.log(
|
|
3892
|
+
console.log(chalk9.dim(" Usage: computer login --api-key <ac_live_...>"));
|
|
3893
|
+
console.log(chalk9.dim(` API: ${getBaseURL()}`));
|
|
3410
3894
|
console.log();
|
|
3411
3895
|
process.exit(1);
|
|
3412
3896
|
}
|
|
@@ -3416,15 +3900,15 @@ var loginCommand = new Command8("login").description("Authenticate the CLI").opt
|
|
|
3416
3900
|
}
|
|
3417
3901
|
if (!apiKey.startsWith("ac_live_")) {
|
|
3418
3902
|
console.log();
|
|
3419
|
-
console.log(
|
|
3903
|
+
console.log(chalk9.red(" API key must start with ac_live_"));
|
|
3420
3904
|
console.log();
|
|
3421
3905
|
process.exit(1);
|
|
3422
3906
|
}
|
|
3423
|
-
const spinner =
|
|
3907
|
+
const spinner = ora8("Authenticating...").start();
|
|
3424
3908
|
try {
|
|
3425
3909
|
const me = await apiWithKey(apiKey, "/v1/me");
|
|
3426
3910
|
setAPIKey(apiKey);
|
|
3427
|
-
spinner.succeed(`Logged in as ${
|
|
3911
|
+
spinner.succeed(`Logged in as ${chalk9.bold(me.user.email)}`);
|
|
3428
3912
|
} catch (error) {
|
|
3429
3913
|
spinner.fail(
|
|
3430
3914
|
error instanceof Error ? error.message : "Failed to validate API key"
|
|
@@ -3433,7 +3917,7 @@ var loginCommand = new Command8("login").description("Authenticate the CLI").opt
|
|
|
3433
3917
|
}
|
|
3434
3918
|
});
|
|
3435
3919
|
async function runBrowserLogin() {
|
|
3436
|
-
const spinner =
|
|
3920
|
+
const spinner = ora8("Starting browser login...").start();
|
|
3437
3921
|
let attempt = null;
|
|
3438
3922
|
try {
|
|
3439
3923
|
attempt = await createBrowserLoginAttempt();
|
|
@@ -3443,14 +3927,14 @@ async function runBrowserLogin() {
|
|
|
3443
3927
|
} catch {
|
|
3444
3928
|
spinner.stop();
|
|
3445
3929
|
console.log();
|
|
3446
|
-
console.log(
|
|
3447
|
-
console.log(
|
|
3930
|
+
console.log(chalk9.yellow(" Browser auto-open failed. Open this URL to continue:"));
|
|
3931
|
+
console.log(chalk9.dim(` ${attempt.loginURL}`));
|
|
3448
3932
|
console.log();
|
|
3449
3933
|
spinner.start("Waiting for browser login...");
|
|
3450
3934
|
}
|
|
3451
3935
|
spinner.text = "Waiting for browser login...";
|
|
3452
3936
|
const result = await attempt.waitForResult();
|
|
3453
|
-
spinner.succeed(`Logged in as ${
|
|
3937
|
+
spinner.succeed(`Logged in as ${chalk9.bold(result.me.user.email)}`);
|
|
3454
3938
|
} catch (error) {
|
|
3455
3939
|
spinner.fail(error instanceof Error ? error.message : "Browser login failed");
|
|
3456
3940
|
process.exit(1);
|
|
@@ -3476,33 +3960,33 @@ async function resolveAPIKeyInput(flagValue, readFromStdin) {
|
|
|
3476
3960
|
}
|
|
3477
3961
|
|
|
3478
3962
|
// src/commands/logout.ts
|
|
3479
|
-
import { Command as
|
|
3480
|
-
import
|
|
3481
|
-
var logoutCommand = new
|
|
3963
|
+
import { Command as Command10 } from "commander";
|
|
3964
|
+
import chalk10 from "chalk";
|
|
3965
|
+
var logoutCommand = new Command10("logout").description("Remove stored API key").action(() => {
|
|
3482
3966
|
if (!getStoredAPIKey()) {
|
|
3483
3967
|
console.log();
|
|
3484
|
-
console.log(
|
|
3968
|
+
console.log(chalk10.dim(" Not logged in."));
|
|
3485
3969
|
if (hasEnvAPIKey()) {
|
|
3486
|
-
console.log(
|
|
3970
|
+
console.log(chalk10.dim(" Environment API key is still active in this shell."));
|
|
3487
3971
|
}
|
|
3488
3972
|
console.log();
|
|
3489
3973
|
return;
|
|
3490
3974
|
}
|
|
3491
3975
|
clearAPIKey();
|
|
3492
3976
|
console.log();
|
|
3493
|
-
console.log(
|
|
3977
|
+
console.log(chalk10.green(" Logged out."));
|
|
3494
3978
|
if (hasEnvAPIKey()) {
|
|
3495
|
-
console.log(
|
|
3979
|
+
console.log(chalk10.dim(" Environment API key is still active in this shell."));
|
|
3496
3980
|
}
|
|
3497
3981
|
console.log();
|
|
3498
3982
|
});
|
|
3499
3983
|
|
|
3500
3984
|
// src/commands/whoami.ts
|
|
3501
|
-
import { Command as
|
|
3502
|
-
import
|
|
3503
|
-
import
|
|
3504
|
-
var whoamiCommand = new
|
|
3505
|
-
const spinner = options.json ? null :
|
|
3985
|
+
import { Command as Command11 } from "commander";
|
|
3986
|
+
import chalk11 from "chalk";
|
|
3987
|
+
import ora9 from "ora";
|
|
3988
|
+
var whoamiCommand = new Command11("whoami").description("Show current user").option("--json", "Print raw JSON").action(async (options) => {
|
|
3989
|
+
const spinner = options.json ? null : ora9("Loading user...").start();
|
|
3506
3990
|
try {
|
|
3507
3991
|
const me = await api("/v1/me");
|
|
3508
3992
|
spinner?.stop();
|
|
@@ -3511,14 +3995,14 @@ var whoamiCommand = new Command10("whoami").description("Show current user").opt
|
|
|
3511
3995
|
return;
|
|
3512
3996
|
}
|
|
3513
3997
|
console.log();
|
|
3514
|
-
console.log(` ${
|
|
3998
|
+
console.log(` ${chalk11.bold.white(me.user.display_name || me.user.email)}`);
|
|
3515
3999
|
if (me.user.display_name) {
|
|
3516
|
-
console.log(` ${
|
|
4000
|
+
console.log(` ${chalk11.dim(me.user.email)}`);
|
|
3517
4001
|
}
|
|
3518
4002
|
if (me.api_key.name) {
|
|
3519
|
-
console.log(` ${
|
|
4003
|
+
console.log(` ${chalk11.dim("Key:")} ${me.api_key.name}`);
|
|
3520
4004
|
}
|
|
3521
|
-
console.log(` ${
|
|
4005
|
+
console.log(` ${chalk11.dim("API:")} ${chalk11.dim(getBaseURL())}`);
|
|
3522
4006
|
console.log();
|
|
3523
4007
|
} catch (error) {
|
|
3524
4008
|
if (spinner) {
|
|
@@ -3535,15 +4019,15 @@ var pkg2 = JSON.parse(
|
|
|
3535
4019
|
readFileSync3(new URL("../package.json", import.meta.url), "utf8")
|
|
3536
4020
|
);
|
|
3537
4021
|
var cliName = process.argv[1] ? basename2(process.argv[1]) : "agentcomputer";
|
|
3538
|
-
var program = new
|
|
4022
|
+
var program = new Command12();
|
|
3539
4023
|
function appendTextSection(lines, title, values) {
|
|
3540
4024
|
if (values.length === 0) {
|
|
3541
4025
|
return;
|
|
3542
4026
|
}
|
|
3543
|
-
lines.push(` ${
|
|
4027
|
+
lines.push(` ${chalk12.dim(title)}`);
|
|
3544
4028
|
lines.push("");
|
|
3545
4029
|
for (const value of values) {
|
|
3546
|
-
lines.push(` ${
|
|
4030
|
+
lines.push(` ${chalk12.white(value)}`);
|
|
3547
4031
|
}
|
|
3548
4032
|
lines.push("");
|
|
3549
4033
|
}
|
|
@@ -3552,10 +4036,10 @@ function appendTableSection(lines, title, entries) {
|
|
|
3552
4036
|
return;
|
|
3553
4037
|
}
|
|
3554
4038
|
const width = Math.max(...entries.map((entry) => entry.term.length), 0) + 2;
|
|
3555
|
-
lines.push(` ${
|
|
4039
|
+
lines.push(` ${chalk12.dim(title)}`);
|
|
3556
4040
|
lines.push("");
|
|
3557
4041
|
for (const entry of entries) {
|
|
3558
|
-
lines.push(` ${
|
|
4042
|
+
lines.push(` ${chalk12.white(padEnd(entry.term, width))}${chalk12.dim(entry.desc)}`);
|
|
3559
4043
|
}
|
|
3560
4044
|
lines.push("");
|
|
3561
4045
|
}
|
|
@@ -3580,17 +4064,17 @@ function formatRootHelp(cmd) {
|
|
|
3580
4064
|
["Other", []]
|
|
3581
4065
|
];
|
|
3582
4066
|
const otherGroup = groups.find(([name]) => name === "Other")[1];
|
|
3583
|
-
lines.push(`${
|
|
4067
|
+
lines.push(`${chalk12.bold(cliName)} ${chalk12.dim(`v${version}`)}`);
|
|
3584
4068
|
lines.push("");
|
|
3585
4069
|
if (cmd.description()) {
|
|
3586
|
-
lines.push(` ${
|
|
4070
|
+
lines.push(` ${chalk12.dim(cmd.description())}`);
|
|
3587
4071
|
lines.push("");
|
|
3588
4072
|
}
|
|
3589
4073
|
appendTextSection(lines, "Usage", [`${cliName} <command> [options]`]);
|
|
3590
4074
|
for (const sub of cmd.commands) {
|
|
3591
4075
|
const name = sub.name();
|
|
3592
4076
|
const entry = { term: name, desc: sub.description() };
|
|
3593
|
-
if (["login", "logout", "whoami", "claude-
|
|
4077
|
+
if (["login", "logout", "whoami", "claude-login", "codex-login"].includes(name)) {
|
|
3594
4078
|
groups[0][1].push(entry);
|
|
3595
4079
|
} else if (["create", "ls", "get", "rm"].includes(name)) {
|
|
3596
4080
|
groups[1][1].push(entry);
|
|
@@ -3634,10 +4118,10 @@ function formatSubcommandHelp(cmd, helper) {
|
|
|
3634
4118
|
term: helper.optionTerm(option),
|
|
3635
4119
|
desc: helper.optionDescription(option)
|
|
3636
4120
|
}));
|
|
3637
|
-
lines.push(
|
|
4121
|
+
lines.push(chalk12.bold(commandPath(cmd)));
|
|
3638
4122
|
lines.push("");
|
|
3639
4123
|
if (description) {
|
|
3640
|
-
lines.push(` ${
|
|
4124
|
+
lines.push(` ${chalk12.dim(description)}`);
|
|
3641
4125
|
lines.push("");
|
|
3642
4126
|
}
|
|
3643
4127
|
appendTextSection(lines, "Usage", [helper.commandUsage(cmd)]);
|
|
@@ -3664,7 +4148,8 @@ program.name(cliName).description("Agent Computer CLI").version(pkg2.version ??
|
|
|
3664
4148
|
program.addCommand(loginCommand);
|
|
3665
4149
|
program.addCommand(logoutCommand);
|
|
3666
4150
|
program.addCommand(whoamiCommand);
|
|
3667
|
-
program.addCommand(
|
|
4151
|
+
program.addCommand(claudeLoginCommand);
|
|
4152
|
+
program.addCommand(codexLoginCommand);
|
|
3668
4153
|
program.addCommand(createCommand);
|
|
3669
4154
|
program.addCommand(lsCommand);
|
|
3670
4155
|
program.addCommand(getCommand);
|