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', () => {
|
it('should preserve existing PATH entries', () => {
|
||||||
const originalPath = process.env.PATH;
|
const originalPath = process.env.PATH;
|
||||||
const testPath = '/test/custom/path';
|
const testPath = '/test/custom/path';
|
||||||
|
|||||||
@@ -84,6 +84,11 @@ export function getExpandedEnv(): NodeJS.ProcessEnv {
|
|||||||
// Node.js
|
// Node.js
|
||||||
path.join(programFiles, 'nodejs'),
|
path.join(programFiles, 'nodejs'),
|
||||||
path.join(localAppData, 'Programs', 'node'),
|
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)
|
// Scoop package manager (OpenCode, other tools)
|
||||||
path.join(home, 'scoop', 'shims'),
|
path.join(home, 'scoop', 'shims'),
|
||||||
path.join(home, 'scoop', 'apps', 'opencode', 'current'),
|
path.join(home, 'scoop', 'apps', 'opencode', 'current'),
|
||||||
@@ -252,6 +257,9 @@ function getWindowsKnownPaths(binaryName: string): string[] {
|
|||||||
// Scoop installation (recommended for OpenCode)
|
// Scoop installation (recommended for OpenCode)
|
||||||
path.join(home, 'scoop', 'shims', 'opencode.exe'),
|
path.join(home, 'scoop', 'shims', 'opencode.exe'),
|
||||||
path.join(home, 'scoop', 'apps', 'opencode', 'current', '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
|
// Chocolatey installation
|
||||||
path.join(
|
path.join(
|
||||||
process.env.ChocolateyInstall || 'C:\\ProgramData\\chocolatey',
|
process.env.ChocolateyInstall || 'C:\\ProgramData\\chocolatey',
|
||||||
@@ -471,13 +479,16 @@ export async function checkBinaryExists(binaryName: string): Promise<BinaryDetec
|
|||||||
.filter((p) => p);
|
.filter((p) => p);
|
||||||
|
|
||||||
if (process.platform === 'win32' && matches.length > 0) {
|
if (process.platform === 'win32' && matches.length > 0) {
|
||||||
// On Windows, prefer .exe over .cmd over extensionless
|
// On Windows, prefer .exe > extensionless (shell scripts) > .cmd
|
||||||
// This helps with proper execution handling
|
// This helps avoid cmd.exe limitations and supports PowerShell/bash scripts
|
||||||
const exeMatch = matches.find((p) => p.toLowerCase().endsWith('.exe'));
|
const exeMatch = matches.find((p) => p.toLowerCase().endsWith('.exe'));
|
||||||
const cmdMatch = matches.find((p) => p.toLowerCase().endsWith('.cmd'));
|
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
|
// Return the best match: .exe > extensionless shell scripts > .cmd > first result
|
||||||
let bestMatch = exeMatch || cmdMatch || matches[0];
|
let bestMatch = exeMatch || extensionlessMatch || cmdMatch || matches[0];
|
||||||
|
|
||||||
// If the first match doesn't have an extension, check if .cmd or .exe version exists
|
// 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
|
// This handles cases where 'where' returns a path without extension
|
||||||
|
|||||||
@@ -452,6 +452,7 @@ ${message}`;
|
|||||||
});
|
});
|
||||||
const configResolution = applyAgentConfigOverrides(agent, baseArgs, {
|
const configResolution = applyAgentConfigOverrides(agent, baseArgs, {
|
||||||
agentConfigValues,
|
agentConfigValues,
|
||||||
|
sessionCustomModel: chat.moderatorConfig?.customModel,
|
||||||
sessionCustomArgs: chat.moderatorConfig?.customArgs,
|
sessionCustomArgs: chat.moderatorConfig?.customArgs,
|
||||||
sessionCustomEnvVars: chat.moderatorConfig?.customEnvVars,
|
sessionCustomEnvVars: chat.moderatorConfig?.customEnvVars,
|
||||||
});
|
});
|
||||||
@@ -849,7 +850,9 @@ export async function routeModeratorResponse(
|
|||||||
|
|
||||||
// Apply SSH wrapping if configured for this session
|
// Apply SSH wrapping if configured for this session
|
||||||
if (sshStore && matchingSession?.sshRemoteConfig) {
|
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(
|
const sshWrapped = await wrapSpawnWithSsh(
|
||||||
{
|
{
|
||||||
command: spawnCommand,
|
command: spawnCommand,
|
||||||
@@ -1149,6 +1152,7 @@ Review the agent responses above. Either:
|
|||||||
});
|
});
|
||||||
const configResolution = applyAgentConfigOverrides(agent, baseArgs, {
|
const configResolution = applyAgentConfigOverrides(agent, baseArgs, {
|
||||||
agentConfigValues,
|
agentConfigValues,
|
||||||
|
sessionCustomModel: chat.moderatorConfig?.customModel,
|
||||||
sessionCustomArgs: chat.moderatorConfig?.customArgs,
|
sessionCustomArgs: chat.moderatorConfig?.customArgs,
|
||||||
sessionCustomEnvVars: chat.moderatorConfig?.customEnvVars,
|
sessionCustomEnvVars: chat.moderatorConfig?.customEnvVars,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -292,7 +292,8 @@ export function registerProcessHandlers(deps: ProcessHandlerDependencies): void
|
|||||||
) as Record<string, string>;
|
) as Record<string, string>;
|
||||||
|
|
||||||
// Determine an explicit shell to use when forcing shell execution on Windows.
|
// 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;
|
const customShellPath = settingsStore.get('customShellPath', '') as string;
|
||||||
if (customShellPath && customShellPath.trim()) {
|
if (customShellPath && customShellPath.trim()) {
|
||||||
shellToUse = customShellPath.trim();
|
shellToUse = customShellPath.trim();
|
||||||
@@ -300,8 +301,20 @@ export function registerProcessHandlers(deps: ProcessHandlerDependencies): void
|
|||||||
customShellPath: shellToUse,
|
customShellPath: shellToUse,
|
||||||
});
|
});
|
||||||
} else if (!shellToUse) {
|
} else if (!shellToUse) {
|
||||||
// Use COMSPEC if available, otherwise default to cmd.exe
|
// Try PowerShell if available (common on modern Windows)
|
||||||
shellToUse = process.env.ComSpec || 'cmd.exe';
|
// 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, {
|
logger.info(`Forcing shell execution for agent on Windows for PATH access`, LOG_CONTEXT, {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
import { spawn } from 'child_process';
|
import { spawn } from 'child_process';
|
||||||
import { EventEmitter } from 'events';
|
import { EventEmitter } from 'events';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
|
import * as fs from 'fs';
|
||||||
import { logger } from '../../utils/logger';
|
import { logger } from '../../utils/logger';
|
||||||
import { getOutputParser } from '../../parsers';
|
import { getOutputParser } from '../../parsers';
|
||||||
import { getAgentCapabilities } from '../../agents';
|
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) {
|
if (isWindows && useShell) {
|
||||||
logger.debug(
|
logger.debug(
|
||||||
'[ProcessManager] Forcing shell=true for agent spawn on Windows (runInShell or auto)',
|
'[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;
|
customPath?: string;
|
||||||
customArgs?: string;
|
customArgs?: string;
|
||||||
customEnvVars?: Record<string, string>;
|
customEnvVars?: Record<string, string>;
|
||||||
|
customModel?: string;
|
||||||
}
|
}
|
||||||
) => {
|
) => {
|
||||||
const chat = await window.maestro.groupChat.create(name, moderatorAgentId, moderatorConfig);
|
const chat = await window.maestro.groupChat.create(name, moderatorAgentId, moderatorConfig);
|
||||||
|
|||||||
@@ -347,23 +347,22 @@ export function EditGroupChatModal({
|
|||||||
) : (
|
) : (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
{/* Dropdown */}
|
{/* Dropdown */}
|
||||||
<div className="relative flex-1">
|
<div className="relative flex-1" style={{ zIndex: 10000 }}>
|
||||||
<select
|
<select
|
||||||
value={selectedAgent || ''}
|
value={selectedAgent || ''}
|
||||||
onChange={(e) => handleAgentChange(e.target.value)}
|
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={{
|
style={{
|
||||||
backgroundColor: theme.colors.bgMain,
|
backgroundColor: theme.colors.bgMain,
|
||||||
borderColor: theme.colors.border,
|
borderColor: theme.colors.border,
|
||||||
color: theme.colors.textMain,
|
color: theme.colors.textMain,
|
||||||
|
zIndex: 10000,
|
||||||
}}
|
}}
|
||||||
aria-label="Select moderator agent"
|
aria-label="Select moderator agent"
|
||||||
>
|
>
|
||||||
{availableTiles.map((tile) => {
|
{availableTiles.map((tile) => {
|
||||||
const isBeta =
|
const isBeta =
|
||||||
tile.id === 'codex' ||
|
tile.id === 'codex' || tile.id === 'opencode' || tile.id === 'factory-droid';
|
||||||
tile.id === 'opencode' ||
|
|
||||||
tile.id === 'factory-droid';
|
|
||||||
return (
|
return (
|
||||||
<option key={tile.id} value={tile.id}>
|
<option key={tile.id} value={tile.id}>
|
||||||
{tile.name}
|
{tile.name}
|
||||||
@@ -374,7 +373,7 @@ export function EditGroupChatModal({
|
|||||||
</select>
|
</select>
|
||||||
<ChevronDown
|
<ChevronDown
|
||||||
className="absolute right-3 top-1/2 -translate-y-1/2 w-4 h-4 pointer-events-none"
|
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>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -163,15 +163,18 @@ export function NewGroupChatModal({
|
|||||||
|
|
||||||
// Build moderator config from state
|
// Build moderator config from state
|
||||||
const buildModeratorConfig = useCallback((): ModeratorConfig | undefined => {
|
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;
|
if (!hasConfig) return undefined;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
customPath: customPath || undefined,
|
customPath: customPath || undefined,
|
||||||
customArgs: customArgs || undefined,
|
customArgs: customArgs || undefined,
|
||||||
customEnvVars: Object.keys(customEnvVars).length > 0 ? customEnvVars : undefined,
|
customEnvVars: Object.keys(customEnvVars).length > 0 ? customEnvVars : undefined,
|
||||||
|
customModel: customModelValue || undefined,
|
||||||
};
|
};
|
||||||
}, [customPath, customArgs, customEnvVars]);
|
}, [customPath, customArgs, customEnvVars, agentConfig]);
|
||||||
|
|
||||||
const handleCreate = useCallback(() => {
|
const handleCreate = useCallback(() => {
|
||||||
if (name.trim() && selectedAgent) {
|
if (name.trim() && selectedAgent) {
|
||||||
@@ -337,23 +340,22 @@ export function NewGroupChatModal({
|
|||||||
) : (
|
) : (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
{/* Dropdown */}
|
{/* Dropdown */}
|
||||||
<div className="relative flex-1">
|
<div className="relative flex-1" style={{ zIndex: 10000 }}>
|
||||||
<select
|
<select
|
||||||
value={selectedAgent || ''}
|
value={selectedAgent || ''}
|
||||||
onChange={(e) => handleAgentChange(e.target.value)}
|
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={{
|
style={{
|
||||||
backgroundColor: theme.colors.bgMain,
|
backgroundColor: theme.colors.bgMain,
|
||||||
borderColor: theme.colors.border,
|
borderColor: theme.colors.border,
|
||||||
color: theme.colors.textMain,
|
color: theme.colors.textMain,
|
||||||
|
zIndex: 10000,
|
||||||
}}
|
}}
|
||||||
aria-label="Select moderator agent"
|
aria-label="Select moderator agent"
|
||||||
>
|
>
|
||||||
{availableTiles.map((tile) => {
|
{availableTiles.map((tile) => {
|
||||||
const isBeta =
|
const isBeta =
|
||||||
tile.id === 'codex' ||
|
tile.id === 'codex' || tile.id === 'opencode' || tile.id === 'factory-droid';
|
||||||
tile.id === 'opencode' ||
|
|
||||||
tile.id === 'factory-droid';
|
|
||||||
return (
|
return (
|
||||||
<option key={tile.id} value={tile.id}>
|
<option key={tile.id} value={tile.id}>
|
||||||
{tile.name}
|
{tile.name}
|
||||||
@@ -364,7 +366,7 @@ export function NewGroupChatModal({
|
|||||||
</select>
|
</select>
|
||||||
<ChevronDown
|
<ChevronDown
|
||||||
className="absolute right-3 top-1/2 -translate-y-1/2 w-4 h-4 pointer-events-none"
|
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>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -73,6 +73,8 @@ export interface ModeratorConfig {
|
|||||||
customArgs?: string;
|
customArgs?: string;
|
||||||
/** Custom environment variables */
|
/** Custom environment variables */
|
||||||
customEnvVars?: Record<string, string>;
|
customEnvVars?: Record<string, string>;
|
||||||
|
/** Custom model selection (e.g., 'ollama/qwen3:8b') */
|
||||||
|
customModel?: string;
|
||||||
/** SSH remote config for remote execution */
|
/** SSH remote config for remote execution */
|
||||||
sshRemoteConfig?: {
|
sshRemoteConfig?: {
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
|
|||||||
Reference in New Issue
Block a user