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 {...@@ -25,6 +25,7 @@ import {
25 parseHomebrewLatestVersion,25 parseHomebrewLatestVersion,
26 ProviderVersionCache,26 ProviderVersionCache,
27 resolveLatestProviderVersion,27 resolveLatestProviderVersion,
28 resolvePackageManagedProviderMaintenance,
28 resolveProviderMaintenanceCapabilitiesEffect,29 resolveProviderMaintenanceCapabilitiesEffect,
29 type ProviderMaintenanceCapabilities,30 type ProviderMaintenanceCapabilities,
30} from "./providerMaintenance.ts";31} from "./providerMaintenance.ts";
...@@ -310,6 +311,42 @@ it.layer(NodeServices.layer)("providerMaintenance", (it) => {...@@ -310,6 +311,42 @@ it.layer(NodeServices.layer)("providerMaintenance", (it) => {
310 ).toBeNull();311 ).toBeNull();
311 });312 });
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
313 it.effect("proves Windows npm ownership from the package manifest beside the shim", () =>350 it.effect("proves Windows npm ownership from the package manifest beside the shim", () =>
314 Effect.gen(function* () {351 Effect.gen(function* () {
315 const tempDir = yield* makeTempDir("t3-npm-windows-capabilities");352 const tempDir = yield* makeTempDir("t3-npm-windows-capabilities");
apps/web/src/components/onboarding/WelcomeWizard.tsx+10-10
...@@ -42,6 +42,7 @@ import {...@@ -42,6 +42,7 @@ import {
42} from "../../onboarding/projectImport.logic";42} from "../../onboarding/projectImport.logic";
43import {43import {
44 getOnboardingProviderState,44 getOnboardingProviderState,
45 resolveOnboardingProviderInstallCommand,
45 resolveOnboardingProviderLoginCommand,46 resolveOnboardingProviderLoginCommand,
46 selectOnboardingProvidersByDriver,47 selectOnboardingProvidersByDriver,
47} from "../../onboarding/providerReadiness.logic";48} from "../../onboarding/providerReadiness.logic";
...@@ -641,11 +642,6 @@ function PairDirectStep({...@@ -641,11 +642,6 @@ function PairDirectStep({
641const PRIMARY_AGENT_DRIVERS = ["claudeAgent", "codex"] as const;642const PRIMARY_AGENT_DRIVERS = ["claudeAgent", "codex"] as const;
642type OnboardingAgentDriver = (typeof PRIMARY_AGENT_DRIVERS)[number];643type 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
649/** Setup values stay fixed while provider probes refresh the surrounding cards. */645/** Setup values stay fixed while provider probes refresh the surrounding cards. */
650interface AgentTerminalSession {646interface AgentTerminalSession {
651 readonly environmentId: EnvironmentId;647 readonly environmentId: EnvironmentId;
...@@ -657,10 +653,11 @@ interface AgentTerminalSession {...@@ -657,10 +653,11 @@ interface AgentTerminalSession {
657}653}
658654
659/**655/**
660 * Claude Code and Codex use live probe status. Install opens the built-in terminal inline656 * Claude Code and Codex use live probe status. Install opens the built-in
661 * with the command pre-typed — the update RPC can't install a binary that657 * terminal inline with the vendor's standalone installer pre-typed. The update
662 * isn't there yet (it infers the package manager from the installed binary's658 * RPC can't install a binary that isn't there yet (it infers the installer from
663 * path), and the terminal also handles the interactive login that follows.659 * the installed binary's path), and the terminal also handles the interactive
660 * login that follows.
664 */661 */
665function AgentsStep({662function AgentsStep({
666 mode,663 mode,
...@@ -761,7 +758,10 @@ function ConnectedAgentsStep({...@@ -761,7 +758,10 @@ function ConnectedAgentsStep({
761 serverConfig.settings,758 serverConfig.settings,
762 serverConfig.environment.platform.os,759 serverConfig.environment.platform.os,
763 )760 )
764 : AGENT_INSTALL_COMMANDS[driver],761 : resolveOnboardingProviderInstallCommand(
762 driver,
763 serverConfig.environment.platform.os,
764 ),
765 keybindings: serverConfig.keybindings,765 keybindings: serverConfig.keybindings,
766 });766 });
767 }}767 }}
apps/web/src/onboarding/providerReadiness.logic.test.ts+21
...@@ -8,6 +8,7 @@ import { describe, expect, it } from "vite-plus/test";...@@ -8,6 +8,7 @@ import { describe, expect, it } from "vite-plus/test";
88
9import {9import {
10 getOnboardingProviderState,10 getOnboardingProviderState,
11 resolveOnboardingProviderInstallCommand,
11 resolveOnboardingProviderLoginCommand,12 resolveOnboardingProviderLoginCommand,
12 selectOnboardingProvidersByDriver,13 selectOnboardingProvidersByDriver,
13} from "./providerReadiness.logic";14} from "./providerReadiness.logic";
...@@ -315,3 +316,23 @@ describe("resolveOnboardingProviderLoginCommand", () => {...@@ -315,3 +316,23 @@ describe("resolveOnboardingProviderLoginCommand", () => {
315 ).toBe("codex login");316 ).toBe("codex login");
316 });317 });
317});318});
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(...@@ -71,6 +71,36 @@ export function selectOnboardingProvidersByDriver(
71 return providersByDriver;71 return providersByDriver;
72}72}
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
74/** Use the selected provider instance's binary when the setup terminal opens its login flow. */104/** Use the selected provider instance's binary when the setup terminal opens its login flow. */
75export function resolveOnboardingProviderLoginCommand(105export function resolveOnboardingProviderLoginCommand(
76 provider: ServerProvider,106 provider: ServerProvider,
docs/user/welcome-wizard.md+3-1
...@@ -26,7 +26,9 @@ unreadable settings with defaults....@@ -26,7 +26,9 @@ unreadable settings with defaults.
2626
27T3 Code checks the selected computer for Claude Code and Codex. If an agent is27T3 Code checks the selected computer for Claude Code and Codex. If an agent is
28not installed or signed in, select its action to open a terminal with the28not 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
31The setup terminal uses the home directory and environment configured for the33The setup terminal uses the home directory and environment configured for the
32selected provider instance. Sensitive values remain redacted in Settings and34selected provider instance. Sensitive values remain redacted in Settings and