| 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,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 | it.effect("falls back to launchctl PATH on macOS when shell probing does not return one", () => | 391 | it.effect("falls back to launchctl PATH on macOS when shell probing does not return one", () => |
| 371 | Effect.gen(function* () { | 392 | Effect.gen(function* () { |
| 372 | const env: NodeJS.ProcessEnv = { | 393 | const env: NodeJS.ProcessEnv = { |
apps/desktop/src/shell/DesktopShellEnvironment.ts+5| ... | @@ -87,6 +87,7 @@ const LOGIN_SHELL_ENV_NAMES = [ | ... | @@ -87,6 +87,7 @@ const LOGIN_SHELL_ENV_NAMES = [ |
| 87 | "XDG_SESSION_TYPE", | 87 | "XDG_SESSION_TYPE", |
| 88 | "WAYLAND_DISPLAY", | 88 | "WAYLAND_DISPLAY", |
| 89 | "T3CODE_TELEMETRY_ENABLED", | 89 | "T3CODE_TELEMETRY_ENABLED", |
| 90 | "__MISE_DIFF", | ||
| 90 | ] as const; | 91 | ] as const; |
| 91 | const WINDOWS_PROFILE_ENV_NAMES = ["PATH", "FNM_DIR", "FNM_MULTISHELL_PATH"] as const; | 92 | const WINDOWS_PROFILE_ENV_NAMES = ["PATH", "FNM_DIR", "FNM_MULTISHELL_PATH"] as const; |
| 92 | const LOCALE_ENV_NAMES = ["LANG", "LC_ALL", "LC_CTYPE"] as const; | 93 | const LOCALE_ENV_NAMES = ["LANG", "LC_ALL", "LC_CTYPE"] as const; |
| ... | @@ -433,6 +434,10 @@ const installPosixEnvironment = Effect.fn("desktop.shellEnvironment.installPosix | ... | @@ -433,6 +434,10 @@ const installPosixEnvironment = Effect.fn("desktop.shellEnvironment.installPosix |
| 433 | "XDG_CURRENT_DESKTOP", | 434 | "XDG_CURRENT_DESKTOP", |
| 434 | "XDG_SESSION_DESKTOP", | 435 | "XDG_SESSION_DESKTOP", |
| 435 | "XDG_SESSION_TYPE", | 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 | ] as const; | 441 | ] as const; |
| 437 | for (const name of shellPreferredEnvNames) { | 442 | for (const name of shellPreferredEnvNames) { |
| 438 | if (shellEnvironment[name]) { | 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,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 | effectIt.effect("repairs Windows PATH even when the desktop marks it prepared", () => | 133 | effectIt.effect("repairs Windows PATH even when the desktop marks it prepared", () => |
| 102 | Effect.gen(function* () { | 134 | Effect.gen(function* () { |
| 103 | const env: NodeJS.ProcessEnv = { PATH: "C:\\Windows" }; | 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,7 +2,7 @@ import * as HostProcess from "@t3tools/shared/HostProcess"; |
| 2 | import { | 2 | import { |
| 3 | listLoginShellCandidates, | 3 | listLoginShellCandidates, |
| 4 | mergePathEntries, | 4 | mergePathEntries, |
| 5 | readPathFromLoginShell, | 5 | readEnvironmentFromLoginShell, |
| 6 | readPathFromLaunchctl, | 6 | readPathFromLaunchctl, |
| 7 | resolveWindowsEnvironment, | 7 | resolveWindowsEnvironment, |
| 8 | } from "@t3tools/shared/shell"; | 8 | } from "@t3tools/shared/shell"; |
| ... | @@ -18,22 +18,29 @@ function logPathHydrationWarning(message: string, error?: unknown): void { | ... | @@ -18,22 +18,29 @@ function logPathHydrationWarning(message: string, error?: unknown): void { |
| 18 | } | 18 | } |
| 19 | 19 | ||
| 20 | function hydratePosixPath(env: NodeJS.ProcessEnv, platform: NodeJS.Platform): void { | 20 | function hydratePosixPath(env: NodeJS.ProcessEnv, platform: NodeJS.Platform): void { |
| 21 | let shellPath: string | undefined; | 21 | let shellEnvironment: Partial<Record<string, string>> = {}; |
| 22 | for (const shell of listLoginShellCandidates(platform, env.SHELL)) { | 22 | for (const shell of listLoginShellCandidates(platform, env.SHELL)) { |
| 23 | try { | 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 | } catch (error) { | 28 | } catch (error) { |
| 26 | logPathHydrationWarning(`Failed to read PATH from login shell ${shell}.`, error); | 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 | const launchctlPath = platform === "darwin" && !shellPath ? readPathFromLaunchctl() : undefined; | 36 | const launchctlPath = platform === "darwin" && !shellPath ? readPathFromLaunchctl() : undefined; |
| 33 | const mergedPath = mergePathEntries(shellPath ?? launchctlPath, env.PATH, platform); | 37 | const mergedPath = mergePathEntries(shellPath ?? launchctlPath, env.PATH, platform); |
| 34 | if (mergedPath) { | 38 | if (mergedPath) { |
| 35 | env.PATH = mergedPath; | 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 | export function hydratePosixHome( | 46 | export function hydratePosixHome( |
packages/shared/src/shell.test.ts+4-3| ... | @@ -22,7 +22,6 @@ import { | ... | @@ -22,7 +22,6 @@ import { |
| 22 | readEnvironmentFromLoginShell, | 22 | readEnvironmentFromLoginShell, |
| 23 | readEnvironmentFromWindowsShell, | 23 | readEnvironmentFromWindowsShell, |
| 24 | readPathFromLaunchctl, | 24 | readPathFromLaunchctl, |
| 25 | readPathFromLoginShell, | ||
| 26 | resolveCommandPath, | 25 | resolveCommandPath, |
| 27 | resolveKnownWindowsCliDirs, | 26 | resolveKnownWindowsCliDirs, |
| 28 | resolveSpawnCommand, | 27 | resolveSpawnCommand, |
| ... | @@ -57,7 +56,7 @@ const recordCommandTrace = Effect.fnUntraced(function* <A, E, R>(effect: Effect. | ... | @@ -57,7 +56,7 @@ const recordCommandTrace = Effect.fnUntraced(function* <A, E, R>(effect: Effect. |
| 57 | return { result, spans }; | 56 | return { result, spans }; |
| 58 | }); | 57 | }); |
| 59 | 58 | ||
| 60 | describe("readPathFromLoginShell", () => { | 59 | describe("readEnvironmentFromLoginShell probe", () => { |
| 61 | it("uses a shell-agnostic printenv PATH probe", () => { | 60 | it("uses a shell-agnostic printenv PATH probe", () => { |
| 62 | const execFile = vi.fn< | 61 | const execFile = vi.fn< |
| 63 | ( | 62 | ( |
| ... | @@ -67,7 +66,9 @@ describe("readPathFromLoginShell", () => { | ... | @@ -67,7 +66,9 @@ describe("readPathFromLoginShell", () => { |
| 67 | ) => string | 66 | ) => string |
| 68 | >(() => "__T3CODE_ENV_PATH_START__\n/a:/b\n__T3CODE_ENV_PATH_END__\n"); | 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 | expect(execFile).toHaveBeenCalledTimes(1); | 72 | expect(execFile).toHaveBeenCalledTimes(1); |
| 72 | 73 | ||
| 73 | const firstCall = execFile.mock.calls[0] as | 74 | const firstCall = execFile.mock.calls[0] as |
packages/shared/src/shell.ts-7| ... | @@ -172,13 +172,6 @@ export function listLoginShellCandidates( | ... | @@ -172,13 +172,6 @@ export function listLoginShellCandidates( |
| 172 | return candidates; | 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 | export function readPathFromLaunchctl( | 175 | export function readPathFromLaunchctl( |
| 183 | execFile: ExecFileSyncLike = NodeChildProcess.execFileSync, | 176 | execFile: ExecFileSyncLike = NodeChildProcess.execFileSync, |
| 184 | ): string | undefined { | 177 | ): string | undefined { |