import * as Context from "effect/Context";import * as Duration from "effect/Duration";import * as Effect from "effect/Effect";import * as Layer from "effect/Layer";import * as Option from "effect/Option";import * as Schema from "effect/Schema";import * as ChildProcess from "effect/unstable/process/ChildProcess";import * as ChildProcessSpawner from "effect/unstable/process/ChildProcessSpawner"; import * as DesktopEnvironment from "../app/DesktopEnvironment.ts"; type EnvironmentPatch = Record<string, string>; interface ShellEnvironmentConfig { readonly env: NodeJS.ProcessEnv; readonly platform: NodeJS.Platform; readonly userShell: Option.Option<string>;} interface WindowsProbeOptions { readonly loadProfile: boolean;} const DesktopShellEnvironmentProbe = Schema.Literals([ "login-shell", "launchctl-path", "powershell-profile", "powershell-no-profile",]);type DesktopShellEnvironmentProbe = typeof DesktopShellEnvironmentProbe.Type; const desktopShellEnvironmentCommandFields = { probe: DesktopShellEnvironmentProbe, executable: Schema.String, argumentCount: Schema.Number,}; export class DesktopShellEnvironmentCommandError extends Schema.TaggedErrorClass<DesktopShellEnvironmentCommandError>()( "DesktopShellEnvironmentCommandError", { ...desktopShellEnvironmentCommandFields, cause: Schema.Defect(), },) { override get message(): string { return `Desktop shell environment ${this.probe} probe (${this.executable}) failed.`; }} export class DesktopShellEnvironmentCommandTimeoutError extends Schema.TaggedErrorClass<DesktopShellEnvironmentCommandTimeoutError>()( "DesktopShellEnvironmentCommandTimeoutError", { ...desktopShellEnvironmentCommandFields, timeoutMs: Schema.Number, },) { override get message(): string { return `Desktop shell environment ${this.probe} probe (${this.executable}) timed out after ${this.timeoutMs}ms.`; }} export class DesktopShellEnvironment extends Context.Service< DesktopShellEnvironment, { readonly installIntoProcess: Effect.Effect<void>; }>()("@t3tools/desktop/shell/DesktopShellEnvironment") {} const LOGIN_SHELL_ENV_NAMES = [ "PATH", "SSH_AUTH_SOCK", "HOMEBREW_PREFIX", "HOMEBREW_CELLAR", "HOMEBREW_REPOSITORY", "XDG_CONFIG_HOME", "XDG_DATA_HOME",] as const;const WINDOWS_PROFILE_ENV_NAMES = ["PATH", "FNM_DIR", "FNM_MULTISHELL_PATH"] as const;const WINDOWS_SHELL_CANDIDATES = ["pwsh.exe", "powershell.exe"] as const;const LOGIN_SHELL_TIMEOUT = Duration.seconds(5);const LAUNCHCTL_TIMEOUT = Duration.seconds(2);const PROCESS_TERMINATE_GRACE = Duration.seconds(1); const trimNonEmpty = (value: string | null | undefined): Option.Option<string> => Option.fromNullishOr(value).pipe( Option.map((entry) => entry.trim()), Option.filter((entry) => entry.length > 0), ); const pathDelimiter = (platform: NodeJS.Platform) => (platform === "win32" ? ";" : ":"); const readEnvPath = (env: NodeJS.ProcessEnv): Option.Option<string> => trimNonEmpty(env.PATH ?? env.Path ?? env.path); const pathComparisonKey = (entry: string, platform: NodeJS.Platform) => { const normalized = entry.trim().replace(/^"+|"+$/g, ""); return platform === "win32" ? normalized.toLowerCase() : normalized;}; const mergePaths = ( platform: NodeJS.Platform, values: ReadonlyArray<Option.Option<string>>,): Option.Option<string> => { const delimiter = pathDelimiter(platform); const entries: string[] = []; const seen = new Set<string>(); for (const value of values) { if (Option.isNone(value)) continue; for (const entry of value.value.split(delimiter)) { const trimmed = entry.trim(); if (trimmed.length === 0) continue; const key = pathComparisonKey(trimmed, platform); if (key.length === 0 || seen.has(key)) continue; seen.add(key); entries.push(trimmed); } } return entries.length > 0 ? Option.some(entries.join(delimiter)) : Option.none();}; const listLoginShellCandidates = (config: ShellEnvironmentConfig): ReadonlyArray<string> => { const fallback = config.platform === "darwin" ? "/bin/zsh" : config.platform === "linux" ? "/bin/bash" : ""; const seen = new Set<string>(); const candidates: string[] = []; for (const candidate of [ trimNonEmpty(config.env.SHELL), config.userShell, trimNonEmpty(fallback), ]) { if (Option.isNone(candidate) || seen.has(candidate.value)) continue; seen.add(candidate.value); candidates.push(candidate.value); } return candidates;}; const knownWindowsCliDirs = (env: NodeJS.ProcessEnv): ReadonlyArray<string> => [ ...trimNonEmpty(env.APPDATA).pipe( Option.match({ onNone: () => [], onSome: (value) => [`${value}\\npm`], }), ), ...trimNonEmpty(env.LOCALAPPDATA).pipe( Option.match({ onNone: () => [], onSome: (value) => [`${value}\\Programs\\nodejs`, `${value}\\Volta\\bin`, `${value}\\pnpm`], }), ), ...trimNonEmpty(env.USERPROFILE).pipe( Option.match({ onNone: () => [], onSome: (value) => [`${value}\\.bun\\bin`, `${value}\\scoop\\shims`], }), ),]; const startMarker = (name: string) => `__T3CODE_ENV_${name}_START__`;const endMarker = (name: string) => `__T3CODE_ENV_${name}_END__`; const executableName = (command: string): string => command.split(/[\\/]/u).at(-1) ?? command; const logShellEnvironmentCommandError = ( error: DesktopShellEnvironmentCommandError | DesktopShellEnvironmentCommandTimeoutError,) => Effect.logWarning(error).pipe( Effect.annotateLogs({ component: "desktop-shell-environment", error, }), ); const capturePosixEnvironmentCommand = (names: ReadonlyArray<string>) => names .map((name) => { return [ `printf '%s\\n' '${startMarker(name)}'`, `printenv ${name} || true`, `printf '%s\\n' '${endMarker(name)}'`, ].join("; "); }) .join("; "); const captureWindowsEnvironmentCommand = (names: ReadonlyArray<string>) => [ "$ErrorActionPreference = 'Stop'", ...names.flatMap((name) => { return [ `Write-Output '${startMarker(name)}'`, `$value = [Environment]::GetEnvironmentVariable('${name}')`, "if ($null -ne $value -and $value.Length -gt 0) { Write-Output $value }", `Write-Output '${endMarker(name)}'`, ]; }), ].join("; "); const extractEnvironment = (output: string, names: ReadonlyArray<string>): EnvironmentPatch => { const environment: EnvironmentPatch = {}; for (const name of names) { const start = output.indexOf(startMarker(name)); if (start === -1) continue; const valueStart = start + startMarker(name).length; const end = output.indexOf(endMarker(name), valueStart); if (end === -1) continue; const value = output .slice(valueStart, end) .replace(/^\r?\n/, "") .replace(/\r?\n$/, ""); if (value.length > 0) { environment[name] = value; } } return environment;}; const runCommandOutput = Effect.fn("desktop.shellEnvironment.runCommandOutput")(function* (input: { readonly probe: DesktopShellEnvironmentProbe; readonly command: string; readonly args: ReadonlyArray<string>; readonly timeout: Duration.Duration; readonly shell?: boolean;}): Effect.fn.Return<string, never, ChildProcessSpawner.ChildProcessSpawner> { const spawner = yield* ChildProcessSpawner.ChildProcessSpawner; const output = yield* spawner .string( ChildProcess.make(input.command, input.args, { shell: input.shell ?? false, stdin: "ignore", stdout: "pipe", stderr: "pipe", killSignal: "SIGTERM", forceKillAfter: PROCESS_TERMINATE_GRACE, }), ) .pipe( Effect.mapError( (cause) => new DesktopShellEnvironmentCommandError({ probe: input.probe, executable: executableName(input.command), argumentCount: input.args.length, cause, }), ), Effect.catchTags({ DesktopShellEnvironmentCommandError: (error) => logShellEnvironmentCommandError(error).pipe(Effect.as("")), }), Effect.timeoutOption(input.timeout), ); if (Option.isSome(output)) { return output.value; } const error = new DesktopShellEnvironmentCommandTimeoutError({ probe: input.probe, executable: executableName(input.command), argumentCount: input.args.length, timeoutMs: Duration.toMillis(input.timeout), }); yield* logShellEnvironmentCommandError(error); return "";}); const readLoginShellEnvironment = ( shell: string, names: ReadonlyArray<string>,): Effect.Effect<EnvironmentPatch, never, ChildProcessSpawner.ChildProcessSpawner> => names.length === 0 ? Effect.succeed({}) : runCommandOutput({ probe: "login-shell", command: shell, args: ["-ilc", capturePosixEnvironmentCommand(names)], timeout: LOGIN_SHELL_TIMEOUT, }).pipe(Effect.map((output) => extractEnvironment(output, names))); const readLaunchctlPath = runCommandOutput({ probe: "launchctl-path", command: "/bin/launchctl", args: ["getenv", "PATH"], timeout: LAUNCHCTL_TIMEOUT,}).pipe(Effect.map(trimNonEmpty)); const readWindowsEnvironment = Effect.fn("desktop.shellEnvironment.readWindowsEnvironment")( function* ( names: ReadonlyArray<string>, options: WindowsProbeOptions, ): Effect.fn.Return<EnvironmentPatch, never, ChildProcessSpawner.ChildProcessSpawner> { if (names.length === 0) return {}; const args = [ "-NoLogo", ...(options.loadProfile ? ([] as const) : (["-NoProfile"] as const)), "-NonInteractive", "-Command", captureWindowsEnvironmentCommand(names), ]; for (const command of WINDOWS_SHELL_CANDIDATES) { const output = yield* runCommandOutput({ probe: options.loadProfile ? "powershell-profile" : "powershell-no-profile", command, args, timeout: LOGIN_SHELL_TIMEOUT, }); const environment = extractEnvironment(output, names); if (Object.keys(environment).length > 0) { return environment; } } return {}; },); const installWindowsEnvironment = Effect.fn("desktop.shellEnvironment.installWindowsEnvironment")( function* ( config: ShellEnvironmentConfig, ): Effect.fn.Return<void, never, ChildProcessSpawner.ChildProcessSpawner> { const noProfile = yield* readWindowsEnvironment(["PATH"], { loadProfile: false }); const profile = yield* readWindowsEnvironment(WINDOWS_PROFILE_ENV_NAMES, { loadProfile: true, }); const mergedPath = mergePaths("win32", [ trimNonEmpty(profile.PATH), trimNonEmpty(knownWindowsCliDirs(config.env).join(";")), trimNonEmpty(noProfile.PATH), readEnvPath(config.env), ]); if (Option.isSome(mergedPath)) { config.env.PATH = mergedPath.value; } if (!config.env.FNM_DIR && profile.FNM_DIR) { config.env.FNM_DIR = profile.FNM_DIR; } if (!config.env.FNM_MULTISHELL_PATH && profile.FNM_MULTISHELL_PATH) { config.env.FNM_MULTISHELL_PATH = profile.FNM_MULTISHELL_PATH; } },); const installPosixEnvironment = Effect.fn("desktop.shellEnvironment.installPosixEnvironment")( function* ( config: ShellEnvironmentConfig, ): Effect.fn.Return<void, never, ChildProcessSpawner.ChildProcessSpawner> { const shellEnvironment: EnvironmentPatch = {}; for (const shell of listLoginShellCandidates(config)) { Object.assign( shellEnvironment, yield* readLoginShellEnvironment(shell, LOGIN_SHELL_ENV_NAMES), ); if (shellEnvironment.PATH) break; } const launchctlPath = config.platform === "darwin" && !shellEnvironment.PATH ? yield* readLaunchctlPath : Option.none<string>(); const mergedPath = mergePaths(config.platform, [ trimNonEmpty(shellEnvironment.PATH).pipe(Option.orElse(() => launchctlPath)), readEnvPath(config.env), ]); if (Option.isSome(mergedPath)) { config.env.PATH = mergedPath.value; } if (!config.env.SSH_AUTH_SOCK && shellEnvironment.SSH_AUTH_SOCK) { config.env.SSH_AUTH_SOCK = shellEnvironment.SSH_AUTH_SOCK; } for (const name of [ "HOMEBREW_PREFIX", "HOMEBREW_CELLAR", "HOMEBREW_REPOSITORY", "XDG_CONFIG_HOME", "XDG_DATA_HOME", ] as const) { if (!config.env[name] && shellEnvironment[name]) { config.env[name] = shellEnvironment[name]; } } },); const installShellEnvironment = ( config: ShellEnvironmentConfig,): Effect.Effect<void, never, ChildProcessSpawner.ChildProcessSpawner> => { if (config.platform === "win32") { return installWindowsEnvironment(config); } if (config.platform === "darwin" || config.platform === "linux") { return installPosixEnvironment(config); } return Effect.void;}; export const make = Effect.gen(function* () { const environment = yield* DesktopEnvironment.DesktopEnvironment; const spawner = yield* ChildProcessSpawner.ChildProcessSpawner; const installIntoProcess: DesktopShellEnvironment["Service"]["installIntoProcess"] = installShellEnvironment({ env: process.env, platform: environment.platform, userShell: Option.none(), }).pipe( Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, spawner), Effect.withSpan("desktop.shellEnvironment.installIntoProcess"), ); return DesktopShellEnvironment.of({ installIntoProcess });}); export const layer = Layer.effect(DesktopShellEnvironment, make);
import * as Context from "effect/Context";
import * as Duration from "effect/Duration";
import * as Effect from "effect/Effect";
import * as Layer from "effect/Layer";
import * as Option from "effect/Option";
import * as Schema from "effect/Schema";
import * as ChildProcess from "effect/unstable/process/ChildProcess";
import * as ChildProcessSpawner from "effect/unstable/process/ChildProcessSpawner";
import * as DesktopEnvironment from "../app/DesktopEnvironment.ts";
type EnvironmentPatch = Record<string, string>;
interface ShellEnvironmentConfig {
readonly env: NodeJS.ProcessEnv;
readonly platform: NodeJS.Platform;
readonly userShell: Option.Option<string>;
}
interface WindowsProbeOptions {
readonly loadProfile: boolean;
}
const DesktopShellEnvironmentProbe = Schema.Literals([
"login-shell",
"launchctl-path",
"powershell-profile",
"powershell-no-profile",
]);
type DesktopShellEnvironmentProbe = typeof DesktopShellEnvironmentProbe.Type;
const desktopShellEnvironmentCommandFields = {
probe: DesktopShellEnvironmentProbe,
executable: Schema.String,
argumentCount: Schema.Number,
};
export class DesktopShellEnvironmentCommandError extends Schema.TaggedErrorClass<DesktopShellEnvironmentCommandError>()(
"DesktopShellEnvironmentCommandError",
{
...desktopShellEnvironmentCommandFields,
cause: Schema.Defect(),
},
) {
override get message(): string {
return `Desktop shell environment ${this.probe} probe (${this.executable}) failed.`;
}
}
export class DesktopShellEnvironmentCommandTimeoutError extends Schema.TaggedErrorClass<DesktopShellEnvironmentCommandTimeoutError>()(
"DesktopShellEnvironmentCommandTimeoutError",
{
...desktopShellEnvironmentCommandFields,
timeoutMs: Schema.Number,
},
) {
override get message(): string {
return `Desktop shell environment ${this.probe} probe (${this.executable}) timed out after ${this.timeoutMs}ms.`;
}
}
export class DesktopShellEnvironment extends Context.Service<
DesktopShellEnvironment,
{
readonly installIntoProcess: Effect.Effect<void>;
}
>()("@t3tools/desktop/shell/DesktopShellEnvironment") {}
const LOGIN_SHELL_ENV_NAMES = [
"PATH",
"SSH_AUTH_SOCK",
"HOMEBREW_PREFIX",
"HOMEBREW_CELLAR",
"HOMEBREW_REPOSITORY",
"XDG_CONFIG_HOME",
"XDG_DATA_HOME",
] as const;
const WINDOWS_PROFILE_ENV_NAMES = ["PATH", "FNM_DIR", "FNM_MULTISHELL_PATH"] as const;
const WINDOWS_SHELL_CANDIDATES = ["pwsh.exe", "powershell.exe"] as const;
const LOGIN_SHELL_TIMEOUT = Duration.seconds(5);
const LAUNCHCTL_TIMEOUT = Duration.seconds(2);
const PROCESS_TERMINATE_GRACE = Duration.seconds(1);
const trimNonEmpty = (value: string | null | undefined): Option.Option<string> =>
Option.fromNullishOr(value).pipe(
Option.map((entry) => entry.trim()),
Option.filter((entry) => entry.length > 0),
);
const pathDelimiter = (platform: NodeJS.Platform) => (platform === "win32" ? ";" : ":");
const readEnvPath = (env: NodeJS.ProcessEnv): Option.Option<string> =>
trimNonEmpty(env.PATH ?? env.Path ?? env.path);
const pathComparisonKey = (entry: string, platform: NodeJS.Platform) => {
const normalized = entry.trim().replace(/^"+|"+$/g, "");
return platform === "win32" ? normalized.toLowerCase() : normalized;
};
const mergePaths = (
platform: NodeJS.Platform,
values: ReadonlyArray<Option.Option<string>>,
): Option.Option<string> => {
const delimiter = pathDelimiter(platform);
const entries: string[] = [];
const seen = new Set<string>();
for (const value of values) {
if (Option.isNone(value)) continue;
for (const entry of value.value.split(delimiter)) {
const trimmed = entry.trim();
if (trimmed.length === 0) continue;
const key = pathComparisonKey(trimmed, platform);
if (key.length === 0 || seen.has(key)) continue;
seen.add(key);
entries.push(trimmed);
}
}
return entries.length > 0 ? Option.some(entries.join(delimiter)) : Option.none();
};
const listLoginShellCandidates = (config: ShellEnvironmentConfig): ReadonlyArray<string> => {
const fallback =
config.platform === "darwin" ? "/bin/zsh" : config.platform === "linux" ? "/bin/bash" : "";
const seen = new Set<string>();
const candidates: string[] = [];
for (const candidate of [
trimNonEmpty(config.env.SHELL),
config.userShell,
trimNonEmpty(fallback),
]) {
if (Option.isNone(candidate) || seen.has(candidate.value)) continue;
seen.add(candidate.value);
candidates.push(candidate.value);
}
return candidates;
};
const knownWindowsCliDirs = (env: NodeJS.ProcessEnv): ReadonlyArray<string> => [
...trimNonEmpty(env.APPDATA).pipe(
Option.match({
onNone: () => [],
onSome: (value) => [`${value}\\npm`],
}),
),
...trimNonEmpty(env.LOCALAPPDATA).pipe(
Option.match({
onNone: () => [],
onSome: (value) => [`${value}\\Programs\\nodejs`, `${value}\\Volta\\bin`, `${value}\\pnpm`],
}),
),
...trimNonEmpty(env.USERPROFILE).pipe(
Option.match({
onNone: () => [],
onSome: (value) => [`${value}\\.bun\\bin`, `${value}\\scoop\\shims`],
}),
),
];
const startMarker = (name: string) => `__T3CODE_ENV_${name}_START__`;
const endMarker = (name: string) => `__T3CODE_ENV_${name}_END__`;
const executableName = (command: string): string => command.split(/[\\/]/u).at(-1) ?? command;
const logShellEnvironmentCommandError = (
error: DesktopShellEnvironmentCommandError | DesktopShellEnvironmentCommandTimeoutError,
) =>
Effect.logWarning(error).pipe(
Effect.annotateLogs({
component: "desktop-shell-environment",
error,
}),
);
const capturePosixEnvironmentCommand = (names: ReadonlyArray<string>) =>
names
.map((name) => {
return [
`printf '%s\\n' '${startMarker(name)}'`,
`printenv ${name} || true`,
`printf '%s\\n' '${endMarker(name)}'`,
].join("; ");
})
.join("; ");
const captureWindowsEnvironmentCommand = (names: ReadonlyArray<string>) =>
[
"$ErrorActionPreference = 'Stop'",
...names.flatMap((name) => {
return [
`Write-Output '${startMarker(name)}'`,
`$value = [Environment]::GetEnvironmentVariable('${name}')`,
"if ($null -ne $value -and $value.Length -gt 0) { Write-Output $value }",
`Write-Output '${endMarker(name)}'`,
];
}),
].join("; ");
const extractEnvironment = (output: string, names: ReadonlyArray<string>): EnvironmentPatch => {
const environment: EnvironmentPatch = {};
for (const name of names) {
const start = output.indexOf(startMarker(name));
if (start === -1) continue;
const valueStart = start + startMarker(name).length;
const end = output.indexOf(endMarker(name), valueStart);
if (end === -1) continue;
const value = output
.slice(valueStart, end)
.replace(/^\r?\n/, "")
.replace(/\r?\n$/, "");
if (value.length > 0) {
environment[name] = value;
}
}
return environment;
};
const runCommandOutput = Effect.fn("desktop.shellEnvironment.runCommandOutput")(function* (input: {
readonly probe: DesktopShellEnvironmentProbe;
readonly command: string;
readonly args: ReadonlyArray<string>;
readonly timeout: Duration.Duration;
readonly shell?: boolean;
}): Effect.fn.Return<string, never, ChildProcessSpawner.ChildProcessSpawner> {
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
const output = yield* spawner
.string(
ChildProcess.make(input.command, input.args, {
shell: input.shell ?? false,
stdin: "ignore",
stdout: "pipe",
stderr: "pipe",
killSignal: "SIGTERM",
forceKillAfter: PROCESS_TERMINATE_GRACE,
}),
)
.pipe(
Effect.mapError(
(cause) =>
new DesktopShellEnvironmentCommandError({
probe: input.probe,
executable: executableName(input.command),
argumentCount: input.args.length,
cause,
}),
),
Effect.catchTags({
DesktopShellEnvironmentCommandError: (error) =>
logShellEnvironmentCommandError(error).pipe(Effect.as("")),
}),
Effect.timeoutOption(input.timeout),
);
if (Option.isSome(output)) {
return output.value;
}
const error = new DesktopShellEnvironmentCommandTimeoutError({
probe: input.probe,
executable: executableName(input.command),
argumentCount: input.args.length,
timeoutMs: Duration.toMillis(input.timeout),
});
yield* logShellEnvironmentCommandError(error);
return "";
});
const readLoginShellEnvironment = (
shell: string,
names: ReadonlyArray<string>,
): Effect.Effect<EnvironmentPatch, never, ChildProcessSpawner.ChildProcessSpawner> =>
names.length === 0
? Effect.succeed({})
: runCommandOutput({
probe: "login-shell",
command: shell,
args: ["-ilc", capturePosixEnvironmentCommand(names)],
timeout: LOGIN_SHELL_TIMEOUT,
}).pipe(Effect.map((output) => extractEnvironment(output, names)));
const readLaunchctlPath = runCommandOutput({
probe: "launchctl-path",
command: "/bin/launchctl",
args: ["getenv", "PATH"],
timeout: LAUNCHCTL_TIMEOUT,
}).pipe(Effect.map(trimNonEmpty));
const readWindowsEnvironment = Effect.fn("desktop.shellEnvironment.readWindowsEnvironment")(
function* (
names: ReadonlyArray<string>,
options: WindowsProbeOptions,
): Effect.fn.Return<EnvironmentPatch, never, ChildProcessSpawner.ChildProcessSpawner> {
if (names.length === 0) return {};
const args = [
"-NoLogo",
...(options.loadProfile ? ([] as const) : (["-NoProfile"] as const)),
"-NonInteractive",
"-Command",
captureWindowsEnvironmentCommand(names),
];
for (const command of WINDOWS_SHELL_CANDIDATES) {
const output = yield* runCommandOutput({
probe: options.loadProfile ? "powershell-profile" : "powershell-no-profile",
command,
args,
timeout: LOGIN_SHELL_TIMEOUT,
});
const environment = extractEnvironment(output, names);
if (Object.keys(environment).length > 0) {
return environment;
}
}
return {};
},
);
const installWindowsEnvironment = Effect.fn("desktop.shellEnvironment.installWindowsEnvironment")(
function* (
config: ShellEnvironmentConfig,
): Effect.fn.Return<void, never, ChildProcessSpawner.ChildProcessSpawner> {
const noProfile = yield* readWindowsEnvironment(["PATH"], { loadProfile: false });
const profile = yield* readWindowsEnvironment(WINDOWS_PROFILE_ENV_NAMES, {
loadProfile: true,
});
const mergedPath = mergePaths("win32", [
trimNonEmpty(profile.PATH),
trimNonEmpty(knownWindowsCliDirs(config.env).join(";")),
trimNonEmpty(noProfile.PATH),
readEnvPath(config.env),
]);
if (Option.isSome(mergedPath)) {
config.env.PATH = mergedPath.value;
}
if (!config.env.FNM_DIR && profile.FNM_DIR) {
config.env.FNM_DIR = profile.FNM_DIR;
}
if (!config.env.FNM_MULTISHELL_PATH && profile.FNM_MULTISHELL_PATH) {
config.env.FNM_MULTISHELL_PATH = profile.FNM_MULTISHELL_PATH;
}
},
);
const installPosixEnvironment = Effect.fn("desktop.shellEnvironment.installPosixEnvironment")(
function* (
config: ShellEnvironmentConfig,
): Effect.fn.Return<void, never, ChildProcessSpawner.ChildProcessSpawner> {
const shellEnvironment: EnvironmentPatch = {};
for (const shell of listLoginShellCandidates(config)) {
Object.assign(
shellEnvironment,
yield* readLoginShellEnvironment(shell, LOGIN_SHELL_ENV_NAMES),
);
if (shellEnvironment.PATH) break;
}
const launchctlPath =
config.platform === "darwin" && !shellEnvironment.PATH
? yield* readLaunchctlPath
: Option.none<string>();
const mergedPath = mergePaths(config.platform, [
trimNonEmpty(shellEnvironment.PATH).pipe(Option.orElse(() => launchctlPath)),
readEnvPath(config.env),
]);
if (Option.isSome(mergedPath)) {
config.env.PATH = mergedPath.value;
}
if (!config.env.SSH_AUTH_SOCK && shellEnvironment.SSH_AUTH_SOCK) {
config.env.SSH_AUTH_SOCK = shellEnvironment.SSH_AUTH_SOCK;
}
for (const name of [
"HOMEBREW_PREFIX",
"HOMEBREW_CELLAR",
"HOMEBREW_REPOSITORY",
"XDG_CONFIG_HOME",
"XDG_DATA_HOME",
] as const) {
if (!config.env[name] && shellEnvironment[name]) {
config.env[name] = shellEnvironment[name];
}
}
},
);
const installShellEnvironment = (
config: ShellEnvironmentConfig,
): Effect.Effect<void, never, ChildProcessSpawner.ChildProcessSpawner> => {
if (config.platform === "win32") {
return installWindowsEnvironment(config);
}
if (config.platform === "darwin" || config.platform === "linux") {
return installPosixEnvironment(config);
}
return Effect.void;
};
export const make = Effect.gen(function* () {
const environment = yield* DesktopEnvironment.DesktopEnvironment;
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
const installIntoProcess: DesktopShellEnvironment["Service"]["installIntoProcess"] =
installShellEnvironment({
env: process.env,
platform: environment.platform,
userShell: Option.none(),
}).pipe(
Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, spawner),
Effect.withSpan("desktop.shellEnvironment.installIntoProcess"),
);
return DesktopShellEnvironment.of({ installIntoProcess });
});
export const layer = Layer.effect(DesktopShellEnvironment, make);