import { Buffer } from "node:buffer"; import { BACKGROUND_CONTEXT } from "@earendil-works/chord/context"; import type { Environment } from "@berry/api"; function variable(env: Environment, name: string): string | undefined { // windows spells its variables in any case return Object.entries(env.variables).find(([key]) => env.platform === "win32" ? key.toUpperCase() === name : key === name )?.[1]; } async function findCommand(env: Environment, command: string): Promise { const windows = env.platform === "win32"; const names = windows ? (variable(env, "PATHEXT") ?? ".EXE").split(";").filter(Boolean).map((ext) => command + ext) : [command]; for (const dir of (variable(env, "PATH") ?? "").split(windows ? ";" : ":").filter(Boolean)) { for (const name of names) { const file = await env.joinPath([dir.replace(/^"(.*)"$/, "$1"), name], BACKGROUND_CONTEXT); if (!file.ok) continue; const info = await env.fileInfo(file.value, BACKGROUND_CONTEXT); if (info.ok && info.value.kind !== "directory") return file.value; } } } const commands = new Map>(); /** Whether `command` is on the environment's PATH: asked once of each machine and PATH. */ export function hasCommand(env: Environment, command: string): Promise { const key = JSON.stringify([ env.id, env.cwd, env.platform, variable(env, "PATH"), variable(env, "PATHEXT"), command, ]); let found = commands.get(key); if (found === undefined) { found = findCommand(env, command).then((path) => path !== undefined); commands.set(key, found); } return found; } /** PowerShell on this machine, favoring PS7 even when only PS5 is on PATH. */ export async function powershellCommand(env: Environment, command: string): Promise { let shell = await findCommand(env, "pwsh"); for ( const [root, parts] of [ ["PROGRAMFILES", ["PowerShell", "7", "pwsh.exe"]], ["PROGRAMW6432", ["PowerShell", "7", "pwsh.exe"]], ] as const ) { if (shell !== undefined) break; const dir = variable(env, root); if (!dir) continue; const file = await env.joinPath([dir, ...parts], BACKGROUND_CONTEXT); if (!file.ok) continue; const info = await env.fileInfo(file.value, BACKGROUND_CONTEXT); if (info.ok && info.value.kind !== "directory") shell = file.value; } shell ??= await findCommand(env, "powershell"); if (shell === undefined) { const root = variable(env, "SYSTEMROOT"); if (root) { const file = await env.joinPath([ root, "System32", "WindowsPowerShell", "v1.0", "powershell.exe", ], BACKGROUND_CONTEXT); if (file.ok) { const info = await env.fileInfo(file.value, BACKGROUND_CONTEXT); if (info.ok && info.value.kind !== "directory") shell = file.value; } } } if (shell === undefined) { throw new Error("PowerShell not found. Install PowerShell 7 or add it to PATH."); } // encoded commands survive windows argument quoting and PS5's legacy input encoding const script = [ "[Console]::OutputEncoding = [System.Text.UTF8Encoding]::new($false)", "$OutputEncoding = [Console]::OutputEncoding", "$ProgressPreference = 'SilentlyContinue'", command, "if ($?) { exit 0 }; if ($LASTEXITCODE) { exit $LASTEXITCODE }; exit 1", ].join("\n"); return [ shell, "-NoLogo", "-NoProfile", "-NonInteractive", "-OutputFormat", "Text", "-EncodedCommand", Buffer.from(script, "utf16le").toString("base64"), ]; }