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 process from 'node:process';
import { spawn } from 'node:child_process';
import { fileURLToPath } from 'node:url';
import { spawnWithWindowsShell } from './lib/windows-shell-spawn.mjs';
const scriptDir = path.dirname(fileURLToPath(import.meta.url));
const repoRoot = path.resolve(scriptDir, '..');
const standalonePort = process.env.STANDALONE_PORT?.trim() || '3456';
@ -13,22 +14,11 @@ const corsOrigin =
process.env.CORS_ORIGIN?.trim() ||
`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) {
return spawn(cmd, args, {
return spawnWithWindowsShell(cmd, args, {
cwd: repoRoot,
env: { ...process.env, ...env },
stdio: 'inherit',
shell: shouldUseWindowsShell(cmd),
});
}

View file

@ -4,11 +4,12 @@ import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import process from 'node:process';
import { spawnSync } from 'node:child_process';
import { once } from 'node:events';
import readline from 'node:readline';
import { fileURLToPath } from 'node:url';
import { spawnSyncWithWindowsShell } from './lib/windows-shell-spawn.mjs';
const scriptDir = path.dirname(fileURLToPath(import.meta.url));
const uiRepoRoot = path.resolve(scriptDir, '..');
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 electronViteArgs = scriptArgs.filter((arg) => arg !== '--print-runtime-path' && arg !== '--');
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 = {}) {
const result = spawnSync(cmd, args, {
const result = spawnSyncWithWindowsShell(cmd, args, {
stdio: 'inherit',
shell: shouldUseWindowsShell(cmd),
...options,
});
@ -56,9 +41,8 @@ function runOrExit(cmd, args, options = {}) {
}
function runAndCapture(cmd, args, options = {}) {
const result = spawnSync(cmd, args, {
const result = spawnSyncWithWindowsShell(cmd, args, {
encoding: 'utf8',
shell: shouldUseWindowsShell(cmd),
...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
import { spawnSync } from 'node:child_process';
import path from 'node:path';
import process from 'node:process';
import { fileURLToPath } from 'node:url';
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 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(`Claude runtime: ${env.CLAUDE_AGENT_TEAMS_ORCHESTRATOR_CLI_PATH}`);
const result = spawnSync(
const result = spawnSyncWithWindowsShell(
'pnpm',
[
'exec',
@ -42,7 +42,6 @@ const result = spawnSync(
cwd: repoRoot,
env,
stdio: 'inherit',
shell: process.platform === 'win32',
}
);

View file

@ -1,6 +1,5 @@
#!/usr/bin/env node
import { spawnSync } from 'node:child_process';
import path from 'node:path';
import process from 'node:process';
import { fileURLToPath } from 'node:url';
@ -10,6 +9,7 @@ import {
exitForSkippedPreflight,
preflightOpenCodeLiveEnvironment,
} from './lib/opencode-live-preflight.mjs';
import { spawnSyncWithWindowsShell } from './lib/windows-shell-spawn.mjs';
const scriptDir = path.dirname(fileURLToPath(import.meta.url));
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 });
exitForSkippedPreflight(preflight);
const result = spawnSync(
const result = spawnSyncWithWindowsShell(
'pnpm',
[
'exec',
@ -56,7 +56,6 @@ const result = spawnSync(
cwd: repoRoot,
env,
stdio: 'inherit',
shell: process.platform === 'win32',
}
);

View file

@ -1,6 +1,5 @@
#!/usr/bin/env node
import { spawnSync } from 'node:child_process';
import path from 'node:path';
import process from 'node:process';
import { fileURLToPath } from 'node:url';
@ -10,6 +9,7 @@ import {
exitForSkippedPreflight,
preflightOpenCodeLiveEnvironment,
} from './lib/opencode-live-preflight.mjs';
import { spawnSyncWithWindowsShell } from './lib/windows-shell-spawn.mjs';
const scriptDir = path.dirname(fileURLToPath(import.meta.url));
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 });
exitForSkippedPreflight(preflight);
const result = spawnSync(
const result = spawnSyncWithWindowsShell(
'pnpm',
[
'exec',
@ -62,7 +62,6 @@ const result = spawnSync(
cwd: repoRoot,
env,
stdio: 'inherit',
shell: process.platform === 'win32',
}
);

View file

@ -1,6 +1,5 @@
#!/usr/bin/env node
import { spawnSync } from 'node:child_process';
import path from 'node:path';
import process from 'node:process';
import { fileURLToPath } from 'node:url';
@ -10,6 +9,7 @@ import {
exitForSkippedPreflight,
preflightOpenCodeLiveEnvironment,
} from './lib/opencode-live-preflight.mjs';
import { spawnSyncWithWindowsShell } from './lib/windows-shell-spawn.mjs';
const scriptDir = path.dirname(fileURLToPath(import.meta.url));
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 });
exitForSkippedPreflight(preflight);
const result = spawnSync(
const result = spawnSyncWithWindowsShell(
'pnpm',
[
'exec',
@ -54,7 +54,6 @@ const result = spawnSync(
cwd: repoRoot,
env,
stdio: 'inherit',
shell: process.platform === 'win32',
}
);

View file

@ -1,6 +1,5 @@
#!/usr/bin/env node
import { spawnSync } from 'node:child_process';
import path from 'node:path';
import process from 'node:process';
import { fileURLToPath } from 'node:url';
@ -10,6 +9,7 @@ import {
exitForSkippedPreflight,
preflightOpenCodeLiveEnvironment,
} from './lib/opencode-live-preflight.mjs';
import { spawnSyncWithWindowsShell } from './lib/windows-shell-spawn.mjs';
const scriptDir = path.dirname(fileURLToPath(import.meta.url));
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 });
exitForSkippedPreflight(preflight);
const result = spawnSync(
const result = spawnSyncWithWindowsShell(
'pnpm',
[
'exec',
@ -52,7 +52,6 @@ const result = spawnSync(
cwd: repoRoot,
env,
stdio: 'inherit',
shell: process.platform === 'win32',
}
);

View file

@ -1,6 +1,5 @@
#!/usr/bin/env node
import { spawnSync } from 'node:child_process';
import path from 'node:path';
import process from 'node:process';
import { fileURLToPath } from 'node:url';
@ -10,6 +9,7 @@ import {
exitForSkippedPreflight,
preflightOpenCodeLiveEnvironment,
} from './lib/opencode-live-preflight.mjs';
import { spawnSyncWithWindowsShell } from './lib/windows-shell-spawn.mjs';
const scriptDir = path.dirname(fileURLToPath(import.meta.url));
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 });
exitForSkippedPreflight(preflight);
const result = spawnSync(
const result = spawnSyncWithWindowsShell(
'pnpm',
[
'exec',
@ -54,7 +54,6 @@ const result = spawnSync(
cwd: repoRoot,
env,
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 { preflightOpenCodeLiveEnvironment } from './lib/opencode-live-preflight.mjs';
import { spawnSyncWithWindowsShell } from './lib/windows-shell-spawn.mjs';
const scriptDir = path.dirname(fileURLToPath(import.meta.url));
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(',');
console.log(`Runnable order: ${env.PROVIDER_LAUNCH_STRESS_ORDER}`);
const result = spawnSync(
const result = spawnSyncWithWindowsShell(
'pnpm',
[
'exec',
@ -85,7 +86,6 @@ const result = spawnSync(
cwd: repoRoot,
env,
stdio: 'inherit',
shell: process.platform === 'win32',
}
);

View file

@ -312,10 +312,25 @@ function spawnRealCli(
) {
const spawnOptions = options ?? {};
const needsWindowsCommandShell = process.platform === 'win32' && /\.(bat|cmd)$/i.test(command);
return spawn(command, [...args], {
...spawnOptions,
...(needsWindowsCommandShell ? { shell: true } : {}),
});
if (needsWindowsCommandShell) {
const commandLine = [command, ...args].map(quoteWindowsCmdArg).join(' ');
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> {