fix(scripts): quote Windows shell invocations

This commit is contained in:
777genius 2026-05-26 19:46:13 +03:00
parent 58a0eb603d
commit 636beb5e42
11 changed files with 99 additions and 57 deletions

View file

@ -2,9 +2,10 @@
import path from 'node:path'; import path from 'node:path';
import process from 'node:process'; import process from 'node:process';
import { spawn } from 'node:child_process';
import { fileURLToPath } from 'node:url'; import { fileURLToPath } from 'node:url';
import { spawnWithWindowsShell } from './lib/windows-shell-spawn.mjs';
const scriptDir = path.dirname(fileURLToPath(import.meta.url)); const scriptDir = path.dirname(fileURLToPath(import.meta.url));
const repoRoot = path.resolve(scriptDir, '..'); const repoRoot = path.resolve(scriptDir, '..');
const standalonePort = process.env.STANDALONE_PORT?.trim() || '3456'; const standalonePort = process.env.STANDALONE_PORT?.trim() || '3456';
@ -13,22 +14,11 @@ const corsOrigin =
process.env.CORS_ORIGIN?.trim() || process.env.CORS_ORIGIN?.trim() ||
`http://127.0.0.1:${webPort},http://localhost:${webPort}`; `http://127.0.0.1:${webPort},http://localhost:${webPort}`;
const WINDOWS_SHELL_COMMANDS = new Set(['pnpm', 'npm', 'npx', 'yarn', 'yarnpkg', 'corepack']);
function shouldUseWindowsShell(cmd) {
if (process.platform !== 'win32') {
return false;
}
return WINDOWS_SHELL_COMMANDS.has(path.basename(cmd).toLowerCase());
}
function spawnProcess(cmd, args, env) { function spawnProcess(cmd, args, env) {
return spawn(cmd, args, { return spawnWithWindowsShell(cmd, args, {
cwd: repoRoot, cwd: repoRoot,
env: { ...process.env, ...env }, env: { ...process.env, ...env },
stdio: 'inherit', stdio: 'inherit',
shell: shouldUseWindowsShell(cmd),
}); });
} }

View file

@ -4,11 +4,12 @@ import fs from 'node:fs';
import os from 'node:os'; import os from 'node:os';
import path from 'node:path'; import path from 'node:path';
import process from 'node:process'; import process from 'node:process';
import { spawnSync } from 'node:child_process';
import { once } from 'node:events'; import { once } from 'node:events';
import readline from 'node:readline'; import readline from 'node:readline';
import { fileURLToPath } from 'node:url'; import { fileURLToPath } from 'node:url';
import { spawnSyncWithWindowsShell } from './lib/windows-shell-spawn.mjs';
const scriptDir = path.dirname(fileURLToPath(import.meta.url)); const scriptDir = path.dirname(fileURLToPath(import.meta.url));
const uiRepoRoot = path.resolve(scriptDir, '..'); const uiRepoRoot = path.resolve(scriptDir, '..');
const runtimeRepoRoot = process.env.CLAUDE_DEV_RUNTIME_ROOT?.trim() ?? ''; const runtimeRepoRoot = process.env.CLAUDE_DEV_RUNTIME_ROOT?.trim() ?? '';
@ -22,26 +23,10 @@ const scriptArgs = process.argv.slice(2);
const shouldPrintRuntimePath = scriptArgs.includes('--print-runtime-path'); const shouldPrintRuntimePath = scriptArgs.includes('--print-runtime-path');
const electronViteArgs = scriptArgs.filter((arg) => arg !== '--print-runtime-path' && arg !== '--'); const electronViteArgs = scriptArgs.filter((arg) => arg !== '--print-runtime-path' && arg !== '--');
const runtimeDisplayName = 'teams orchestrator'; const runtimeDisplayName = 'teams orchestrator';
const WINDOWS_SHELL_COMMANDS = new Set(['pnpm', 'npm', 'npx', 'yarn', 'yarnpkg', 'corepack']);
function shouldUseWindowsShell(cmd) {
if (process.platform !== 'win32') {
return false;
}
const extension = path.extname(cmd).toLowerCase();
if (extension === '.cmd' || extension === '.bat') {
return true;
}
const commandName = path.basename(cmd).toLowerCase();
return WINDOWS_SHELL_COMMANDS.has(commandName);
}
function runOrExit(cmd, args, options = {}) { function runOrExit(cmd, args, options = {}) {
const result = spawnSync(cmd, args, { const result = spawnSyncWithWindowsShell(cmd, args, {
stdio: 'inherit', stdio: 'inherit',
shell: shouldUseWindowsShell(cmd),
...options, ...options,
}); });
@ -56,9 +41,8 @@ function runOrExit(cmd, args, options = {}) {
} }
function runAndCapture(cmd, args, options = {}) { function runAndCapture(cmd, args, options = {}) {
const result = spawnSync(cmd, args, { const result = spawnSyncWithWindowsShell(cmd, args, {
encoding: 'utf8', encoding: 'utf8',
shell: shouldUseWindowsShell(cmd),
...options, ...options,
}); });

View file

@ -0,0 +1,59 @@
import { spawn, spawnSync } from 'node:child_process';
import path from 'node:path';
import process from 'node:process';
const WINDOWS_SHELL_COMMANDS = new Set(['pnpm', 'npm', 'npx', 'yarn', 'yarnpkg', 'corepack']);
export function quoteWindowsCmdArg(value) {
const text = String(value);
if (text.length === 0) {
return '""';
}
if (!/[ \t\r\n"&|<>^()%!]/.test(text)) {
return text;
}
return `"${text.replace(/%/g, '%%').replace(/(["^&|<>])/g, '^$1')}"`;
}
export function shouldUseWindowsShell(command) {
if (process.platform !== 'win32') {
return false;
}
const extension = path.extname(command).toLowerCase();
if (extension === '.cmd' || extension === '.bat') {
return true;
}
return WINDOWS_SHELL_COMMANDS.has(path.basename(command).toLowerCase());
}
function toWindowsShellCommand(command, args) {
return [command, ...args].map(quoteWindowsCmdArg).join(' ');
}
export function spawnWithWindowsShell(command, args, options = {}) {
if (!shouldUseWindowsShell(command)) {
return spawn(command, args, options);
}
const safeOptions = { ...options };
delete safeOptions.shell;
return spawn(toWindowsShellCommand(command, args), {
...safeOptions,
shell: true,
});
}
export function spawnSyncWithWindowsShell(command, args, options = {}) {
if (!shouldUseWindowsShell(command)) {
return spawnSync(command, args, options);
}
const safeOptions = { ...options };
delete safeOptions.shell;
return spawnSync(toWindowsShellCommand(command, args), {
...safeOptions,
shell: true,
});
}

View file

@ -1,11 +1,11 @@
#!/usr/bin/env node #!/usr/bin/env node
import { spawnSync } from 'node:child_process';
import path from 'node:path'; import path from 'node:path';
import process from 'node:process'; import process from 'node:process';
import { fileURLToPath } from 'node:url'; import { fileURLToPath } from 'node:url';
import { resolveLiveSmokeOrchestratorCliPath } from './lib/live-smoke-runtime.mjs'; import { resolveLiveSmokeOrchestratorCliPath } from './lib/live-smoke-runtime.mjs';
import { spawnSyncWithWindowsShell } from './lib/windows-shell-spawn.mjs';
const scriptDir = path.dirname(fileURLToPath(import.meta.url)); const scriptDir = path.dirname(fileURLToPath(import.meta.url));
const repoRoot = path.resolve(scriptDir, '..'); const repoRoot = path.resolve(scriptDir, '..');
@ -26,7 +26,7 @@ if (!env.CLAUDE_AGENT_TEAMS_ORCHESTRATOR_CLI_PATH?.trim()) {
console.log('Running agent CLI launch live smoke'); console.log('Running agent CLI launch live smoke');
console.log(`Claude runtime: ${env.CLAUDE_AGENT_TEAMS_ORCHESTRATOR_CLI_PATH}`); console.log(`Claude runtime: ${env.CLAUDE_AGENT_TEAMS_ORCHESTRATOR_CLI_PATH}`);
const result = spawnSync( const result = spawnSyncWithWindowsShell(
'pnpm', 'pnpm',
[ [
'exec', 'exec',
@ -42,7 +42,6 @@ const result = spawnSync(
cwd: repoRoot, cwd: repoRoot,
env, env,
stdio: 'inherit', stdio: 'inherit',
shell: process.platform === 'win32',
} }
); );

View file

@ -1,6 +1,5 @@
#!/usr/bin/env node #!/usr/bin/env node
import { spawnSync } from 'node:child_process';
import path from 'node:path'; import path from 'node:path';
import process from 'node:process'; import process from 'node:process';
import { fileURLToPath } from 'node:url'; import { fileURLToPath } from 'node:url';
@ -10,6 +9,7 @@ import {
exitForSkippedPreflight, exitForSkippedPreflight,
preflightOpenCodeLiveEnvironment, preflightOpenCodeLiveEnvironment,
} from './lib/opencode-live-preflight.mjs'; } from './lib/opencode-live-preflight.mjs';
import { spawnSyncWithWindowsShell } from './lib/windows-shell-spawn.mjs';
const scriptDir = path.dirname(fileURLToPath(import.meta.url)); const scriptDir = path.dirname(fileURLToPath(import.meta.url));
const repoRoot = path.resolve(scriptDir, '..'); const repoRoot = path.resolve(scriptDir, '..');
@ -40,7 +40,7 @@ console.log(`Multi-lane: ${env.OPENCODE_E2E_MIXED_RECOVERY_MULTI === '1' ? 'enab
const preflight = await preflightOpenCodeLiveEnvironment({ repoRoot }); const preflight = await preflightOpenCodeLiveEnvironment({ repoRoot });
exitForSkippedPreflight(preflight); exitForSkippedPreflight(preflight);
const result = spawnSync( const result = spawnSyncWithWindowsShell(
'pnpm', 'pnpm',
[ [
'exec', 'exec',
@ -56,7 +56,6 @@ const result = spawnSync(
cwd: repoRoot, cwd: repoRoot,
env, env,
stdio: 'inherit', stdio: 'inherit',
shell: process.platform === 'win32',
} }
); );

View file

@ -1,6 +1,5 @@
#!/usr/bin/env node #!/usr/bin/env node
import { spawnSync } from 'node:child_process';
import path from 'node:path'; import path from 'node:path';
import process from 'node:process'; import process from 'node:process';
import { fileURLToPath } from 'node:url'; import { fileURLToPath } from 'node:url';
@ -10,6 +9,7 @@ import {
exitForSkippedPreflight, exitForSkippedPreflight,
preflightOpenCodeLiveEnvironment, preflightOpenCodeLiveEnvironment,
} from './lib/opencode-live-preflight.mjs'; } from './lib/opencode-live-preflight.mjs';
import { spawnSyncWithWindowsShell } from './lib/windows-shell-spawn.mjs';
const scriptDir = path.dirname(fileURLToPath(import.meta.url)); const scriptDir = path.dirname(fileURLToPath(import.meta.url));
const repoRoot = path.resolve(scriptDir, '..'); const repoRoot = path.resolve(scriptDir, '..');
@ -46,7 +46,7 @@ console.log(`Orchestrator CLI: ${env.CLAUDE_AGENT_TEAMS_ORCHESTRATOR_CLI_PATH}`)
const preflight = await preflightOpenCodeLiveEnvironment({ repoRoot }); const preflight = await preflightOpenCodeLiveEnvironment({ repoRoot });
exitForSkippedPreflight(preflight); exitForSkippedPreflight(preflight);
const result = spawnSync( const result = spawnSyncWithWindowsShell(
'pnpm', 'pnpm',
[ [
'exec', 'exec',
@ -62,7 +62,6 @@ const result = spawnSync(
cwd: repoRoot, cwd: repoRoot,
env, env,
stdio: 'inherit', stdio: 'inherit',
shell: process.platform === 'win32',
} }
); );

View file

@ -1,6 +1,5 @@
#!/usr/bin/env node #!/usr/bin/env node
import { spawnSync } from 'node:child_process';
import path from 'node:path'; import path from 'node:path';
import process from 'node:process'; import process from 'node:process';
import { fileURLToPath } from 'node:url'; import { fileURLToPath } from 'node:url';
@ -10,6 +9,7 @@ import {
exitForSkippedPreflight, exitForSkippedPreflight,
preflightOpenCodeLiveEnvironment, preflightOpenCodeLiveEnvironment,
} from './lib/opencode-live-preflight.mjs'; } from './lib/opencode-live-preflight.mjs';
import { spawnSyncWithWindowsShell } from './lib/windows-shell-spawn.mjs';
const scriptDir = path.dirname(fileURLToPath(import.meta.url)); const scriptDir = path.dirname(fileURLToPath(import.meta.url));
const repoRoot = path.resolve(scriptDir, '..'); const repoRoot = path.resolve(scriptDir, '..');
@ -38,7 +38,7 @@ console.log(`Orchestrator CLI: ${env.CLAUDE_AGENT_TEAMS_ORCHESTRATOR_CLI_PATH}`)
const preflight = await preflightOpenCodeLiveEnvironment({ repoRoot }); const preflight = await preflightOpenCodeLiveEnvironment({ repoRoot });
exitForSkippedPreflight(preflight); exitForSkippedPreflight(preflight);
const result = spawnSync( const result = spawnSyncWithWindowsShell(
'pnpm', 'pnpm',
[ [
'exec', 'exec',
@ -54,7 +54,6 @@ const result = spawnSync(
cwd: repoRoot, cwd: repoRoot,
env, env,
stdio: 'inherit', stdio: 'inherit',
shell: process.platform === 'win32',
} }
); );

View file

@ -1,6 +1,5 @@
#!/usr/bin/env node #!/usr/bin/env node
import { spawnSync } from 'node:child_process';
import path from 'node:path'; import path from 'node:path';
import process from 'node:process'; import process from 'node:process';
import { fileURLToPath } from 'node:url'; import { fileURLToPath } from 'node:url';
@ -10,6 +9,7 @@ import {
exitForSkippedPreflight, exitForSkippedPreflight,
preflightOpenCodeLiveEnvironment, preflightOpenCodeLiveEnvironment,
} from './lib/opencode-live-preflight.mjs'; } from './lib/opencode-live-preflight.mjs';
import { spawnSyncWithWindowsShell } from './lib/windows-shell-spawn.mjs';
const scriptDir = path.dirname(fileURLToPath(import.meta.url)); const scriptDir = path.dirname(fileURLToPath(import.meta.url));
const repoRoot = path.resolve(scriptDir, '..'); const repoRoot = path.resolve(scriptDir, '..');
@ -36,7 +36,7 @@ console.log(`Orchestrator CLI: ${env.CLAUDE_AGENT_TEAMS_ORCHESTRATOR_CLI_PATH}`)
const preflight = await preflightOpenCodeLiveEnvironment({ repoRoot }); const preflight = await preflightOpenCodeLiveEnvironment({ repoRoot });
exitForSkippedPreflight(preflight); exitForSkippedPreflight(preflight);
const result = spawnSync( const result = spawnSyncWithWindowsShell(
'pnpm', 'pnpm',
[ [
'exec', 'exec',
@ -52,7 +52,6 @@ const result = spawnSync(
cwd: repoRoot, cwd: repoRoot,
env, env,
stdio: 'inherit', stdio: 'inherit',
shell: process.platform === 'win32',
} }
); );

View file

@ -1,6 +1,5 @@
#!/usr/bin/env node #!/usr/bin/env node
import { spawnSync } from 'node:child_process';
import path from 'node:path'; import path from 'node:path';
import process from 'node:process'; import process from 'node:process';
import { fileURLToPath } from 'node:url'; import { fileURLToPath } from 'node:url';
@ -10,6 +9,7 @@ import {
exitForSkippedPreflight, exitForSkippedPreflight,
preflightOpenCodeLiveEnvironment, preflightOpenCodeLiveEnvironment,
} from './lib/opencode-live-preflight.mjs'; } from './lib/opencode-live-preflight.mjs';
import { spawnSyncWithWindowsShell } from './lib/windows-shell-spawn.mjs';
const scriptDir = path.dirname(fileURLToPath(import.meta.url)); const scriptDir = path.dirname(fileURLToPath(import.meta.url));
const repoRoot = path.resolve(scriptDir, '..'); const repoRoot = path.resolve(scriptDir, '..');
@ -38,7 +38,7 @@ console.log(`Orchestrator CLI: ${env.CLAUDE_AGENT_TEAMS_ORCHESTRATOR_CLI_PATH}`)
const preflight = await preflightOpenCodeLiveEnvironment({ repoRoot }); const preflight = await preflightOpenCodeLiveEnvironment({ repoRoot });
exitForSkippedPreflight(preflight); exitForSkippedPreflight(preflight);
const result = spawnSync( const result = spawnSyncWithWindowsShell(
'pnpm', 'pnpm',
[ [
'exec', 'exec',
@ -54,7 +54,6 @@ const result = spawnSync(
cwd: repoRoot, cwd: repoRoot,
env, env,
stdio: 'inherit', stdio: 'inherit',
shell: process.platform === 'win32',
} }
); );

View file

@ -9,6 +9,7 @@ import { fileURLToPath } from 'node:url';
import { resolveLiveSmokeOrchestratorCliPath } from './lib/live-smoke-runtime.mjs'; import { resolveLiveSmokeOrchestratorCliPath } from './lib/live-smoke-runtime.mjs';
import { preflightOpenCodeLiveEnvironment } from './lib/opencode-live-preflight.mjs'; import { preflightOpenCodeLiveEnvironment } from './lib/opencode-live-preflight.mjs';
import { spawnSyncWithWindowsShell } from './lib/windows-shell-spawn.mjs';
const scriptDir = path.dirname(fileURLToPath(import.meta.url)); const scriptDir = path.dirname(fileURLToPath(import.meta.url));
const repoRoot = path.resolve(scriptDir, '..'); const repoRoot = path.resolve(scriptDir, '..');
@ -69,7 +70,7 @@ if (preflight.skipped.length > 0 && process.env.PROVIDER_LAUNCH_STRESS_STRICT ==
env.PROVIDER_LAUNCH_STRESS_ORDER = preflight.order.join(','); env.PROVIDER_LAUNCH_STRESS_ORDER = preflight.order.join(',');
console.log(`Runnable order: ${env.PROVIDER_LAUNCH_STRESS_ORDER}`); console.log(`Runnable order: ${env.PROVIDER_LAUNCH_STRESS_ORDER}`);
const result = spawnSync( const result = spawnSyncWithWindowsShell(
'pnpm', 'pnpm',
[ [
'exec', 'exec',
@ -85,7 +86,6 @@ const result = spawnSync(
cwd: repoRoot, cwd: repoRoot,
env, env,
stdio: 'inherit', stdio: 'inherit',
shell: process.platform === 'win32',
} }
); );

View file

@ -312,10 +312,25 @@ function spawnRealCli(
) { ) {
const spawnOptions = options ?? {}; const spawnOptions = options ?? {};
const needsWindowsCommandShell = process.platform === 'win32' && /\.(bat|cmd)$/i.test(command); const needsWindowsCommandShell = process.platform === 'win32' && /\.(bat|cmd)$/i.test(command);
return spawn(command, [...args], { if (needsWindowsCommandShell) {
...spawnOptions, const commandLine = [command, ...args].map(quoteWindowsCmdArg).join(' ');
...(needsWindowsCommandShell ? { shell: true } : {}), return spawn(commandLine, {
}); ...spawnOptions,
shell: true,
});
}
return spawn(command, [...args], spawnOptions);
}
function quoteWindowsCmdArg(value: string) {
if (value.length === 0) {
return '""';
}
if (!/[ \t\r\n"&|<>^()%!]/.test(value)) {
return value;
}
return `"${value.replace(/%/g, '%%').replace(/(["^&|<>])/g, '^$1')}"`;
} }
async function removeTempRoot(dirPath: string): Promise<void> { async function removeTempRoot(dirPath: string): Promise<void> {