feat: prefer PowerShell and auto-detect shell scripts on Windows

Improvements to Windows shell execution for OpenCode and other shell-based tools:

1. **Binary detection priority**: Changed Windows detection to prefer extensionless
   shell scripts (like opencode) over .cmd wrappers. The .cmd files execute through
   cmd.exe which has command line length limits and less robust script handling.

2. **Auto-detect shell scripts**: Added shebang detection in ChildProcessSpawner to
   automatically enable shell execution for POSIX scripts, preventing ENOENT errors
   when trying to execute shell scripts without explicit shell.

3. **Use PowerShell by default**: Changed Windows agent execution to prefer PowerShell
   over cmd.exe when available via PSHOME environment variable. PowerShell provides:
   - Better handling of POSIX shell scripts (with shebangs)
   - Avoids cmd.exe command line length limitations
   - Better compatibility with modern tooling

   Falls back to cmd.exe if PowerShell unavailable, respects user customizations.

4. **Improved path detection**: Updated Windows path probing to correctly prefer
   .exe > extensionless scripts > .cmd in the selection order.
This commit is contained in:
chr1syy
2026-02-05 18:21:06 +01:00
parent e65266f8d9
commit 3a0affb149
3 changed files with 42 additions and 7 deletions

View File

@@ -474,13 +474,16 @@ export async function checkBinaryExists(binaryName: string): Promise<BinaryDetec
.filter((p) => p);
if (process.platform === 'win32' && matches.length > 0) {
// On Windows, prefer .exe over .cmd over extensionless
// This helps with proper execution handling
// On Windows, prefer .exe > extensionless (shell scripts) > .cmd
// This helps avoid cmd.exe limitations and supports PowerShell/bash scripts
const exeMatch = matches.find((p) => p.toLowerCase().endsWith('.exe'));
const cmdMatch = matches.find((p) => p.toLowerCase().endsWith('.cmd'));
const extensionlessMatch = matches.find(
(p) => !p.toLowerCase().endsWith('.exe') && !p.toLowerCase().endsWith('.cmd')
);
// Return the best match: .exe > .cmd > first result
let bestMatch = exeMatch || cmdMatch || matches[0];
// Return the best match: .exe > extensionless shell scripts > .cmd > first result
let bestMatch = exeMatch || extensionlessMatch || 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

View File

@@ -283,7 +283,8 @@ export function registerProcessHandlers(deps: ProcessHandlerDependencies): void
) as Record<string, string>;
// Determine an explicit shell to use when forcing shell execution on Windows.
// Prefer a user-configured custom shell path, otherwise fall back to COMSPEC/cmd.exe.
// Prefer a user-configured custom shell path, then PowerShell, then ComSpec/cmd.exe.
// PowerShell is preferred over cmd.exe for better script handling and to avoid cmd.exe limits.
const customShellPath = settingsStore.get('customShellPath', '') as string;
if (customShellPath && customShellPath.trim()) {
shellToUse = customShellPath.trim();
@@ -291,8 +292,20 @@ export function registerProcessHandlers(deps: ProcessHandlerDependencies): void
customShellPath: shellToUse,
});
} else if (!shellToUse) {
// Use COMSPEC if available, otherwise default to cmd.exe
shellToUse = process.env.ComSpec || 'cmd.exe';
// Try PowerShell if available (common on modern Windows)
// If not, fall back to ComSpec/cmd.exe
// PowerShell handles shell scripts better and avoids cmd.exe command line length limits
const powerShellPath = process.env.PSHOME
? `${process.env.PSHOME}\\powershell.exe`
: 'powershell';
shellToUse = powerShellPath;
logger.debug(
'Using PowerShell for agent execution on Windows (shell script support)',
LOG_CONTEXT,
{
shellPath: shellToUse,
}
);
}
logger.info(`Forcing shell execution for agent on Windows for PATH access`, LOG_CONTEXT, {

View File

@@ -3,6 +3,7 @@
import { spawn } from 'child_process';
import { EventEmitter } from 'events';
import * as path from 'path';
import * as fs from 'fs';
import { logger } from '../../utils/logger';
import { getOutputParser } from '../../parsers';
import { getAgentCapabilities } from '../../agents';
@@ -184,6 +185,24 @@ export class ChildProcessSpawner {
);
}
// Auto-enable shell for Windows when command is a shell script (extensionless with shebang)
// This handles tools like OpenCode installed via npm with shell scripts
if (isWindows && !useShell && !commandExt && commandHasPath) {
try {
const fileContent = fs.readFileSync(spawnCommand, 'utf8');
if (fileContent.startsWith('#!')) {
useShell = true;
logger.info(
'[ProcessManager] Auto-enabling shell for Windows to execute shell script',
'ProcessManager',
{ command: spawnCommand, shebang: fileContent.split('\n')[0] }
);
}
} catch {
// If we can't read the file, just continue without special handling
}
}
if (isWindows && useShell) {
logger.debug(
'[ProcessManager] Forcing shell=true for agent spawn on Windows (runInShell or auto)',