Windows support take 3

This commit is contained in:
Pedram Amini
2025-12-22 15:17:48 -06:00
parent 5e48750d48
commit 5bc2a1254c
3 changed files with 78 additions and 3 deletions

View File

@@ -325,6 +325,8 @@ export class AgentDetector {
path.join(localAppData, 'npm'),
// Claude Code CLI install location (npm global)
path.join(appData, 'npm', 'node_modules', '@anthropic-ai', 'claude-code', 'cli'),
// User local bin (Claude Code standalone installer)
path.join(home, '.local', 'bin'),
// User local programs
path.join(localAppData, 'Programs'),
path.join(localAppData, 'Microsoft', 'WindowsApps'),
@@ -384,6 +386,7 @@ export class AgentDetector {
/**
* Check if a binary exists in PATH
* On Windows, this also handles .cmd and .exe extensions properly
*/
private async checkBinaryExists(binaryName: string): Promise<{ exists: boolean; path?: string }> {
try {
@@ -396,9 +399,56 @@ export class AgentDetector {
const result = await execFileNoThrow(command, [binaryName], undefined, env);
if (result.exitCode === 0 && result.stdout.trim()) {
// Get all matches (Windows 'where' can return multiple)
const matches = result.stdout.trim().split('\n').map(p => p.trim()).filter(p => p);
if (process.platform === 'win32' && matches.length > 0) {
// On Windows, prefer .exe over .cmd over extensionless
// This helps with proper execution handling
const exeMatch = matches.find(p => p.toLowerCase().endsWith('.exe'));
const cmdMatch = matches.find(p => p.toLowerCase().endsWith('.cmd'));
// Return the best match: .exe > .cmd > first result
let bestMatch = exeMatch || cmdMatch || matches[0];
// If the first match doesn't have an extension, check if .cmd or .exe version exists
// This handles cases where 'where' returns a path without extension
if (!bestMatch.toLowerCase().endsWith('.exe') && !bestMatch.toLowerCase().endsWith('.cmd')) {
const cmdPath = bestMatch + '.cmd';
const exePath = bestMatch + '.exe';
// Check if the .exe or .cmd version exists
try {
await fs.promises.access(exePath, fs.constants.F_OK);
bestMatch = exePath;
logger.debug(`Found .exe version of ${binaryName}`, 'AgentDetector', { path: exePath });
} catch {
try {
await fs.promises.access(cmdPath, fs.constants.F_OK);
bestMatch = cmdPath;
logger.debug(`Found .cmd version of ${binaryName}`, 'AgentDetector', { path: cmdPath });
} catch {
// Neither .exe nor .cmd exists, use the original path
}
}
}
logger.debug(`Windows binary detection for ${binaryName}`, 'AgentDetector', {
allMatches: matches,
selectedMatch: bestMatch,
isCmd: bestMatch.toLowerCase().endsWith('.cmd'),
isExe: bestMatch.toLowerCase().endsWith('.exe'),
});
return {
exists: true,
path: bestMatch,
};
}
return {
exists: true,
path: result.stdout.trim().split('\n')[0], // First match
path: matches[0], // First match for Unix
};
}

View File

@@ -537,10 +537,26 @@ export class ProcessManager extends EventEmitter {
hasStdio: 'default (pipe)'
});
const childProcess = spawn(command, finalArgs, {
// On Windows, .cmd files (npm-installed CLIs) need special handling
// They must be executed through cmd.exe since spawn() with shell:false
// cannot execute batch scripts directly
let spawnCommand = command;
let spawnArgs = finalArgs;
let useShell = false;
if (isWindows && command.toLowerCase().endsWith('.cmd')) {
// For .cmd files, we need to use shell:true to execute them properly
// This is safe because we're executing a specific file path, not user input
useShell = true;
logger.debug('[ProcessManager] Using shell=true for Windows .cmd file', 'ProcessManager', {
command,
});
}
const childProcess = spawn(spawnCommand, spawnArgs, {
cwd,
env,
shell: false, // Explicitly disable shell to prevent injection
shell: useShell, // Enable shell only for .cmd files on Windows
stdio: ['pipe', 'pipe', 'pipe'], // Explicitly set stdio to pipe
});

View File

@@ -15,6 +15,9 @@ export interface ExecResult {
/**
* Safely execute a command without shell injection vulnerabilities
* Uses execFile instead of exec to prevent shell interpretation
*
* On Windows, .cmd files (npm-installed CLIs) are handled by enabling shell mode,
* since execFile cannot directly execute batch scripts without the shell.
*/
export async function execFileNoThrow(
command: string,
@@ -23,11 +26,17 @@ export async function execFileNoThrow(
env?: NodeJS.ProcessEnv
): Promise<ExecResult> {
try {
// On Windows, .cmd files need shell execution
// This is safe because we're executing a specific file path, not user input
const isWindows = process.platform === 'win32';
const needsShell = isWindows && command.toLowerCase().endsWith('.cmd');
const { stdout, stderr } = await execFileAsync(command, args, {
cwd,
env,
encoding: 'utf8',
maxBuffer: EXEC_MAX_BUFFER,
shell: needsShell,
});
return {