process.execPath is never realpath-resolved, so argv-filtering breaks when node is reached via a symlink/junction (scoop, nvm, fnm, volta)
- Dominant language
- JavaScript
- Stars
- 8
- Forks
- 0
- PR merge metrics
- No merged PRs in 30d
Description
## Summary
Any CLI built with `pargs` fails with `Unexpected argument ''. This command does not take positional arguments`, even when no positional argument was actually passed — as long as Node.js is reached through a symlink or junction (e.g. Scoop's "current" version pointer on Windows, or nvm/fnm/volta depending on their setup).
## Environment
- `pargs@1.4.2` (latest published at the time of this report)
- Node.js installed via a version manager that exposes a "current" pointer as a symlink/junction rather than a plain directory
- Reproduced identically under Git Bash and PowerShell — shell-independent
## Root cause
In `index.mjs`, the `process.argv` filter that is supposed to strip the node executable and the script path out of the real arguments:
```js
const realEntrypointPath = realpathSync(entrypointPath);
const argv = process.argv.flatMap((arg) => {
try {
const realpathedArg = realpathSync(arg);
if (
realpathedArg === process.execPath
|| realpathedArg === realEntrypointPath
) {
return [];
}
} catch { /**/ }
return arg;
});
```
`arg` is resolved through `realpathSync()` before the comparison, but `process.execPath` never is. When Node is invoked through a symlink/junction that the OS does not resolve at process start, `process.execPath` stays the unresolved path while `realpathSync(arg)` (for that very same `arg`, i.e. `process.argv[0]`) returns the real target — so the two are never equal, and the executable's path survives the filter and gets treated as an unexpected positional argument.
## Minimal reproduction
```js
import { realpathSync } from 'fs'
console.log('execPath :', process.execPath)
console.log('realpath(argv[0]) :', realpathSync(process.argv[0]))
console.log('equal? :', realpathSync(process.argv[0]) === process.execPath)
```
Output on a machine with an active Scoop "current" symlink:
```
execPath : C:\Users\kevin.gossent\scoop\apps\nodejs-lts\current\node.exe
realpath(argv[0]) : C:\Users\kevin.gossent\scoop\apps\nodejs-lts\24.18.1\node.exe
equal? : false
```
## Suggested fix
Resolve `process.execPath` once, the same way `entrypointPath` already is:
```diff
const realEntrypointPath = realpathSync(entrypointPath);
+const realExecPath = realpathSync(process.execPath);
const argv = process.argv.flatMap((arg) => {
try {
const realpathedArg = realpathSync(arg);
if (
- realpathedArg === process.execPath
+ realpathedArg === realExecPath
|| realpathedArg === realEntrypointPath
) {
```
## Impact
Affects every package that depends on `pargs` (in my case, `ls-engines`) for anyone using a symlink/junction-based Node version manager instead of a directly-installed binary.
Contributor guide
Research direction
Start in index.mjs at the process.argv filtering logic and compare how entrypointPath and process.execPath are resolved. Resolve process.execPath consistently, then verify that a symlink or junction-based Node installation no longer leaves the executable path to be reported as an unexpected positional argument.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- javascript
- Domain
- cli
- Issue type
- Bug
- Difficulty
- 1/5
- Estimated time
- Under an hour
- Activity status
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 88/100