Skip to content

feat(default): support default sub command#156

Merged
pi0 merged 7 commits intounjs:mainfrom
fu050409:feat/default-subcmd
Apr 1, 2026
Merged

feat(default): support default sub command#156
pi0 merged 7 commits intounjs:mainfrom
fu050409:feat/default-subcmd

Conversation

@fu050409
Copy link
Copy Markdown
Contributor

@fu050409 fu050409 commented Jul 5, 2024

Support default sub command

Resolved: #153

Summary by CodeRabbit

  • New Features
    • Commands can declare a resolvable default sub-command that runs when no sub-command is provided; explicit sub-command names still take precedence.
  • Bug Fixes
    • Errors now surface for invalid default names and for conflicts when both a default and a top-level handler are defined.
    • Unconsumed CLI args are correctly forwarded to the chosen sub-command.
  • Tests
    • Added tests for default execution, explicit precedence, invalid default, conflict detection, and args forwarding.

@fu050409 fu050409 changed the title feat(default): support default sub command (#153) feat(default): support default sub command Jul 5, 2024
@smorimoto
Copy link
Copy Markdown

We really need this!

Support `default` option in command definition to specify a fallback
sub command when no args are provided (resolves unjs#153).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Mar 15, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 1eea67b5-6c49-4ada-aabf-6bfbd4635764

📥 Commits

Reviewing files that changed from the base of the PR and between 6c034f6 and 6b34f91.

📒 Files selected for processing (2)
  • src/command.ts
  • test/main.test.ts
✅ Files skipped from review due to trivial changes (1)
  • test/main.test.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/command.ts

📝 Walkthrough

Walkthrough

When a command has subCommands, the CLI can resolve a default (resolvable string) to select a subcommand when no explicit subcommand token is present. Conflicts between default and top-level run are rejected; missing defaults produce an unknown-command error. Default invocation forwards original rawArgs.

Changes

Cohort / File(s) Summary
Command dispatch
src/command.ts
Add logic to detect explicit subcommand token vs. no-token default resolution; throw E_DEFAULT_CONFLICT if default and top-level run both exist; resolve and validate default via subcommand lookup and throw E_UNKNOWN_COMMAND when missing; forward original opts.rawArgs when invoking resolved default.
Type updates
src/types.ts
Add optional default?: Resolvable<string> to CommandDef to allow specifying a resolvable default subcommand name.
Tests
test/main.test.ts
Add tests for default subcommand behavior: executing resolved default when no token, explicit token overriding default, conflict when both default and run present, unknown-default error, and propagation of unconsumed CLI args to default subcommand.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant CLI_Core as CLI Core
    participant CmdResolver as Command Resolver
    participant SubCmd as SubCommand Runner

    User->>CLI_Core: invoke command (with or without subcommand token)
    CLI_Core->>CmdResolver: parse opts, detect subCommands
    CmdResolver->>CmdResolver: resolve `cmd.default` if present
    alt `default` and top-level `run` present
        CmdResolver-->>CLI_Core: throw E_DEFAULT_CONFLICT
        CLI_Core-->>User: error
    else `default` present
        CmdResolver->>CmdResolver: validate default exists via subcommand lookup
        alt default not found
            CmdResolver-->>CLI_Core: throw E_UNKNOWN_COMMAND
            CLI_Core-->>User: error
        else default found
            CmdResolver->>SubCmd: select subCommandName (explicit token or default)
            alt explicit token present
                CLI_Core->>SubCmd: execute with rawArgs sliced after token
            else no explicit token (using default)
                CLI_Core->>SubCmd: execute with original rawArgs
            end
            SubCmd-->>User: result
        end
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Poem

🐰 I sniff the args and hop aboard,
A default path found in my hoard.
No token? I follow the named light,
Say it loud and I won't take flight.
Carrots clapped — the commands align, delight!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat(default): support default sub command' clearly and specifically summarizes the main feature addition: support for a default subcommand behavior.
Linked Issues check ✅ Passed The PR implementation fully meets the requirements from issue #153: it adds a default field to CommandDef, executes the default subcommand when no explicit subcommand is provided, prevents both main run and default from being set simultaneously, and validates that default points to an existing subcommand.
Out of Scope Changes check ✅ Passed All changes are directly scoped to implementing default subcommand support: type definition extensions, command resolution logic, and comprehensive test coverage with no extraneous modifications.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/command.ts (1)

62-72: ⚠️ Potential issue | 🟠 Major

Don't replay the parent's argv into the default child.

When there is no explicit subcommand token, Line 62 yields -1, so Line 64 falls back to defaultSubCommand but Line 71 still does slice(0). A call like cli --config foo will hand --config foo to the default child, so parent-only options get reparsed there instead of behaving like cli <default>. Either restrict the fallback to the true empty-argv case or strip the parent args before recursing.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/command.ts` around lines 62 - 72, The code currently falls back to
defaultSubCommand when findSubCommandIndex returns -1 but then passes
opts.rawArgs.slice(subCommandArgIndex + 1) which becomes slice(0) and replay
parent args into the child; fix by computing a childRawArgs variable: if
subCommandArgIndex === -1 set childRawArgs = [] (strip parent args) otherwise
set childRawArgs = opts.rawArgs.slice(subCommandArgIndex + 1), then call
runCommand(subCommand, { rawArgs: childRawArgs }); update the block around
findSubCommandIndex, subCommandArgIndex, defaultSubCommand and runCommand
accordingly so the default subcommand doesn't reparse parent-only options.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/command.ts`:
- Around line 37-44: resolveSubCommand() currently ignores cmd.default and only
treats an explicit bare token as a subcommand, causing inconsistency with
runCommand() which honors cmd.default; update resolveSubCommand() to thread the
command's default value (cmd.default) into its resolution logic so that when
rawArgs is empty or lacks an explicit subcommand it returns the resolved default
subcommand instead of the parent command. Specifically, inside
resolveSubCommand() (and the alternate branch referenced around lines 123-133),
consult resolveValue(cmd.default) the same way runCommand() does, validate
conflicts with cmd.run, and return the resolved default command token so both
entry points agree on the active command.

---

Outside diff comments:
In `@src/command.ts`:
- Around line 62-72: The code currently falls back to defaultSubCommand when
findSubCommandIndex returns -1 but then passes
opts.rawArgs.slice(subCommandArgIndex + 1) which becomes slice(0) and replay
parent args into the child; fix by computing a childRawArgs variable: if
subCommandArgIndex === -1 set childRawArgs = [] (strip parent args) otherwise
set childRawArgs = opts.rawArgs.slice(subCommandArgIndex + 1), then call
runCommand(subCommand, { rawArgs: childRawArgs }); update the block around
findSubCommandIndex, subCommandArgIndex, defaultSubCommand and runCommand
accordingly so the default subcommand doesn't reparse parent-only options.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: bc725d13-3407-49ba-8906-3c2b5539caef

📥 Commits

Reviewing files that changed from the base of the PR and between ea428c7 and 39c2317.

📒 Files selected for processing (3)
  • src/command.ts
  • src/types.ts
  • test/main.test.ts

Comment thread src/command.ts Outdated
Comment on lines +37 to +44
// Resolve default sub command
const defaultSubCommand = await resolveValue(cmd.default);
if (defaultSubCommand && cmd.run) {
throw new CLIError(
`Command has a handler specified and a default sub command.`,
"E_DUPLICATE_COMMAND",
);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Thread default through resolveSubCommand() too.

runCommand() now honors cmd.default, but resolveSubCommand() still only resolves an explicit bare token and returns the parent command for rawArgs = []. That makes the two exported entry points disagree about which command is active for the same definition.

Also applies to: 123-133

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/command.ts` around lines 37 - 44, resolveSubCommand() currently ignores
cmd.default and only treats an explicit bare token as a subcommand, causing
inconsistency with runCommand() which honors cmd.default; update
resolveSubCommand() to thread the command's default value (cmd.default) into its
resolution logic so that when rawArgs is empty or lacks an explicit subcommand
it returns the resolved default subcommand instead of the parent command.
Specifically, inside resolveSubCommand() (and the alternate branch referenced
around lines 123-133), consult resolveValue(cmd.default) the same way
runCommand() does, validate conflicts with cmd.run, and return the resolved
default command token so both entry points agree on the active command.

pi0 and others added 2 commits March 15, 2026 23:00
Only resolve `cmd.default` when no explicit sub command arg is provided,
avoiding unnecessary resolution in the common case.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
test/main.test.ts (1)

479-492: Strengthen the conflict test by asserting the CLI error code.

The behavior contract includes E_DUPLICATE_COMMAND; asserting code is less brittle than message text alone.

Proposed test assertion update
-    await expect(commandModule.runCommand(command, { rawArgs: [] })).rejects.toThrow(
-      /handler specified and a default sub command/,
-    );
+    await expect(commandModule.runCommand(command, { rawArgs: [] })).rejects.toMatchObject({
+      name: "CLIError",
+      code: "E_DUPLICATE_COMMAND",
+    });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/main.test.ts` around lines 479 - 492, Update the test that expects a
rejection when both a default and run handler are specified to also assert the
CLI error code; specifically, when calling commandModule.runCommand with the
command created by defineCommand (the test named "throws when both default and
run are specified"), capture the thrown error and assert that error.code ===
"E_DUPLICATE_COMMAND" in addition to the existing message assertion to make the
test less brittle.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@test/main.test.ts`:
- Around line 479-492: Update the test that expects a rejection when both a
default and run handler are specified to also assert the CLI error code;
specifically, when calling commandModule.runCommand with the command created by
defineCommand (the test named "throws when both default and run are specified"),
capture the thrown error and assert that error.code === "E_DUPLICATE_COMMAND" in
addition to the existing message assertion to make the test less brittle.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: df112454-db50-402f-a164-d62c6729f295

📥 Commits

Reviewing files that changed from the base of the PR and between da33dcc and 14befe0.

📒 Files selected for processing (1)
  • test/main.test.ts

@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 1, 2026

Codecov Report

❌ Patch coverage is 86.66667% with 2 lines in your changes missing coverage. Please review.
⚠️ Please upload report for BASE (main@033daea). Learn more about missing BASE report.

Files with missing lines Patch % Lines
src/command.ts 86.66% 1 Missing and 1 partial ⚠️
Additional details and impacted files
@@           Coverage Diff           @@
##             main     #156   +/-   ##
=======================================
  Coverage        ?   97.65%           
=======================================
  Files           ?        8           
  Lines           ?      384           
  Branches        ?      134           
=======================================
  Hits            ?      375           
  Misses          ?        8           
  Partials        ?        1           

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

pi0 added 2 commits April 1, 2026 19:15
Validate `default + run` conflict eagerly, check that default references
an existing sub command, pass full rawArgs when using default, and fix
the type definition.
avoid eagerly resolving `cmd.default` and loading the default subcommand
when an explicit sub command is provided
@pi0 pi0 merged commit f40f1a2 into unjs:main Apr 1, 2026
3 checks passed
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.

Setup a default behavior when no subcommand used

3 participants