src / shell.ts
src / shell.ts
import * as path from "path";
import { expandPath } from "./securityEnhanced";
import { detectPlatform } from "./platform";
import { resolvePwshPath } from "./platform";
import { ExecOptions, ShellInfo } from "./types";
// ── Shell Resolution ──
export function resolveShell(shellPath?: string, windowsShell?: "powershell" | "cmd"): ShellInfo {
const platform = detectPlatform();
if (shellPath) {
const expanded = expandPath(shellPath);
return { name: path.basename(expanded), path: expanded, platform };
}
if (platform === "windows") {
const shell = windowsShell || (process.env.WINDOWS_SHELL as "powershell" | "cmd" | undefined) || "powershell";
const resolved = shell === "cmd" ? "cmd.exe" : resolvePwshPath();
return { name: path.basename(resolved), path: resolved, platform };
}
return { name: "bash", path: "/bin/bash", platform };
}
// ── Command Style Detection ──
/**
* Detect if a command uses cmd.exe-style syntax that PowerShell's -Command
* would misinterpret or fail to execute correctly.
*/
export function isCmdStyleCommand(command: string): boolean {
const trimmed = command.trim();
// Known cmd.exe builtins
const cmdBuiltins = /^(dir|copy|xcopy|del|erase|ren|rename|move|cls|type|more|find|findstr|assoc|ftype|fc|format|tree|chkdsk|diskpart|attrib|subst|path|set|if|for|call|shift|goto|start|pause|break|ver|vol|title|prompt|color|help|exit|rem|echo|pushd|popd|cd|chdir|md|mkdir|rd|rmdir|sort|comp|compact|cipher|cacls|icacls|takeown|robocopy|where|endlocal|defined|exist)\s/i;
if (cmdBuiltins.test(trimmed)) return true;
// Bare / flags (cmd switches, not PowerShell --style)
if (/\b\/[a-zA-Z]+\b/.test(trimmed)) return true;
// cmd-style chaining: ||, &&, | (pipe), &
if (/\|\||&&|\|[^|]|&[^&]/.test(trimmed)) return true;
// cmd-style redirection: >, >>, <
if (/>[^>]|<[^>]/.test(trimmed)) return true;
return false;
}
/**
* Detect if a command starts with an absolute file path (executable path).
* PowerShell's -Command parser mangles paths with spaces and special chars,
* so we route these to cmd /c for reliable execution.
*/
export function isAbsolutePathCommand(command: string): boolean {
const trimmed = command.trim();
if (/^[a-zA-Z]:[\\/]/.test(trimmed)) return true;
if (/^\//.test(trimmed)) return true;
return false;
}
// ── Exec Invocation Builder ──
export function buildExecInvocation(
command: string,
options: ExecOptions,
shellInfo: ShellInfo,
): { command: string; args: string[] } {
if (options.args && options.args.length > 0) {
return { command, args: options.args };
}
if (shellInfo.platform === "windows") {
const forceCmd = options.windowsShell === "cmd" || isCmdStyleCommand(command) || isAbsolutePathCommand(command);
if (forceCmd) {
return { command: "cmd.exe", args: ["/d", "/s", "/c", command] };
}
return { command: shellInfo.path, args: ["-NoLogo", "-NoProfile", "-Command", command] };
}
return { command: shellInfo.path, args: ["-lc", command] };
}
import * as path from "path";
import { expandPath } from "./securityEnhanced";
import { detectPlatform } from "./platform";
import { resolvePwshPath } from "./platform";
import { ExecOptions, ShellInfo } from "./types";
// ── Shell Resolution ──
export function resolveShell(shellPath?: string, windowsShell?: "powershell" | "cmd"): ShellInfo {
const platform = detectPlatform();
if (shellPath) {
const expanded = expandPath(shellPath);
return { name: path.basename(expanded), path: expanded, platform };
}
if (platform === "windows") {
const shell = windowsShell || (process.env.WINDOWS_SHELL as "powershell" | "cmd" | undefined) || "powershell";
const resolved = shell === "cmd" ? "cmd.exe" : resolvePwshPath();
return { name: path.basename(resolved), path: resolved, platform };
}
return { name: "bash", path: "/bin/bash", platform };
}
// ── Command Style Detection ──
/**
* Detect if a command uses cmd.exe-style syntax that PowerShell's -Command
* would misinterpret or fail to execute correctly.
*/
export function isCmdStyleCommand(command: string): boolean {
const trimmed = command.trim();
// Known cmd.exe builtins
const cmdBuiltins = /^(dir|copy|xcopy|del|erase|ren|rename|move|cls|type|more|find|findstr|assoc|ftype|fc|format|tree|chkdsk|diskpart|attrib|subst|path|set|if|for|call|shift|goto|start|pause|break|ver|vol|title|prompt|color|help|exit|rem|echo|pushd|popd|cd|chdir|md|mkdir|rd|rmdir|sort|comp|compact|cipher|cacls|icacls|takeown|robocopy|where|endlocal|defined|exist)\s/i;
if (cmdBuiltins.test(trimmed)) return true;
// Bare / flags (cmd switches, not PowerShell --style)
if (/\b\/[a-zA-Z]+\b/.test(trimmed)) return true;
// cmd-style chaining: ||, &&, | (pipe), &
if (/\|\||&&|\|[^|]|&[^&]/.test(trimmed)) return true;
// cmd-style redirection: >, >>, <
if (/>[^>]|<[^>]/.test(trimmed)) return true;
return false;
}
/**
* Detect if a command starts with an absolute file path (executable path).
* PowerShell's -Command parser mangles paths with spaces and special chars,
* so we route these to cmd /c for reliable execution.
*/
export function isAbsolutePathCommand(command: string): boolean {
const trimmed = command.trim();
if (/^[a-zA-Z]:[\\/]/.test(trimmed)) return true;
if (/^\//.test(trimmed)) return true;
return false;
}
// ── Exec Invocation Builder ──
export function buildExecInvocation(
command: string,
options: ExecOptions,
shellInfo: ShellInfo,
): { command: string; args: string[] } {
if (options.args && options.args.length > 0) {
return { command, args: options.args };
}
if (shellInfo.platform === "windows") {
const forceCmd = options.windowsShell === "cmd" || isCmdStyleCommand(command) || isAbsolutePathCommand(command);
if (forceCmd) {
return { command: "cmd.exe", args: ["/d", "/s", "/c", command] };
}
return { command: shellInfo.path, args: ["-NoLogo", "-NoProfile", "-Command", command] };
}
return { command: shellInfo.path, args: ["-lc", command] };
}