Skip to content

feat(js-exec): accept node's -e and -p, and give process.argv node's shape - #428

Open
mutewinter wants to merge 3 commits into
vercel-labs:mainfrom
mutewinter:feat/js-exec-node-inline-options
Open

mutewinter wants to merge 3 commits into
vercel-labs:mainfrom
mutewinter:feat/js-exec-node-inline-options

Conversation

@mutewinter

Copy link
Copy Markdown
Contributor

Problem

An agent reaches for node's flag by reflex:

js-exec -e 'console.log(1 + 2)'
# js-exec: unrecognized option '-e'

It costs a turn to read --help and retry with -c. In a real transcript this was the first of three failures in a five-call attempt to list one directory.

The second is silent. process.argv is [scriptPath, ...args], so the universal idiom drops the first argument at exit 0:

printf 'console.log(process.argv.slice(2))' > args.js
js-exec args.js foo bar
# ["bar"]

Cause

parseArgs in js-exec.ts knows -c alone. run-runtime.ts builds const argv = [options.scriptPath, ...options.scriptArgs], with no executable slot; the existing test only asserts that foo and bar appear somewhere in the array.

Fix

-e/--eval (and --eval=CODE) are -c. -p/--print evaluate one expression and print its value, which is what node -p prints for a single expression statement; a multi-statement program under -p is a syntax error, reported as one. process.argv is ["js-exec", scriptPath, ...args] for a file and ["js-exec", ...args] for inline code, node's two shapes, with argv0 and execPath alongside. The one example that indexed argv[1] now indexes argv[2].

Scope

Unchanged: -c, -m, --strip-types, --, stdin, and the help flag anywhere in the arguments. A script's own -e after the file name still reaches the script.

Not addressed, deliberately: -p over a statement list (node prints the completion value; this prints an expression). Column offsets in an error from -p code are shifted by the wrapper.

Tests

js-exec.test.ts: -e, --eval, --eval=, -p with a trailing semicolon, --print passing arguments through, -e after -m, -e with no argument, and a script's own -e; the argv test now asserts both exact shapes. What they cannot prove: that a model finds the flag, which is the point of accepting it. js-exec wasm suite 315 passed across 14 files; the three js-exec security suites 13 passed; tsc, biome, and lint:banned clean.


Authored with Claude Opus 5

…shape

An agent writes `js-exec -e` by reflex, since that is node's flag, and gets `unrecognized option '-e'`; `-e`/`--eval` now mean `-c`, and `-p`/`--print` print the value of an expression.

process.argv was [scriptPath, ...args], so the universal `process.argv.slice(2)` dropped the script's first argument at exit 0. It is now ["js-exec", scriptPath, ...args] for a file and ["js-exec", ...args] for inline code, node's two shapes, with argv0 and execPath alongside.
@vercel

vercel Bot commented Sep 13, 2026

Copy link
Copy Markdown
Contributor

@mutewinter is attempting to deploy a commit to the Vercel Labs Team on Vercel.

A member of the Team first needs to authorize it.

@auto-maintain

auto-maintain Bot commented Sep 13, 2026

Copy link
Copy Markdown

🤖 auto-maintain review

Automated, advisory triage for @mutewinter's PR. Facts below are read from the GitHub API.

Check Result
Author's merged PRs (this repo) 10
Account established ✅ (age 5934d · 190 followers · 130 public repos)
Commits signed/verified ✅ 3/3
Changeset included ✅ (.changeset/js-exec-node-inline-options.md)

Review panel: 🟡 medium highest severity

just-bash maintainer code review: 🟡 medium

The PR has correctness defects in -p expression handling and misclassifies its breaking API change.

  • packages/just-bash/src/commands/js-exec/js-exec.ts:179 — The trailing-comment scanner is not JavaScript-aware. A valid expression such as `/[//]/` is truncated at `//` to `/[`, so `-p` reports a syntax error instead of printing the RegExp.
  • packages/just-bash/src/commands/js-exec/js-exec.ts:201 — The injected `__jbPrint` function is visible to evaluated code and uses mutable `console.log`. Thus `-p '__jbPrint'` observes an implementation detail instead of throwing, while `(console.log=()=>{},42)` exits successfully without printing 42.
  • .changeset/js-exec-node-inline-options.md:2 — This changes the documented process.argv indices incompatibly, but the changeset requests only a minor bump for the stable 3.x package. Consumers can receive silently wrong arguments without opting into a major upgrade.

General code review: 🟡 medium

The new options work for covered cases, but valid print expressions and stdin argv handling have concrete compatibility defects.

  • packages/just-bash/src/commands/js-exec/js-exec.ts:183 — The quote heuristic misclassifies quotes inside regex literals. `js-exec -p "/'/.source; // comment"` retains the semicolon inside generated parentheses and turns valid input into a syntax error.
  • packages/just-bash/src/commands/js-exec/run-runtime.ts:826 — Only `-c` is treated as having no script slot, so piped stdin exposes the synthetic `<stdin>` as `argv[1]`; Node omits that slot when executing implicit stdin, defeating the promised Node-compatible argv shape.

Adversarial security: 🟡 medium

The new -p preprocessing introduces a host-side denial-of-service path that should be fixed before merge.

  • packages/just-bash/src/commands/js-exec/js-exec.ts:183 — The trailing-comment scanner is quadratic: each `//` candidate rescans the entire prefix with `quotesBalanced`. A large `-p` string containing many slashes can monopolize the trusted host before `executeWithRun` starts its timeout, enabling denial of service. Parse once or enforce a bound before scanning.

Adversarial security (second opinion): 🟡 medium

Flag acceptance and the argv reshape look sound and contain no backdoor, networking, or sandbox-boundary change; the one actionable issue is unbounded quadratic host-side parsing in the new `-p` expression trimmer, which a sandboxed caller can use to hang the host process outside all existing limits.

  • packages/just-bash/src/commands/js-exec/js-exec.ts:173 — `-p`/`--print` runs `trailingExpression()`/`quotesBalanced()` on the raw inline code on the host thread, before any size or timeout limit applies (the guest-source byte cap in run-runtime.ts and the QuickJS timeout are only reached later). Both loops are quadratic in the input: e.g. `js-exec -p "1$(printf '/**/%.0s' $(seq 200000))"` makes the outer `for(;;)` strip one trailing block comment per iteration while `quotesBalanced(trimmed.slice(0, start))` rescans the whole prefix each time (the `//` branch on line 183 is quadratic the same way with an unterminated quote plus many `//`). Since command arguments can be up to `maxStringLength` (64 MB default), sandboxed script content can wedge the embedding Node process synchronously — a denial of service the threat model (§3.7) and the project's "reasonable limits to prevent runaway compute" rule both cover. Bound the code length accepted by `-p` and/or make the trim single-pass.

Standard Bash and host portability: 🟡 medium

The new -p implementation rejects valid regular-expression expressions.

  • packages/just-bash/src/commands/js-exec/js-exec.ts:181 — The trailing-comment scan mistakes `//` inside a regex character class for a comment. For example, `js-exec -p '/[//]/'` truncates the expression to `/[` and fails, while Node prints `/[//]/`. Use JavaScript-aware tokenization instead of quote balancing.

Posted by auto-maintain. This automated code review is advisory; a human maintainer makes the call.

…mments

-p handed its value to console.log, whose JSON formatting printed a RegExp as {} and a Symbol or a function as nothing; a hoisted printer now shows strings as they are and the rest as node inspects them, with objects and arrays staying JSON as they are everywhere in this runtime. A trailing semicolon, line comment, or block comment after the expression is stripped (with a // inside a string left alone), and an empty program prints undefined, as node does. The printer is a declaration below the expression so an error inside the expression is still reported on line 1.

The changeset and the argv comment claimed slice(2) for inline code too; under node that is slice(1), and the note now says so, along with what a script indexing argv directly has to check.
@mutewinter

Copy link
Copy Markdown
Contributor Author

All of it held, and the second commit takes each one, with a test per case.

-p no longer hands its value to console.log's JSON: a hoisted __jbPrint prints a string as it is and the rest the way node inspects them where JSON lost them, so /x/g prints /x/g, Symbol('x') prints Symbol(x), 10n prints 10n, a function prints [Function: f] or [Function (anonymous)], a Date its ISO form, and undefined/null/NaN themselves. Objects and arrays stay JSON, which is what console.log prints for them everywhere in this runtime; a -p that inspected differently from console.log would be the odder result. The printer is a declaration placed after the expression, so the expression stays on line 1 and an error in it is reported at -c:1:12 rather than several hundred columns in.

A trailing semicolon, line comment, or block comment after the expression is stripped before wrapping, so 1; // comment prints 1 and 1 /* two */; prints 1; a // inside a string is left alone ('http://x' // url prints http://x). An empty program and a bare ; print undefined, as node does. What stays a syntax error, deliberately, is a program of several statements, which node prints the completion value of; the help and README say EXPR.

The argv claim was wrong as written: for inline code node's own shape is [execPath, ...args], so slice(1), and the comment and changeset now say so for each shape. The changeset also carries the migration note the second-opinion review asked for: argv[0] was the script and is "js-exec", argv[1] was the first argument and is the script path, and a script indexing argv directly keeps running with the wrong values. It stays minor since the runtime is explicitly Node-compatible and this is the node shape; happy to make it major if you would rather.

js-exec suite: 329 passed across 14 files, up from 315 with the 14 new -p cases and the location test; tsc, biome, and lint:banned clean.

Comment thread packages/just-bash/src/commands/js-exec/js-exec.ts
…he comment scan

/https:\/\// ends in \/ followed by /, which the trailing-comment scan took for a line comment and cut, leaving a syntax error; a // whose first slash is escaped is now passed over.

This branch has not been deployed

No deployments
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