Project Files
src / runtimeResolver.ts
import * as fs from "fs";
import * as os from "os";
import * as path from "path";
import * as child_process from "child_process";
import { detectPlatform, Platform } from "./executor";
import { resolvePwshPath } from "./utils";
export type Runtime =
| "python" | "node" | "bun" | "deno" | "ts-node"
| "ruby" | "perl" | "php" | "lua" | "rscript"
| "julia" | "go-run" | "swift" | "rust" | "bash" | "zsh" | "pwsh";
const EXTENSION_MAP: Record<string, Runtime> = {
".py": "python",
".js": "node",
".mjs": "node",
".cjs": "node",
".jsx": "node",
".ts": "bun",
".tsx": "bun",
".mts": "bun",
".cts": "bun",
".rb": "ruby",
".pl": "perl",
".pm": "perl",
".php": "php",
".lua": "lua",
".r": "rscript",
".R": "rscript",
".jl": "julia",
".go": "go-run",
".swift": "swift",
".rs": "rust",
".sh": "bash",
".bash": "bash",
".zsh": "zsh",
".ps1": "pwsh",
};
const RUNTIME_EXTENSION: Record<Runtime, string> = {
python: ".py",
node: ".js",
bun: ".ts",
deno: ".ts",
"ts-node": ".ts",
ruby: ".rb",
perl: ".pl",
php: ".php",
lua: ".lua",
rscript: ".R",
julia: ".jl",
"go-run": ".go",
swift: ".swift",
rust: ".rs",
bash: ".sh",
zsh: ".zsh",
pwsh: ".ps1",
};
const SHEBANG_MAP: Record<string, Runtime> = {
python: "python",
python3: "python",
node: "node",
bun: "bun",
deno: "deno",
"ts-node": "ts-node",
ruby: "ruby",
perl: "perl",
php: "php",
lua: "lua",
rscript: "rscript",
Rscript: "rscript",
R: "rscript",
julia: "julia",
go: "go-run",
swift: "swift",
rust: "rust",
bash: "bash",
sh: "bash",
zsh: "zsh",
pwsh: "pwsh",
powershell: "pwsh",
};
export interface RuntimeCommand {
bin: string;
preFileArgs: string[];
label: string;
}
// Python binary cache - per-platform to avoid redundant lookups
const _pythonBinCache = new Map<string, string>();
function resolvePythonBin(platform: Platform): string {
const cached = _pythonBinCache.get(platform.toString());
if (cached) return cached;
const set = (bin: string): string => {
_pythonBinCache.set(platform.toString(), bin);
return bin;
};
if (platform === "windows") {
const appdata = process.env.LOCALAPPDATA ?? "";
if (appdata) {
try {
const base = path.join(appdata, "Programs", "Python");
if (fs.existsSync(base)) {
const versions = fs.readdirSync(base).sort().reverse();
for (const version of versions) {
const p = path.join(base, version, "python.exe");
if (fs.existsSync(p)) return set(p);
}
}
} catch {
// ignore
}
const store = path.join(appdata, "Microsoft", "WindowsApps", "python.exe");
if (fs.existsSync(store)) return set(store);
}
for (const bin of ["py", "python", "python3"]) {
try {
child_process.execSync(`where ${bin}`, { stdio: "ignore" });
return set(bin);
} catch {
// ignore
}
}
return set("python");
}
for (const bin of ["python3", "python"]) {
try {
child_process.execSync(`which ${bin}`, { stdio: "ignore" });
return set(bin);
} catch {
// ignore
}
}
return set("python3");
}
export function resolveRuntimeCommand(
runtime: Runtime,
pythonOverride?: string,
): RuntimeCommand {
const platform = detectPlatform();
switch (runtime) {
case "python": {
const bin = pythonOverride?.trim() || resolvePythonBin(platform);
return { bin, preFileArgs: [], label: `Python (${bin})` };
}
case "node":
return { bin: "node", preFileArgs: [], label: "Node.js" };
case "bun":
return { bin: "bun", preFileArgs: ["run"], label: "Bun" };
case "deno":
return { bin: "deno", preFileArgs: ["run", "--allow-all"], label: "Deno" };
case "ts-node":
return { bin: "npx", preFileArgs: ["--yes", "ts-node"], label: "ts-node (via npx)" };
case "ruby":
return { bin: "ruby", preFileArgs: [], label: "Ruby" };
case "perl":
return { bin: "perl", preFileArgs: [], label: "Perl" };
case "php":
return { bin: "php", preFileArgs: [], label: "PHP" };
case "lua":
return { bin: "lua", preFileArgs: [], label: "Lua" };
case "rscript":
return { bin: "Rscript", preFileArgs: [], label: "R (Rscript)" };
case "julia":
return { bin: "julia", preFileArgs: [], label: "Julia" };
case "go-run":
return { bin: "go", preFileArgs: ["run"], label: "Go (go run)" };
case "swift":
return { bin: "swift", preFileArgs: [], label: "Swift" };
case "rust":
return { bin: "rustc", preFileArgs: [], label: "Rust (rustc)" };
case "bash":
return { bin: "bash", preFileArgs: [], label: "Bash" };
case "zsh":
return { bin: "zsh", preFileArgs: [], label: "Zsh" };
case "pwsh": {
const bin = platform === "windows" ? resolvePwshPath() ?? "pwsh" : "pwsh";
return {
bin,
preFileArgs: ["-NoProfile", "-NonInteractive", "-File"],
label: "PowerShell (pwsh)",
};
}
}
}
function resolveRuntimeFromShebang(filePath: string): Runtime | undefined {
try {
const content = fs.readFileSync(filePath, "utf-8");
const firstLine = content.split(/\r?\n/, 1)?.[0]?.trim();
if (!firstLine?.startsWith("#!")) return undefined;
const shebang = firstLine.slice(2).trim().split(/\s+/);
let command = shebang[0];
if (path.basename(command).toLowerCase() === "env" && shebang[1]) {
command = shebang[1];
}
const normalized = path.basename(command).toLowerCase();
if (SHEBANG_MAP[normalized]) return SHEBANG_MAP[normalized];
for (const [key, runtime] of Object.entries(SHEBANG_MAP)) {
if (normalized.startsWith(key.toLowerCase())) return runtime;
}
return undefined;
} catch {
return undefined;
}
}
export function detectRuntimeFromExtension(filePath: string): Runtime | undefined {
const extOrig = path.extname(filePath);
const ext = extOrig.toLowerCase();
return EXTENSION_MAP[extOrig] ?? EXTENSION_MAP[ext] ?? resolveRuntimeFromShebang(filePath);
}
export interface TempFile {
filePath: string;
cleanup(): void;
}
export function writeTempFile(code: string, runtime: Runtime): TempFile {
const ext = RUNTIME_EXTENSION[runtime];
const filePath = path.join(
os.tmpdir(),
`lms-terminal-${Date.now()}-${Math.random().toString(36).slice(2)}${ext}`,
);
fs.writeFileSync(filePath, code, "utf-8");
return {
filePath,
cleanup() {
try {
fs.unlinkSync(filePath);
} catch {
// ignore
}
},
};
}