Skip to content

Official commander docs example compiles successfully, but the generated binary ignores CLI arguments at runtime #187

Closed

Description

@mktbsh

What happened

I copied the official Perry commander example and compiled it successfully, but the resulting native binary ignores the CLI at runtime. --help and serve --port 3000 both exit with status 0 and print nothing to stdout or stderr.

What you expected

I expected the compiled binary to behave like the commander example normally does: --help should print help text, and serve --port 3000 should run the action and print Starting server on port 3000.

Minimal reproduction

This is the exact sample from the Perry docs commander section.

import { Command } from "commander";

const program = new Command();
program.name("my-cli").version("1.0.0").description("My CLI tool");

program
  .command("serve")
  .option("-p, --port <number>", "Port number")
  .option("--verbose", "Verbose output")
  .action((options) => {
    console.log(`Starting server on port ${options.port}`);
  });

program.parse(process.argv);

Command you ran:

npm install commander
perry main.ts -o app
./app --help
./app serve --port 3000
./app serve --port 3000 --verbose

Environment

  • Perry version: perry 0.5.178
  • Host OS: macOS 26.4.1 (arm64)
  • Target: native
  • Installed via: brew

Diagnostic output

perry main.ts -o app succeeds:

Collecting modules...
Found 1 module(s): 1 native, 0 JavaScript
Generating code...
Wrote object file: tmp_perry_commander_docs_ts.o
Linking (with stdlib)...
Wrote executable: .tmp-perry-commander-docs-bin
Binary size: 2.9MB

Runtime output is empty for all of these commands, even though they exit successfully:

$ ./app --help
# exit code: 0
# stdout: empty
# stderr: empty

$ ./app serve --port 3000
# exit code: 0
# stdout: empty
# stderr: empty

$ ./app serve --port 3000 --verbose
# exit code: 0
# stdout: empty
# stderr: empty

perry doctor on this machine:

Perry Doctor

Environment Checks
──────────────────
  [OK] perry version: 0.5.178
  [OK] update status: no cached info (run `perry update --check-only`)
  [OK] system linker (cc): Apple clang version 17.0.0 (clang-1700.6.4.2)
  [WARN] runtime library: not found - run: cargo build --release -p perry-runtime
  [OK] project config (perry.toml): found

All critical checks passed with some warnings.

Anything else

  • I copied this from the Perry docs commander example, so this currently looks like a docs-backed reproduction rather than custom project code.
  • A manual process.argv.slice(2) parser works as a workaround in the same environment.
  • I also observed that process.argv itself is populated in Perry, so the failure seems specific to commander behavior under Perry rather than missing argv data.

Activity

  1. added
    bugConfirmed defect or regression
    on Apr 25, 2026
  2. proggeramlug commented on Apr 25, 2026

    @proggeramlug
    Contributor

    I'm taking a detailed look, getting back to you soon!

  3. added 4 commits that reference this issue on Apr 25, 2026
  4. proggeramlug commented on Apr 25, 2026

    @proggeramlug
    Contributor

    Reproduced and fixed.

    Root cause was two layers deep:

    1. new Command() allocated a placeholder. Expr::New { class_name: "Command" } fell into lower_call.rs::lower_new's empty-class fallback, producing an empty js_object_alloc(0,0) ObjectHeader instead of calling js_commander_new(). So the receiver every fluent method dispatched against was junk.

    2. No method dispatch entries. crates/perry-codegen/src/lower_call.rs::NATIVE_MODULE_TABLE had zero rows for module: "commander", so every program.command(...) / .option(...) / .action(...) / .parse(...) call fell through the unknown-method sentinel and silently returned double 0.0. The .action() callback in particular was taken as _callback: i64 and immediately discarded — there was even a comment in the runtime saying "Action callbacks are stored but not automatically invoked".

    3. HIR fluent chain. Even fixing the above, only the innermost program.name(...) got recognized as a NativeMethodCall — the chained .version(...).description(...) were generic Call+PropertyGet because the receiver was no longer a LocalGet of a registered native instance.

    Fixed across three commits on main:

    • 6f109b12 (v0.5.249) — HIR fluent-chain detector extended to commander so chained methods keep dispatching as NativeMethodCalls.
    • 22ac6595 (v0.5.250) — crates/perry-stdlib/src/commander.rs rewrite: subcommand registration, action callback storage, real parse_and_dispatch, built-in --help/-h/--version/-V, options-object construction via js_object_alloc + js_object_set_field_by_name, action invocation via js_closure_call1, GC root scanner pinning stored callbacks across collections. Plus 9 NATIVE_MODULE_TABLE rows + a "Command" branch in lower_builtin_new calling js_commander_new().
    • 120208a6 (v0.5.270) — bonus: same bug shape applied to Redis (ioredis) plus 9 dispatch-table-symbol fixes. Sibling work from this session also closed the same shape on EventEmitter, LRUCache, WebSocketServer, Decimal (decimal.js), AsyncLocalStorage, pg Client/Pool, and MongoClient.

    Verified end-to-end against your exact repro (commander@14.0.0 from npm):

    $ ./app --help
    My CLI tool
    
    Usage: my-cli [options] [command]
    
    Options:
      -V, --version             output the version number
      -h, --help                display help for command
    
    Commands:
      serve
    
    $ ./app --version
    1.0.0
    
    $ ./app serve --port 3000
    Starting server on port 3000
    
    $ ./app serve --port 3000 --verbose
    Starting server on port 3000
    

    To get the fix today: build from main. Otherwise it'll be in the next release (cutting later today, within a few hours from this comment). Thanks for the clean repro — it surfaced a whole family of latent stub-grade native-class bugs that have now all been swept.

  5. added a commit that references this issue on Apr 25, 2026
    6676ea3
  6. mktbsh commented on Apr 26, 2026

    @mktbsh
    Author

    The issue has been successfully resolved. I truly appreciate your quick support in fixing this.

    If you ever decide to set up GitHub Sponsors, I would love to become a sponsor.

  7. proggeramlug commented on Apr 26, 2026

    @proggeramlug
    Contributor

    @mktbsh Waiting for approval, will tag you here once it is through :)

    Thank you so much, it means a lot to me!

  8. proggeramlug commented on Apr 26, 2026

    @proggeramlug
    Contributor

    @mktbsh if you want to, Ko-Fi is up already :) https://ko-fi.com/perryts

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugConfirmed defect or regression

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions