Providers use plugins/common.ts, shell-command.ts, and managed-runtime-status; isNodeExecutable recognizes the running executable or a node-named binary. Claude-Session: https://claude.ai/code/session_01Agoc9bRVE9KL7mmWtey9Vq
129 lines
4.8 KiB
TypeScript
129 lines
4.8 KiB
TypeScript
import path from 'node:path';
|
|
import { samePath } from './common.js';
|
|
|
|
export interface ParsedArgument {
|
|
readonly value: string;
|
|
readonly next: number;
|
|
}
|
|
|
|
export type CommandParts = readonly [executable: string, script: string];
|
|
|
|
const POSIX_APOSTROPHE = `'"'"'`;
|
|
const POWERSHELL_EXIT_SUFFIX = '; exit $LASTEXITCODE';
|
|
const ENCODED_COMMAND_FLAGS = '-NoLogo -NoProfile -NonInteractive -EncodedCommand';
|
|
|
|
export function quotePosix(value: string): string {
|
|
return `'${value.replaceAll("'", POSIX_APOSTROPHE)}'`;
|
|
}
|
|
|
|
export function quotePowerShell(value: string): string {
|
|
return `'${value.replaceAll("'", "''")}'`;
|
|
}
|
|
|
|
export function windowsPowerShellPath(): string {
|
|
const configuredRoot = process.platform === 'win32' ? process.env.SystemRoot : undefined;
|
|
const systemRoot = configuredRoot
|
|
&& path.win32.isAbsolute(configuredRoot)
|
|
&& !/["%\r\n]/.test(configuredRoot)
|
|
? configuredRoot
|
|
: 'C:\\Windows';
|
|
return path.win32.join(systemRoot, 'System32', 'WindowsPowerShell', 'v1.0', 'powershell.exe');
|
|
}
|
|
|
|
export function posixSource(scriptPath: string): string {
|
|
return `${quotePosix(process.execPath)} ${quotePosix(scriptPath)}`;
|
|
}
|
|
|
|
export function powerShellSource(scriptPath: string): string {
|
|
return `& ${quotePowerShell(process.execPath)} ${quotePowerShell(scriptPath)}${POWERSHELL_EXIT_SUFFIX}`;
|
|
}
|
|
|
|
export function encodePowerShellSource(scriptPath: string): string {
|
|
return Buffer.from(powerShellSource(scriptPath), 'utf16le').toString('base64');
|
|
}
|
|
|
|
export function encodedWindowsCommand(scriptPath: string): string {
|
|
return `"${windowsPowerShellPath()}" ${ENCODED_COMMAND_FLAGS} ${encodePowerShellSource(scriptPath)}`;
|
|
}
|
|
|
|
// The running executable, or a binary named node (node.exe case-insensitive on Windows).
|
|
export function isNodeExecutable(filePath: string): boolean {
|
|
if (samePath(filePath, process.execPath)) return true;
|
|
const basename = path.basename(filePath);
|
|
return basename === 'node' || basename.toLowerCase() === 'node.exe';
|
|
}
|
|
|
|
export function isPowerShellExecutable(filePath: string): boolean {
|
|
return path.win32.isAbsolute(filePath)
|
|
&& path.win32.basename(filePath).toLowerCase() === 'powershell.exe';
|
|
}
|
|
|
|
export function readPosixArgument(command: string, start: number): ParsedArgument | undefined {
|
|
if (command[start] !== "'") return undefined;
|
|
let value = '';
|
|
for (let index = start + 1; index < command.length;) {
|
|
if (command.startsWith(POSIX_APOSTROPHE, index)) {
|
|
value += "'";
|
|
index += POSIX_APOSTROPHE.length;
|
|
continue;
|
|
}
|
|
if (command[index] === "'") return { value, next: index + 1 };
|
|
value += command[index];
|
|
index += 1;
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
export function parsePosixCommand(command: string): CommandParts | undefined {
|
|
const executable = readPosixArgument(command, 0);
|
|
if (!executable || command[executable.next] !== ' ') return undefined;
|
|
const script = readPosixArgument(command, executable.next + 1);
|
|
if (!script || script.next !== command.length) return undefined;
|
|
return [executable.value, script.value];
|
|
}
|
|
|
|
function readPowerShellArgument(command: string, start: number): ParsedArgument | undefined {
|
|
if (command[start] !== "'") return undefined;
|
|
let value = '';
|
|
for (let index = start + 1; index < command.length; index += 1) {
|
|
if (command[index] !== "'") {
|
|
value += command[index];
|
|
continue;
|
|
}
|
|
if (command[index + 1] === "'") {
|
|
value += "'";
|
|
index += 1;
|
|
continue;
|
|
}
|
|
return { value, next: index + 1 };
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
export function parsePowerShellSource(source: string): CommandParts | undefined {
|
|
if (!source.startsWith('& ')) return undefined;
|
|
const executable = readPowerShellArgument(source, 2);
|
|
if (!executable || source[executable.next] !== ' ') return undefined;
|
|
const script = readPowerShellArgument(source, executable.next + 1);
|
|
if (!script || source.slice(script.next) !== POWERSHELL_EXIT_SUFFIX) return undefined;
|
|
return [executable.value, script.value];
|
|
}
|
|
|
|
export function decodePowerShellSource(encoded: string): CommandParts | undefined {
|
|
if (!/^[A-Za-z0-9+/]+={0,2}$/.test(encoded)) return undefined;
|
|
const buffer = Buffer.from(encoded, 'base64');
|
|
if (buffer.toString('base64') !== encoded || buffer.length % 2 !== 0) return undefined;
|
|
return parsePowerShellSource(buffer.toString('utf16le'));
|
|
}
|
|
|
|
export function parseEncodedWindowsCommand(command: string): CommandParts | undefined {
|
|
const match = /^"([^"\r\n]+)" -NoLogo -NoProfile -NonInteractive -EncodedCommand ([A-Za-z0-9+/]+={0,2})$/.exec(command);
|
|
if (!match || !isPowerShellExecutable(match[1])) return undefined;
|
|
return decodePowerShellSource(match[2]);
|
|
}
|
|
|
|
export function parseLegacyWindowsCommand(command: string): CommandParts | undefined {
|
|
const match = /^"([^"\r\n]+)" "([^"\r\n]+)"$/.exec(command);
|
|
return match ? [match[1], match[2]] : undefined;
|
|
}
|