Skip to content

fix(bin): rewrite launcher as cross-platform Node.js script (fixes Windows global install)#577

Open
kaimys wants to merge 1 commit intotobi:mainfrom
kaimys:fix/windows-bin-launcher
Open

fix(bin): rewrite launcher as cross-platform Node.js script (fixes Windows global install)#577
kaimys wants to merge 1 commit intotobi:mainfrom
kaimys:fix/windows-bin-launcher

Conversation

@kaimys
Copy link
Copy Markdown

@kaimys kaimys commented Apr 15, 2026

Problem

npm install -g @tobilu/qmd on Windows installs successfully but produces a qmd command that fails in every shell (cmd, PowerShell, Git Bash).

Root cause: bin/qmd has a #!/bin/sh shebang. When npm generates its cmd/ps1/sh shims on Windows, it reads that shebang and routes them through /bin/sh — a path that doesn't exist on Windows:

IF EXIST "%dp0%/bin/sh.exe" (
SET "_prog=%dp0%/bin/sh.exe"
) ELSE (
SET "_prog=/bin/sh"

So the shim dies before any qmd code runs.

Fix

Rewrite bin/qmd as a Node.js script with a #!/usr/bin/env node shebang. npm then generates native shims that invoke node directly on every platform. The runtime-detection logic is preserved verbatim:

  • package-lock.json present → node (npm install)
  • bun.lock / bun.lockb present → bun
  • no lockfile → node

Implementation notes:

  • Node is launched via process.execPath (guaranteed to exist, skips PATH resolution, avoids the child_process shell: true deprecation warning).
  • Bun is launched by name; on Windows this requires shell: true so PATHEXT resolves bun.exe / bun.cmd.
  • Symlink resolution still happens via fs.realpathSync, matching the old shell readlink loop, so npm link keeps working.
  • Signals from the parent are re-raised after the child exits, so Ctrl-C behaves the same as before.

Testing

  • Verified locally on Windows 11: qmd --help works from cmd, PowerShell, and Git Bash after npm install -g.
  • test/launcher-detection.test.sh still documents the intended detection behavior; its detect_runtime helper mirrors the JS implementation. A platform-agnostic vitest version can follow in a separate PR (would also let Windows CI catch regressions — currently CI only runs Linux + macOS).

Alternative considered

Pointing bin.qmd in package.json directly at dist/cli/qmd.js (which already has a Node shebang). That's simpler but drops the bun-vs-node lockfile detection, so I kept the launcher.

The previous launcher was a POSIX shell script with `#!/bin/sh`. When
`npm install -g @tobilu/qmd` runs on Windows, npm reads that shebang
and generates cmd/ps1/sh shims that try to route through `/bin/sh` —
which doesn't exist on Windows, so `qmd` fails immediately after a
successful install from every shell (cmd, PowerShell, Git Bash).

Rewrite the launcher in Node.js with a `#!/usr/bin/env node` shebang so
npm generates native shims that invoke `node` directly on every
platform. The runtime detection logic is preserved verbatim:

- package-lock.json present → node (npm install)
- bun.lock or bun.lockb present → bun
- no lockfile → node

Node is launched via `process.execPath` (guaranteed to exist, no PATH
lookup). Bun is launched by name; on Windows this needs `shell: true`
so PATHEXT resolves `bun.exe`/`bun.cmd`.

The shell-based test at `test/launcher-detection.test.sh` continues to
document the intended detection behavior; its `detect_runtime` helper
mirrors the JS implementation. A platform-agnostic vitest version can
follow in a separate PR.
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