Fix #1213: pass JAVA_OPTS as argv array instead of interpolating it into a shell string
Summary
Fixes #1213.
pass-through.service.ts built the java command by joining ['java', process.env['JAVA_OPTS'], '-jar "..."', ...] into a single string and spawning it with shell: true. Any character that is meaningful to a shell therefore changed what was executed. The pipe in a legitimate JAVA_OPTS like
JAVA_OPTS=-Dhttp.proxyHost=localhost -Dhttp.proxyPort=9000 -Dhttps.proxyHost=localhost -Dhttps.proxyPort=9000 -Dhttp.nonProxyHosts=127.*|localhost|*.local
was interpreted as a shell pipe, so the CLI failed with
Error: 'localhost' is not recognized as an internal or external command
This also affected the exec()-based command discovery (help/completion are probed at startup), i.e. every CLI invocation crashed before a command even ran. Joining args into one string additionally triggered the Node.js deprecation warning DEP0190 (passing args to a shell: true spawn).
What changed
cmd()now returns a proper argv array instead of a shell string:['java', ...splitArgs(JAVA_OPTS), '-jar', cliPath](and['docker', 'run', '--rm', '-v', cwd:/local, image]in docker mode).JAVA_OPTSis split by a lightweight tokenizer that respects single quotes, double quotes and backslash escapes, so-Dhttp.nonProxyHosts=127.*|localhost(quoted or unquoted) arrives at the java process as one verbatim argument.passThrough()and thehelp/completiondiscovery now spawn with an args array andshell: false;stdio: 'inherit'and the exit-code handling stay the same. Removing the shell eliminates the entire class of escaping problems (including DEP0190) instead of trying to quote for cmd.exe/POSIX at once.- Quoting the jar/classpath arguments is no longer necessary (paths with spaces keep working, as each path is a single argv entry).
Note: version-manager.service.ts still spawns docker pull/docker rmi via a shell string — those commands contain no user-controlled input, so I left them untouched to keep this change focused.
Reproduction / verification
# before: Error: 'localhost' is not recognized as an internal or external command
JAVA_OPTS='-Dhttp.nonProxyHosts=127.*|localhost' openapi-generator-cli help
# after: prints usage, exit code 0
JAVA_OPTS='-Dhttp.nonProxyHosts=127.*|localhost' openapi-generator-cli help
The second case was verified against the built CLI (Node 24, Windows): it downloads the selected generator, runs the help/completion discovery via java and prints the usage text with exit code 0.
Tests
pass-through.service.spec.tsnow mocksspawn(returning anEventEmitterchild) for both the discovery calls and the passthrough spawn, and asserts the exact argv arrays.- New cases:
JAVA_OPTSwith an unquoted pipe is passed as a single argument, and quotedJAVA_OPTS(-Dhttp.nonProxyHosts="127.*|localhost|*.local",-Dsome.opt='a b') is split into separate arguments — both withshell: false. - Existing cases updated from the string+options form to the
spawn(command, args, options)form, keeping the same scenarios (docker, plain,JAVA_OPTS, custom generator with/without the standard jar). npx nx test generator-cli: 255 tests passing;npx nx lint generator-cliclean.
Summary by cubic
Fixes every CLI command crashing when JAVA_OPTS contains shell-special characters. The java command was built by interpolating JAVA_OPTS into a single string and spawned with shell: true, so a legitimate value like -Dhttp.nonProxyHosts=127.*|localhost was parsed as a pipe and failed startup; commands are now spawned as an argv array with shell: false. JAVA_OPTS is split by a tokenizer that respects quotes; unquoted backslashes are kept verbatim so Windows paths like C:\certs\client.jks survive, while backslash escapes are interpreted only inside double quotes.
Details
help/completiondiscovery now usesspawnwith an args array instead ofexec, and decodes output as utf8 so multi-byte characters split across chunks aren't rewritten as replacement characters.- Quoting jar/classpath paths is no longer needed since each path is a single argv entry.
version-managerdocker pull/rm still uses shell strings since they contain no user-controlled input.