// @effect-diagnostics nodeBuiltinImport:offimport * as NodeOS from "node:os";import * as NodePath from "node:path";import * as NodeChildProcess from "node:child_process";import * as NodeFS from "node:fs";import * as Clock from "effect/Clock";import * as Data from "effect/Data";import * as Effect from "effect/Effect";import * as FileSystem from "effect/FileSystem";import * as Path from "effect/Path"; import { HostProcessEnvironment, HostProcessPlatform } from "./hostProcess.ts";import * as Context from "effect/Context"; const PATH_CAPTURE_START = "__T3CODE_PATH_START__";const PATH_CAPTURE_END = "__T3CODE_PATH_END__";const SHELL_ENV_NAME_PATTERN = /^[A-Z0-9_]+$/;const WINDOWS_PATH_DELIMITER = ";";const POSIX_PATH_DELIMITER = ":";const WINDOWS_SHELL_CANDIDATES = ["pwsh.exe", "powershell.exe"] as const; type ExecFileSyncLike = ( file: string, args: ReadonlyArray<string>, options: { encoding: "utf8"; timeout: number },) => string; function canExecuteFile(filePath: string): boolean { try { NodeFS.accessSync(filePath, NodeFS.constants.X_OK); return true; } catch { return false; }} export interface CommandAvailabilityOptions { readonly env?: NodeJS.ProcessEnv; readonly extendEnv?: boolean;} export type CommandAvailabilityChecker = ( command: string, options?: CommandAvailabilityOptions,) => Effect.Effect<boolean, never, FileSystem.FileSystem | Path.Path>; export class CommandResolutionError extends Data.TaggedError("CommandResolutionError")<{ readonly command: string; readonly reason: "not-found";}> {} const WINDOWS_SHELL_META_CHARS = /([()\][%!^"`<>&|;, *?])/g; /** * Escapes a single argument for `cmd.exe` shell mode (`spawn(..., { shell: true })` * on Windows). Node joins the command and arguments with spaces and hands the * resulting string to `cmd.exe` without any quoting, so every dynamic argument * must be escaped to survive both cmd.exe parsing and the target program's * `CommandLineToArgvW` parsing. Mirrors cross-spawn's argument escaping. */function escapeWindowsShellArg(arg: string): string { // Double up backslashes that precede a double quote, then escape the quote // itself so it survives CommandLineToArgvW. let escaped = arg.replace(/(\\*)"/g, '$1$1\\"'); // Double up trailing backslashes so the closing quote is not escaped away. escaped = escaped.replace(/(\\*)$/, "$1$1"); // Quote the whole argument so embedded whitespace is preserved. escaped = `"${escaped}"`; // Escape cmd.exe metacharacters so cmd passes them through verbatim. return escaped.replace(WINDOWS_SHELL_META_CHARS, "^$1");} /** * Escapes arguments for shell-mode spawns: applies {@link escapeWindowsShellArg} * when the platform is `win32` (where `shell: true` routes through `cmd.exe`) * and returns the arguments untouched everywhere else. */function sanitizeShellModeArgsForPlatform( args: ReadonlyArray<string>, platform: NodeJS.Platform,): Array<string> { return platform === "win32" ? args.map(escapeWindowsShellArg) : [...args];} export interface ResolvedSpawnCommand { readonly command: string; readonly args: ReadonlyArray<string>; readonly shell: boolean;} export type SpawnExecutableResolver = ( command: string, platform: NodeJS.Platform, env: NodeJS.ProcessEnv,) => string | undefined; function resolveSpawnExecutableWithNode( command: string, platform: NodeJS.Platform, env: NodeJS.ProcessEnv,): string | undefined { const path = platform === "win32" ? NodePath.win32 : NodePath.posix; const windowsPathExtensions = platform === "win32" ? resolveWindowsPathExtensions(env) : []; const candidates = resolveCommandCandidates( command, platform, windowsPathExtensions, path.extname, ); const isExecutable = (candidate: string) => { try { if (!NodeFS.statSync(candidate).isFile()) return false; if (platform === "win32") { return windowsPathExtensions.includes(path.extname(candidate).toUpperCase()); } return canExecuteFile(candidate); } catch { return false; } }; if (command.includes("/") || command.includes("\\")) { return candidates.find(isExecutable); } for (const pathEntry of (readEnvPath(env) ?? "").split(pathDelimiterForPlatform(platform))) { const normalizedPathEntry = stripWrappingQuotes(pathEntry.trim()); if (normalizedPathEntry.length === 0) continue; for (const candidate of candidates) { const candidatePath = path.join(normalizedPathEntry, candidate); if (isExecutable(candidatePath)) return candidatePath; } } return undefined;} export const SpawnExecutableResolution = Context.Reference<SpawnExecutableResolver>( "@t3tools/shared/shell/SpawnExecutableResolution", { defaultValue: () => resolveSpawnExecutableWithNode, },); export interface WindowsEnvironmentProbeOptions { readonly loadProfile?: boolean;} function trimNonEmpty(value: string | null | undefined): string | undefined { const trimmed = value?.trim(); return trimmed && trimmed.length > 0 ? trimmed : undefined;} function readUserLoginShell(): string | undefined { try { return trimNonEmpty(NodeOS.userInfo().shell); } catch { return undefined; }} export function listLoginShellCandidates( platform: NodeJS.Platform, shell: string | undefined, userShell = readUserLoginShell(),): ReadonlyArray<string> { const fallbackShell = platform === "darwin" ? "/bin/zsh" : platform === "linux" ? "/bin/bash" : undefined; const seen = new Set<string>(); const candidates: string[] = []; for (const candidate of [trimNonEmpty(shell), trimNonEmpty(userShell), fallbackShell]) { if (!candidate || seen.has(candidate)) { continue; } seen.add(candidate); candidates.push(candidate); } return candidates;} export function extractPathFromShellOutput(output: string): string | null { const startIndex = output.indexOf(PATH_CAPTURE_START); if (startIndex === -1) return null; const valueStartIndex = startIndex + PATH_CAPTURE_START.length; const endIndex = output.indexOf(PATH_CAPTURE_END, valueStartIndex); if (endIndex === -1) return null; const pathValue = output.slice(valueStartIndex, endIndex).trim(); return pathValue.length > 0 ? pathValue : null;} export function readPathFromLoginShell( shell: string, execFile: ExecFileSyncLike = NodeChildProcess.execFileSync,): string | undefined { return readEnvironmentFromLoginShell(shell, ["PATH"], execFile).PATH;} export function readPathFromLaunchctl( execFile: ExecFileSyncLike = NodeChildProcess.execFileSync,): string | undefined { try { return trimNonEmpty( execFile("/bin/launchctl", ["getenv", "PATH"], { encoding: "utf8", timeout: 2000, }), ); } catch { return undefined; }} export function mergePathEntries( preferredPath: string | undefined, inheritedPath: string | undefined, platform: NodeJS.Platform,): string | undefined { const delimiter = platform === "win32" ? ";" : ":"; const merged: string[] = []; const seen = new Set<string>(); for (const pathValue of [preferredPath, inheritedPath]) { if (!pathValue) continue; for (const entry of pathValue.split(delimiter)) { const trimmedEntry = entry.trim(); if (!trimmedEntry || seen.has(trimmedEntry)) { continue; } seen.add(trimmedEntry); merged.push(trimmedEntry); } } return merged.length > 0 ? merged.join(delimiter) : undefined;} function envCaptureStart(name: string): string { return `__T3CODE_ENV_${name}_START__`;} function envCaptureEnd(name: string): string { return `__T3CODE_ENV_${name}_END__`;} function buildEnvironmentCaptureCommand(names: ReadonlyArray<string>): string { return names .map((name) => { if (!SHELL_ENV_NAME_PATTERN.test(name)) { throw new Error(`Unsupported environment variable name: ${name}`); } return [ `printf '%s\\n' '${envCaptureStart(name)}'`, `printenv ${name} || true`, `printf '%s\\n' '${envCaptureEnd(name)}'`, ].join("; "); }) .join("; ");} function buildWindowsEnvironmentCaptureCommand(names: ReadonlyArray<string>): string { return [ "$ErrorActionPreference = 'Stop'", ...names.flatMap((name) => { if (!SHELL_ENV_NAME_PATTERN.test(name)) { throw new Error(`Unsupported environment variable name: ${name}`); } return [ `Write-Output '${envCaptureStart(name)}'`, `$value = [Environment]::GetEnvironmentVariable('${name}')`, "if ($null -ne $value -and $value.Length -gt 0) { Write-Output $value }", `Write-Output '${envCaptureEnd(name)}'`, ]; }), ].join("; ");} function extractEnvironmentValue(output: string, name: string): string | undefined { const startMarker = envCaptureStart(name); const endMarker = envCaptureEnd(name); const startIndex = output.indexOf(startMarker); if (startIndex === -1) return undefined; const valueStartIndex = startIndex + startMarker.length; const endIndex = output.indexOf(endMarker, valueStartIndex); if (endIndex === -1) return undefined; const value = output .slice(valueStartIndex, endIndex) .replace(/^\r?\n/, "") .replace(/\r?\n$/, ""); return value.length > 0 ? value : undefined;} export type ShellEnvironmentReader = ( shell: string, names: ReadonlyArray<string>, execFile?: ExecFileSyncLike,) => Partial<Record<string, string>>; export const readEnvironmentFromLoginShell: ShellEnvironmentReader = ( shell, names, execFile = NodeChildProcess.execFileSync,) => { if (names.length === 0) { return {}; } const output = execFile(shell, ["-ilc", buildEnvironmentCaptureCommand(names)], { encoding: "utf8", timeout: 5000, }); const environment: Partial<Record<string, string>> = {}; for (const name of names) { const value = extractEnvironmentValue(output, name); if (value !== undefined) { environment[name] = value; } } return environment;}; export type WindowsShellEnvironmentReader = ( names: ReadonlyArray<string>, options?: WindowsEnvironmentProbeOptions,) => Partial<Record<string, string>>; export const WindowsShellEnvironment = Context.Reference<WindowsShellEnvironmentReader>( "@t3tools/shared/shell/WindowsShellEnvironment", { defaultValue: () => readEnvironmentFromWindowsShell, },); export const CommandAvailability = Context.Reference<CommandAvailabilityChecker>( "@t3tools/shared/shell/CommandAvailability", { defaultValue: () => isCommandAvailable, },); export function readEnvironmentFromWindowsShell( names: ReadonlyArray<string>, execFile?: ExecFileSyncLike,): Partial<Record<string, string>>;export function readEnvironmentFromWindowsShell( names: ReadonlyArray<string>, options?: WindowsEnvironmentProbeOptions, execFile?: ExecFileSyncLike,): Partial<Record<string, string>>;export function readEnvironmentFromWindowsShell( names: ReadonlyArray<string>, optionsOrExecFile?: WindowsEnvironmentProbeOptions | ExecFileSyncLike, maybeExecFile?: ExecFileSyncLike,): Partial<Record<string, string>> { if (names.length === 0) { return {}; } const options = typeof optionsOrExecFile === "function" ? ({} satisfies WindowsEnvironmentProbeOptions) : (optionsOrExecFile ?? {}); const execFile: ExecFileSyncLike = typeof optionsOrExecFile === "function" ? optionsOrExecFile : (maybeExecFile ?? (NodeChildProcess.execFileSync as ExecFileSyncLike)); const command = buildWindowsEnvironmentCaptureCommand(names); const args = [ "-NoLogo", ...(options.loadProfile ? ([] as const) : (["-NoProfile"] as const)), "-NonInteractive", "-Command", command, ]; for (const shell of WINDOWS_SHELL_CANDIDATES) { try { const output = execFile(shell, args, { encoding: "utf8", timeout: 5000 }); const environment: Partial<Record<string, string>> = {}; for (const name of names) { const value = extractEnvironmentValue(output, name); if (value !== undefined) { environment[name] = value; } } return environment; } catch { continue; } } return {};} function stripWrappingQuotes(value: string): string { return value.replace(/^"+|"+$/g, "");} function pathDelimiterForPlatform(platform: NodeJS.Platform): string { return platform === "win32" ? WINDOWS_PATH_DELIMITER : POSIX_PATH_DELIMITER;} function normalizePathEntryForComparison(entry: string, platform: NodeJS.Platform): string { const normalized = stripWrappingQuotes(entry.trim()); return platform === "win32" ? normalized.toLowerCase() : normalized;} function sanitizePathEntry(entry: string, platform: NodeJS.Platform): string { return platform === "win32" ? entry.replaceAll('"', "") : entry;} export function mergePathValues( preferredPath: string | undefined, inheritedPath: string | undefined, platform: NodeJS.Platform,): string | undefined { const delimiter = pathDelimiterForPlatform(platform); const merged: string[] = []; const seen = new Set<string>(); for (const rawValue of [preferredPath, inheritedPath]) { if (!rawValue) continue; for (const entry of rawValue.split(delimiter)) { const sanitized = sanitizePathEntry(entry.trim(), platform); if (sanitized.length === 0) continue; const normalized = normalizePathEntryForComparison(sanitized, platform); if (normalized.length === 0 || seen.has(normalized)) continue; seen.add(normalized); merged.push(sanitized); } } return merged.length > 0 ? merged.join(delimiter) : undefined;} function readEnvPath(env: NodeJS.ProcessEnv): string | undefined { return env.PATH ?? env.Path ?? env.path;} function resolvePathEnvironmentVariable(env: NodeJS.ProcessEnv): string { return readEnvPath(env) ?? "";} function resolveWindowsPathExtensions(env: NodeJS.ProcessEnv): ReadonlyArray<string> { const rawValue = env.PATHEXT; const fallback = [".COM", ".EXE", ".BAT", ".CMD"]; if (!rawValue) return fallback; const parsed: string[] = []; for (const entry of rawValue.split(";")) { const trimmed = entry.trim(); if (trimmed.length === 0) continue; parsed.push(trimmed.startsWith(".") ? trimmed.toUpperCase() : `.${trimmed.toUpperCase()}`); } return parsed.length > 0 ? Array.from(new Set(parsed)) : fallback;} function resolveCommandCandidates( command: string, platform: NodeJS.Platform, windowsPathExtensions: ReadonlyArray<string>, extname: (path: string) => string,): ReadonlyArray<string> { if (platform !== "win32") return [command]; const extension = extname(command); const normalizedExtension = extension.toUpperCase(); if (extension.length > 0 && windowsPathExtensions.includes(normalizedExtension)) { const commandWithoutExtension = command.slice(0, -extension.length); return Array.from( new Set([ command, `${commandWithoutExtension}${normalizedExtension}`, `${commandWithoutExtension}${normalizedExtension.toLowerCase()}`, ]), ); } const candidates: string[] = []; for (const candidateExtension of windowsPathExtensions) { candidates.push(`${command}${candidateExtension}`); candidates.push(`${command}${candidateExtension.toLowerCase()}`); } return Array.from(new Set(candidates));} // Session bootstrap resolves the same commands over and over, each PATH scan// costing hundreds of 'shell.isExecutableFile' filesystem probes (tens ofLines 1–500 of 761. Load more
// @effect-diagnostics nodeBuiltinImport:off import * as NodeOS from "node:os"; import * as NodePath from "node:path"; import * as NodeChildProcess from "node:child_process"; import * as NodeFS from "node:fs"; import * as Clock from "effect/Clock"; import * as Data from "effect/Data"; import * as Effect from "effect/Effect"; import * as FileSystem from "effect/FileSystem"; import * as Path from "effect/Path"; import { HostProcessEnvironment, HostProcessPlatform } from "./hostProcess.ts"; import * as Context from "effect/Context"; const PATH_CAPTURE_START = "__T3CODE_PATH_START__"; const PATH_CAPTURE_END = "__T3CODE_PATH_END__"; const SHELL_ENV_NAME_PATTERN = /^[A-Z0-9_]+$/; const WINDOWS_PATH_DELIMITER = ";"; const POSIX_PATH_DELIMITER = ":"; const WINDOWS_SHELL_CANDIDATES = ["pwsh.exe", "powershell.exe"] as const; type ExecFileSyncLike = ( file: string, args: ReadonlyArray<string>, options: { encoding: "utf8"; timeout: number }, ) => string; function canExecuteFile(filePath: string): boolean { try { NodeFS.accessSync(filePath, NodeFS.constants.X_OK); return true; } catch { return false; } } export interface CommandAvailabilityOptions { readonly env?: NodeJS.ProcessEnv; readonly extendEnv?: boolean; } export type CommandAvailabilityChecker = ( command: string, options?: CommandAvailabilityOptions, ) => Effect.Effect<boolean, never, FileSystem.FileSystem | Path.Path>; export class CommandResolutionError extends Data.TaggedError("CommandResolutionError")<{ readonly command: string; readonly reason: "not-found"; }> {} const WINDOWS_SHELL_META_CHARS = /([()\][%!^"`<>&|;, *?])/g; /** * Escapes a single argument for `cmd.exe` shell mode (`spawn(..., { shell: true })` * on Windows). Node joins the command and arguments with spaces and hands the * resulting string to `cmd.exe` without any quoting, so every dynamic argument * must be escaped to survive both cmd.exe parsing and the target program's * `CommandLineToArgvW` parsing. Mirrors cross-spawn's argument escaping. */ function escapeWindowsShellArg(arg: string): string { // Double up backslashes that precede a double quote, then escape the quote // itself so it survives CommandLineToArgvW. let escaped = arg.replace(/(\\*)"/g, '$1$1\\"'); // Double up trailing backslashes so the closing quote is not escaped away. escaped = escaped.replace(/(\\*)$/, "$1$1"); // Quote the whole argument so embedded whitespace is preserved. escaped = `"${escaped}"`; // Escape cmd.exe metacharacters so cmd passes them through verbatim. return escaped.replace(WINDOWS_SHELL_META_CHARS, "^$1"); } /** * Escapes arguments for shell-mode spawns: applies {@link escapeWindowsShellArg} * when the platform is `win32` (where `shell: true` routes through `cmd.exe`) * and returns the arguments untouched everywhere else. */ function sanitizeShellModeArgsForPlatform( args: ReadonlyArray<string>, platform: NodeJS.Platform, ): Array<string> { return platform === "win32" ? args.map(escapeWindowsShellArg) : [...args]; } export interface ResolvedSpawnCommand { readonly command: string; readonly args: ReadonlyArray<string>; readonly shell: boolean; } export type SpawnExecutableResolver = ( command: string, platform: NodeJS.Platform, env: NodeJS.ProcessEnv, ) => string | undefined; function resolveSpawnExecutableWithNode( command: string, platform: NodeJS.Platform, env: NodeJS.ProcessEnv, ): string | undefined { const path = platform === "win32" ? NodePath.win32 : NodePath.posix; const windowsPathExtensions = platform === "win32" ? resolveWindowsPathExtensions(env) : []; const candidates = resolveCommandCandidates( command, platform, windowsPathExtensions, path.extname, ); const isExecutable = (candidate: string) => { try { if (!NodeFS.statSync(candidate).isFile()) return false; if (platform === "win32") { return windowsPathExtensions.includes(path.extname(candidate).toUpperCase()); } return canExecuteFile(candidate); } catch { return false; } }; if (command.includes("/") || command.includes("\\")) { return candidates.find(isExecutable); } for (const pathEntry of (readEnvPath(env) ?? "").split(pathDelimiterForPlatform(platform))) { const normalizedPathEntry = stripWrappingQuotes(pathEntry.trim()); if (normalizedPathEntry.length === 0) continue; for (const candidate of candidates) { const candidatePath = path.join(normalizedPathEntry, candidate); if (isExecutable(candidatePath)) return candidatePath; } } return undefined; } export const SpawnExecutableResolution = Context.Reference<SpawnExecutableResolver>( "@t3tools/shared/shell/SpawnExecutableResolution", { defaultValue: () => resolveSpawnExecutableWithNode, }, ); export interface WindowsEnvironmentProbeOptions { readonly loadProfile?: boolean; } function trimNonEmpty(value: string | null | undefined): string | undefined { const trimmed = value?.trim(); return trimmed && trimmed.length > 0 ? trimmed : undefined; } function readUserLoginShell(): string | undefined { try { return trimNonEmpty(NodeOS.userInfo().shell); } catch { return undefined; } } export function listLoginShellCandidates( platform: NodeJS.Platform, shell: string | undefined, userShell = readUserLoginShell(), ): ReadonlyArray<string> { const fallbackShell = platform === "darwin" ? "/bin/zsh" : platform === "linux" ? "/bin/bash" : undefined; const seen = new Set<string>(); const candidates: string[] = []; for (const candidate of [trimNonEmpty(shell), trimNonEmpty(userShell), fallbackShell]) { if (!candidate || seen.has(candidate)) { continue; } seen.add(candidate); candidates.push(candidate); } return candidates; } export function extractPathFromShellOutput(output: string): string | null { const startIndex = output.indexOf(PATH_CAPTURE_START); if (startIndex === -1) return null; const valueStartIndex = startIndex + PATH_CAPTURE_START.length; const endIndex = output.indexOf(PATH_CAPTURE_END, valueStartIndex); if (endIndex === -1) return null; const pathValue = output.slice(valueStartIndex, endIndex).trim(); return pathValue.length > 0 ? pathValue : null; } export function readPathFromLoginShell( shell: string, execFile: ExecFileSyncLike = NodeChildProcess.execFileSync, ): string | undefined { return readEnvironmentFromLoginShell(shell, ["PATH"], execFile).PATH; } export function readPathFromLaunchctl( execFile: ExecFileSyncLike = NodeChildProcess.execFileSync, ): string | undefined { try { return trimNonEmpty( execFile("/bin/launchctl", ["getenv", "PATH"], { encoding: "utf8", timeout: 2000, }), ); } catch { return undefined; } } export function mergePathEntries( preferredPath: string | undefined, inheritedPath: string | undefined, platform: NodeJS.Platform, ): string | undefined { const delimiter = platform === "win32" ? ";" : ":"; const merged: string[] = []; const seen = new Set<string>(); for (const pathValue of [preferredPath, inheritedPath]) { if (!pathValue) continue; for (const entry of pathValue.split(delimiter)) { const trimmedEntry = entry.trim(); if (!trimmedEntry || seen.has(trimmedEntry)) { continue; } seen.add(trimmedEntry); merged.push(trimmedEntry); } } return merged.length > 0 ? merged.join(delimiter) : undefined; } function envCaptureStart(name: string): string { return `__T3CODE_ENV_${name}_START__`; } function envCaptureEnd(name: string): string { return `__T3CODE_ENV_${name}_END__`; } function buildEnvironmentCaptureCommand(names: ReadonlyArray<string>): string { return names .map((name) => { if (!SHELL_ENV_NAME_PATTERN.test(name)) { throw new Error(`Unsupported environment variable name: ${name}`); } return [ `printf '%s\\n' '${envCaptureStart(name)}'`, `printenv ${name} || true`, `printf '%s\\n' '${envCaptureEnd(name)}'`, ].join("; "); }) .join("; "); } function buildWindowsEnvironmentCaptureCommand(names: ReadonlyArray<string>): string { return [ "$ErrorActionPreference = 'Stop'", ...names.flatMap((name) => { if (!SHELL_ENV_NAME_PATTERN.test(name)) { throw new Error(`Unsupported environment variable name: ${name}`); } return [ `Write-Output '${envCaptureStart(name)}'`, `$value = [Environment]::GetEnvironmentVariable('${name}')`, "if ($null -ne $value -and $value.Length -gt 0) { Write-Output $value }", `Write-Output '${envCaptureEnd(name)}'`, ]; }), ].join("; "); } function extractEnvironmentValue(output: string, name: string): string | undefined { const startMarker = envCaptureStart(name); const endMarker = envCaptureEnd(name); const startIndex = output.indexOf(startMarker); if (startIndex === -1) return undefined; const valueStartIndex = startIndex + startMarker.length; const endIndex = output.indexOf(endMarker, valueStartIndex); if (endIndex === -1) return undefined; const value = output .slice(valueStartIndex, endIndex) .replace(/^\r?\n/, "") .replace(/\r?\n$/, ""); return value.length > 0 ? value : undefined; } export type ShellEnvironmentReader = ( shell: string, names: ReadonlyArray<string>, execFile?: ExecFileSyncLike, ) => Partial<Record<string, string>>; export const readEnvironmentFromLoginShell: ShellEnvironmentReader = ( shell, names, execFile = NodeChildProcess.execFileSync, ) => { if (names.length === 0) { return {}; } const output = execFile(shell, ["-ilc", buildEnvironmentCaptureCommand(names)], { encoding: "utf8", timeout: 5000, }); const environment: Partial<Record<string, string>> = {}; for (const name of names) { const value = extractEnvironmentValue(output, name); if (value !== undefined) { environment[name] = value; } } return environment; }; export type WindowsShellEnvironmentReader = ( names: ReadonlyArray<string>, options?: WindowsEnvironmentProbeOptions, ) => Partial<Record<string, string>>; export const WindowsShellEnvironment = Context.Reference<WindowsShellEnvironmentReader>( "@t3tools/shared/shell/WindowsShellEnvironment", { defaultValue: () => readEnvironmentFromWindowsShell, }, ); export const CommandAvailability = Context.Reference<CommandAvailabilityChecker>( "@t3tools/shared/shell/CommandAvailability", { defaultValue: () => isCommandAvailable, }, ); export function readEnvironmentFromWindowsShell( names: ReadonlyArray<string>, execFile?: ExecFileSyncLike, ): Partial<Record<string, string>>; export function readEnvironmentFromWindowsShell( names: ReadonlyArray<string>, options?: WindowsEnvironmentProbeOptions, execFile?: ExecFileSyncLike, ): Partial<Record<string, string>>; export function readEnvironmentFromWindowsShell( names: ReadonlyArray<string>, optionsOrExecFile?: WindowsEnvironmentProbeOptions | ExecFileSyncLike, maybeExecFile?: ExecFileSyncLike, ): Partial<Record<string, string>> { if (names.length === 0) { return {}; } const options = typeof optionsOrExecFile === "function" ? ({} satisfies WindowsEnvironmentProbeOptions) : (optionsOrExecFile ?? {}); const execFile: ExecFileSyncLike = typeof optionsOrExecFile === "function" ? optionsOrExecFile : (maybeExecFile ?? (NodeChildProcess.execFileSync as ExecFileSyncLike)); const command = buildWindowsEnvironmentCaptureCommand(names); const args = [ "-NoLogo", ...(options.loadProfile ? ([] as const) : (["-NoProfile"] as const)), "-NonInteractive", "-Command", command, ]; for (const shell of WINDOWS_SHELL_CANDIDATES) { try { const output = execFile(shell, args, { encoding: "utf8", timeout: 5000 }); const environment: Partial<Record<string, string>> = {}; for (const name of names) { const value = extractEnvironmentValue(output, name); if (value !== undefined) { environment[name] = value; } } return environment; } catch { continue; } } return {}; } function stripWrappingQuotes(value: string): string { return value.replace(/^"+|"+$/g, ""); } function pathDelimiterForPlatform(platform: NodeJS.Platform): string { return platform === "win32" ? WINDOWS_PATH_DELIMITER : POSIX_PATH_DELIMITER; } function normalizePathEntryForComparison(entry: string, platform: NodeJS.Platform): string { const normalized = stripWrappingQuotes(entry.trim()); return platform === "win32" ? normalized.toLowerCase() : normalized; } function sanitizePathEntry(entry: string, platform: NodeJS.Platform): string { return platform === "win32" ? entry.replaceAll('"', "") : entry; } export function mergePathValues( preferredPath: string | undefined, inheritedPath: string | undefined, platform: NodeJS.Platform, ): string | undefined { const delimiter = pathDelimiterForPlatform(platform); const merged: string[] = []; const seen = new Set<string>(); for (const rawValue of [preferredPath, inheritedPath]) { if (!rawValue) continue; for (const entry of rawValue.split(delimiter)) { const sanitized = sanitizePathEntry(entry.trim(), platform); if (sanitized.length === 0) continue; const normalized = normalizePathEntryForComparison(sanitized, platform); if (normalized.length === 0 || seen.has(normalized)) continue; seen.add(normalized); merged.push(sanitized); } } return merged.length > 0 ? merged.join(delimiter) : undefined; } function readEnvPath(env: NodeJS.ProcessEnv): string | undefined { return env.PATH ?? env.Path ?? env.path; } function resolvePathEnvironmentVariable(env: NodeJS.ProcessEnv): string { return readEnvPath(env) ?? ""; } function resolveWindowsPathExtensions(env: NodeJS.ProcessEnv): ReadonlyArray<string> { const rawValue = env.PATHEXT; const fallback = [".COM", ".EXE", ".BAT", ".CMD"]; if (!rawValue) return fallback; const parsed: string[] = []; for (const entry of rawValue.split(";")) { const trimmed = entry.trim(); if (trimmed.length === 0) continue; parsed.push(trimmed.startsWith(".") ? trimmed.toUpperCase() : `.${trimmed.toUpperCase()}`); } return parsed.length > 0 ? Array.from(new Set(parsed)) : fallback; } function resolveCommandCandidates( command: string, platform: NodeJS.Platform, windowsPathExtensions: ReadonlyArray<string>, extname: (path: string) => string, ): ReadonlyArray<string> { if (platform !== "win32") return [command]; const extension = extname(command); const normalizedExtension = extension.toUpperCase(); if (extension.length > 0 && windowsPathExtensions.includes(normalizedExtension)) { const commandWithoutExtension = command.slice(0, -extension.length); return Array.from( new Set([ command, `${commandWithoutExtension}${normalizedExtension}`, `${commandWithoutExtension}${normalizedExtension.toLowerCase()}`, ]), ); } const candidates: string[] = []; for (const candidateExtension of windowsPathExtensions) { candidates.push(`${command}${candidateExtension}`); candidates.push(`${command}${candidateExtension.toLowerCase()}`); } return Array.from(new Set(candidates)); } // Session bootstrap resolves the same commands over and over, each PATH scan // costing hundreds of 'shell.isExecutableFile' filesystem probes (tens of