import { existsSync } from "node:fs"; import { win32 } from "node:path"; import { spawnSync } from "node:child_process"; import type { PwshNativeConfig } from "./config.ts"; const KNOWN_PWSH_PATH = "C:\\Program Files\\PowerShell\\7\\pwsh.exe"; const VERSION_PROBE = "[Console]::Out.Write($PSVersionTable.PSVersion.ToString())"; export interface PowerShellRuntime { executable: string; version: string; } export interface RuntimeDependencies { exists: (path: string) => boolean; findOnPath: () => string[]; probe: (path: string) => string | null; } export class RuntimeError extends Error { constructor(message: string) { super(message); this.name = "RuntimeError"; } } function defaultFindOnPath(): string[] { const result = spawnSync("where.exe", ["pwsh.exe"], { encoding: "utf8", timeout: 5_000, windowsHide: true, }); if (result.error || result.status !== 0 || !result.stdout) return []; return result.stdout .split(/\r?\n/) .map((path) => path.trim()) .filter(Boolean); } function defaultProbe(path: string): string | null { const result = spawnSync( path, ["-NoLogo", "-NoProfile", "-NonInteractive", "-Command", VERSION_PROBE], { encoding: "utf8", timeout: 5_000, windowsHide: true, }, ); if (result.error || result.status !== 0) return null; const version = result.stdout.trim(); return /^\d+(?:\.\d+){1,3}(?:[-+][0-9A-Za-z.-]+)?$/.test(version) ? version : null; } const DEFAULT_DEPENDENCIES: RuntimeDependencies = { exists: existsSync, findOnPath: defaultFindOnPath, probe: defaultProbe, }; function majorVersion(version: string): number | null { const match = /^(\d+)/.exec(version); return match ? Number(match[1]) : null; } function uniquePaths(paths: string[]): string[] { const seen = new Set(); const result: string[] = []; for (const path of paths) { const normalized = win32.normalize(path); const key = normalized.toLowerCase(); if (seen.has(key)) continue; seen.add(key); result.push(normalized); } return result; } export function resolvePowerShellRuntime( config: PwshNativeConfig, dependencies: RuntimeDependencies = DEFAULT_DEPENDENCIES, ): PowerShellRuntime { const explicit = config.executable !== "auto"; const candidates = explicit ? [config.executable] : uniquePaths([...dependencies.findOnPath(), KNOWN_PWSH_PATH]); const failures: string[] = []; for (const candidate of candidates) { const path = win32.normalize(candidate); if (!win32.isAbsolute(path)) { failures.push(`${path}: not an absolute path`); continue; } if (!dependencies.exists(path)) { failures.push(`${path}: file not found`); continue; } const version = dependencies.probe(path); if (!version) { failures.push(`${path}: failed to start or returned an invalid version`); continue; } const major = majorVersion(version); if (major === null || major < 7) { failures.push(`${path}: PowerShell ${version} is unsupported; version 7 or newer is required`); continue; } return { executable: path, version }; } const prefix = explicit ? `Configured PowerShell executable ${JSON.stringify(config.executable)} is unavailable.` : "PowerShell 7 or newer was not found."; const details = failures.length > 0 ? ` Checked: ${failures.join("; ")}.` : ""; throw new RuntimeError(`${prefix}${details} Install PowerShell 7 or configure an absolute pwsh.exe path.`); }