Skip to content

[hooks] Detect the Vercel CLI on Windows - #143

Open
oulay2002 wants to merge 1 commit into
vercel:mainfrom
oulay2002:fix/windows-vercel-cli-detection
Open

[hooks] Detect the Vercel CLI on Windows#143
oulay2002 wants to merge 1 commit into
vercel:mainfrom
oulay2002:fix/windows-vercel-cli-detection

Conversation

@oulay2002

Copy link
Copy Markdown

Problem

On Windows, session-start-profiler emits this at the top of every session, even when the CLI is installed, on PATH, and authenticated:

IMPORTANT: The Vercel CLI is not installed.
Strongly recommend the user install it with `npm i -g vercel` ...

The agent then acts on it and recommends a reinstall. The outdated-version branch is unreachable on Windows for the same underlying reason, so needsUpdate never fires either.

Root cause

Two independent defects in checkVercelCli's resolve-then-spawn path.

1. Candidate ordering picks npm's POSIX shim. getBinaryPathCandidates put the bare name ahead of every PATHEXT suffix:

const suffixes = hasExecutableExtension ? [""] : ["", ...WINDOWS_EXECUTABLE_EXTENSIONS];

npm lays down three entries per global binary in %APPDATA%\npm: vercel (an sh script for Git Bash), vercel.CMD, and vercel.ps1. accessSync(path, X_OK) succeeds on all three — on Windows it degrades to an existence check — so the bare sh script wins, and spawnSync rejects it with ENOENT.

2. .cmd cannot be spawned without a shell. Even after resolving vercel.CMD, execFileSync fails: since the fix for CVE-2024-27980, Node refuses .cmd/.bat without shell: true, with EINVAL. This also silently killed the npm view vercel version probe, since npm resolves to npm.CMD.

Measured on Windows 11, Node 24.18.0, Vercel CLI 58.9.0 on PATH:

candidate execFileSync result
vercel (no extension) ENOENT
vercel.CMD EINVAL
vercel.ps1 EFTYPE

Every reachable candidate failed, so checkVercelCli returned { installed: false } in all cases.

Fix

  • Order candidates by what Node can actually spawn: .EXE/.COM/.CMD/.BAT first, remaining PATHEXT entries next, bare name last. This also keeps .PS1 from outranking a real executable on machines that add it to PATHEXTspawnSync cannot run it either.
  • Route batch wrappers through shell: true, with the path quoted, since cmd.exe re-parses the command line and would otherwise split an unquoted path on its spaces.
  • Stop returning installed: false when the binary resolved but the version probe failed. The CLI is on PATH; telling the user to install it is the one answer that cannot help. It now reports installed: true with no version, which suppresses both messages rather than emitting a wrong one.

getBinaryPathCandidates and the new binaryNeedsShell take optional platform / pathExtensions arguments so the Windows behaviour is testable from any host. Runtime behaviour is unchanged — both default to process.platform and the module-level PATHEXT list.

Verification

Hook run exactly as hooks.json invokes it, before and after:

PATH before after
CLI installed not installed detected, 58.9.0 ✅
%APPDATA%\npm removed not installed not installed

The negative control matters: the "installed" case must not be silent for the wrong reason. With the fix the npm registry probe also runs for the first time on Windows, correctly reporting 58.9.0 → 59.0.0.

New tests in hooks/session-start-profiler-binary-resolution.test.ts (8 tests) cover candidate ordering, PATHEXT handling, already-qualified names, and the shell requirement. Confirmed they fail against the current ordering logic and pass with the fix.

bun run typecheck exits 0. Full suite: 849 pass / 125 fail before, 857 pass / 125 fail after — the 8 new tests, no change in failures. (The 125 are pre-existing Windows failures on main, mostly path-separator assumptions in the snapshot and context tests; happy to open a separate issue if useful.)

Notes for reviewers

  • hooks/session-start-profiler.mjs is the committed tsup output, rebuilt with bun run build:hooks. Other .mjs files were left untouched.
  • I could not find a CONTRIBUTING.md; let me know if you'd prefer a different branch naming or commit convention.

🤖 Generated with Claude Code

The session-start profiler tells every Windows user to install a CLI they
already have. Two independent defects, both in the resolve-then-spawn path:

1. `getBinaryPathCandidates` put the bare name ahead of every PATHEXT
   suffix. npm lays down three entries per global binary in %APPDATA%\npm:
   `vercel` (a POSIX sh shim), `vercel.CMD` and `vercel.ps1`. The bare shim
   won, and spawnSync rejects it with ENOENT.

2. Even once `vercel.CMD` is selected, execFileSync cannot run it. Since the
   fix for CVE-2024-27980, Node refuses .cmd/.bat without `shell: true`
   (EINVAL). This hit the `npm view vercel version` probe too, so the
   outdated-version branch was dead code on Windows.

Observed on Windows 11, Node 24.18.0, with Vercel CLI 58.9.0 on PATH:

    (no extension)  execFileSync -> ENOENT
    .CMD            execFileSync -> EINVAL
    .ps1            execFileSync -> EFTYPE

Order candidates by what Node can actually spawn (.EXE/.COM/.CMD/.BAT
first, remaining PATHEXT entries next, bare name last), and route batch
wrappers through the shell with the path quoted, since cmd.exe re-parses
the command line and would split an unquoted path on its spaces.

Also stop reporting `installed: false` when the binary resolved but the
version probe failed - the CLI is on PATH, and advising a reinstall is
the one answer that cannot help.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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