import * as HostProcess from "@t3tools/shared/HostProcess"; import { listLoginShellCandidates, LoginShellEnvironment, mergePathEntries, readEnvironmentFromLoginShell, readPathFromLaunchctl, resolveWindowsEnvironment, setWindowsPath, } from "@t3tools/shared/shell"; import * as Effect from "effect/Effect"; import * as FileSystem from "effect/FileSystem"; import * as Path from "effect/Path"; // @effect-diagnostics-next-line nodeBuiltinImport:off -- the shared login shell probe runs synchronously through execFileSync. import * as NodeChildProcess from "node:child_process"; import * as NodeOS from "node:os"; function logPathHydrationWarning(message: string, error?: unknown): void { process.stderr.write( `[server] ${message} ${error instanceof Error ? error.message : (error ?? "")}\n`, ); } // mise's record of the PATH entries it added. It must travel with the // shell's PATH, or `mise x` can rebuild PATH behind a same-named wrapper // (`exec mise x claude -- claude`) that then re-runs itself forever. const LOGIN_SHELL_ENVIRONMENT_NAMES = ["PATH", "__MISE_DIFF", "JAVA_HOME"]; const SLOW_LOGIN_SHELL_TIMEOUT_MS = 20_000; function readSlowLoginShellEnvironment(shell: string): Partial> { return readEnvironmentFromLoginShell( shell, LOGIN_SHELL_ENVIRONMENT_NAMES, (file, args, options) => NodeChildProcess.execFileSync(file, args, { ...options, timeout: SLOW_LOGIN_SHELL_TIMEOUT_MS, }), ); } function hydratePosixPath(env: NodeJS.ProcessEnv, platform: NodeJS.Platform): void { let shellEnvironment: Partial> = {}; let waitedForSlowShell = false; for (const shell of listLoginShellCandidates(platform, env.SHELL)) { try { shellEnvironment = readEnvironmentFromLoginShell(shell, LOGIN_SHELL_ENVIRONMENT_NAMES); } catch (error) { logPathHydrationWarning(`Failed to read PATH from login shell ${shell}.`, error); // A profile can be slow only while the machine boots. Wait for it once, // here: providers copy this env when they are built, so a later read // would never reach them. if (!waitedForSlowShell && (error as NodeJS.ErrnoException | null)?.code === "ETIMEDOUT") { waitedForSlowShell = true; try { shellEnvironment = readSlowLoginShellEnvironment(shell); } catch (retryError) { logPathHydrationWarning( `Failed to read PATH from slow login shell ${shell}.`, retryError, ); } } } if (shellEnvironment.PATH) break; } const shellPath = shellEnvironment.PATH; const launchctlPath = platform === "darwin" && !shellPath ? readPathFromLaunchctl() : undefined; const mergedPath = mergePathEntries(shellPath ?? launchctlPath, env.PATH, platform); if (mergedPath) { env.PATH = mergedPath; } if (shellPath && shellEnvironment.__MISE_DIFF) { env.__MISE_DIFF = shellEnvironment.__MISE_DIFF; } // Android SDK tools are Java programs. A keg-only JDK (Homebrew's openjdk) // is only reachable through the JAVA_HOME the shell profile sets. if (!env.JAVA_HOME && shellEnvironment.JAVA_HOME) { env.JAVA_HOME = shellEnvironment.JAVA_HOME; } } export function hydratePosixHome( env: NodeJS.ProcessEnv, resolveHomeDir = () => NodeOS.userInfo().homedir, ): void { if ((env.HOME?.trim() ?? "").length > 0) return; const homeDir = resolveHomeDir(); if (homeDir.length > 0) { env.HOME = homeDir; } } export const fixPath = Effect.fn("fixPath")(function* (options?: { readonly shellEnvironmentPrepared?: boolean | undefined; }): Effect.fn.Return { const platform = yield* HostProcess.Platform; const env = yield* HostProcess.Environment; if (platform === "win32") { // Desktop already populated the child env; repeating PowerShell probes costs seconds. if (options?.shellEnvironmentPrepared === true) return; const repairedEnvironment = yield* resolveWindowsEnvironment(env).pipe( Effect.catchDefect((defect) => Effect.sync(() => { logPathHydrationWarning("Failed to hydrate PATH from the user environment.", defect); return {} as Partial; }), ), ); for (const [key, value] of Object.entries(repairedEnvironment)) { if (value === undefined) continue; if (key === "PATH") { setWindowsPath(env, value); } else { env[key] = value; } } return; } if (platform !== "darwin" && platform !== "linux") return; yield* Effect.sync(() => hydratePosixHome(env)).pipe( Effect.catchDefect((defect) => Effect.sync(() => { logPathHydrationWarning("Failed to hydrate HOME from the user account.", defect); }), ), ); if (options?.shellEnvironmentPrepared === true) return; yield* Effect.sync(() => hydratePosixPath(env, platform)).pipe( Effect.catchDefect((defect) => Effect.sync(() => { logPathHydrationWarning("Failed to hydrate PATH from the user environment.", defect); }), ), ); }); /** Re-reads the login shell PATH after startup; true when it added entries to PATH. */ export const rereadLoginShellPath = Effect.fn("rereadLoginShellPath")(function* () { const platform = yield* HostProcess.Platform; if (platform !== "darwin" && platform !== "linux") return false; const env = yield* HostProcess.Environment; const readEnvironment = yield* LoginShellEnvironment; for (const shell of listLoginShellCandidates(platform, env.SHELL)) { const shellEnvironment = yield* readEnvironment(shell, ["PATH", "__MISE_DIFF"]); const shellPath = shellEnvironment.PATH; if (!shellPath) continue; const current = new Set((env.PATH ?? "").split(":")); if (shellPath.split(":").every((entry) => !entry.trim() || current.has(entry.trim()))) { return false; } env.PATH = mergePathEntries(shellPath, env.PATH, platform); if (shellEnvironment.__MISE_DIFF) env.__MISE_DIFF = shellEnvironment.__MISE_DIFF; return true; } return false; }); export const expandHomePath = Effect.fn(function* (input: string) { const { join } = yield* Path.Path; const home = yield* HostProcess.HomeDirectory; if (input === "~") { return home; } if (input.startsWith("~/") || input.startsWith("~\\")) { return join(home, input.slice(2)); } return input; }); export const resolveBaseDir = Effect.fn(function* (raw: string | undefined) { const { join, resolve } = yield* Path.Path; if (!raw || raw.trim().length === 0) { return join(yield* HostProcess.HomeDirectory, ".t3"); } return resolve(yield* expandHomePath(raw.trim())); });