authorgravatar for me@t3.ggTheo Browne <me@t3.gg> 2026-09-06 14:59:11-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-09-06 14:59:11-07:00
logc2c4185e175daea86f8fd6336fd8839a81cc616e
tree22b1092577f069e6c76487ab1c48060074fb6936
parentea646c0834a3394ecb0be4a30c5d367e5a9002bd
signature Signed by PGP key B5690EEEBB952194

fix(web): onboarding installs agents without needing Node or npm (#10402)

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>

5 files changed, 101 insertions(+), 11 deletions(-)

apps/server/src/provider/providerMaintenance.test.ts+37
......@@ -25,6 +25,7 @@ import {
2525 parseHomebrewLatestVersion,
2626 ProviderVersionCache,
2727 resolveLatestProviderVersion,
28 resolvePackageManagedProviderMaintenance,
2829 resolveProviderMaintenanceCapabilitiesEffect,
2930 type ProviderMaintenanceCapabilities,
3031} from "./providerMaintenance.ts";
......@@ -310,6 +311,42 @@ it.layer(NodeServices.layer)("providerMaintenance", (it) => {
310311 ).toBeNull();
311312 });
312313
314 // The Codex Windows installer exposes `%LOCALAPPDATA%\\Programs\\OpenAI\\Codex\\bin`
315 // as a junction into `%CODEX_HOME%\\packages\\standalone\\current\\bin`. Node's
316 // realpath follows junctions, so the real path carries the standalone marker
317 // even though the visible path does not.
318 it.effect("recognizes a Windows standalone install through its junctioned bin dir", () =>
319 Effect.gen(function* () {
320 const visiblePath =
321 "C:\\Users\\Theo\\AppData\\Local\\Programs\\OpenAI\\Codex\\bin\\codex.exe";
322 const realPath =
323 "C:\\Users\\Theo\\.codex\\packages\\standalone\\releases\\0.120.0-x86_64\\bin\\codex.exe";
324 const capabilities = yield* resolvePackageManagedProviderMaintenance(
325 {
326 provider: driver("codex"),
327 npmPackageName: "@openai/codex",
328 nativeUpdate: {
329 args: ["update"],
330 isCommandPath: isNativeTestCommandPath("/packages/standalone/"),
331 },
332 },
333 {
334 binaryPath: "codex",
335 resolvedCommandPath: visiblePath,
336 realCommandPath: realPath,
337 env: {},
338 platform: "win32",
339 },
340 ).pipe(Effect.provideService(HostProcessPlatform, "win32"));
341
342 expect(capabilities.update).toMatchObject({
343 executable: visiblePath,
344 args: ["update"],
345 lockKey: "codex-native",
346 });
347 }),
348 );
349
313350 it.effect("proves Windows npm ownership from the package manifest beside the shim", () =>
314351 Effect.gen(function* () {
315352 const tempDir = yield* makeTempDir("t3-npm-windows-capabilities");
apps/web/src/components/onboarding/WelcomeWizard.tsx+10-10
......@@ -42,6 +42,7 @@ import {
4242} from "../../onboarding/projectImport.logic";
4343import {
4444 getOnboardingProviderState,
45 resolveOnboardingProviderInstallCommand,
4546 resolveOnboardingProviderLoginCommand,
4647 selectOnboardingProvidersByDriver,
4748} from "../../onboarding/providerReadiness.logic";
......@@ -641,11 +642,6 @@ function PairDirectStep({
641642const PRIMARY_AGENT_DRIVERS = ["claudeAgent", "codex"] as const;
642643type OnboardingAgentDriver = (typeof PRIMARY_AGENT_DRIVERS)[number];
643644
644const AGENT_INSTALL_COMMANDS: Record<OnboardingAgentDriver, string> = {
645 claudeAgent: "npm install -g @anthropic-ai/claude-code",
646 codex: "npm install -g @openai/codex",
647};
648
649645/** Setup values stay fixed while provider probes refresh the surrounding cards. */
650646interface AgentTerminalSession {
651647 readonly environmentId: EnvironmentId;
......@@ -657,10 +653,11 @@ interface AgentTerminalSession {
657653}
658654
659655/**
660 * Claude Code and Codex use live probe status. Install opens the built-in terminal inline
661 * with the command pre-typed — the update RPC can't install a binary that
662 * isn't there yet (it infers the package manager from the installed binary's
663 * path), and the terminal also handles the interactive login that follows.
656 * Claude Code and Codex use live probe status. Install opens the built-in
657 * terminal inline with the vendor's standalone installer pre-typed. The update
658 * RPC can't install a binary that isn't there yet (it infers the installer from
659 * the installed binary's path), and the terminal also handles the interactive
660 * login that follows.
664661 */
665662function AgentsStep({
666663 mode,
......@@ -761,7 +758,10 @@ function ConnectedAgentsStep({
761758 serverConfig.settings,
762759 serverConfig.environment.platform.os,
763760 )
764 : AGENT_INSTALL_COMMANDS[driver],
761 : resolveOnboardingProviderInstallCommand(
762 driver,
763 serverConfig.environment.platform.os,
764 ),
765765 keybindings: serverConfig.keybindings,
766766 });
767767 }}
apps/web/src/onboarding/providerReadiness.logic.test.ts+21
......@@ -8,6 +8,7 @@ import { describe, expect, it } from "vite-plus/test";
88
99import {
1010 getOnboardingProviderState,
11 resolveOnboardingProviderInstallCommand,
1112 resolveOnboardingProviderLoginCommand,
1213 selectOnboardingProvidersByDriver,
1314} from "./providerReadiness.logic";
......@@ -315,3 +316,23 @@ describe("resolveOnboardingProviderLoginCommand", () => {
315316 ).toBe("codex login");
316317 });
317318});
319
320describe("resolveOnboardingProviderInstallCommand", () => {
321 it("uses the PowerShell installer on Windows environments", () => {
322 expect(resolveOnboardingProviderInstallCommand("codex", "windows")).toBe(
323 "irm https://chatgpt.com/codex/install.ps1 | iex",
324 );
325 expect(resolveOnboardingProviderInstallCommand("claudeAgent", "windows")).toBe(
326 "irm https://claude.ai/install.ps1 | iex",
327 );
328 });
329
330 it.each(["darwin", "linux", "unknown"] as const)("uses the shell installer on %s", (platform) => {
331 expect(resolveOnboardingProviderInstallCommand("codex", platform)).toBe(
332 "curl -fsSL https://chatgpt.com/codex/install.sh | sh",
333 );
334 expect(resolveOnboardingProviderInstallCommand("claudeAgent", platform)).toBe(
335 "curl -fsSL https://claude.ai/install.sh | bash",
336 );
337 });
338});
apps/web/src/onboarding/providerReadiness.logic.ts+30
......@@ -71,6 +71,36 @@ export function selectOnboardingProvidersByDriver(
7171 return providersByDriver;
7272}
7373
74/**
75 * Official standalone installers. Neither needs Node or npm, and both land in
76 * the paths the server's provider maintenance recognizes as native, so the
77 * one-click updater in Settings keeps working after install.
78 */
79const NATIVE_INSTALL_COMMANDS = {
80 claudeAgent: {
81 windows: "irm https://claude.ai/install.ps1 | iex",
82 posix: "curl -fsSL https://claude.ai/install.sh | bash",
83 },
84 codex: {
85 windows: "irm https://chatgpt.com/codex/install.ps1 | iex",
86 posix: "curl -fsSL https://chatgpt.com/codex/install.sh | sh",
87 },
88} as const;
89
90/**
91 * Install command for the setup terminal, keyed on the environment's platform
92 * (not the client's): a Windows desktop driving a WSL server gets the shell
93 * script. Unknown platforms get the shell script too, since the terminal there
94 * is a POSIX shell in practice.
95 */
96export function resolveOnboardingProviderInstallCommand(
97 driver: keyof typeof NATIVE_INSTALL_COMMANDS,
98 platform: ExecutionEnvironmentPlatformOs,
99): string {
100 const commands = NATIVE_INSTALL_COMMANDS[driver];
101 return platform === "windows" ? commands.windows : commands.posix;
102}
103
74104/** Use the selected provider instance's binary when the setup terminal opens its login flow. */
75105export function resolveOnboardingProviderLoginCommand(
76106 provider: ServerProvider,
docs/user/welcome-wizard.md+3-1
......@@ -26,7 +26,9 @@ unreadable settings with defaults.
2626
2727T3 Code checks the selected computer for Claude Code and Codex. If an agent is
2828not installed or signed in, select its action to open a terminal with the
29correct command ready to run. Other providers can be enabled in Settings.
29correct command ready to run. Install uses the vendor's standalone installer,
30which does not need Node or npm and keeps **Update now** working in Settings.
31Other providers can be enabled in Settings.
3032
3133The setup terminal uses the home directory and environment configured for the
3234selected provider instance. Sensitive values remain redacted in Settings and