| author | |
| committer | |
| log | 7ee2839cb58263742301895a6be826505daa64ea |
| tree | 62c09653213bfc76a9440d42451fbe93a9b8811e |
| parent | 9a01524f3faca628699cdf65b43ad020b40b86d8 |
| signature | Signed by PGP key B5690EEEBB952194 |
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>6 files changed, 73 insertions(+), 14 deletions(-)
apps/desktop/src/shell/DesktopShellEnvironment.test.ts+21| ... | ... | @@ -367,6 +367,27 @@ describe("DesktopShellEnvironment", () => { |
| 367 | 367 | }), |
| 368 | 368 | ); |
| 369 | 369 | |
| 370 | it.effect("keeps mise's PATH record with the login shell PATH", () => | |
| 371 | Effect.gen(function* () { | |
| 372 | const env: NodeJS.ProcessEnv = { | |
| 373 | SHELL: "/bin/bash", | |
| 374 | PATH: "/usr/bin", | |
| 375 | }; | |
| 376 | ||
| 377 | yield* runShellEnvironment({ | |
| 378 | env, | |
| 379 | platform: "linux", | |
| 380 | handler: () => | |
| 381 | envOutput({ | |
| 382 | PATH: "/home/me/.local/bin:/mise/installs/claude/latest:/mise/shims", | |
| 383 | __MISE_DIFF: "mise-diff", | |
| 384 | }), | |
| 385 | }); | |
| 386 | ||
| 387 | assert.equal(env.__MISE_DIFF, "mise-diff"); | |
| 388 | }), | |
| 389 | ); | |
| 390 | ||
| 370 | 391 | it.effect("falls back to launchctl PATH on macOS when shell probing does not return one", () => |
| 371 | 392 | Effect.gen(function* () { |
| 372 | 393 | const env: NodeJS.ProcessEnv = { |
apps/desktop/src/shell/DesktopShellEnvironment.ts+5| ... | ... | @@ -87,6 +87,7 @@ const LOGIN_SHELL_ENV_NAMES = [ |
| 87 | 87 | "XDG_SESSION_TYPE", |
| 88 | 88 | "WAYLAND_DISPLAY", |
| 89 | 89 | "T3CODE_TELEMETRY_ENABLED", |
| 90 | "__MISE_DIFF", | |
| 90 | 91 | ] as const; |
| 91 | 92 | const WINDOWS_PROFILE_ENV_NAMES = ["PATH", "FNM_DIR", "FNM_MULTISHELL_PATH"] as const; |
| 92 | 93 | const LOCALE_ENV_NAMES = ["LANG", "LC_ALL", "LC_CTYPE"] as const; |
| ... | ... | @@ -433,6 +434,10 @@ const installPosixEnvironment = Effect.fn("desktop.shellEnvironment.installPosix |
| 433 | 434 | "XDG_CURRENT_DESKTOP", |
| 434 | 435 | "XDG_SESSION_DESKTOP", |
| 435 | 436 | "XDG_SESSION_TYPE", |
| 437 | // mise's record of the PATH entries it added. It must travel with the | |
| 438 | // shell's PATH, or `mise x` can rebuild PATH behind a same-named wrapper | |
| 439 | // (`exec mise x claude -- claude`) that then re-runs itself forever. | |
| 440 | "__MISE_DIFF", | |
| 436 | 441 | ] as const; |
| 437 | 442 | for (const name of shellPreferredEnvNames) { |
| 438 | 443 | if (shellEnvironment[name]) { |
apps/server/src/os-jank.test.ts+32| ... | ... | @@ -98,6 +98,38 @@ effectIt.effect("hydrates PATH when the desktop handoff is absent", () => |
| 98 | 98 | ), |
| 99 | 99 | ); |
| 100 | 100 | |
| 101 | effectIt.effect("keeps mise's PATH record with the login shell PATH", () => | |
| 102 | Effect.acquireUseRelease( | |
| 103 | Effect.sync(() => | |
| 104 | vi | |
| 105 | .spyOn(NodeChildProcess, "execFileSync") | |
| 106 | .mockReturnValue( | |
| 107 | [ | |
| 108 | "__T3CODE_ENV_PATH_START__", | |
| 109 | "/home/me/.local/bin:/mise/installs/claude/latest:/mise/shims", | |
| 110 | "__T3CODE_ENV_PATH_END__", | |
| 111 | "__T3CODE_ENV___MISE_DIFF_START__", | |
| 112 | "mise-diff", | |
| 113 | "__T3CODE_ENV___MISE_DIFF_END__", | |
| 114 | ].join("\n"), | |
| 115 | ), | |
| 116 | ), | |
| 117 | () => | |
| 118 | Effect.gen(function* () { | |
| 119 | const env: NodeJS.ProcessEnv = { SHELL: "/bin/bash", PATH: "/usr/bin" }; | |
| 120 | ||
| 121 | yield* fixPath().pipe( | |
| 122 | Effect.provideService(HostProcess.Platform, "linux"), | |
| 123 | Effect.provideService(HostProcess.Environment, env), | |
| 124 | Effect.provide(NodeServices.layer), | |
| 125 | ); | |
| 126 | ||
| 127 | assert.equal(env.__MISE_DIFF, "mise-diff"); | |
| 128 | }), | |
| 129 | (probe) => Effect.sync(() => probe.mockRestore()), | |
| 130 | ), | |
| 131 | ); | |
| 132 | ||
| 101 | 133 | effectIt.effect("repairs Windows PATH even when the desktop marks it prepared", () => |
| 102 | 134 | Effect.gen(function* () { |
| 103 | 135 | const env: NodeJS.ProcessEnv = { PATH: "C:\\Windows" }; |
apps/server/src/os-jank.ts+11-4| ... | ... | @@ -2,7 +2,7 @@ import * as HostProcess from "@t3tools/shared/HostProcess"; |
| 2 | 2 | import { |
| 3 | 3 | listLoginShellCandidates, |
| 4 | 4 | mergePathEntries, |
| 5 | readPathFromLoginShell, | |
| 5 | readEnvironmentFromLoginShell, | |
| 6 | 6 | readPathFromLaunchctl, |
| 7 | 7 | resolveWindowsEnvironment, |
| 8 | 8 | } from "@t3tools/shared/shell"; |
| ... | ... | @@ -18,22 +18,29 @@ function logPathHydrationWarning(message: string, error?: unknown): void { |
| 18 | 18 | } |
| 19 | 19 | |
| 20 | 20 | function hydratePosixPath(env: NodeJS.ProcessEnv, platform: NodeJS.Platform): void { |
| 21 | let shellPath: string | undefined; | |
| 21 | let shellEnvironment: Partial<Record<string, string>> = {}; | |
| 22 | 22 | for (const shell of listLoginShellCandidates(platform, env.SHELL)) { |
| 23 | 23 | try { |
| 24 | shellPath = readPathFromLoginShell(shell); | |
| 24 | // mise's record of the PATH entries it added. It must travel with the | |
| 25 | // shell's PATH, or `mise x` can rebuild PATH behind a same-named wrapper | |
| 26 | // (`exec mise x claude -- claude`) that then re-runs itself forever. | |
| 27 | shellEnvironment = readEnvironmentFromLoginShell(shell, ["PATH", "__MISE_DIFF"]); | |
| 25 | 28 | } catch (error) { |
| 26 | 29 | logPathHydrationWarning(`Failed to read PATH from login shell ${shell}.`, error); |
| 27 | 30 | } |
| 28 | 31 | |
| 29 | if (shellPath) break; | |
| 32 | if (shellEnvironment.PATH) break; | |
| 30 | 33 | } |
| 31 | 34 | |
| 35 | const shellPath = shellEnvironment.PATH; | |
| 32 | 36 | const launchctlPath = platform === "darwin" && !shellPath ? readPathFromLaunchctl() : undefined; |
| 33 | 37 | const mergedPath = mergePathEntries(shellPath ?? launchctlPath, env.PATH, platform); |
| 34 | 38 | if (mergedPath) { |
| 35 | 39 | env.PATH = mergedPath; |
| 36 | 40 | } |
| 41 | if (shellPath && shellEnvironment.__MISE_DIFF) { | |
| 42 | env.__MISE_DIFF = shellEnvironment.__MISE_DIFF; | |
| 43 | } | |
| 37 | 44 | } |
| 38 | 45 | |
| 39 | 46 | export function hydratePosixHome( |
packages/shared/src/shell.test.ts+4-3| ... | ... | @@ -22,7 +22,6 @@ import { |
| 22 | 22 | readEnvironmentFromLoginShell, |
| 23 | 23 | readEnvironmentFromWindowsShell, |
| 24 | 24 | readPathFromLaunchctl, |
| 25 | readPathFromLoginShell, | |
| 26 | 25 | resolveCommandPath, |
| 27 | 26 | resolveKnownWindowsCliDirs, |
| 28 | 27 | resolveSpawnCommand, |
| ... | ... | @@ -57,7 +56,7 @@ const recordCommandTrace = Effect.fnUntraced(function* <A, E, R>(effect: Effect. |
| 57 | 56 | return { result, spans }; |
| 58 | 57 | }); |
| 59 | 58 | |
| 60 | describe("readPathFromLoginShell", () => { | |
| 59 | describe("readEnvironmentFromLoginShell probe", () => { | |
| 61 | 60 | it("uses a shell-agnostic printenv PATH probe", () => { |
| 62 | 61 | const execFile = vi.fn< |
| 63 | 62 | ( |
| ... | ... | @@ -67,7 +66,9 @@ describe("readPathFromLoginShell", () => { |
| 67 | 66 | ) => string |
| 68 | 67 | >(() => "__T3CODE_ENV_PATH_START__\n/a:/b\n__T3CODE_ENV_PATH_END__\n"); |
| 69 | 68 | |
| 70 | expect(readPathFromLoginShell("/opt/homebrew/bin/fish", execFile)).toBe("/a:/b"); | |
| 69 | expect(readEnvironmentFromLoginShell("/opt/homebrew/bin/fish", ["PATH"], execFile).PATH).toBe( | |
| 70 | "/a:/b", | |
| 71 | ); | |
| 71 | 72 | expect(execFile).toHaveBeenCalledTimes(1); |
| 72 | 73 | |
| 73 | 74 | const firstCall = execFile.mock.calls[0] as |
packages/shared/src/shell.ts-7| ... | ... | @@ -172,13 +172,6 @@ export function listLoginShellCandidates( |
| 172 | 172 | return candidates; |
| 173 | 173 | } |
| 174 | 174 | |
| 175 | export function readPathFromLoginShell( | |
| 176 | shell: string, | |
| 177 | execFile: ExecFileSyncLike = NodeChildProcess.execFileSync, | |
| 178 | ): string | undefined { | |
| 179 | return readEnvironmentFromLoginShell(shell, ["PATH"], execFile).PATH; | |
| 180 | } | |
| 181 | ||
| 182 | 175 | export function readPathFromLaunchctl( |
| 183 | 176 | execFile: ExecFileSyncLike = NodeChildProcess.execFileSync, |
| 184 | 177 | ): string | undefined { |