aicomputer 0.1.12 → 0.1.13
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 +329 -116
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -7,7 +7,6 @@ import { readFileSync as readFileSync3 } from "fs";
|
|
|
7
7
|
import { basename as basename2 } from "path";
|
|
8
8
|
|
|
9
9
|
// src/commands/access.ts
|
|
10
|
-
import { spawn } from "child_process";
|
|
11
10
|
import { Command } from "commander";
|
|
12
11
|
import chalk3 from "chalk";
|
|
13
12
|
import ora from "ora";
|
|
@@ -447,6 +446,59 @@ async function generateSSHKey() {
|
|
|
447
446
|
return { publicKeyPath, privateKeyPath };
|
|
448
447
|
}
|
|
449
448
|
|
|
449
|
+
// src/lib/ssh-access.ts
|
|
450
|
+
import { spawn } from "child_process";
|
|
451
|
+
async function prepareSSHConnection(computer) {
|
|
452
|
+
const registered = await ensureDefaultSSHKeyRegistered();
|
|
453
|
+
const info = await getConnectionInfo(computer.id);
|
|
454
|
+
if (!info.connection.ssh_available) {
|
|
455
|
+
throw new Error("SSH is not available for this computer");
|
|
456
|
+
}
|
|
457
|
+
return {
|
|
458
|
+
computer,
|
|
459
|
+
command: formatSSHCommand(
|
|
460
|
+
info.connection.ssh_user,
|
|
461
|
+
info.connection.ssh_host,
|
|
462
|
+
info.connection.ssh_port
|
|
463
|
+
),
|
|
464
|
+
args: [
|
|
465
|
+
"-i",
|
|
466
|
+
registered.privateKeyPath,
|
|
467
|
+
"-p",
|
|
468
|
+
String(info.connection.ssh_port),
|
|
469
|
+
`${info.connection.ssh_user}@${info.connection.ssh_host}`
|
|
470
|
+
]
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
async function prepareSSHConnectionByIdentifier(identifier) {
|
|
474
|
+
const computer = await resolveComputer(identifier);
|
|
475
|
+
return prepareSSHConnection(computer);
|
|
476
|
+
}
|
|
477
|
+
async function openSSHConnection(connection) {
|
|
478
|
+
await new Promise((resolve, reject) => {
|
|
479
|
+
const child = spawn("ssh", connection.args, {
|
|
480
|
+
stdio: "inherit"
|
|
481
|
+
});
|
|
482
|
+
child.on("error", reject);
|
|
483
|
+
child.on("exit", (code) => {
|
|
484
|
+
if (code === 0) {
|
|
485
|
+
resolve();
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
488
|
+
reject(new Error(`ssh exited with code ${code ?? 1}`));
|
|
489
|
+
});
|
|
490
|
+
});
|
|
491
|
+
}
|
|
492
|
+
function formatSSHCommand(user, host, port) {
|
|
493
|
+
if (!user.trim() || !host.trim()) {
|
|
494
|
+
return "ssh unavailable";
|
|
495
|
+
}
|
|
496
|
+
if (port <= 0 || port === 22) {
|
|
497
|
+
return `ssh ${user}@${host}`;
|
|
498
|
+
}
|
|
499
|
+
return `ssh -p ${port} ${user}@${host}`;
|
|
500
|
+
}
|
|
501
|
+
|
|
450
502
|
// src/lib/open-browser.ts
|
|
451
503
|
import { constants } from "fs";
|
|
452
504
|
import { access } from "fs/promises";
|
|
@@ -528,41 +580,39 @@ var openCommand = new Command("open").description("Open a computer in your brows
|
|
|
528
580
|
await openBrowserURL(access2.access_url);
|
|
529
581
|
console.log(chalk3.dim(` ${access2.access_url}`));
|
|
530
582
|
} catch (error) {
|
|
531
|
-
spinner.fail(
|
|
583
|
+
spinner.fail(
|
|
584
|
+
error instanceof Error ? error.message : "Failed to open computer"
|
|
585
|
+
);
|
|
532
586
|
process.exit(1);
|
|
533
587
|
}
|
|
534
588
|
});
|
|
535
|
-
var sshCommand = new Command("ssh").description("Open an SSH session to a computer").argument("[id-or-handle]", "Computer id or handle").option("--setup", "Register key and configure a global SSH alias").option("--alias <alias>", "SSH host alias", "agentcomputer.ai").option("--host <host>", "SSH gateway host", "ssh.agentcomputer.ai").option("--port <port>", "SSH gateway port", "22").action(
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
589
|
+
var sshCommand = new Command("ssh").description("Open an SSH session to a computer").argument("[id-or-handle]", "Computer id or handle").option("--setup", "Register key and configure a global SSH alias").option("--alias <alias>", "SSH host alias", "agentcomputer.ai").option("--host <host>", "SSH gateway host", "ssh.agentcomputer.ai").option("--port <port>", "SSH gateway port", "22").action(
|
|
590
|
+
async (identifier, options) => {
|
|
591
|
+
if (options.setup) {
|
|
592
|
+
await setupSSHAlias(options);
|
|
593
|
+
return;
|
|
594
|
+
}
|
|
595
|
+
const spinner = ora(
|
|
596
|
+
identifier ? "Preparing SSH access..." : "Fetching computers..."
|
|
597
|
+
).start();
|
|
598
|
+
try {
|
|
599
|
+
const computer = await resolveSSHComputer(identifier, spinner);
|
|
600
|
+
const connection = await prepareSSHConnection(computer);
|
|
601
|
+
spinner.succeed(`Connecting to ${chalk3.bold(computer.handle)}`);
|
|
602
|
+
console.log(chalk3.dim(` ${connection.command}`));
|
|
603
|
+
console.log();
|
|
604
|
+
await openSSHConnection(connection);
|
|
605
|
+
} catch (error) {
|
|
606
|
+
spinner.fail(
|
|
607
|
+
error instanceof Error ? error.message : "Failed to prepare SSH access"
|
|
608
|
+
);
|
|
609
|
+
process.exit(1);
|
|
548
610
|
}
|
|
549
|
-
const registered = await ensureDefaultSSHKeyRegistered();
|
|
550
|
-
spinner.succeed(`Connecting to ${chalk3.bold(computer.handle)}`);
|
|
551
|
-
console.log(chalk3.dim(` ${formatSSHCommand(info.connection.ssh_user, info.connection.ssh_host, info.connection.ssh_port)}`));
|
|
552
|
-
console.log();
|
|
553
|
-
await runSSH([
|
|
554
|
-
"-i",
|
|
555
|
-
registered.privateKeyPath,
|
|
556
|
-
"-p",
|
|
557
|
-
String(info.connection.ssh_port),
|
|
558
|
-
`${info.connection.ssh_user}@${info.connection.ssh_host}`
|
|
559
|
-
]);
|
|
560
|
-
} catch (error) {
|
|
561
|
-
spinner.fail(error instanceof Error ? error.message : "Failed to prepare SSH access");
|
|
562
|
-
process.exit(1);
|
|
563
611
|
}
|
|
564
|
-
|
|
565
|
-
var portsCommand = new Command("ports").description(
|
|
612
|
+
);
|
|
613
|
+
var portsCommand = new Command("ports").description(
|
|
614
|
+
"Manage published app ports"
|
|
615
|
+
);
|
|
566
616
|
portsCommand.command("ls").description("List published ports for a computer").argument("<id-or-handle>", "Computer id or handle").action(async (identifier) => {
|
|
567
617
|
const spinner = ora("Fetching ports...").start();
|
|
568
618
|
try {
|
|
@@ -588,17 +638,20 @@ portsCommand.command("ls").description("List published ports for a computer").ar
|
|
|
588
638
|
console.log(
|
|
589
639
|
` ${padEnd(port.subdomain, subWidth + 2)}${padEnd(String(port.target_port), 8)}${port.protocol}`
|
|
590
640
|
);
|
|
591
|
-
console.log(
|
|
592
|
-
` ${chalk3.dim(url)}`
|
|
593
|
-
);
|
|
641
|
+
console.log(` ${chalk3.dim(url)}`);
|
|
594
642
|
}
|
|
595
643
|
console.log();
|
|
596
644
|
} catch (error) {
|
|
597
|
-
spinner.fail(
|
|
645
|
+
spinner.fail(
|
|
646
|
+
error instanceof Error ? error.message : "Failed to fetch ports"
|
|
647
|
+
);
|
|
598
648
|
process.exit(1);
|
|
599
649
|
}
|
|
600
650
|
});
|
|
601
|
-
portsCommand.command("publish").description("Publish an HTTP app port").argument("<id-or-handle>", "Computer id or handle").argument("<port>", "Target port").option(
|
|
651
|
+
portsCommand.command("publish").description("Publish an HTTP app port").argument("<id-or-handle>", "Computer id or handle").argument("<port>", "Target port").option(
|
|
652
|
+
"--subdomain <value>",
|
|
653
|
+
"Custom left-hand host label before --<handle>"
|
|
654
|
+
).option("--protocol <value>", "http or https", "http").action(async (identifier, port, options) => {
|
|
602
655
|
const spinner = ora("Publishing port...").start();
|
|
603
656
|
try {
|
|
604
657
|
const targetPort = Number.parseInt(port, 10);
|
|
@@ -612,10 +665,14 @@ portsCommand.command("publish").description("Publish an HTTP app port").argument
|
|
|
612
665
|
protocol: options.protocol
|
|
613
666
|
});
|
|
614
667
|
const url = `https://${published.subdomain}--${computer.handle}.computer.agentcomputer.ai`;
|
|
615
|
-
spinner.succeed(
|
|
668
|
+
spinner.succeed(
|
|
669
|
+
`Published port ${chalk3.bold(String(published.target_port))}`
|
|
670
|
+
);
|
|
616
671
|
console.log(chalk3.dim(` ${url}`));
|
|
617
672
|
} catch (error) {
|
|
618
|
-
spinner.fail(
|
|
673
|
+
spinner.fail(
|
|
674
|
+
error instanceof Error ? error.message : "Failed to publish port"
|
|
675
|
+
);
|
|
619
676
|
process.exit(1);
|
|
620
677
|
}
|
|
621
678
|
});
|
|
@@ -630,25 +687,12 @@ portsCommand.command("rm").description("Unpublish an app port").argument("<id-or
|
|
|
630
687
|
await deletePublishedPort(computer.id, targetPort);
|
|
631
688
|
spinner.succeed(`Removed port ${chalk3.bold(String(targetPort))}`);
|
|
632
689
|
} catch (error) {
|
|
633
|
-
spinner.fail(
|
|
690
|
+
spinner.fail(
|
|
691
|
+
error instanceof Error ? error.message : "Failed to remove port"
|
|
692
|
+
);
|
|
634
693
|
process.exit(1);
|
|
635
694
|
}
|
|
636
695
|
});
|
|
637
|
-
async function runSSH(args) {
|
|
638
|
-
await new Promise((resolve, reject) => {
|
|
639
|
-
const child = spawn("ssh", args, {
|
|
640
|
-
stdio: "inherit"
|
|
641
|
-
});
|
|
642
|
-
child.on("error", reject);
|
|
643
|
-
child.on("exit", (code) => {
|
|
644
|
-
if (code === 0) {
|
|
645
|
-
resolve();
|
|
646
|
-
return;
|
|
647
|
-
}
|
|
648
|
-
reject(new Error(`ssh exited with code ${code ?? 1}`));
|
|
649
|
-
});
|
|
650
|
-
});
|
|
651
|
-
}
|
|
652
696
|
async function setupSSHAlias(options) {
|
|
653
697
|
const spinner = ora("Configuring global SSH access...").start();
|
|
654
698
|
try {
|
|
@@ -672,7 +716,9 @@ async function setupSSHAlias(options) {
|
|
|
672
716
|
console.log(` ${chalk3.bold("Direct:")} ssh <handle>@${alias}`);
|
|
673
717
|
console.log();
|
|
674
718
|
} catch (error) {
|
|
675
|
-
spinner.fail(
|
|
719
|
+
spinner.fail(
|
|
720
|
+
error instanceof Error ? error.message : "Failed to configure SSH alias"
|
|
721
|
+
);
|
|
676
722
|
process.exit(1);
|
|
677
723
|
}
|
|
678
724
|
}
|
|
@@ -682,7 +728,9 @@ function normalizeSSHAlias(value) {
|
|
|
682
728
|
throw new Error("ssh alias cannot be empty");
|
|
683
729
|
}
|
|
684
730
|
if (!/^[A-Za-z0-9._-]+$/.test(alias)) {
|
|
685
|
-
throw new Error(
|
|
731
|
+
throw new Error(
|
|
732
|
+
"ssh alias may contain only letters, numbers, dot, dash, or underscore"
|
|
733
|
+
);
|
|
686
734
|
}
|
|
687
735
|
return alias;
|
|
688
736
|
}
|
|
@@ -703,15 +751,6 @@ function parseSSHPort(value) {
|
|
|
703
751
|
}
|
|
704
752
|
return parsed;
|
|
705
753
|
}
|
|
706
|
-
function formatSSHCommand(user, host, port) {
|
|
707
|
-
if (!user.trim() || !host.trim()) {
|
|
708
|
-
return "ssh unavailable";
|
|
709
|
-
}
|
|
710
|
-
if (port <= 0 || port === 22) {
|
|
711
|
-
return `ssh ${user}@${host}`;
|
|
712
|
-
}
|
|
713
|
-
return `ssh -p ${port} ${user}@${host}`;
|
|
714
|
-
}
|
|
715
754
|
async function resolveSSHComputer(identifier, spinner) {
|
|
716
755
|
const trimmed = identifier?.trim();
|
|
717
756
|
if (trimmed) {
|
|
@@ -720,7 +759,10 @@ async function resolveSSHComputer(identifier, spinner) {
|
|
|
720
759
|
const computers = await listComputers();
|
|
721
760
|
spinner.stop();
|
|
722
761
|
try {
|
|
723
|
-
return await promptForSSHComputer(
|
|
762
|
+
return await promptForSSHComputer(
|
|
763
|
+
computers,
|
|
764
|
+
"Select a computer to SSH into"
|
|
765
|
+
);
|
|
724
766
|
} finally {
|
|
725
767
|
spinner.start("Preparing SSH access...");
|
|
726
768
|
}
|
|
@@ -1736,6 +1778,16 @@ var CLAUDE_OAUTH_TOKEN_URL = process.env.CLAUDE_OAUTH_TOKEN_URL ?? "https://plat
|
|
|
1736
1778
|
var CLAUDE_OAUTH_REDIRECT_URL = process.env.CLAUDE_OAUTH_REDIRECT_URL ?? "https://platform.claude.com/oauth/code/callback";
|
|
1737
1779
|
var CLAUDE_OAUTH_SCOPES = (process.env.CLAUDE_OAUTH_SCOPES ?? "user:profile user:inference user:sessions:claude_code user:mcp_servers user:file_upload").split(/\s+/).filter(Boolean);
|
|
1738
1780
|
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) => {
|
|
1781
|
+
try {
|
|
1782
|
+
await runClaudeLogin(options);
|
|
1783
|
+
} catch (error) {
|
|
1784
|
+
const message = error instanceof Error ? error.message : "Failed to authenticate Claude";
|
|
1785
|
+
console.error(chalk5.red(`
|
|
1786
|
+
${message}`));
|
|
1787
|
+
process.exit(1);
|
|
1788
|
+
}
|
|
1789
|
+
});
|
|
1790
|
+
async function runClaudeLogin(options) {
|
|
1739
1791
|
const todos = createTodoList();
|
|
1740
1792
|
let target = null;
|
|
1741
1793
|
let helperCreated = false;
|
|
@@ -1749,12 +1801,7 @@ var claudeLoginCommand = new Command4("claude-login").alias("claude-auth").descr
|
|
|
1749
1801
|
target = prepared.computer;
|
|
1750
1802
|
helperCreated = prepared.helperCreated;
|
|
1751
1803
|
sharedInstall = prepared.sharedInstall;
|
|
1752
|
-
markTodo(
|
|
1753
|
-
todos,
|
|
1754
|
-
"target",
|
|
1755
|
-
"done",
|
|
1756
|
-
prepared.detail
|
|
1757
|
-
);
|
|
1804
|
+
markTodo(todos, "target", "done", prepared.detail);
|
|
1758
1805
|
activeTodoID = "ready";
|
|
1759
1806
|
target = await waitForRunning(target);
|
|
1760
1807
|
markTodo(todos, "ready", "done", `${target.handle} is running`);
|
|
@@ -1771,7 +1818,10 @@ var claudeLoginCommand = new Command4("claude-login").alias("claude-auth").descr
|
|
|
1771
1818
|
sharedInstall ? `installed Claude login on shared home via ${target.handle}` : `installed Claude login on ${target.handle}`
|
|
1772
1819
|
);
|
|
1773
1820
|
activeTodoID = "verify-primary";
|
|
1774
|
-
const primaryVerification = await verifyTargetMachine(
|
|
1821
|
+
const primaryVerification = await verifyTargetMachine(
|
|
1822
|
+
target.handle,
|
|
1823
|
+
sshTarget
|
|
1824
|
+
);
|
|
1775
1825
|
markVerificationTodo(
|
|
1776
1826
|
todos,
|
|
1777
1827
|
"verify-primary",
|
|
@@ -1817,12 +1867,7 @@ var claudeLoginCommand = new Command4("claude-login").alias("claude-auth").descr
|
|
|
1817
1867
|
markTodo(todos, "cleanup", "failed", message);
|
|
1818
1868
|
}
|
|
1819
1869
|
} else if (helperCreated && target && options.keepHelper) {
|
|
1820
|
-
markTodo(
|
|
1821
|
-
todos,
|
|
1822
|
-
"cleanup",
|
|
1823
|
-
"skipped",
|
|
1824
|
-
`kept helper ${target.handle}`
|
|
1825
|
-
);
|
|
1870
|
+
markTodo(todos, "cleanup", "skipped", `kept helper ${target.handle}`);
|
|
1826
1871
|
} else {
|
|
1827
1872
|
markTodo(todos, "cleanup", "skipped", "no helper created");
|
|
1828
1873
|
}
|
|
@@ -1831,9 +1876,7 @@ var claudeLoginCommand = new Command4("claude-login").alias("claude-auth").descr
|
|
|
1831
1876
|
}
|
|
1832
1877
|
}
|
|
1833
1878
|
if (failureMessage) {
|
|
1834
|
-
|
|
1835
|
-
${failureMessage}`));
|
|
1836
|
-
process.exit(1);
|
|
1879
|
+
throw new Error(failureMessage);
|
|
1837
1880
|
}
|
|
1838
1881
|
if (target) {
|
|
1839
1882
|
console.log(
|
|
@@ -1841,15 +1884,23 @@ ${failureMessage}`));
|
|
|
1841
1884
|
);
|
|
1842
1885
|
console.log();
|
|
1843
1886
|
}
|
|
1844
|
-
}
|
|
1887
|
+
}
|
|
1845
1888
|
function createTodoList() {
|
|
1846
1889
|
return [
|
|
1847
1890
|
{ id: "target", label: "Pick target computer", state: "pending" },
|
|
1848
1891
|
{ id: "ready", label: "Wait for machine readiness", state: "pending" },
|
|
1849
1892
|
{ id: "oauth", label: "Complete Claude browser auth", state: "pending" },
|
|
1850
1893
|
{ id: "install", label: "Install stored Claude login", state: "pending" },
|
|
1851
|
-
{
|
|
1852
|
-
|
|
1894
|
+
{
|
|
1895
|
+
id: "verify-primary",
|
|
1896
|
+
label: "Verify on target machine",
|
|
1897
|
+
state: "pending"
|
|
1898
|
+
},
|
|
1899
|
+
{
|
|
1900
|
+
id: "verify-shared",
|
|
1901
|
+
label: "Verify shared-home propagation",
|
|
1902
|
+
state: "pending"
|
|
1903
|
+
},
|
|
1853
1904
|
{ id: "cleanup", label: "Clean up temporary helper", state: "pending" }
|
|
1854
1905
|
];
|
|
1855
1906
|
}
|
|
@@ -1889,10 +1940,14 @@ async function prepareTargetMachine(options) {
|
|
|
1889
1940
|
async function runManualOAuthFlow() {
|
|
1890
1941
|
const codeVerifier = base64url(randomBytes2(32));
|
|
1891
1942
|
const state = randomBytes2(16).toString("hex");
|
|
1892
|
-
const codeChallenge = base64url(
|
|
1943
|
+
const codeChallenge = base64url(
|
|
1944
|
+
createHash("sha256").update(codeVerifier).digest()
|
|
1945
|
+
);
|
|
1893
1946
|
const url = buildAuthorizationURL(codeChallenge, state);
|
|
1894
1947
|
console.log("We will open your browser so you can authenticate with Claude.");
|
|
1895
|
-
console.log(
|
|
1948
|
+
console.log(
|
|
1949
|
+
"If the browser does not open automatically, use the URL below:\n"
|
|
1950
|
+
);
|
|
1896
1951
|
console.log(url);
|
|
1897
1952
|
console.log();
|
|
1898
1953
|
try {
|
|
@@ -1903,7 +1958,9 @@ async function runManualOAuthFlow() {
|
|
|
1903
1958
|
console.log(
|
|
1904
1959
|
"After completing authentication, copy the code shown on the success page."
|
|
1905
1960
|
);
|
|
1906
|
-
console.log(
|
|
1961
|
+
console.log(
|
|
1962
|
+
"You can paste either the full URL, or a value formatted as CODE#STATE.\n"
|
|
1963
|
+
);
|
|
1907
1964
|
const pasted = (await textInput({
|
|
1908
1965
|
message: "Paste the authorization code (or URL) here:"
|
|
1909
1966
|
})).trim();
|
|
@@ -1970,7 +2027,9 @@ function parseAuthorizationInput(value, expectedState) {
|
|
|
1970
2027
|
throw new Error("pasted URL is missing code or state");
|
|
1971
2028
|
}
|
|
1972
2029
|
if (state2 !== expectedState) {
|
|
1973
|
-
throw new Error(
|
|
2030
|
+
throw new Error(
|
|
2031
|
+
"state mismatch detected; restart the authentication flow"
|
|
2032
|
+
);
|
|
1974
2033
|
}
|
|
1975
2034
|
return { code: code2, state: state2 };
|
|
1976
2035
|
}
|
|
@@ -1984,10 +2043,16 @@ function parseAuthorizationInput(value, expectedState) {
|
|
|
1984
2043
|
return { code, state };
|
|
1985
2044
|
}
|
|
1986
2045
|
async function installClaudeAuth(target, oauth) {
|
|
1987
|
-
const spinner = ora4(
|
|
2046
|
+
const spinner = ora4(
|
|
2047
|
+
`Installing Claude auth on ${chalk5.bold(target.handle)}...`
|
|
2048
|
+
).start();
|
|
1988
2049
|
try {
|
|
1989
2050
|
const installScript = buildInstallScript(oauth.refreshToken, oauth.scope);
|
|
1990
|
-
const result = await runRemoteCommand(
|
|
2051
|
+
const result = await runRemoteCommand(
|
|
2052
|
+
target,
|
|
2053
|
+
["bash", "-s"],
|
|
2054
|
+
installScript
|
|
2055
|
+
);
|
|
1991
2056
|
if (result.stdout.trim()) {
|
|
1992
2057
|
spinner.succeed(`Installed Claude auth on ${chalk5.bold(target.handle)}`);
|
|
1993
2058
|
return;
|
|
@@ -2001,7 +2066,9 @@ async function installClaudeAuth(target, oauth) {
|
|
|
2001
2066
|
}
|
|
2002
2067
|
}
|
|
2003
2068
|
async function verifyTargetMachine(handle, target) {
|
|
2004
|
-
const spinner = ora4(
|
|
2069
|
+
const spinner = ora4(
|
|
2070
|
+
`Verifying Claude login on ${chalk5.bold(handle)}...`
|
|
2071
|
+
).start();
|
|
2005
2072
|
const result = await verifyStoredAuth(target);
|
|
2006
2073
|
if (result.status === "verified") {
|
|
2007
2074
|
spinner.succeed(`Verified Claude login on ${chalk5.bold(handle)}`);
|
|
@@ -2021,7 +2088,9 @@ async function verifySharedInstall(primaryHandle, primaryComputerID, sharedInsta
|
|
|
2021
2088
|
verify
|
|
2022
2089
|
);
|
|
2023
2090
|
if (result.status === "verified") {
|
|
2024
|
-
spinner.succeed(
|
|
2091
|
+
spinner.succeed(
|
|
2092
|
+
`Verified shared-home Claude login on ${chalk5.bold(result.handle)}`
|
|
2093
|
+
);
|
|
2025
2094
|
return result;
|
|
2026
2095
|
}
|
|
2027
2096
|
spinner.info(result.reason);
|
|
@@ -2934,6 +3003,16 @@ import { Command as Command7 } from "commander";
|
|
|
2934
3003
|
import chalk7 from "chalk";
|
|
2935
3004
|
import ora6 from "ora";
|
|
2936
3005
|
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) => {
|
|
3006
|
+
try {
|
|
3007
|
+
await runCodexLogin(options);
|
|
3008
|
+
} catch (error) {
|
|
3009
|
+
const message = error instanceof Error ? error.message : "Failed to authenticate Codex";
|
|
3010
|
+
console.error(chalk7.red(`
|
|
3011
|
+
${message}`));
|
|
3012
|
+
process.exit(1);
|
|
3013
|
+
}
|
|
3014
|
+
});
|
|
3015
|
+
async function runCodexLogin(options) {
|
|
2937
3016
|
const todos = createTodoList2();
|
|
2938
3017
|
let target = null;
|
|
2939
3018
|
let helperCreated = false;
|
|
@@ -2964,7 +3043,10 @@ var codexLoginCommand = new Command7("codex-login").alias("codex-auth").descript
|
|
|
2964
3043
|
sharedInstall ? `installed Codex login on shared home via ${target.handle}` : `installed Codex login on ${target.handle}`
|
|
2965
3044
|
);
|
|
2966
3045
|
activeTodoID = "verify-primary";
|
|
2967
|
-
const primaryVerification = await verifyTargetMachine2(
|
|
3046
|
+
const primaryVerification = await verifyTargetMachine2(
|
|
3047
|
+
target.handle,
|
|
3048
|
+
sshTarget
|
|
3049
|
+
);
|
|
2968
3050
|
markVerificationTodo2(
|
|
2969
3051
|
todos,
|
|
2970
3052
|
"verify-primary",
|
|
@@ -3019,9 +3101,7 @@ var codexLoginCommand = new Command7("codex-login").alias("codex-auth").descript
|
|
|
3019
3101
|
}
|
|
3020
3102
|
}
|
|
3021
3103
|
if (failureMessage) {
|
|
3022
|
-
|
|
3023
|
-
${failureMessage}`));
|
|
3024
|
-
process.exit(1);
|
|
3104
|
+
throw new Error(failureMessage);
|
|
3025
3105
|
}
|
|
3026
3106
|
if (target) {
|
|
3027
3107
|
console.log(
|
|
@@ -3029,15 +3109,23 @@ ${failureMessage}`));
|
|
|
3029
3109
|
);
|
|
3030
3110
|
console.log();
|
|
3031
3111
|
}
|
|
3032
|
-
}
|
|
3112
|
+
}
|
|
3033
3113
|
function createTodoList2() {
|
|
3034
3114
|
return [
|
|
3035
3115
|
{ id: "target", label: "Pick target computer", state: "pending" },
|
|
3036
3116
|
{ id: "ready", label: "Wait for machine readiness", state: "pending" },
|
|
3037
3117
|
{ id: "local-auth", label: "Complete local Codex auth", state: "pending" },
|
|
3038
3118
|
{ id: "install", label: "Install stored Codex login", state: "pending" },
|
|
3039
|
-
{
|
|
3040
|
-
|
|
3119
|
+
{
|
|
3120
|
+
id: "verify-primary",
|
|
3121
|
+
label: "Verify on target machine",
|
|
3122
|
+
state: "pending"
|
|
3123
|
+
},
|
|
3124
|
+
{
|
|
3125
|
+
id: "verify-shared",
|
|
3126
|
+
label: "Verify shared-home propagation",
|
|
3127
|
+
state: "pending"
|
|
3128
|
+
},
|
|
3041
3129
|
{ id: "cleanup", label: "Clean up temporary helper", state: "pending" }
|
|
3042
3130
|
];
|
|
3043
3131
|
}
|
|
@@ -3083,10 +3171,16 @@ async function ensureLocalCodexAuth() {
|
|
|
3083
3171
|
};
|
|
3084
3172
|
}
|
|
3085
3173
|
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
3086
|
-
throw new Error(
|
|
3174
|
+
throw new Error(
|
|
3175
|
+
"local Codex login is required when not running interactively"
|
|
3176
|
+
);
|
|
3087
3177
|
}
|
|
3088
|
-
console.log(
|
|
3089
|
-
|
|
3178
|
+
console.log(
|
|
3179
|
+
"We will open your browser so you can authenticate Codex locally."
|
|
3180
|
+
);
|
|
3181
|
+
console.log(
|
|
3182
|
+
"If Codex falls back to device auth, complete that flow and return here.\n"
|
|
3183
|
+
);
|
|
3090
3184
|
await runInteractiveCodexLogin();
|
|
3091
3185
|
const refreshedStatus = await getLocalCodexStatus();
|
|
3092
3186
|
if (!refreshedStatus.loggedIn) {
|
|
@@ -3166,7 +3260,9 @@ async function captureLocalCommand(command, args) {
|
|
|
3166
3260
|
});
|
|
3167
3261
|
}
|
|
3168
3262
|
async function installCodexAuth(target, authJSON) {
|
|
3169
|
-
const spinner = ora6(
|
|
3263
|
+
const spinner = ora6(
|
|
3264
|
+
`Installing Codex login on ${chalk7.bold(target.handle)}...`
|
|
3265
|
+
).start();
|
|
3170
3266
|
try {
|
|
3171
3267
|
const installScript = buildInstallScript2(authJSON);
|
|
3172
3268
|
await runRemoteCommand(target, ["bash", "-s"], installScript);
|
|
@@ -3179,7 +3275,9 @@ async function installCodexAuth(target, authJSON) {
|
|
|
3179
3275
|
}
|
|
3180
3276
|
}
|
|
3181
3277
|
async function verifyTargetMachine2(handle, target) {
|
|
3182
|
-
const spinner = ora6(
|
|
3278
|
+
const spinner = ora6(
|
|
3279
|
+
`Verifying Codex login on ${chalk7.bold(handle)}...`
|
|
3280
|
+
).start();
|
|
3183
3281
|
const result = await verifyStoredCodexAuth(target);
|
|
3184
3282
|
if (result.status === "verified") {
|
|
3185
3283
|
spinner.succeed(`Verified Codex login on ${chalk7.bold(handle)}`);
|
|
@@ -3199,7 +3297,9 @@ async function verifySharedInstall2(primaryHandle, primaryComputerID, sharedInst
|
|
|
3199
3297
|
verify
|
|
3200
3298
|
);
|
|
3201
3299
|
if (result.status === "verified") {
|
|
3202
|
-
spinner.succeed(
|
|
3300
|
+
spinner.succeed(
|
|
3301
|
+
`Verified shared-home Codex login on ${chalk7.bold(result.handle)}`
|
|
3302
|
+
);
|
|
3203
3303
|
return result;
|
|
3204
3304
|
}
|
|
3205
3305
|
spinner.info(result.reason);
|
|
@@ -3653,10 +3753,12 @@ import ora8 from "ora";
|
|
|
3653
3753
|
|
|
3654
3754
|
// src/lib/browser-login.ts
|
|
3655
3755
|
import { randomBytes as randomBytes3 } from "crypto";
|
|
3656
|
-
import {
|
|
3756
|
+
import {
|
|
3757
|
+
createServer
|
|
3758
|
+
} from "http";
|
|
3657
3759
|
var CALLBACK_HOST = "127.0.0.1";
|
|
3658
3760
|
var CALLBACK_PATH = "/callback";
|
|
3659
|
-
var LOGIN_TIMEOUT_MS =
|
|
3761
|
+
var LOGIN_TIMEOUT_MS = 15 * 60 * 1e3;
|
|
3660
3762
|
async function createBrowserLoginAttempt() {
|
|
3661
3763
|
const state = randomBytes3(16).toString("hex");
|
|
3662
3764
|
const deferred = createDeferred();
|
|
@@ -3736,7 +3838,11 @@ async function handleRequest(input) {
|
|
|
3736
3838
|
return;
|
|
3737
3839
|
}
|
|
3738
3840
|
if (settledRef.current) {
|
|
3739
|
-
writeHTML(
|
|
3841
|
+
writeHTML(
|
|
3842
|
+
response,
|
|
3843
|
+
409,
|
|
3844
|
+
renderErrorPage("This login link has already been used.")
|
|
3845
|
+
);
|
|
3740
3846
|
return;
|
|
3741
3847
|
}
|
|
3742
3848
|
const returnedState = url.searchParams.get("state")?.trim();
|
|
@@ -3763,6 +3869,9 @@ async function handleRequest(input) {
|
|
|
3763
3869
|
writeHTML(response, 400, renderErrorPage(error.message));
|
|
3764
3870
|
return;
|
|
3765
3871
|
}
|
|
3872
|
+
const machineHandle = url.searchParams.get("machine_handle")?.trim() || void 0;
|
|
3873
|
+
const provider = parseProvider(url.searchParams.get("provider"));
|
|
3874
|
+
const autoSSH = parseAutoSSH(url.searchParams.get("auto_ssh"));
|
|
3766
3875
|
try {
|
|
3767
3876
|
const me = await apiWithKey(apiKey, "/v1/me");
|
|
3768
3877
|
setAPIKey(apiKey);
|
|
@@ -3771,9 +3880,21 @@ async function handleRequest(input) {
|
|
|
3771
3880
|
apiKey,
|
|
3772
3881
|
callbackURL,
|
|
3773
3882
|
loginURL: buildBrowserLoginURL(callbackURL, state),
|
|
3774
|
-
me
|
|
3883
|
+
me,
|
|
3884
|
+
machineHandle,
|
|
3885
|
+
provider,
|
|
3886
|
+
autoSSH
|
|
3775
3887
|
});
|
|
3776
|
-
writeHTML(
|
|
3888
|
+
writeHTML(
|
|
3889
|
+
response,
|
|
3890
|
+
200,
|
|
3891
|
+
renderSuccessPage({
|
|
3892
|
+
autoSSH,
|
|
3893
|
+
email: me.user.email,
|
|
3894
|
+
machineHandle,
|
|
3895
|
+
provider
|
|
3896
|
+
})
|
|
3897
|
+
);
|
|
3777
3898
|
} catch (error) {
|
|
3778
3899
|
settledRef.current = true;
|
|
3779
3900
|
const message = error instanceof Error ? error.message : "Failed to validate browser login";
|
|
@@ -3781,6 +3902,22 @@ async function handleRequest(input) {
|
|
|
3781
3902
|
writeHTML(response, 401, renderErrorPage(message));
|
|
3782
3903
|
}
|
|
3783
3904
|
}
|
|
3905
|
+
function parseProvider(rawValue) {
|
|
3906
|
+
switch (rawValue?.trim()) {
|
|
3907
|
+
case "claude":
|
|
3908
|
+
case "codex":
|
|
3909
|
+
case "skip":
|
|
3910
|
+
return rawValue.trim();
|
|
3911
|
+
default:
|
|
3912
|
+
return void 0;
|
|
3913
|
+
}
|
|
3914
|
+
}
|
|
3915
|
+
function parseAutoSSH(rawValue) {
|
|
3916
|
+
if (rawValue === null) {
|
|
3917
|
+
return void 0;
|
|
3918
|
+
}
|
|
3919
|
+
return !(rawValue === "0" || rawValue === "false");
|
|
3920
|
+
}
|
|
3784
3921
|
function createDeferred() {
|
|
3785
3922
|
let resolve;
|
|
3786
3923
|
let reject;
|
|
@@ -3824,7 +3961,9 @@ function writeHTML(response, statusCode, body) {
|
|
|
3824
3961
|
response.setHeader("content-type", "text/html; charset=utf-8");
|
|
3825
3962
|
response.end(body);
|
|
3826
3963
|
}
|
|
3827
|
-
function renderSuccessPage(
|
|
3964
|
+
function renderSuccessPage(input) {
|
|
3965
|
+
const providerMessage = input.provider && input.provider !== "skip" ? ` The CLI will continue with ${escapeHTML(input.provider)} setup.` : "";
|
|
3966
|
+
const machineMessage = input.machineHandle ? ` Your new sandbox <code>${escapeHTML(input.machineHandle)}</code> is ready.${providerMessage} Return to the terminal to finish setup.` : " You can close this tab.";
|
|
3828
3967
|
return `<!doctype html>
|
|
3829
3968
|
<html lang="en">
|
|
3830
3969
|
<head>
|
|
@@ -3842,7 +3981,7 @@ function renderSuccessPage(email) {
|
|
|
3842
3981
|
<body>
|
|
3843
3982
|
<main>
|
|
3844
3983
|
<h1>Computer CLI login complete</h1>
|
|
3845
|
-
<p>Signed in as <code>${escapeHTML(email)}</code
|
|
3984
|
+
<p>Signed in as <code>${escapeHTML(input.email)}</code>.${machineMessage}</p>
|
|
3846
3985
|
</main>
|
|
3847
3986
|
<script>
|
|
3848
3987
|
window.setTimeout(() => window.close(), 150);
|
|
@@ -3881,7 +4020,9 @@ var loginCommand = new Command9("login").description("Authenticate the CLI").opt
|
|
|
3881
4020
|
const existingKey = getStoredAPIKey();
|
|
3882
4021
|
if (existingKey && !options.force) {
|
|
3883
4022
|
console.log();
|
|
3884
|
-
console.log(
|
|
4023
|
+
console.log(
|
|
4024
|
+
chalk9.yellow(" Already logged in. Use --force to overwrite.")
|
|
4025
|
+
);
|
|
3885
4026
|
console.log();
|
|
3886
4027
|
return;
|
|
3887
4028
|
}
|
|
@@ -3927,7 +4068,9 @@ async function runBrowserLogin() {
|
|
|
3927
4068
|
} catch {
|
|
3928
4069
|
spinner.stop();
|
|
3929
4070
|
console.log();
|
|
3930
|
-
console.log(
|
|
4071
|
+
console.log(
|
|
4072
|
+
chalk9.yellow(" Browser auto-open failed. Open this URL to continue:")
|
|
4073
|
+
);
|
|
3931
4074
|
console.log(chalk9.dim(` ${attempt.loginURL}`));
|
|
3932
4075
|
console.log();
|
|
3933
4076
|
spinner.start("Waiting for browser login...");
|
|
@@ -3935,8 +4078,11 @@ async function runBrowserLogin() {
|
|
|
3935
4078
|
spinner.text = "Waiting for browser login...";
|
|
3936
4079
|
const result = await attempt.waitForResult();
|
|
3937
4080
|
spinner.succeed(`Logged in as ${chalk9.bold(result.me.user.email)}`);
|
|
4081
|
+
await continueFirstLoginFlow(result);
|
|
3938
4082
|
} catch (error) {
|
|
3939
|
-
spinner.fail(
|
|
4083
|
+
spinner.fail(
|
|
4084
|
+
error instanceof Error ? error.message : "Browser login failed"
|
|
4085
|
+
);
|
|
3940
4086
|
process.exit(1);
|
|
3941
4087
|
} finally {
|
|
3942
4088
|
await attempt?.close();
|
|
@@ -3958,6 +4104,73 @@ async function resolveAPIKeyInput(flagValue, readFromStdin) {
|
|
|
3958
4104
|
}
|
|
3959
4105
|
return Buffer.concat(chunks).toString("utf8").trim();
|
|
3960
4106
|
}
|
|
4107
|
+
async function continueFirstLoginFlow(result) {
|
|
4108
|
+
const machineHandle = result.machineHandle?.trim();
|
|
4109
|
+
if (!machineHandle) {
|
|
4110
|
+
return;
|
|
4111
|
+
}
|
|
4112
|
+
console.log();
|
|
4113
|
+
console.log(
|
|
4114
|
+
chalk9.cyan(
|
|
4115
|
+
`Continuing first-time setup for ${chalk9.bold(machineHandle)}...
|
|
4116
|
+
`
|
|
4117
|
+
)
|
|
4118
|
+
);
|
|
4119
|
+
try {
|
|
4120
|
+
await runSelectedProvider(result.provider, machineHandle);
|
|
4121
|
+
if (result.autoSSH === false) {
|
|
4122
|
+
printNextStep(machineHandle);
|
|
4123
|
+
return;
|
|
4124
|
+
}
|
|
4125
|
+
const spinner = ora8(`Preparing SSH access for ${machineHandle}...`).start();
|
|
4126
|
+
try {
|
|
4127
|
+
const connection = await prepareSSHConnectionByIdentifier(machineHandle);
|
|
4128
|
+
spinner.succeed(`Connecting to ${chalk9.bold(machineHandle)}`);
|
|
4129
|
+
console.log(chalk9.dim(` ${connection.command}`));
|
|
4130
|
+
console.log();
|
|
4131
|
+
await openSSHConnection(connection);
|
|
4132
|
+
} catch (error) {
|
|
4133
|
+
spinner.fail(
|
|
4134
|
+
error instanceof Error ? error.message : "Failed to prepare SSH access"
|
|
4135
|
+
);
|
|
4136
|
+
throw error;
|
|
4137
|
+
}
|
|
4138
|
+
} catch (error) {
|
|
4139
|
+
const message = error instanceof Error ? error.message : "Failed to finish first-time setup";
|
|
4140
|
+
console.error(chalk9.red(`
|
|
4141
|
+
${message}`));
|
|
4142
|
+
console.log();
|
|
4143
|
+
if (result.provider === "claude") {
|
|
4144
|
+
console.log(
|
|
4145
|
+
chalk9.dim(` computer claude-login --machine ${machineHandle}`)
|
|
4146
|
+
);
|
|
4147
|
+
} else if (result.provider === "codex") {
|
|
4148
|
+
console.log(
|
|
4149
|
+
chalk9.dim(` computer codex-login --machine ${machineHandle}`)
|
|
4150
|
+
);
|
|
4151
|
+
}
|
|
4152
|
+
console.log(chalk9.dim(` computer ssh ${machineHandle}`));
|
|
4153
|
+
console.log();
|
|
4154
|
+
process.exit(1);
|
|
4155
|
+
}
|
|
4156
|
+
}
|
|
4157
|
+
async function runSelectedProvider(provider, machineHandle) {
|
|
4158
|
+
if (provider === "claude") {
|
|
4159
|
+
await runClaudeLogin({ machine: machineHandle });
|
|
4160
|
+
return;
|
|
4161
|
+
}
|
|
4162
|
+
if (provider === "codex") {
|
|
4163
|
+
await runCodexLogin({ machine: machineHandle });
|
|
4164
|
+
return;
|
|
4165
|
+
}
|
|
4166
|
+
console.log(chalk9.green(`Sandbox ${chalk9.bold(machineHandle)} is ready.`));
|
|
4167
|
+
console.log();
|
|
4168
|
+
}
|
|
4169
|
+
function printNextStep(machineHandle) {
|
|
4170
|
+
console.log(chalk9.green(`Sandbox ${chalk9.bold(machineHandle)} is ready.`));
|
|
4171
|
+
console.log(chalk9.dim(` computer ssh ${machineHandle}`));
|
|
4172
|
+
console.log();
|
|
4173
|
+
}
|
|
3961
4174
|
|
|
3962
4175
|
// src/commands/logout.ts
|
|
3963
4176
|
import { Command as Command10 } from "commander";
|