Project Files
src / utils.ts
import * as child_process from "child_process";
import * as fs from "fs";
import * as os from "os";
import * as path from "path";
// Optimized path expansion - handles both ~ and %ENVVAR% formats
export function expandPath(p: string): string {
let expanded = p.trim();
// Handle home directory first (must be before env var expansion)
if (expanded.startsWith("~")) {
expanded = expanded.replace(/^~(?=[/\\]|$)/, os.homedir());
}
// Expand environment variables (%VAR%) format
expanded = expanded.replace(/%([^%]+)%/g, (_, key) => process.env[key] ?? `%${key}%`);
return expanded;
}
// Resolve working directory with validation
export function resolveCwd(cwd?: string): string {
if (!cwd) return os.homedir();
const expanded = expandPath(cwd);
try {
if (fs.existsSync(expanded) && fs.statSync(expanded).isDirectory()) {
return expanded;
}
} catch {
// ignore invalid directories
}
return os.homedir();
}
// Optimized PowerShell path resolution with caching
const PWSH_CACHE = new Map<string, string>();
function getCachedPwshPath(): string | null {
if (PWSH_CACHE.size > 5) {
const keys = Array.from(PWSH_CACHE.keys());
for (let i = 0; i < Math.floor(keys.length / 2); i++) {
PWSH_CACHE.delete(keys[i]);
}
}
const val = PWSH_CACHE.get("");
return val ?? null;
}
function setCachedPwshPath(value: string | null): void {
PWSH_CACHE.set("", value ?? "");
}
const PWSH_CANDIDATES = [
"C:\\Program Files\\PowerShell\\7\\pwsh.exe",
`${process.env.USERPROFILE ?? "C:\\Users\\Default"}\\scoop\\apps\\pwsh\\current\\pwsh.exe`,
...(process.env.LOCALAPPDATA
? [`${process.env.LOCALAPPDATA}\\Microsoft\\WindowsApps\\pwsh.exe`]
: []),
"C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe",
];
export function resolvePwshPath(): string | null {
const cached = getCachedPwshPath();
if (cached) return cached;
for (const candidate of PWSH_CANDIDATES) {
try {
if (fs.existsSync(candidate)) {
setCachedPwshPath(candidate);
return candidate;
}
} catch {
// skip inaccessible paths
}
}
// Fallback to default PowerShell
const fallback = process.env.SystemRoot ? `${process.env.SystemRoot}\\System32\\WindowsPowerShell\\v1.0\\powershell.exe` : null;
setCachedPwshPath(fallback);
return fallback;
}
// Check if a file is hidden on Windows
export function isWindowsHidden(fullPath: string): boolean {
if (process.platform !== "win32") return false;
try {
const out = child_process.execSync(`attrib "${fullPath}"`, {
encoding: "utf-8",
stdio: ["ignore", "pipe", "ignore"],
timeout: 2_000,
});
return /^[^\s]*H/i.test(out.trim().split(/\s+/)[0] ?? "");
} catch {
return false;
}
}
// Normalize line endings to Unix format
export function normalizeLineEndings(text: string): string {
return text.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
}
// Expand Windows paths with drive letter normalization
export function expandWindowsPath(p: string): string {
if (process.platform !== "win32") return p;
// Normalize backslashes to forward slashes for consistency
let normalized = p.replace(/\\/g, "/");
// Handle drive letters
const driveMatch = normalized.match(/^([a-zA-Z]):(.*)$/);
if (driveMatch) {
normalized = `/mnt/${driveMatch[1].toLowerCase()}/${driveMatch[2]}`;
}
return normalized;
}
// Validate path for traversal attacks
export function sanitizePath(p: string): string {
let sanitized = p.replace(/%00/g, "").replace(/\\.+/g, "/");
try {
sanitized = path.resolve(expandPath(sanitized));
} catch {
// Invalid path
}
return sanitized;
}
// Format bytes to human-readable string
export function formatBytes(bytes: number, decimals = 2): string {
if (bytes === 0) return "0 Bytes";
const k = 1024;
const dm = decimals < 0 ? 0 : decimals;
const sizes = ["Bytes", "KB", "MB", "GB", "TB"];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + " " + sizes[i];
}
// Escape shell arguments safely to prevent injection
export function shellEscape(arg: string): string {
// If already quoted, don't double-quote
if (/^["'].*["']$/.test(arg)) return arg;
const isWindows = process.platform === "win32";
if (isWindows) {
// Windows escaping: wrap in quotes and double internal quotes
const escaped = arg.replace(/"/g, '""');
return `"${escaped}"`;
} else {
// Unix escaping: single-quote everything except safe chars
if (/^[a-zA-Z0-9_./-]+$/.test(arg)) return arg;
const escaped = arg.replace(/'/g, "'\\''");
return `'${escaped}'`;
}
}