Summary
pnpm run-local, the command the README gives for trying the Workshop locally, cannot start on
Windows. It fails on the first nested pnpm call, before anything is installed or built.
> gadgets@1.0.0 run-local D:\...\cloudflare-os
> node scripts/run-local.mjs
> pnpm install
node:internal/child_process:1120
result.error = new ErrnoException(result.error, 'spawnSync ' + options.file);
^
<ref *1> Error: spawnSync pnpm ENOENT
at execFileSync (node:child_process:911:15)
at run (file:///D:/.../cloudflare-os/scripts/run-local.mjs:118:3)
Environment: Windows 11, Node v22.12.0, pnpm 10.6.3, clean clone of main (e1ab8fb).
Cause
On Windows the pnpm on PATH is a .cmd shim. execFileSync starts a process directly instead
of going through a shell, so there is no extensionless pnpm for it to execute and the call fails
with ENOENT. Naming the shim explicitly does not help either — since the fix for CVE-2024-27980,
Node refuses to spawn .bat/.cmd without a shell and returns EINVAL:
execFileSync('pnpm.cmd', ['--version']) -> EINVAL spawnSync pnpm.cmd EINVAL
execFileSync('pnpm', ['--version']) -> ENOENT spawnSync pnpm ENOENT
Affected call sites
Four sites are on the pnpm run-local / pnpm dev-server path. Fixing only the first just moves
the failure to the next one:
| File |
What it runs |
scripts/run-local.mjs (run()) |
pnpm install, and both builds |
run-dev-server.js |
pnpm exec wrangler dev |
packages/gatekeeper-context/build-app.mjs |
pnpm exec vite build |
packages/gatekeeper-scheduler/build-app.mjs |
pnpm exec vite build |
scripts/release/build-release.mjs has the same pattern in its own run() helper, but that path is
CI-only, so it is not part of the failure above.
Everything else in these scripts already spawns process.execPath or git, which are real
executables and unaffected.
Two things worth knowing before fixing it
shell: true is not a safe fix here. It makes the call work, but a shell re-splits the command
line, and run-dev-server.js passes wrangler absolute config paths built from gk.dir. On a
checkout whose path contains a space — C:\Users\Some Name\... — those arguments break apart.
npm_execpath needs a guard. It holds pnpm's own JS entry point, which node can run directly
with no shell, so arguments keep their exact values. But under npm run the same variable points at
npm instead:
pnpm run ... -> npm_execpath = ...\pnpm\bin\pnpm.cjs
npm run ... -> npm_execpath = ...\npm\bin\npm-cli.js
Substituting it unchecked would silently run npm install against a pnpm workspace.
Patch
Happy to send this as a PR if you want it — flagging it here first because it lands at about 30
changed lines across five files, which is past the guideline in CONTRIBUTING.md.
A small shared helper keeps it to one copy (packages/* already reference ../../scripts/, e.g.
build:configurator):
// scripts/pnpm-command.js
export function pnpmCommand(args) {
const execPath = process.env.npm_execpath ?? "";
return process.platform === "win32" && /[\\/]pnpm\.[cm]?js$/i.test(execPath)
? [process.execPath, [execPath, ...args]]
: ["pnpm", args];
}
used as const [file, argv] = pnpmCommand([...]); execFileSync(file, argv, opts); at the four sites.
It is a no-op off Windows, and on Windows it falls back to today's behaviour whenever the launcher
was not pnpm, so the failure stays loud rather than turning into the wrong package manager.
Verification
On Windows 11 / Node v22.12.0, from a clean clone:
- before:
pnpm run-local dies at pnpm install with the ENOENT above;
- after: it completes install,
@gadgets/typed-storage build and the frontend vite build, runs
both gatekeeper build-app.mjs bundles, and reaches
[wrangler:info] Ready on http://127.0.0.1:8787 with the Workshop serving.
pnpm lint:check passes.
- The helper was exercised under
pnpm run, npm run, and bare node to confirm the guard picks
pnpm's entry point only in the first case.
Not verified: Linux/macOS behaviour is unchanged by inspection only (the platform check makes it a
no-op), and I did not run pnpm test — the release-manifest golden test already fails on Windows
before any change, from CRLF line endings.
Summary
pnpm run-local, the command the README gives for trying the Workshop locally, cannot start onWindows. It fails on the first nested pnpm call, before anything is installed or built.
Environment: Windows 11, Node v22.12.0, pnpm 10.6.3, clean clone of
main(e1ab8fb).Cause
On Windows the
pnpmonPATHis a.cmdshim.execFileSyncstarts a process directly insteadof going through a shell, so there is no extensionless
pnpmfor it to execute and the call failswith
ENOENT. Naming the shim explicitly does not help either — since the fix for CVE-2024-27980,Node refuses to spawn
.bat/.cmdwithout a shell and returnsEINVAL:Affected call sites
Four sites are on the
pnpm run-local/pnpm dev-serverpath. Fixing only the first just movesthe failure to the next one:
scripts/run-local.mjs(run())pnpm install, and both buildsrun-dev-server.jspnpm exec wrangler devpackages/gatekeeper-context/build-app.mjspnpm exec vite buildpackages/gatekeeper-scheduler/build-app.mjspnpm exec vite buildscripts/release/build-release.mjshas the same pattern in its ownrun()helper, but that path isCI-only, so it is not part of the failure above.
Everything else in these scripts already spawns
process.execPathorgit, which are realexecutables and unaffected.
Two things worth knowing before fixing it
shell: trueis not a safe fix here. It makes the call work, but a shell re-splits the commandline, and
run-dev-server.jspasses wrangler absolute config paths built fromgk.dir. On acheckout whose path contains a space —
C:\Users\Some Name\...— those arguments break apart.npm_execpathneeds a guard. It holds pnpm's own JS entry point, whichnodecan run directlywith no shell, so arguments keep their exact values. But under
npm runthe same variable points atnpm instead:
Substituting it unchecked would silently run
npm installagainst a pnpm workspace.Patch
Happy to send this as a PR if you want it — flagging it here first because it lands at about 30
changed lines across five files, which is past the guideline in CONTRIBUTING.md.
A small shared helper keeps it to one copy (
packages/*already reference../../scripts/, e.g.build:configurator):used as
const [file, argv] = pnpmCommand([...]); execFileSync(file, argv, opts);at the four sites.It is a no-op off Windows, and on Windows it falls back to today's behaviour whenever the launcher
was not pnpm, so the failure stays loud rather than turning into the wrong package manager.
Verification
On Windows 11 / Node v22.12.0, from a clean clone:
pnpm run-localdies atpnpm installwith theENOENTabove;@gadgets/typed-storagebuild and the frontendvite build, runsboth gatekeeper
build-app.mjsbundles, and reaches[wrangler:info] Ready on http://127.0.0.1:8787with the Workshop serving.pnpm lint:checkpasses.pnpm run,npm run, and barenodeto confirm the guard pickspnpm's entry point only in the first case.
Not verified: Linux/macOS behaviour is unchanged by inspection only (the platform check makes it a
no-op), and I did not run
pnpm test— the release-manifest golden test already fails on Windowsbefore any change, from CRLF line endings.