src / powershellPath.ts
import { access, stat } from "fs/promises";
import { basename, delimiter, join } from "path";
import { constants } from "fs";
import type { PowerShell7Config, PowerShellPathMode } from "./config";
export type PowerShellResolution =
| { ok: true; path: string; mode: PowerShellPathMode }
| { ok: false; error: string; mode: PowerShellPathMode };
export async function resolvePowerShellExecutable(
config: PowerShell7Config,
): Promise<PowerShellResolution> {
if (config.powershellPathMode === "manual") {
return resolveManualPowerShellExecutable(config);
}
const candidates = getAutoCandidates();
for (const candidate of candidates) {
const resolved = await resolveCandidate(candidate);
if (resolved) {
return { ok: true, path: resolved, mode: "auto" };
}
}
return {
ok: false,
mode: "auto",
error:
"PowerShell 7 was not found. Install PowerShell 7 or set PowerShell 7 Path Mode to Manual in LM Studio plugin settings with a path to pwsh.exe/pwsh.",
};
}
async function resolveManualPowerShellExecutable(config: PowerShell7Config): Promise<PowerShellResolution> {
const manualPath = config.manualPowerShellPath?.trim();
if (!manualPath) {
return {
ok: false,
mode: "manual",
error: "Manual PowerShell path mode is enabled, but Manual PowerShell 7 Path is empty.",
};
}
const baseName = basename(manualPath).toLowerCase();
if (baseName === "powershell.exe") {
return {
ok: false,
mode: "manual",
error: "Manual PowerShell path is invalid: powershell.exe is not PowerShell 7 and is not allowed.",
};
}
if (baseName !== "pwsh.exe" && baseName !== "pwsh") {
return {
ok: false,
mode: "manual",
error: "Manual PowerShell path is invalid: basename must be pwsh.exe or pwsh.",
};
}
try {
const fileStat = await stat(manualPath);
if (!fileStat.isFile()) {
return { ok: false, mode: "manual", error: "Manual PowerShell path is invalid: path is not a file." };
}
await access(manualPath, constants.X_OK);
return { ok: true, path: manualPath, mode: "manual" };
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
return { ok: false, mode: "manual", error: `Manual PowerShell path is invalid: ${message}` };
}
}
function getAutoCandidates(): string[] {
if (process.platform === "win32") {
return [
"pwsh.exe",
"pwsh",
"C:\\Program Files\\PowerShell\\7\\pwsh.exe",
"C:\\Program Files\\PowerShell\\7-preview\\pwsh.exe",
];
}
return ["pwsh", "/usr/bin/pwsh", "/usr/local/bin/pwsh", "/opt/homebrew/bin/pwsh"];
}
async function resolveCandidate(candidate: string): Promise<string | undefined> {
if (candidate.includes("/") || candidate.includes("\\")) {
return (await isExecutableFile(candidate)) ? candidate : undefined;
}
for (const directory of getPathDirectories()) {
const fullPath = join(directory, candidate);
if (await isExecutableFile(fullPath)) {
return fullPath;
}
}
return undefined;
}
function getPathDirectories(): string[] {
const pathValue = process.env.PATH ?? process.env.Path ?? process.env.path ?? "";
return pathValue.split(delimiter).filter((entry) => entry.trim().length > 0);
}
async function isExecutableFile(filePath: string): Promise<boolean> {
try {
const fileStat = await stat(filePath);
return fileStat.isFile();
} catch {
return false;
}
}