Skip to content

Fix Windows CLI shim execution#445

Open
ayskobtw-lil wants to merge 7 commits into
profullstack:masterfrom
ayskobtw-lil:codex/windows-pandoc-test-shim
Open

Fix Windows CLI shim execution#445
ayskobtw-lil wants to merge 7 commits into
profullstack:masterfrom
ayskobtw-lil:codex/windows-pandoc-test-shim

Conversation

@ayskobtw-lil
Copy link
Copy Markdown

Summary

  • Run shared exec() commands through the Windows shell on Windows so .cmd shims such as npx, vsce, jsr, and pandoc resolve correctly
  • Keep non-Windows execution unchanged
  • Make the docs-pandoc fake CLI test install a Windows-compatible shim and use the platform PATH delimiter

Why

On Windows, child_process.spawn('npx', ...) and spawn('pandoc', ...) fail with ENOENT in this environment because command shims are exposed as .cmd files. That means target/doc adapters that rely on external CLIs cannot execute even when the CLI is installed on PATH.

Validation

  • pnpm exec vitest run packages/docs/pandoc/src/index.test.ts -> 5 passed
  • pnpm --filter @profullstack/sh1pt-core typecheck
  • pnpm --filter @profullstack/sh1pt-docs-pandoc typecheck

For the ugig bug-fix bounty: this fixes a Windows CLI execution bug found while testing the repository locally.

@ayskobtw-lil
Copy link
Copy Markdown
Author

Opened the matching bug report as #446 for the ugig bug-fix trail. This PR fixes the Windows .cmd shim execution issue.

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented May 26, 2026

Greptile Summary

This PR fixes Windows CLI shim execution by routing bare command names through cmd.exe with delayed variable expansion (/v:on), so .cmd shims like npx, vsce, and pandoc resolve correctly on Windows. It also adds isWindowsCommandNotFound to detect exit-code 9009 in ensureCli, and updates the pandoc fake-CLI test to install a .cmd shim and use the platform-aware PATH delimiter.

  • exec.ts: spawns cmd.exe /d /s /v:on /c on Windows for non-path commands, stashing each argument in a numbered env var and referencing it via \"!NAME!\" — the strategy is sound, but windowsEnvArg incorrectly appends ^! for ! characters (see inline comment).
  • exec.test.ts: adds roundtrip and ensureCli tests; the roundtrip test runs on all platforms but only exercises the Windows code path on Windows, so the !-corruption bug won't surface on Linux CI.
  • index.test.ts (pandoc): cross-platform improvements to PATH construction and fake-pandoc shim installation — clean changes.

Confidence Score: 3/5

The cmd.exe routing strategy is architecturally correct, but windowsEnvArg corrupts arguments containing ! characters before they reach the child process on Windows.

The delayed-expansion approach of stashing arguments in numbered env vars is the right design for avoiding cmd.exe injection. However, windowsEnvArg adds ^! for every ! in a value: since cmd.exe does not re-scan substituted !NAME! values, the ^ is not consumed as an escape and passes through CommandLineToArgvW as a literal character. The roundtrip test includes hello!world and will fail on Windows as written.

packages/core/src/exec.ts — specifically the windowsEnvArg function's !-escaping step.

Important Files Changed

Filename Overview
packages/core/src/exec.ts Introduces Windows cmd.exe shell routing via delayed variable expansion to resolve .cmd shims; the argument-escaping helper incorrectly appends ^! for ! characters, corrupting any argument containing ! on Windows.
packages/core/src/exec.test.ts New test suite covering argument preservation and ensureCli detection; the argument-roundtrip test runs on all platforms but only exercises the Windows code path on Windows, so the ! corruption bug won't be caught on Linux CI.
packages/docs/pandoc/src/index.test.ts Adds a Windows-compatible pandoc.cmd shim to the fake-pandoc installer and replaces the hard-coded : PATH separator with the platform-aware delimiter — both changes are correct.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A["exec(cmd, args, opts)"] --> B{shouldUseWindowsCmd?}
    B -- "win32 AND no path sep AND not .exe/.com" --> C["windowsCommandLine(cmd, args, env)"]
    C --> D["Store each arg as SH1PT_EXEC_ARG_N in env via windowsEnvArg"]
    D --> E["Build command string with !NAME! references"]
    E --> F["spawn cmd.exe /d /s /v:on /c command"]
    B -- "non-Windows OR has path sep OR .exe/.com" --> G["spawn(cmd, args, spawnOptions)"]
    F --> H["Collect stdout/stderr"]
    G --> H
    H --> I["resolve ExecResult"]
    I --> J{ensureCli caller?}
    J -- yes --> K{isWindowsCommandNotFound?}
    K -- "win32 AND exitCode=9009 OR not-recognized text" --> L["throw command not found"]
    K -- otherwise --> M["CLI considered present"]
Loading

Reviews (8): Last reviewed commit: "Preserve Windows shim metachar args" | Re-trigger Greptile

Comment thread packages/core/src/exec.ts Outdated
Comment thread packages/core/src/exec.ts Outdated
@ayskobtw-lil
Copy link
Copy Markdown
Author

Fixed the Greptile review items in 99b9eee: restored stdin to ignore via explicit stdio, kept stdout/stderr piped, and hardened Windows shell quoting for trailing backslashes before quotes. Re-ran packages/docs/pandoc/src/index.test.ts (5 passed), @profullstack/sh1pt-core typecheck, and @profullstack/sh1pt-docs-pandoc typecheck.

Comment thread packages/core/src/exec.ts Outdated
@ayskobtw-lil
Copy link
Copy Markdown
Author

Fixed the latest Greptile finding in commit bb071a9: �nsureCli now treats non-zero --version exits as missing, with coverage for the Windows 9009-style case. I also limited Windows shell dispatch to bare commands so absolute .exe paths avoid shell quoting/percent-expansion concerns. Re-ran packages/core/src/exec.test.ts + packages/docs/pandoc/src/index.test.ts (7 passed), plus core and docs-pandoc typechecks.

@ayskobtw-lil
Copy link
Copy Markdown
Author

Fixed the latest Windows argument-handling concern in e8b8dea: bare Windows commands now run through cmd.exe /d /s /c with the command arguments passed separately, so we no longer build one custom-quoted command string that can corrupt percent-wrapped args or trailing backslashes. I also updated the regression test so it exercises a bare .cmd command on PATH and verifies both %SH1PT_EXEC_LITERAL% and a trailing-backslash path survive.\n\nVerification run locally:\n- npx --yes pnpm@9.12.0 exec vitest run packages/core/src/exec.test.ts packages/docs/pandoc/src/index.test.ts -> 7 passed\n- npx --yes pnpm@9.12.0 --filter @profullstack/sh1pt-core typecheck -> passed\n- npx --yes pnpm@9.12.0 --filter @profullstack/sh1pt-docs-pandoc typecheck -> passed

Comment thread packages/core/src/exec.ts Outdated
@github-actions
Copy link
Copy Markdown

🤖 Auto-rebase: The branch was rebased successfully locally but could not be pushed to the fork. Please enable 'Allow edits from maintainers' in the PR settings, or rebase manually: git fetch upstream master && git rebase upstream/master.

@ayskobtw-lil ayskobtw-lil force-pushed the codex/windows-pandoc-test-shim branch from e8b8dea to ec35ac5 Compare May 27, 2026 15:39
@ayskobtw-lil
Copy link
Copy Markdown
Author

Fixed the latest ensureCli review item in ec35ac5 after rebasing onto current upstream master. ensureCli now treats non-zero --version as missing only for Windows command-not-found signatures (9009 or cmd.exe's 'is not recognized...' output), preserving the previous non-Windows behavior for installed CLIs whose --version exits non-zero.\n\nVerification after rebase:\n- npx --yes pnpm@9.12.0 exec vitest run packages/core/src/exec.test.ts packages/docs/pandoc/src/index.test.ts -> 7 passed, 1 Windows-only test skipped here\n- npx --yes pnpm@9.12.0 --filter @profullstack/sh1pt-core typecheck -> passed\n- npx --yes pnpm@9.12.0 --filter @profullstack/sh1pt-docs-pandoc typecheck -> passed

@github-actions
Copy link
Copy Markdown

🤖 Auto-rebase: The branch was rebased successfully locally but could not be pushed to the fork. Please enable 'Allow edits from maintainers' in the PR settings, or rebase manually: git fetch upstream master && git rebase upstream/master.

@ayskobtw-lil ayskobtw-lil force-pushed the codex/windows-pandoc-test-shim branch from ec35ac5 to a0eda03 Compare May 27, 2026 21:39
@ayskobtw-lil
Copy link
Copy Markdown
Author

Rebased again onto current upstream master after the latest auto-rebase bot comment. New head is a0eda03.\n\nVerification after rebase on Windows (node process.platform = win32):\n- npx --yes pnpm@9.12.0 exec vitest run packages/core/src/exec.test.ts packages/docs/pandoc/src/index.test.ts -> 7 passed, 1 non-Windows-only test skipped\n- npx --yes pnpm@9.12.0 --filter @profullstack/sh1pt-core typecheck -> passed\n- npx --yes pnpm@9.12.0 --filter @profullstack/sh1pt-docs-pandoc typecheck -> passed\n\nThe remaining Greptile note about % arguments and trailing-backslash args is covered by the Windows run of packages/core/src/exec.test.ts: it executes a bare .cmd shim through the cmd.exe dispatch path and asserts that both %SH1PT_EXEC_LITERAL% and C:\tmp\path\ survive unchanged.

@ayskobtw-lil
Copy link
Copy Markdown
Author

Fixed the remaining Windows cmd.exe argument-escaping review concern in 635e7e5. The Windows cmd dispatch now builds an escaped command line for cmd.exe, escaping percent signs and other cmd metacharacters and preserving trailing-backslash args. I also strengthened the regression test so SH1PT_EXEC_LITERAL is present in the environment; the test still verifies the child receives the literal %SH1PT_EXEC_LITERAL% plus C:\tmp\path\ unchanged. Also removed the redundant slash from the fake pandoc.cmd %~dp0 helper path.\n\nVerification on Windows:\n- npx --yes pnpm@9.12.0 exec vitest run packages/core/src/exec.test.ts packages/docs/pandoc/src/index.test.ts -> 7 passed, 1 non-Windows-only test skipped\n- npx --yes pnpm@9.12.0 --filter @profullstack/sh1pt-core typecheck -> passed\n- npx --yes pnpm@9.12.0 --filter @profullstack/sh1pt-docs-pandoc typecheck -> passed

@ayskobtw-lil
Copy link
Copy Markdown
Author

Fixed the latest Windows cmd.exe quoting review item in b8b50e7. The cmd.exe shim path now stores child arguments in temporary environment variables and invokes them through delayed expansion, with escaping for !, embedded quotes, and trailing backslashes. That avoids leaking literal carets into quoted arguments like "Foo & Bar" while still preserving %SH1PT_EXEC_LITERAL%, C:\tmp\path, hello!world, and quoted values.

Verification on Windows:

  • npx --yes pnpm@9.12.0 exec vitest run packages/core/src/exec.test.ts packages/docs/pandoc/src/index.test.ts -> 7 passed, 1 non-Windows-only test skipped
  • npx --yes pnpm@9.12.0 --filter @profullstack/sh1pt-core typecheck -> passed
  • npx --yes pnpm@9.12.0 --filter @profullstack/sh1pt-docs-pandoc typecheck -> passed

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented May 28, 2026

Want your agent to iterate on Greptile's feedback? Try greploops.

Comment thread packages/core/src/exec.ts
Comment on lines +105 to +110
function windowsEnvArg(value: string): string {
return value
.replace(/(\\*)"/g, '$1$1\\"')
.replace(/\\+$/, '$&$&')
.replace(/!/g, '^!');
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 windowsEnvArg incorrectly escapes ! as ^!. cmd.exe's delayed expansion (/v:on) substitutes !NAME! into the output buffer and continues scanning from after the closing ! in the original input — the substituted value is never re-scanned for further ! patterns. Because ^ is also not an escape character inside CommandLineToArgvW (CRT) double-quoted strings, a value like hello!world stored as hello^!world will arrive in the child process as the literal string hello^!world. The test case 'hello!world' asserts the round-trip is clean, which is the right contract, but the assertion will fail on Windows. Because the test is not guarded by itOnWindows, the breakage won't surface on Linux CI.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant