Skip to content

Commit c79ac4d

Browse files
StiensWoutcodex
andcommitted
fix: mark desktop shell env probes
Co-authored-by: Codex <codex@openai.com>
1 parent 4abf8b4 commit c79ac4d

2 files changed

Lines changed: 104 additions & 1 deletion

File tree

apps/desktop/src/shell/DesktopShellEnvironment.test.ts

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,11 @@ function makeProcess(output: string): ChildProcessSpawner.ChildProcessHandle {
4545
});
4646
}
4747

48+
function standardCommand(command: ChildProcess.Command): ChildProcess.StandardCommand {
49+
assert.equal(command._tag, "StandardCommand");
50+
return command as ChildProcess.StandardCommand;
51+
}
52+
4853
function withProcessEnv<A, E, R>(
4954
env: NodeJS.ProcessEnv,
5055
effect: Effect.Effect<A, E, R>,
@@ -173,6 +178,60 @@ describe("DesktopShellEnvironment", () => {
173178
}),
174179
);
175180

181+
it.effect("marks POSIX login shell probes and supplies TERM=dumb when TERM is absent", () =>
182+
Effect.gen(function* () {
183+
const env: NodeJS.ProcessEnv = {
184+
SHELL: "/bin/zsh",
185+
PATH: "/usr/bin",
186+
};
187+
const commands: ChildProcess.Command[] = [];
188+
189+
yield* runShellEnvironment({
190+
env,
191+
platform: "linux",
192+
handler: (command) => {
193+
commands.push(command);
194+
return envOutput({ PATH: "/usr/local/bin:/usr/bin" });
195+
},
196+
});
197+
198+
const command = standardCommand(commands[0] as ChildProcess.Command);
199+
assert.deepInclude(command.args, "-ilc");
200+
assert.equal(command.options.extendEnv, true);
201+
assert.deepEqual(command.options.env, {
202+
T3CODE_RESOLVING_ENVIRONMENT: "1",
203+
TERM: "dumb",
204+
});
205+
}),
206+
);
207+
208+
it.effect("preserves inherited TERM for POSIX login shell probes", () =>
209+
Effect.gen(function* () {
210+
const env: NodeJS.ProcessEnv = {
211+
SHELL: "/bin/zsh",
212+
PATH: "/usr/bin",
213+
TERM: "xterm-256color",
214+
};
215+
const commands: ChildProcess.Command[] = [];
216+
217+
yield* runShellEnvironment({
218+
env,
219+
platform: "darwin",
220+
handler: (command) => {
221+
commands.push(command);
222+
return envOutput({ PATH: "/opt/homebrew/bin:/usr/bin" });
223+
},
224+
});
225+
226+
const command = standardCommand(commands[0] as ChildProcess.Command);
227+
assert.equal(command.options.extendEnv, true);
228+
assert.deepEqual(command.options.env, {
229+
T3CODE_RESOLVING_ENVIRONMENT: "1",
230+
TERM: "xterm-256color",
231+
});
232+
}),
233+
);
234+
176235
it.effect("falls back to launchctl PATH on macOS when shell probing does not return one", () =>
177236
Effect.gen(function* () {
178237
const env: NodeJS.ProcessEnv = {
@@ -243,6 +302,31 @@ describe("DesktopShellEnvironment", () => {
243302
}),
244303
);
245304

305+
it.effect("does not add POSIX probe env to Windows PowerShell probes", () =>
306+
Effect.gen(function* () {
307+
const env: NodeJS.ProcessEnv = {
308+
PATH: "C:\\Windows\\System32",
309+
};
310+
const commands: ChildProcess.Command[] = [];
311+
312+
yield* runShellEnvironment({
313+
env,
314+
platform: "win32",
315+
handler: (command) => {
316+
commands.push(command);
317+
return envOutput({ PATH: "C:\\Windows\\System32" });
318+
},
319+
});
320+
321+
assert.isAtLeast(commands.length, 1);
322+
for (const command of commands) {
323+
const standard = standardCommand(command);
324+
assert.isUndefined(standard.options.env);
325+
assert.isUndefined(standard.options.extendEnv);
326+
}
327+
}),
328+
);
329+
246330
it.effect("logs command failures with safe probe context and the exact cause", () => {
247331
const env: NodeJS.ProcessEnv = {
248332
SHELL: "/bin/bash",

apps/desktop/src/shell/DesktopShellEnvironment.ts

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ const WINDOWS_SHELL_CANDIDATES = ["pwsh.exe", "powershell.exe"] as const;
8080
const LOGIN_SHELL_TIMEOUT = Duration.seconds(5);
8181
const LAUNCHCTL_TIMEOUT = Duration.seconds(2);
8282
const PROCESS_TERMINATE_GRACE = Duration.seconds(1);
83+
const ENVIRONMENT_RESOLUTION_MARKER = "T3CODE_RESOLVING_ENVIRONMENT";
8384

8485
const trimNonEmpty = (value: string | null | undefined): Option.Option<string> =>
8586
Option.fromNullishOr(value).pipe(
@@ -92,6 +93,13 @@ const pathDelimiter = (platform: NodeJS.Platform) => (platform === "win32" ? ";"
9293
const readEnvPath = (env: NodeJS.ProcessEnv): Option.Option<string> =>
9394
trimNonEmpty(env.PATH ?? env.Path ?? env.path);
9495

96+
const loginShellProbeEnv = (
97+
env: NodeJS.ProcessEnv,
98+
): Readonly<Record<string, string | undefined>> => ({
99+
[ENVIRONMENT_RESOLUTION_MARKER]: "1",
100+
TERM: env.TERM ?? "dumb",
101+
});
102+
95103
const pathComparisonKey = (entry: string, platform: NodeJS.Platform) => {
96104
const normalized = entry.trim().replace(/^"+|"+$/g, "");
97105
return platform === "win32" ? normalized.toLowerCase() : normalized;
@@ -231,11 +239,19 @@ const runCommandOutput = Effect.fn("desktop.shellEnvironment.runCommandOutput")(
231239
readonly args: ReadonlyArray<string>;
232240
readonly timeout: Duration.Duration;
233241
readonly shell?: boolean;
242+
readonly env?: Readonly<Record<string, string | undefined>>;
243+
readonly extendEnv?: boolean;
234244
}): Effect.fn.Return<string, never, ChildProcessSpawner.ChildProcessSpawner> {
235245
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
236246
const output = yield* spawner
237247
.string(
238248
ChildProcess.make(input.command, input.args, {
249+
...(input.env === undefined
250+
? {}
251+
: {
252+
env: input.env,
253+
extendEnv: input.extendEnv ?? false,
254+
}),
239255
shell: input.shell ?? false,
240256
stdin: "ignore",
241257
stdout: "pipe",
@@ -277,13 +293,16 @@ const runCommandOutput = Effect.fn("desktop.shellEnvironment.runCommandOutput")(
277293
const readLoginShellEnvironment = (
278294
shell: string,
279295
names: ReadonlyArray<string>,
296+
env: NodeJS.ProcessEnv,
280297
): Effect.Effect<EnvironmentPatch, never, ChildProcessSpawner.ChildProcessSpawner> =>
281298
names.length === 0
282299
? Effect.succeed({})
283300
: runCommandOutput({
284301
probe: "login-shell",
285302
command: shell,
286303
args: ["-ilc", capturePosixEnvironmentCommand(names)],
304+
env: loginShellProbeEnv(env),
305+
extendEnv: true,
287306
timeout: LOGIN_SHELL_TIMEOUT,
288307
}).pipe(Effect.map((output) => extractEnvironment(output, names)));
289308

@@ -362,7 +381,7 @@ const installPosixEnvironment = Effect.fn("desktop.shellEnvironment.installPosix
362381
for (const shell of listLoginShellCandidates(config)) {
363382
Object.assign(
364383
shellEnvironment,
365-
yield* readLoginShellEnvironment(shell, LOGIN_SHELL_ENV_NAMES),
384+
yield* readLoginShellEnvironment(shell, LOGIN_SHELL_ENV_NAMES, config.env),
366385
);
367386
if (shellEnvironment.PATH) break;
368387
}

0 commit comments

Comments
 (0)