Skip to content
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Security

- Claude plugin marketplace/install/update/uninstall in `bin/cli.js` now spawn via `execFileSync` with an argv array, so no plugin ID reaches a shell (#388).
- The cmd.exe argument guard in `bin/cli.js` ended in `$`, which in JavaScript also matches before a trailing newline - so an id like `core@agentsys\n` passed as safe, and whatever followed the newline was dropped by cmd.exe rather than checked. It now asserts end of input. `planShimSpawn` refuses CR and LF in an argument for the same reason.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is an auto review done by revuto.


This ### Security entry documents a vulnerability that did not exist, and states an incorrect fact about JavaScript regex semantics.

ended in $, which in JavaScript also matches before a trailing newline - so an id like core@agentsys\n passed as safe

Without the m flag, JS $ matches only at end of Input (ECMAScript Assertion :: $). core@agentsys\n was rejected by the base regex:

"agentsys-core@agentsys\n"   /^[A-Za-z0-9@._:\\/+-]+$/ deems safe? false
"a\n&calc"                   /^[A-Za-z0-9@._:\\/+-]+$/ deems safe? false

A published ### Security note asserting a bypass that was never reachable is worth correcting before release — it pollutes downstream security triage, and readers will take the JS-$ claim at face value.

The second half of the sentence is fine and worth keeping: planShimSpawn genuinely does need the CR/LF refusal, because unlike bin/cli.js it has no allowlist and would otherwise let 'bench\ncalc' through to the cmd.exe command line. Suggest demoting this to ### Fixed, scoping it to the planShimSpawn CR/LF guard plus the quoteForCmd ReDoS fix (which is real — CodeQL alert 111), and dropping the bin/cli.js bypass claim.


### Fixed

- Windows: the Claude Code executable is now resolved from `where.exe` instead of assuming the npm shim. `execFileSync` does not apply PATHEXT, and the previous hardcoded `claude.cmd` did not exist for native-installer users who have `claude.exe` - those calls raised `ENOENT`, the error was swallowed, and the CLI reported success while installing nothing. A directly launchable `claude.exe` is preferred over a batch shim regardless of PATH order.
- Windows: an npm-global `claude.cmd` shim is launched through `cmd.exe` rather than handed to `execFileSync`, which fails with `EINVAL` - Node's `src` has disallowed direct `.bat`/`.cmd` spawning since the CVE-2024-27980 fix in 18.20.2 / 20.12.2 / 21.7.3. Arguments are rejected unless they are free of whitespace and shell metacharacters, so the extra hop cannot reintroduce the injection surface #388 closed.
- `agentsys install` no longer reports `[OK] Installed ... successfully` when Claude Code rejected a plugin. It names the plugins that failed, with the errno when the shim could not be spawned at all, and how to retry. The same now applies when the `claude` CLI is not on PATH at all (`~/.claude` alone was enough to mark the platform as installed) and when a dependency id would be rejected. Such a plugin is also no longer recorded against `claude` in `installed.json` - `agentsys list` and `agentsys remove` read those platforms back - though a registration recorded by an earlier successful install is preserved, since a failed re-install is not evidence the first one never landed. The process now exits non-zero, matching the `--tool` path so `agentsys install x && ...` stops.
- Windows: `agentsys-dev test`, `runBenchmark`, and `runProfiling` all handed a `.cmd` shim straight to `spawnSync`/`execFileSync`, which fails with `EINVAL` for the same reason the Claude shim did - `resolveExecutableForPlatform` turns `npm` into `npm.cmd` and `node_modules/.bin/vitest` into `vitest.cmd`, so every benchmark, profile, and dev test run died on Windows. These now route through `cmd.exe` via a shared `planShimSpawn` in `lib/utils/command-parser.js`, which `bin/cli.js` uses as well so the two mechanisms cannot drift apart. Unlike the Claude call sites, these carry user-written commands, so arguments are quoted for `cmd.exe` rather than rejected for containing whitespace or metacharacters; a literal `"` or `%` is still refused, since neither survives `cmd.exe` intact.
- `lib/utils/command-parser.js` used a raw null byte where `'\0'` was intended. Git and grep classified the file as binary, so changes to it could not be reviewed as a diff. Also normalized to LF, the only CRLF-encoded source file in the repo.

## [6.0.1] - 2026-07-22
Expand Down
10 changes: 7 additions & 3 deletions __tests__/cli-args.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -229,8 +229,8 @@ describe('claudeSpawnPlan', () => {
const args = ['plugin', 'install', 'agentsys-core@agentsys'];

test('spawns a posix or native executable directly', () => {
expect(claudeSpawnPlan('claude', args)).toEqual({ file: 'claude', args });
expect(claudeSpawnPlan('C:\\bin\\claude.exe', args)).toEqual({ file: 'C:\\bin\\claude.exe', args });
expect(claudeSpawnPlan('claude', args)).toEqual({ file: 'claude', args, verbatim: false });
expect(claudeSpawnPlan('C:\\bin\\claude.exe', args)).toEqual({ file: 'C:\\bin\\claude.exe', args, verbatim: false });
});

test('routes a batch shim through cmd.exe, which execFileSync cannot spawn', () => {
Expand All @@ -239,7 +239,7 @@ describe('claudeSpawnPlan', () => {
const shim = 'C:\\npm\\claude.cmd';
expect(claudeSpawnPlan(shim, args)).toEqual({
file: 'cmd.exe',
args: ['/d', '/s', '/c', '""C:\\npm\\claude.cmd" plugin install agentsys-core@agentsys"'],
args: ['/d', '/s', '/c', '""C:\\npm\\claude.cmd" "plugin" "install" "agentsys-core@agentsys""'],
verbatim: true
});
expect(claudeSpawnPlan('C:\\npm\\claude.bat', args).file).toBe('cmd.exe');
Expand All @@ -257,6 +257,10 @@ describe('claudeSpawnPlan', () => {
expect(() => claudeSpawnPlan('claude.cmd', ['plugin', 'install', 'a|b'])).toThrow(/Refusing to pass/);
expect(() => claudeSpawnPlan('claude.cmd', ['plugin', 'install', 'a b'])).toThrow(/Refusing to pass/);
expect(() => claudeSpawnPlan('claude.cmd', ['plugin', 'install', 'a"b'])).toThrow(/Refusing to pass/);
// JavaScript's $ also matches before a trailing newline, so the guard has to
// assert end of input or 'id\n' - and 'id\n&calc' - would pass as safe.
expect(() => claudeSpawnPlan('claude.cmd', ['plugin', 'install', 'agentsys-core@agentsys\n'])).toThrow(/Refusing to pass/);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is an auto review done by revuto.


These two cases don't pin the behaviour the comment describes — they pass identically against the pre-PR regex, so they'd stay green if (?![\s\S]) were reverted to $:

BASE /^[A-Za-z0-9@._:\\/+-]+$/ on "agentsys-core@agentsys\n" -> no match -> claudeSpawnPlan throws
BASE /^[A-Za-z0-9@._:\\/+-]+$/ on "a\n&calc"                 -> no match -> claudeSpawnPlan throws

The assertions themselves are good additions — CR/LF in a plugin id should be refused, and locking that down is worthwhile. It's the comment on lines 260-261 that's wrong: JS $ does not match before a trailing newline unless the regex carries m. Since the comment explains why the test exists, a future reader will conclude the anchor style is load-bearing when it isn't. Recommend rewording to just state the invariant ("a plugin id containing CR or LF is refused") without the $ rationale.

expect(() => claudeSpawnPlan('claude.cmd', ['plugin', 'install', 'a\n&calc'])).toThrow(/Refusing to pass/);
});

test('passes the same arguments through unchecked when no shell is involved', () => {
Expand Down
75 changes: 74 additions & 1 deletion __tests__/command-parser.test.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
const fs = require('fs');
const path = require('path');
const { parseCommand, resolveExecutableForPlatform } = require('../lib/utils/command-parser');
const { parseCommand, resolveExecutableForPlatform, planShimSpawn, shimSpawnOptions } = require('../lib/utils/command-parser');

describe('command parser', () => {
test('parses simple command into executable and args', () => {
Expand Down Expand Up @@ -70,6 +70,79 @@ describe('resolveExecutableForPlatform', () => {
});
});

describe('planShimSpawn', () => {
test('leaves a directly spawnable executable alone', () => {
const args = ['run', 'bench'];
expect(planShimSpawn('npm', args)).toEqual({ file: 'npm', args, verbatim: false });
expect(planShimSpawn('node.exe', args)).toEqual({ file: 'node.exe', args, verbatim: false });
expect(planShimSpawn('/usr/bin/node', args)).toEqual({ file: '/usr/bin/node', args, verbatim: false });
});

test('routes a batch shim through cmd.exe, which spawn cannot launch', () => {
// Node's src disallows direct .bat/.cmd spawning since the CVE-2024-27980
// fix, so spawnSync/execFileSync fail with EINVAL on a shim.
expect(planShimSpawn('npm.cmd', ['run', 'bench'])).toEqual({
file: 'cmd.exe',
args: ['/d', '/s', '/c', '""npm.cmd" "run" "bench""'],
verbatim: true
});
expect(planShimSpawn('yarn.bat', []).args).toEqual(['/d', '/s', '/c', '""yarn.bat""']);
});

test('quotes arguments so cmd.exe cannot reinterpret them', () => {
// Inside double quotes cmd.exe leaves these alone, so a benchmark command
// carrying them runs instead of being split into extra commands.
const [, , , payload] = planShimSpawn('npm.cmd', ['run', 'a && calc', 'x|y', 'a>b']).args;
expect(payload).toBe('""npm.cmd" "run" "a && calc" "x|y" "a>b""');
});

test('preserves an empty argument', () => {
const [, , , payload] = planShimSpawn('npm.cmd', ['run', '']).args;
expect(payload).toBe('""npm.cmd" "run" """');
});

test('doubles trailing backslashes so the closing quote survives', () => {
// "C:\dir\" would read as an escaped quote when the child parses argv back.
const [, , , payload] = planShimSpawn('npm.cmd', ['--cwd', 'C:\\dir\\']).args;
expect(payload).toBe('""npm.cmd" "--cwd" "C:\\dir\\\\""');
});

test('doubles a long run of backslashes without backtracking', () => {
const [, , , payload] = planShimSpawn('npm.cmd', ['\\'.repeat(5000)]).args;
expect(payload).toBe(`""npm.cmd" "${'\\'.repeat(10000)}""`);
});

test('refuses arguments cmd.exe cannot carry faithfully', () => {
// % is expanded even inside quotes, and a literal " ends the quoting.
expect(() => planShimSpawn('npm.cmd', ['run', '%PATH%'])).toThrow(/not representable/);
expect(() => planShimSpawn('npm.cmd', ['run', 'say "hi"'])).toThrow(/not representable/);
// A newline ends the command line, so what follows is dropped or run alone.
expect(() => planShimSpawn('npm.cmd', ['run', 'bench\ncalc'])).toThrow(/not representable/);
expect(() => planShimSpawn('npm.cmd', ['run', 'bench\r'])).toThrow(/not representable/);
expect(() => planShimSpawn('npm.cmd', ['run', 'a\0b'])).toThrow(/null byte/);
expect(() => planShimSpawn('npm.cmd', ['run', 42])).toThrow(/must be a string/);
});

test('leaves those arguments alone when no shell is involved', () => {
// Nothing reparses an execFileSync argv, so the restriction is shim-only.
expect(planShimSpawn('npm', ['run', '%PATH%', 'say "hi"']).args)
.toEqual(['run', '%PATH%', 'say "hi"']);
});

test('honours an explicit comspec', () => {
expect(planShimSpawn('npm.cmd', [], { comspec: 'C:\\Windows\\system32\\cmd.exe' }).file)
.toBe('C:\\Windows\\system32\\cmd.exe');
});

test('shimSpawnOptions adds windowsVerbatimArguments only for a shim', () => {
const plan = planShimSpawn('npm.cmd', ['run']);
expect(shimSpawnOptions(plan, { cwd: '/tmp' }))
.toEqual({ cwd: '/tmp', windowsVerbatimArguments: true });
expect(shimSpawnOptions(planShimSpawn('npm', ['run']), { cwd: '/tmp' }))
.toEqual({ cwd: '/tmp' });
});
});

describe('command-parser source hygiene', () => {
test('rejects a null byte in an argument', () => {
expect(() => parseCommand('node --eval a\0b')).toThrow(/null byte/);
Expand Down
9 changes: 8 additions & 1 deletion __tests__/dev-cli.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -245,10 +245,17 @@ describe('dev-cli module', () => {

test('test command resolves npm executable without shell interpolation', () => {
expect(cliSource).toContain('resolveExecutableForPlatform');
expect(cliSource).toContain('spawnSync(npmExecutable');
expect(cliSource).toContain('shell: false');
});

test('test command routes the npm.cmd shim through a spawn plan', () => {
// npm resolves to npm.cmd on Windows, and a direct spawn of it fails with
// EINVAL since the CVE-2024-27980 fix, so the plan must not be bypassed.
expect(cliSource).toContain('planShimSpawn(npmExecutable');
expect(cliSource).toContain('spawnSync(plan.file, plan.args, shimSpawnOptions(plan');
expect(cliSource).not.toContain('spawnSync(npmExecutable');
});

test('exports parseArgs, COMMANDS, VALIDATE_SUBCOMMANDS, NEW_SUBCOMMANDS, route', () => {
expect(cliSource).toContain('module.exports');
expect(typeof parseArgs).toBe('function');
Expand Down
13 changes: 13 additions & 0 deletions __tests__/perf-benchmark-runner.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,19 @@ describe('runBenchmark', () => {
}));
});

it('runs a batch shim through cmd.exe instead of spawning it directly', () => {
// Node fails with EINVAL on a direct .cmd spawn since the CVE-2024-27980
// fix, so an npm-based benchmark command needs the cmd.exe hop.
execFileSync.mockImplementation(() => 'output');

runBenchmark('npm.cmd run bench', { allowShort: true });
expect(execFileSync).toHaveBeenCalledWith(
'cmd.exe',
['/d', '/s', '/c', '""npm.cmd" "run" "bench""'],
expect.objectContaining({ windowsVerbatimArguments: true })
);
});

it('sets PERF_RUN_DURATION env variable by default', () => {
execFileSync.mockImplementation(() => 'output');

Expand Down
26 changes: 26 additions & 0 deletions __tests__/perf-profiling-runner.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,32 @@ describe('perf profiling runner', () => {
});
});

it('runs a batch shim profiler command through cmd.exe', () => {
// A direct .cmd spawn fails with EINVAL since the CVE-2024-27980 fix.
jest.resetModules();
const execFileSync = jest.fn();
jest.doMock('child_process', () => ({ execFileSync }));
jest.doMock('../lib/perf/profilers', () => ({
selectProfiler: () => ({
id: 'fake',
buildCommand: () => 'npx.cmd clinic doctor',
parseOutput: () => ({ tool: 'fake', hotspots: [], artifacts: [] })
})
}));

const runner = require('../lib/perf/profiling-runner');
expect(runner.runProfiling().ok).toBe(true);
expect(execFileSync).toHaveBeenCalledWith(
'cmd.exe',
['/d', '/s', '/c', '""npx.cmd" "clinic" "doctor""'],
expect.objectContaining({ windowsVerbatimArguments: true })
);

jest.dontMock('child_process');
jest.dontMock('../lib/perf/profilers');
jest.resetModules();
});

it('does not enforce timeout when timeoutMs is not provided', () => {
jest.resetModules();
const execFileSync = jest.fn();
Expand Down
33 changes: 16 additions & 17 deletions bin/cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ const VERSION = require('../package.json').version;
const PACKAGE_DIR = path.join(__dirname, '..');
const discovery = require('../lib/discovery');
const transforms = require('../lib/adapter-transforms');
const { resolveExecutableForPlatform } = require('../lib/utils/command-parser');
const { resolveExecutableForPlatform, planShimSpawn, shimSpawnOptions } = require('../lib/utils/command-parser');

// Valid tool names
const VALID_TOOLS = ['claude', 'opencode', 'codex', 'cursor', 'kiro'];
Expand Down Expand Up @@ -72,7 +72,9 @@ const WINDOWS_DIRECT_EXEC = /\.(exe|com)$/i;
// to execFileSync fails with EINVAL rather than running it.
const WINDOWS_BATCH_SHIM = /\.(cmd|bat)$/i;
// Arguments safe to hand to cmd.exe: no whitespace, no shell metacharacters.
const CMD_SAFE_ARG = /^[A-Za-z0-9@._:\\/+-]+$/;
// Ends with (?![\s\S]) rather than $, which in JavaScript also matches before a
// trailing newline - so 'plugin\n' would otherwise pass as safe.
const CMD_SAFE_ARG = /^[A-Za-z0-9@._:\\/+-]+(?![\s\S])/;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is an auto review done by revuto.


This change is a no-op, and the comment justifying it states something that isn't true of JavaScript.

In JS, $ without the m flag matches only at the end of Input — ECMAScript Assertion :: $ is defined as: if [[Multiline]] is false, the assertion succeeds only when the index equals the input length. Matching before a single trailing newline is Python/Perl semantics, not JS:

/^a$/.test('a\n')   // false   <- JS
/^a$/m.test('a\n')  // true    <- only with the m flag
re.match(r'^a$', 'a\n')  # True  <- Python does

So 'plugin\n' never "passed as safe" under the old $. I fuzzed the two forms exhaustively over strings of length 0–3 drawn from 31 characters (the allowlist chars plus \n \r \u2028 \u2029 \u0085 \v \f \t, space, & | < > ^ ( ) " %, NUL, and an astral pair) — 30,784 strings, zero behavioural divergence between /^[A-Za-z0-9@._:\\/+-]+$/ and /^[A-Za-z0-9@._:\\/+-]+(?![\s\S])/.

Separately, \r and \n aren't members of the [A-Za-z0-9@._:\\/+-] class, so a CR or LF anywhere in the argument — not just trailing — already failed the allowlist at base. The guard was double-covered.

No objection to keeping (?![\s\S]) if you prefer an explicit end-of-input assertion, but the comment should not assert the JS $ behaviour it describes. Suggest something like: // Explicit end-of-input assertion; equivalent to $ here since the pattern is not /m.


/**
* Pick the Claude Code executable from a `where.exe claude` result.
Expand Down Expand Up @@ -100,23 +102,20 @@ function pickClaudeExecutable(platform, whereOutput) {
/**
* Build the file and argv for one Claude Code invocation.
*
* A batch shim cannot be handed to execFileSync at all, so it is launched
* through cmd.exe. cmd.exe re-parses its command line, so every argument is
* checked first: callers only ever pass literal subcommands and validated
* `<plugin>@<marketplace>` ids, so anything else is a bug rather than a string
* to escape. The quoting mirrors how Node wraps a shell command - /s strips the
* outer quote pair, leaving the quoted shim path as the first token.
* planShimSpawn does the cmd.exe routing a batch shim needs. Every argument is
* checked against a stricter rule than that quoting requires: callers only ever
* pass literal subcommands and validated `<plugin>@<marketplace>` ids, so
* anything carrying whitespace or a shell metacharacter is a bug rather than a
* string to escape.
*/
function claudeSpawnPlan(executable, args, comspec) {
if (!WINDOWS_BATCH_SHIM.test(executable)) {
return { file: executable, args };
}
const unsafe = args.find(arg => !CMD_SAFE_ARG.test(arg));
if (unsafe !== undefined) {
throw new Error(`Refusing to pass ${JSON.stringify(unsafe)} to cmd.exe`);
if (WINDOWS_BATCH_SHIM.test(executable)) {
const unsafe = args.find(arg => !CMD_SAFE_ARG.test(arg));
if (unsafe !== undefined) {
throw new Error(`Refusing to pass ${JSON.stringify(unsafe)} to cmd.exe`);
}
}
const command = [`"${executable}"`, ...args].join(' ');
return { file: comspec || 'cmd.exe', args: ['/d', '/s', '/c', `"${command}"`], verbatim: true };
return planShimSpawn(executable, args, { comspec });
}

let claudeBinCache;
Expand Down Expand Up @@ -144,7 +143,7 @@ function claudeExecutable() {
*/
function claudeSpawn(args, options) {
const plan = claudeSpawnPlan(claudeExecutable(), args, process.env.comspec);
return execFileSync(plan.file, plan.args, plan.verbatim ? { ...options, windowsVerbatimArguments: true } : options);
return execFileSync(plan.file, plan.args, shimSpawnOptions(plan, options));
}

function copyDirRecursive(src, dest) {
Expand Down
8 changes: 5 additions & 3 deletions bin/dev-cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@

const path = require('path');
const { execSync, spawnSync } = require('child_process');
const { resolveExecutableForPlatform } = require('../lib/utils/command-parser');
const { resolveExecutableForPlatform, planShimSpawn, shimSpawnOptions } = require('../lib/utils/command-parser');

const VERSION = require('../package.json').version;
const ROOT_DIR = path.join(__dirname, '..');
Expand Down Expand Up @@ -284,12 +284,14 @@ const COMMANDS = {
cmdArgs.push(...args);
}
const npmExecutable = resolveExecutableForPlatform('npm');
const result = spawnSync(npmExecutable, cmdArgs, {
// npm resolves to npm.cmd on Windows, which cannot be spawned directly.
const plan = planShimSpawn(npmExecutable, cmdArgs);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is an auto review done by revuto.


planShimSpawn can now throw from inside this handler — cmdArgs includes everything after agentsys-dev test --, and on win32 an argument containing % or " hits assertCmdPassable (lib/utils/command-parser.js:129). The surrounding catch (err) { return err.status || 1; } (line 299-301) discards the message, so agentsys-dev test -- --grep=%foo% on Windows exits 1 with no output at all, where previously spawnSync ran and reported the child's own failure.

Since this call site was explicitly chosen to quote rather than reject because it carries user-written arguments, the refusal path is user-reachable here and deserves the error text printed (e.g. console.error(err.message) before returning) rather than a bare exit code.

const result = spawnSync(plan.file, plan.args, shimSpawnOptions(plan, {
cwd: ROOT_DIR,
stdio: 'inherit',
shell: false,
windowsHide: true
});
}));
if (result.error) {
throw result.error;
}
Expand Down
8 changes: 5 additions & 3 deletions lib/perf/benchmark-runner.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

const { execFileSync } = require('child_process');
const { validateBaseline } = require('./schemas');
const { parseCommand, resolveExecutableForPlatform } = require('../utils/command-parser');
const { parseCommand, resolveExecutableForPlatform, planShimSpawn, shimSpawnOptions } = require('../utils/command-parser');

const DEFAULT_MIN_DURATION = 60;
const BINARY_SEARCH_MIN_DURATION = 30;
Expand Down Expand Up @@ -74,13 +74,15 @@ function runBenchmark(command, options = {}) {
const start = Date.now();
let output;
try {
output = execFileSync(executable, parsedCommand.args, {
// A .cmd/.bat shim cannot be spawned directly; route it through cmd.exe.
const plan = planShimSpawn(executable, parsedCommand.args);
output = execFileSync(plan.file, plan.args, shimSpawnOptions(plan, {
stdio: 'pipe',
encoding: 'utf8',
env,
windowsHide: true,
cwd: options.cwd || process.cwd()
});
}));
} catch (error) {
const stderr = error.stderr ? String(error.stderr).trim() : '';
const stdout = error.stdout ? String(error.stdout).trim() : '';
Expand Down
6 changes: 4 additions & 2 deletions lib/perf/profiling-runner.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

const { execFileSync } = require('child_process');
const profilers = require('./profilers');
const { parseCommand, resolveExecutableForPlatform } = require('../utils/command-parser');
const { parseCommand, resolveExecutableForPlatform, planShimSpawn, shimSpawnOptions } = require('../utils/command-parser');

/**
* Run a profiling command and return artifacts/hotspots metadata.
Expand Down Expand Up @@ -48,7 +48,9 @@ function runProfiling(options = {}) {
execOptions.timeout = timeoutMs;
}

execFileSync(executable, parsedCommand.args, execOptions);
// A .cmd/.bat shim cannot be spawned directly; route it through cmd.exe.
const plan = planShimSpawn(executable, parsedCommand.args);
execFileSync(plan.file, plan.args, shimSpawnOptions(plan, execOptions));
} catch (error) {
const stderr = error.stderr ? String(error.stderr).trim() : '';
const stdout = error.stdout ? String(error.stdout).trim() : '';
Expand Down
Loading
Loading