Skip to content

feat(default): support default sub command#156

Open
fu050409 wants to merge 4 commits intounjs:mainfrom
fu050409:feat/default-subcmd
Open

feat(default): support default sub command#156
fu050409 wants to merge 4 commits intounjs:mainfrom
fu050409:feat/default-subcmd

Conversation

@fu050409
Copy link
Copy Markdown

@fu050409 fu050409 commented Jul 5, 2024

Support default sub command

Resolved: #153

Summary by CodeRabbit

  • New Features
    • Commands can declare a default sub-command that runs when no sub-command is provided.
  • Bug Fixes
    • Prevents conflicting setups where a default sub-command and a top-level handler are both specified (now raises an error).
  • Tests
    • Added tests for default sub-command execution, explicit sub-command precedence, and the conflict case.

@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

📝 Walkthrough

Walkthrough

When a command has subCommands and a default is specified, the CLI infers the sub-command name from that default when no explicit sub-command token is present. If a default is used while the main command also defines a top-level run, an E_DUPLICATE_COMMAND error is thrown.

Changes

Cohort / File(s) Summary
Default Sub-command Resolution
src/command.ts
Infer defaultSubCommand from cmd.default when no sub-command token is present; set subCommandName to that default or throw E_DUPLICATE_COMMAND if main command has run.
Type Definition
src/types.ts
Add default?: Resolvable<keyof ArgsDef | string> to CommandDef to declare a default sub-command or argument.
Test Coverage
test/main.test.ts
Add tests for default sub-command behavior: runs default when no args, explicit sub-command overrides default, and error when both default and main run exist (duplicate test blocks present).

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 (no subcommand token)
    CLI_Core->>CmdResolver: parse args, detect no subcommand
    CmdResolver->>CmdResolver: check `cmd.default`
    alt default present and main has no run
        CmdResolver->>SubCmd: resolve default subcommand name
        CLI_Core->>SubCmd: execute subcommand
        SubCmd-->>User: return result
    else default present and main has run
        CmdResolver-->>User: throw E_DUPLICATE_COMMAND
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Suggested reviewers

  • pi0

Poem

🐰 I sniff the args and hop with glee,
A hidden path called "dev" waits just for me.
No token, no trouble — I follow the trail,
If you call it directly, I won't derail.
A carrot-bound cheer for tidy CLI detail!

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning The test suite contains duplicate 'default sub command' test blocks within the same file, which appears to be an unintended duplication rather than intentional scope. Remove the duplicate test suite block to eliminate redundant test cases and maintain clean test organization.
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 (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main feature being added: support for specifying a default subcommand.
Linked Issues check ✅ Passed The PR implements the requested feature from #153 by adding a default property to CommandDef, allowing specification of a default subcommand when no args are provided.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
📝 Coding Plan
  • Generate coding plan for human review comments

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

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

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