Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 33 additions & 21 deletions npm-shrinkwrap.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@
"@opentelemetry/sdk-metrics": "^2.6.1",
"@smithy/shared-ini-file-loader": "^4.4.2",
"commander": "^14.0.2",
"cross-spawn": "^7.0.6",
"dotenv": "^17.2.3",
"fast-json-stable-stringify": "^2.1.0",
"fflate": "^0.8.2",
Expand Down Expand Up @@ -132,6 +133,7 @@
"@playwright/test": "^1.59.1",
"@secretlint/secretlint-rule-preset-recommend": "^12.2.0",
"@trivago/prettier-plugin-sort-imports": "^6.0.2",
"@types/cross-spawn": "^6.0.6",
"@types/js-yaml": "^4.0.9",
"@types/node": "^25.0.3",
"@types/react": "^19.2.7",
Expand Down
3 changes: 1 addition & 2 deletions src/cli/operations/init/files.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,7 @@ export interface InitGitRepoResult {
* Skips if already in a git repo or if git is not available.
*/
export async function initGitRepo(projectRoot: string): Promise<InitGitRepoResult> {
// All git commands use shell: false to avoid Windows cmd argument parsing issues
const gitOptions = { cwd: projectRoot, stdio: 'pipe' as const, shell: false };
const gitOptions = { cwd: projectRoot, stdio: 'pipe' as const };

// Check if git is available
const gitCheck = await runSubprocessCapture('git', ['--version'], gitOptions);
Expand Down
67 changes: 10 additions & 57 deletions src/lib/utils/subprocess.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { isWindows } from './platform';
import { spawn, spawnSync } from 'child_process';
import type { StdioOptions } from 'child_process';
import crossSpawn from 'cross-spawn';

/**
* Subprocess utilities for AgentCore.
Expand All @@ -11,38 +10,21 @@ import type { StdioOptions } from 'child_process';
* Sync functions (runSubprocessCaptureSync, checkSubprocessSync) block the event loop
* and are ONLY safe in CDK bundling contexts (which run in a subprocess). They are
* intentionally NOT exported from the public API to prevent accidental UI freezes.
*
* Nothing spawns through a shell. cross-spawn resolves Windows .cmd/.bat wrappers and
* escapes arguments so a metacharacter in an argument (an `&` in a path, say) reaches
* the child intact instead of being interpreted by cmd.exe.
*/

export interface SubprocessOptions {
cwd?: string;
env?: NodeJS.ProcessEnv;
stdio?: StdioOptions;
shell?: boolean;
}

/**
* When shell mode is enabled, merge args into the command string so that
* Node.js does not receive both a non-empty args array and `shell: true`.
* Passing both triggers DEP0190 on Node ≥ 22 (and a warning on earlier
* versions) because the arguments are concatenated without escaping.
*/
function resolveCommand(command: string, args: string[], useShell: boolean): { cmd: string; cmdArgs: string[] } {
if (useShell) {
return { cmd: [command, ...args].join(' '), cmdArgs: [] };
}
return { cmd: command, cmdArgs: args };
}

export async function runSubprocess(command: string, args: string[], options: SubprocessOptions = {}): Promise<void> {
const shell = options.shell ?? isWindows;
const { cmd, cmdArgs } = resolveCommand(command, args, shell);
return new Promise((resolve, reject) => {
const child = spawn(cmd, cmdArgs, {
cwd: options.cwd,
env: options.env,
stdio: options.stdio ?? 'inherit',
shell,
});
const child = crossSpawn(command, args, { ...options, stdio: options.stdio ?? 'inherit' });

child.on('error', reject);
child.on('close', (code, signal) => {
Expand All @@ -61,15 +43,8 @@ export async function checkSubprocess(
args: string[],
options: SubprocessOptions = {}
): Promise<boolean> {
const shell = options.shell ?? isWindows;
const { cmd, cmdArgs } = resolveCommand(command, args, shell);
return new Promise(resolve => {
const child = spawn(cmd, cmdArgs, {
cwd: options.cwd,
env: options.env,
stdio: options.stdio ?? 'ignore',
shell,
});
const child = crossSpawn(command, args, { ...options, stdio: options.stdio ?? 'ignore' });

child.on('error', () => resolve(false));
child.on('close', code => resolve(code === 0));
Expand All @@ -88,15 +63,8 @@ export async function runSubprocessCapture(
args: string[],
options: SubprocessOptions = {}
): Promise<SubprocessResult> {
const shell = options.shell ?? isWindows;
const { cmd, cmdArgs } = resolveCommand(command, args, shell);
return new Promise(resolve => {
const child = spawn(cmd, cmdArgs, {
cwd: options.cwd,
env: options.env,
stdio: 'pipe',
shell,
});
const child = crossSpawn(command, args, { ...options, stdio: 'pipe' });
Comment thread
tejaskash marked this conversation as resolved.
Dismissed

let stdout = '';
let stderr = '';
Expand Down Expand Up @@ -124,15 +92,7 @@ export function runSubprocessCaptureSync(
args: string[],
options: SubprocessOptions = {}
): SubprocessResult {
const shell = options.shell ?? isWindows;
const { cmd, cmdArgs } = resolveCommand(command, args, shell);
const result = spawnSync(cmd, cmdArgs, {
cwd: options.cwd,
env: options.env,
stdio: 'pipe',
shell,
encoding: 'utf-8',
});
const result = crossSpawn.sync(command, args, { ...options, stdio: 'pipe', encoding: 'utf-8' });
Comment thread
tejaskash marked this conversation as resolved.
Dismissed

return {
stdout: result.stdout ?? '',
Expand All @@ -143,15 +103,8 @@ export function runSubprocessCaptureSync(
}

export function checkSubprocessSync(command: string, args: string[], options: SubprocessOptions = {}): boolean {
const shell = options.shell ?? isWindows;
const { cmd, cmdArgs } = resolveCommand(command, args, shell);
try {
const result = spawnSync(cmd, cmdArgs, {
cwd: options.cwd,
env: options.env,
stdio: options.stdio ?? 'ignore',
shell,
});
const result = crossSpawn.sync(command, args, { ...options, stdio: options.stdio ?? 'ignore' });
return result.status === 0;
} catch {
return false;
Expand Down
Loading