Skip to content

fix: prevent registerSubcommand from injecting false positional args#111

Merged
nicknisi merged 7 commits intomainfrom
fix/register-subcommand-positional-args
Mar 31, 2026
Merged

fix: prevent registerSubcommand from injecting false positional args#111
nicknisi merged 7 commits intomainfrom
fix/register-subcommand-positional-args

Conversation

@nicknisi
Copy link
Copy Markdown
Member

@nicknisi nicknisi commented Mar 31, 2026

Summary

Fixes a bug where commands using named --options with demandOption: true (e.g., permission create, role create, invitation create) rejected valid invocations with:

Not enough non-option arguments: got 0, need at least 4

Root cause

registerSubcommand() probes the builder to discover required options, then enriches the usage string:

create  →  create --slug <string> --name <string>

This enriched string was passed directly as the command argument to yargs.command(). Yargs interprets <...> tokens in the command string as required positional arguments, so it parsed this as:

Token Yargs interpretation
create command name
--slug positional arg #1
<string> positional arg #2
--name positional arg #3
<string> positional arg #4

This created 4 phantom positional requirements on top of the actual --slug and --name options, making the command impossible to use with standard flag syntax.

Workaround users discovered

# Had to pass values as positional args to satisfy the phantom requirement
workos permission create test-perm empty "Test permission" empty --description "testing desc"

Fix

  • Pass only the original usage string (e.g., create) as the yargs command — preserving any real positionals like <slug> on commands that use them (e.g., update <slug>, delete <slug>)
  • Set the enriched string via .usage(), which is display-only and only affects --help output

Before (broken)

return parentYargs.command(enrichedUsage, description, builder, handler);
//                         ^^^^^^^^^^^^^ "create --slug <string> --name <string>"
//                         yargs treats <string> as positional args

After (fixed)

return parentYargs.command(usage, description, (y) => {
//                         ^^^^^ "create" — no phantom positionals
  const built = builder(y);
  if (enrichedUsage !== usage) {
    built.usage(`$0 ${enrichedUsage}`);  // display-only for --help
  }
  return built;
}, handler);

Affected commands

Any subcommand registered via registerSubcommand with demandOption options:

  • permission create / role create / invitation create
  • seed (multiple required options)
  • setup-org, onboard-user (mixed positional + required options)
  • Several others across organization, user, membership, etc.

Commands using only positional args (e.g., permission delete <slug>) or no required options (e.g., permission list) were unaffected.

Verified

  • Old build: workos permission create --slug "test-perm" --name "Test Permission"Not enough non-option arguments: got 0, need at least 4
  • New build: ./dist/bin.js permission create --slug "test-perm" --name "Test Permission" → successfully creates permission

Test plan

  • pnpm build passes
  • pnpm test passes (all 1509 tests across 117 files)
  • pnpm typecheck passes
  • Manually verified permission create --slug --name works on patched build
  • Manually verified old build reproduces the reported bug
  • Verify --help output still shows required options in usage line
  • Verify commands with real positionals (update <slug>, delete <slug>) still work

The enriched usage string (e.g., `create --slug <string> --name <string>`)
was passed directly as the yargs command string. Yargs interprets `<...>`
tokens in command strings as required positional arguments, causing commands
like `permission create` and `role create` to demand phantom positional
args alongside the actual named options.

Move the enriched string to `.usage()` (display-only for help text) and
pass only the original command string to `yargs.command()`.

Fixes: commands with demandOption flags (permission create, role create,
invitation create, seed, etc.) rejecting valid `--flag value` invocations
with "Not enough non-option arguments".
Exercises yargs arg parsing end-to-end for all 16 commands that use
registerSubcommand with demandOption named flags. Each test verifies
that standard --flag syntax is accepted without "Not enough non-option
arguments" errors.

The existing smoke test bypasses yargs entirely (calls handlers directly),
so it could not catch the parsing bug. These tests fill that gap.
Move the required-flag annotation from the yargs command string to the
description. This fixes two issues with the previous approach:

- P2: `$0` in `.usage()` resolved to the root script name, so nested
  commands like `workos role create --help` showed `workos create ...`
  instead of `workos role create ...`
- P3: parent help lost enrichment entirely since the command string was
  reverted to plain usage

Now parent help shows e.g.:
  create   Create a permission (requires --slug, --name)

Positional args already visible in the command string (e.g., <slug>) are
excluded from the description enrichment to avoid redundancy.
Commands like `role delete` and `role remove-permission` already include
`(requires --org)` in their description. The enrichment helper now
filters out flags that the description already references, preventing
duplicated text like `(requires --org) (requires --org)`.
Convert 15 copy-paste test cases into data-driven it.each blocks and
remove unused YargsOptions fields (string, number, boolean arrays).
@nicknisi nicknisi merged commit effe187 into main Mar 31, 2026
5 checks passed
@nicknisi nicknisi deleted the fix/register-subcommand-positional-args branch March 31, 2026 20:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

1 participant