vercel / vercel/vercel-plugin

Windows: SessionStart hook always reports "Vercel CLI is not installed" (two bugs in checkVercelCli)

Open
#152 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
TypeScript
Stars
287
Forks
58
Avg merge
1d 1h
Merged PRs (30d)
17

Description

Summary

On Windows, checkVercelCli() in hooks/session-start-profiler.mjs can never detect an npm-installed Vercel CLI. Every session starts with:

IMPORTANT: The Vercel CLI is not installed.
Strongly recommend the user install it with npm i -g vercel to unlock agentic features like vercel env pull, vercel deploy, and vercel logs.

...on a machine where the CLI is installed and authenticated.

This is worse than a cosmetic wrong message. Agents believe it. In my case an agent needed to set a production environment variable, read that line at session start, concluded the capability was unavailable, and handed the task back to the user rather than running vercel env add. The hook meant to help the agent use Vercel actively prevented it.

Plugin version 0.45.1. Windows 10, Node 22.19.0, npm-installed CLI 54.20.1.

Two independent bugs, either one fatal

1. Candidate order resolves to the one file Node cannot execute
function getBinaryPathCandidates(binaryName) {
  if (process.platform !== "win32") return [binaryName];
  const hasExecutableExtension = /\.[^./\]+$/.test(binaryName);
  const suffixes = hasExecutableExtension ? [""] : ["", ...WINDOWS_EXECUTABLE_EXTENSIONS];
  return suffixes.map((suffix) => `${binaryName}${suffix}`);
}

The bare, extensionless name is tried first. On Windows, npm installs two files into its bin directory:

C:\Users\<user>\AppData\Roaming\npm\vercel.cmd   <- the Windows shim
C:\Users\<user>\AppData\Roaming\npm\vercel       <- a Unix shell script, for Git Bash / MSYS

Both satisfy accessSync(path, X_OK), so resolveBinaryFromPath returns the shell script, which Node cannot spawn. execFileSync throws ENOENT, the catch returns { installed: false }, and the hook reports the CLI missing.

2. The correct file would also have failed

Node >= 18.20 / 20.12 refuses execFileSync on .cmd / .bat without shell: true — the fix for CVE-2024-27980. So the shim throws EINVAL.

Reproduced directly:

import { execFileSync } from 'node:child_process';
for (const p of [String.raw`C:\Users\Cara\AppData\Roaming\npm\vercel`,
                 String.raw`C:\Users\Cara\AppData\Roaming\npm\vercel.cmd`]) {
  try { console.log('OK   ', p, execFileSync(p, ['--version'], {encoding:'utf-8'}).trim()); }
  catch (e) { console.log('THROW', p, e.code); }
}
THROW C:\Users\Cara\AppData\Roaming\npm\vercel      -> ENOENT
THROW C:\Users\Cara\AppData\Roaming\npm\vercel.cmd  -> EINVAL

Fixing the ordering alone is not enough; both need addressing.

Knock-on effect

checkVercelCli() resolves npm through the same helper for the npm view vercel version call, so the "your CLI is outdated" branch is dead on Windows for the same two reasons. In my case that hid a CLI five major versions behind (54.20.1 vs 59.5.0) — invisible for as long as the check kept answering "not installed".

Suggested fix

Two changes in hooks/src/session-start-profiler.mts (and the built .mjs):

Put the extensions first, the bare name last.

function getBinaryPathCandidates(binaryName) {
  if (process.platform !== "win32") return [binaryName];
  if (/\.[^./\]+$/.test(binaryName)) return [binaryName];
  // A bare, extensionless file in a Windows PATH directory is almost always the
  // Unix shell script npm writes beside its .cmd shim, and Node cannot exec it.
  return [...WINDOWS_EXECUTABLE_EXTENSIONS.map((s) => `${binaryName}${s}`), binaryName];
}

Route .cmd / .bat through a shell, quoted.

function execBinarySync(binaryPath, args) {
  const needsShell = process.platform === "win32" && /\.(cmd|bat)$/i.test(binaryPath);
  if (needsShell) {
    return execFileSync(`"${binaryPath}"`, args.map((a) => `"${a}"`), {
      timeout: EXEC_SYNC_TIMEOUT_MS, encoding: "utf-8", stdio: SPAWN_STDIO, shell: true,
    });
  }
  return execFileSync(binaryPath, args, {
    timeout: EXEC_SYNC_TIMEOUT_MS, encoding: "utf-8", stdio: SPAWN_STDIO,
  });
}

Then call execBinarySync at both execFileSync sites in checkVercelCli().

Verified locally

Applied to the cached 0.45.1 copy:

before: {"installed":false,"needsUpdate":false}
after : {"installed":true,"currentVersion":"54.20.1","latestVersion":"59.5.0","needsUpdate":true}

The hook then correctly printed the outdated message instead of the not installed one, and after upgrading to 59.5.0 it correctly prints nothing at all.

Detection takes ~850 ms end to end, comfortably inside the existing 3 s EXEC_SYNC_TIMEOUT_MS, so no timeout change is needed.

Happy to open a PR if that is useful.

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Read hooks/src/session-start-profiler.mts and the built hooks/session-start-profiler.mjs, focusing on checkVercelCli(), getBinaryPathCandidates(), and the two execution sites. Reproduce the Windows npm-installed case described in the issue, then verify the hook distinguishes installed, outdated, and missing Vercel CLI states without exceeding the existing timeout.

Written by the indexing model from the issue text.

Assessment

Tech stack
node.js, typescript
Domain
cli, tooling
Issue type
Bug
Difficulty
3/5
Estimated time
1-2 days
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
76/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.