mirror of
https://github.com/jlengrand/Maestro.git
synced 2026-03-10 08:31:19 +00:00
Merge pull request #306 from chr1syy/fix-windows-probing
Windows enhancements and fixes
This commit is contained in:
@@ -63,6 +63,21 @@ describe('path-prober', () => {
|
||||
}
|
||||
});
|
||||
|
||||
it('should include nvm4w and npm paths on Windows', () => {
|
||||
const originalPlatform = process.platform;
|
||||
Object.defineProperty(process, 'platform', { value: 'win32', configurable: true });
|
||||
|
||||
try {
|
||||
const env = getExpandedEnv();
|
||||
// Check for nvm4w paths (OpenCode commonly installed here)
|
||||
expect(env.PATH).toContain('C:\\nvm4w\\nodejs');
|
||||
// Check for npm global paths
|
||||
expect(env.PATH).toMatch(/AppData[\\\/](npm|Roaming[\\\/]npm)/);
|
||||
} finally {
|
||||
Object.defineProperty(process, 'platform', { value: originalPlatform, configurable: true });
|
||||
}
|
||||
});
|
||||
|
||||
it('should preserve existing PATH entries', () => {
|
||||
const originalPath = process.env.PATH;
|
||||
const testPath = '/test/custom/path';
|
||||
|
||||
@@ -84,6 +84,11 @@ export function getExpandedEnv(): NodeJS.ProcessEnv {
|
||||
// Node.js
|
||||
path.join(programFiles, 'nodejs'),
|
||||
path.join(localAppData, 'Programs', 'node'),
|
||||
// Node Version Manager for Windows (nvm4w) - OpenCode commonly installed here
|
||||
'C:\\nvm4w\\nodejs',
|
||||
path.join(home, 'nvm4w', 'nodejs'),
|
||||
// Volta - Node version manager for Windows/macOS/Linux (installs shims to .volta/bin)
|
||||
path.join(home, '.volta', 'bin'),
|
||||
// Scoop package manager (OpenCode, other tools)
|
||||
path.join(home, 'scoop', 'shims'),
|
||||
path.join(home, 'scoop', 'apps', 'opencode', 'current'),
|
||||
@@ -252,6 +257,9 @@ function getWindowsKnownPaths(binaryName: string): string[] {
|
||||
// Scoop installation (recommended for OpenCode)
|
||||
path.join(home, 'scoop', 'shims', 'opencode.exe'),
|
||||
path.join(home, 'scoop', 'apps', 'opencode', 'current', 'opencode.exe'),
|
||||
// Volta - Node version manager (OpenCode commonly installed via Volta)
|
||||
path.join(home, '.volta', 'bin', 'opencode'),
|
||||
path.join(home, '.volta', 'bin', 'opencode.cmd'),
|
||||
// Chocolatey installation
|
||||
path.join(
|
||||
process.env.ChocolateyInstall || 'C:\\ProgramData\\chocolatey',
|
||||
@@ -471,13 +479,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
|
||||
|
||||
@@ -452,6 +452,7 @@ ${message}`;
|
||||
});
|
||||
const configResolution = applyAgentConfigOverrides(agent, baseArgs, {
|
||||
agentConfigValues,
|
||||
sessionCustomModel: chat.moderatorConfig?.customModel,
|
||||
sessionCustomArgs: chat.moderatorConfig?.customArgs,
|
||||
sessionCustomEnvVars: chat.moderatorConfig?.customEnvVars,
|
||||
});
|
||||
@@ -849,7 +850,9 @@ export async function routeModeratorResponse(
|
||||
|
||||
// Apply SSH wrapping if configured for this session
|
||||
if (sshStore && matchingSession?.sshRemoteConfig) {
|
||||
console.log(`[GroupChat:Debug] Applying SSH wrapping for participant ${participantName}...`);
|
||||
console.log(
|
||||
`[GroupChat:Debug] Applying SSH wrapping for participant ${participantName}...`
|
||||
);
|
||||
const sshWrapped = await wrapSpawnWithSsh(
|
||||
{
|
||||
command: spawnCommand,
|
||||
@@ -1149,6 +1152,7 @@ Review the agent responses above. Either:
|
||||
});
|
||||
const configResolution = applyAgentConfigOverrides(agent, baseArgs, {
|
||||
agentConfigValues,
|
||||
sessionCustomModel: chat.moderatorConfig?.customModel,
|
||||
sessionCustomArgs: chat.moderatorConfig?.customArgs,
|
||||
sessionCustomEnvVars: chat.moderatorConfig?.customEnvVars,
|
||||
});
|
||||
|
||||
@@ -292,7 +292,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();
|
||||
@@ -300,8 +301,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, {
|
||||
|
||||
@@ -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';
|
||||
@@ -188,6 +189,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)',
|
||||
|
||||
@@ -8433,6 +8433,7 @@ You are taking over this conversation. Based on the context above, provide a bri
|
||||
customPath?: string;
|
||||
customArgs?: string;
|
||||
customEnvVars?: Record<string, string>;
|
||||
customModel?: string;
|
||||
}
|
||||
) => {
|
||||
const chat = await window.maestro.groupChat.create(name, moderatorAgentId, moderatorConfig);
|
||||
|
||||
@@ -347,23 +347,22 @@ export function EditGroupChatModal({
|
||||
) : (
|
||||
<div className="flex items-center gap-2">
|
||||
{/* Dropdown */}
|
||||
<div className="relative flex-1">
|
||||
<div className="relative flex-1" style={{ zIndex: 10000 }}>
|
||||
<select
|
||||
value={selectedAgent || ''}
|
||||
onChange={(e) => handleAgentChange(e.target.value)}
|
||||
className="w-full px-3 py-2 pr-10 rounded-lg border outline-none appearance-none cursor-pointer text-sm"
|
||||
className="w-full px-3 py-2 pr-10 rounded-lg border outline-none appearance-none cursor-pointer text-sm relative"
|
||||
style={{
|
||||
backgroundColor: theme.colors.bgMain,
|
||||
borderColor: theme.colors.border,
|
||||
color: theme.colors.textMain,
|
||||
zIndex: 10000,
|
||||
}}
|
||||
aria-label="Select moderator agent"
|
||||
>
|
||||
{availableTiles.map((tile) => {
|
||||
const isBeta =
|
||||
tile.id === 'codex' ||
|
||||
tile.id === 'opencode' ||
|
||||
tile.id === 'factory-droid';
|
||||
tile.id === 'codex' || tile.id === 'opencode' || tile.id === 'factory-droid';
|
||||
return (
|
||||
<option key={tile.id} value={tile.id}>
|
||||
{tile.name}
|
||||
@@ -374,7 +373,7 @@ export function EditGroupChatModal({
|
||||
</select>
|
||||
<ChevronDown
|
||||
className="absolute right-3 top-1/2 -translate-y-1/2 w-4 h-4 pointer-events-none"
|
||||
style={{ color: theme.colors.textDim }}
|
||||
style={{ color: theme.colors.textDim, zIndex: 10001 }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -163,15 +163,18 @@ export function NewGroupChatModal({
|
||||
|
||||
// Build moderator config from state
|
||||
const buildModeratorConfig = useCallback((): ModeratorConfig | undefined => {
|
||||
const hasConfig = customPath || customArgs || Object.keys(customEnvVars).length > 0;
|
||||
const customModelValue = agentConfig.model;
|
||||
const hasConfig =
|
||||
customPath || customArgs || Object.keys(customEnvVars).length > 0 || customModelValue;
|
||||
if (!hasConfig) return undefined;
|
||||
|
||||
return {
|
||||
customPath: customPath || undefined,
|
||||
customArgs: customArgs || undefined,
|
||||
customEnvVars: Object.keys(customEnvVars).length > 0 ? customEnvVars : undefined,
|
||||
customModel: customModelValue || undefined,
|
||||
};
|
||||
}, [customPath, customArgs, customEnvVars]);
|
||||
}, [customPath, customArgs, customEnvVars, agentConfig]);
|
||||
|
||||
const handleCreate = useCallback(() => {
|
||||
if (name.trim() && selectedAgent) {
|
||||
@@ -337,23 +340,22 @@ export function NewGroupChatModal({
|
||||
) : (
|
||||
<div className="flex items-center gap-2">
|
||||
{/* Dropdown */}
|
||||
<div className="relative flex-1">
|
||||
<div className="relative flex-1" style={{ zIndex: 10000 }}>
|
||||
<select
|
||||
value={selectedAgent || ''}
|
||||
onChange={(e) => handleAgentChange(e.target.value)}
|
||||
className="w-full px-3 py-2 pr-10 rounded-lg border outline-none appearance-none cursor-pointer text-sm"
|
||||
className="w-full px-3 py-2 pr-10 rounded-lg border outline-none appearance-none cursor-pointer text-sm relative"
|
||||
style={{
|
||||
backgroundColor: theme.colors.bgMain,
|
||||
borderColor: theme.colors.border,
|
||||
color: theme.colors.textMain,
|
||||
zIndex: 10000,
|
||||
}}
|
||||
aria-label="Select moderator agent"
|
||||
>
|
||||
{availableTiles.map((tile) => {
|
||||
const isBeta =
|
||||
tile.id === 'codex' ||
|
||||
tile.id === 'opencode' ||
|
||||
tile.id === 'factory-droid';
|
||||
tile.id === 'codex' || tile.id === 'opencode' || tile.id === 'factory-droid';
|
||||
return (
|
||||
<option key={tile.id} value={tile.id}>
|
||||
{tile.name}
|
||||
@@ -364,7 +366,7 @@ export function NewGroupChatModal({
|
||||
</select>
|
||||
<ChevronDown
|
||||
className="absolute right-3 top-1/2 -translate-y-1/2 w-4 h-4 pointer-events-none"
|
||||
style={{ color: theme.colors.textDim }}
|
||||
style={{ color: theme.colors.textDim, zIndex: 10001 }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -73,6 +73,8 @@ export interface ModeratorConfig {
|
||||
customArgs?: string;
|
||||
/** Custom environment variables */
|
||||
customEnvVars?: Record<string, string>;
|
||||
/** Custom model selection (e.g., 'ollama/qwen3:8b') */
|
||||
customModel?: string;
|
||||
/** SSH remote config for remote execution */
|
||||
sshRemoteConfig?: {
|
||||
enabled: boolean;
|
||||
|
||||
Reference in New Issue
Block a user