mirror of
https://github.com/jlengrand/Maestro.git
synced 2026-03-10 08:31:19 +00:00
Windows support take 3
This commit is contained in:
@@ -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
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
});
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user