Skip to content

fix(#499): Parse arguments for lazy loaded command objects#500

Open
cameronhunter wants to merge 1 commit intokazupon:mainfrom
cameronhunter:cameronhunter/499/lazy-loaded-commands-issue
Open

fix(#499): Parse arguments for lazy loaded command objects#500
cameronhunter wants to merge 1 commit intokazupon:mainfrom
cameronhunter:cameronhunter/499/lazy-loaded-commands-issue

Conversation

@cameronhunter
Copy link

@cameronhunter cameronhunter commented Feb 22, 2026

Description

Command line arguments are not being parsed for lazy-loaded sub-commands as described in Alternative Loader Return Types. The arguments are also not showing up in the usage. The issue was that the command wasn't resolved until after the arguments were resolved.

Linked Issues

Summary by CodeRabbit

  • Bug Fixes

    • Improved lazy-loaded sub-command argument handling and defaults
    • Fixed usage information generation for lazy sub-commands
  • Tests

    • Added test suite for lazy sub-command argument parsing behavior

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 22, 2026

📝 Walkthrough

Walkthrough

Changes modify the CLI argument parsing flow to resolve lazy commands earlier, before argument extraction, and to apply level-specific subCommands overrides earlier in the sequence. A new generate function is added for usage generation testing.

Changes

Cohort / File(s) Summary
Test Suite Addition
packages/gunshi/src/cli.test.ts
New test suite for "lazy command args not parsed" verifying lazy sub-command argument defaulting, parsing behavior, and usage output generation via generate() function.
Generator Function
packages/gunshi/src/generator.ts
New exported generate() function accepting name, mainCommand, and CliOptions to produce usage strings for commands.
Core Resolution Logic
packages/gunshi/src/cli/core.ts
Refactored sequence: level-specific subCommands override applied first, lazy commands eagerly resolved before argument extraction, and toKebab now uses resolved command state for proper command-name rendering.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Possibly related PRs

Suggested labels

bug

Poem

🐰 A lazy command once hid in the shadows deep,
But now we resolve it ere arguments we reap,
The order is rightful, the parsing pristine,
With generate's help, the usage shines keen! ✨

🚥 Pre-merge checks | ✅ 3
✅ 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 directly references the main fix: parsing arguments for lazy-loaded command objects, matching the PR's core objective and primary code changes.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

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

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Tip

Issue Planner is now in beta. Read the docs and try it out! Share your feedback on Discord.


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
Contributor

@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: 2

🧹 Nitpick comments (1)
packages/gunshi/src/cli/core.ts (1)

139-149: isLazyCommand branch in getCommandArgs is now unreachable from its only call site.

After the eager-resolution change, getCommandArgs is always called with a fully resolved Command<G>. The isLazyCommand guard (Line 142) will never be true. You could simplify to only handle the object and fallthrough cases, or remove LazyCommand<G> from the parameter union.

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

In `@packages/gunshi/src/cli/core.ts` around lines 139 - 149, getCommandArgs
currently includes an unreachable isLazyCommand<G> branch because callers now
always pass a resolved Command<G>; remove LazyCommand<G> from the function
signature and delete the isLazyCommand branch so the function simply checks if
cmd is an object and returns cmd.args || create<ExtractArgs<G>>(), otherwise
returns create<ExtractArgs<G>>(); update the signature to getCommandArgs<G
extends GunshiParamsConstraint>(cmd?: Command<G>) to reflect the narrower type.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/gunshi/src/cli/core.ts`:
- Around line 90-94: When calling resolveArgs in cli/core.ts, ensure the lazy
command's toKebab hint is preserved by copying it from the original LazyCommand
into resolvedCommand if missing; specifically, after resolveLazyCommand returns,
check resolvedCommand.toKebab and if undefined but the original command (the
LazyCommand passed into resolveLazyCommand) has command.toKebab set (e.g. set
via lazy(loader, { toKebab: true })), assign that value onto
resolvedCommand.toKebab before using resolvedCommand in resolveArgs — this
avoids changing utils.ts and fixes the kebab-casing regression for lazy
commands.
- Around line 74-83: When resolving lazy commands the toKebab flag is not
preserved; update the lazy-resolution code so the resolved Command includes
toKebab copied from the lazy definition: in the baseCommand construction (where
baseCommand is created from the lazy cmd) add toKebab: cmd.toKebab, and in the
needRunResolving/loaded branch (after resolveLazyCommand returns loaded) include
toKebab: loaded.toKebab when building the final resolvedCommand so toKebab flows
through isLazyCommand/resolveLazyCommand paths.

---

Nitpick comments:
In `@packages/gunshi/src/cli/core.ts`:
- Around line 139-149: getCommandArgs currently includes an unreachable
isLazyCommand<G> branch because callers now always pass a resolved Command<G>;
remove LazyCommand<G> from the function signature and delete the isLazyCommand
branch so the function simply checks if cmd is an object and returns cmd.args ||
create<ExtractArgs<G>>(), otherwise returns create<ExtractArgs<G>>(); update the
signature to getCommandArgs<G extends GunshiParamsConstraint>(cmd?: Command<G>)
to reflect the narrower type.

Comment on lines +74 to +83
// override subCommands with level-specific sub-commands for rendering
if (levelSubCommands) {
cliOptions.subCommands = levelSubCommands
}

// resolve lazy commands before parsing so loader-defined args are available
const resolvedCommand = isLazyCommand<G>(command)
? await resolveLazyCommand<G>(command, name, true)
: command

Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# First, verify the code context around lines 74-83
cat -n packages/gunshi/src/cli/core.ts | sed -n '65,90p'

Repository: kazupon/gunshi

Length of output: 1277


🏁 Script executed:

# Search for playground examples that test lazy commands
find . -type f \( -name "*playground*" -o -name "*example*" -o -name "*demo*" \) | grep -E "\.(ts|js|tsx|jsx)$"

Repository: kazupon/gunshi

Length of output: 429


🏁 Script executed:

# Search for lazy command usage patterns in the codebase
rg -n --type=ts 'resolveLazyCommand|isLazyCommand' --max-count=20

Repository: kazupon/gunshi

Length of output: 1535


🏁 Script executed:

# Check if there are test files that cover lazy commands
find . -type f -name "*.test.ts" -o -name "*.spec.ts" | xargs grep -l "lazy" 2>/dev/null | head -10

Repository: kazupon/gunshi

Length of output: 217


🏁 Script executed:

# Check the playground test file for lazy command examples
cat -n e2e/playground.spec.ts | head -100

Repository: kazupon/gunshi

Length of output: 2289


🏁 Script executed:

# Look at the resolveLazyCommand implementation to understand toKebab handling
cat -n packages/gunshi/src/utils.ts | sed -n '22,70p'

Repository: kazupon/gunshi

Length of output: 2200


🏁 Script executed:

# Search for toKebab usage in the codebase to understand if it's affected by lazy resolution
rg -n 'toKebab' --type=ts packages/gunshi/src/

Repository: kazupon/gunshi

Length of output: 1497


🏁 Script executed:

# Get the complete resolveLazyCommand implementation
cat -n packages/gunshi/src/utils.ts | sed -n '36,100p'

Repository: kazupon/gunshi

Length of output: 2612


🏁 Script executed:

# Check the Command type definition to see what properties should be copied
cat -n packages/gunshi/src/types.ts | sed -n '540,580p'

Repository: kazupon/gunshi

Length of output: 1336


🏁 Script executed:

# Check what happens at line 92 where toKebab is used
cat -n packages/gunshi/src/cli/core.ts | sed -n '85,100p'

Repository: kazupon/gunshi

Length of output: 712


🏁 Script executed:

# Find how toKebab is handled in the definition
cat -n packages/gunshi/src/definition.ts | sed -n '280,295p'

Repository: kazupon/gunshi

Length of output: 789


🏁 Script executed:

# Search for playground examples that use lazy commands
find e2e/fixture -name "config.json" -exec grep -l "lazy" {} \; | xargs dirname

Repository: kazupon/gunshi

Length of output: 127


🏁 Script executed:

# Check if any playground examples have toKebab in their definition
find playground -type f -name "*.ts" | xargs grep -l "toKebab" 2>/dev/null

Repository: kazupon/gunshi

Length of output: 42


🏁 Script executed:

# Look for test cases covering lazy commands with toKebab
grep -r "toKebab.*lazy\|lazy.*toKebab" packages/gunshi/src --type=ts

Repository: kazupon/gunshi

Length of output: 178


🏁 Script executed:

# Check the cli.test.ts to see if there are existing tests for lazy commands and toKebab
cat -n packages/gunshi/src/cli.test.ts | sed -n '1095,1145p'

Repository: kazupon/gunshi

Length of output: 1390


🏁 Script executed:

# Search for where toKebab is handled in lazy command resolution more carefully
rg -A10 -B5 "resolveLazyCommand" packages/gunshi/src/utils.ts | head -80

Repository: kazupon/gunshi

Length of output: 764


🏁 Script executed:

# Check if toKebab is supposed to be copied from the lazy command definition
cat -n packages/gunshi/src/utils.ts | sed -n '36,90p'

Repository: kazupon/gunshi

Length of output: 2175


🏁 Script executed:

# Look at what properties are in LazyCommand type
rg -B5 -A20 "type LazyCommand" packages/gunshi/src/types.ts | head -40

Repository: kazupon/gunshi

Length of output: 932


🏁 Script executed:

# Check if there are any tests for lazy commands with toKebab
rg -n "lazy.*toKebab|toKebab.*lazy" packages/gunshi/src/cli.test.ts

Repository: kazupon/gunshi

Length of output: 40


🏁 Script executed:

# Verify what happens when toKebab is set on a lazy command
cat -n packages/gunshi/src/definition.ts | sed -n '275,310p'

Repository: kazupon/gunshi

Length of output: 1326


🏁 Script executed:

# Check if toKebab should be in baseCommand when resolving lazy commands
cat -n packages/gunshi/src/types.ts | sed -n '400,440p'

Repository: kazupon/gunshi

Length of output: 1676


🏁 Script executed:

# Look for existing tests with lazy commands to understand expected behavior
rg -B5 -A15 "lazyCommand\|lazy.*toKebab" packages/gunshi/src/definition.test.ts | head -80

Repository: kazupon/gunshi

Length of output: 40


🏁 Script executed:

# Verify the exact issue: toKebab is NOT in the baseCommand properties copied from lazy command
cat -n packages/gunshi/src/utils.ts | sed -n '42,56p'

Repository: kazupon/gunshi

Length of output: 613


🏁 Script executed:

# Check if toKebab might be copied in the needRunResolving block
cat -n packages/gunshi/src/utils.ts | sed -n '57,79p'

Repository: kazupon/gunshi

Length of output: 1105


🏁 Script executed:

# Check definition.test.ts to see if there are tests verifying toKebab on lazy commands
rg -B3 -A10 "toKebab.*true" packages/gunshi/src/definition.test.ts | grep -A10 -B3 "lazy"

Repository: kazupon/gunshi

Length of output: 1439


🏁 Script executed:

# Look for any tests that verify lazy command with toKebab works correctly
rg -n "LazyCommand.*toKebab\|toKebab.*LazyCommand" packages/gunshi/src/

Repository: kazupon/gunshi

Length of output: 40


🏁 Script executed:

# Check if toKebab should be in the baseCommand or in needRunResolving block
# Let's see if there's a pattern - check if toKebab is handled elsewhere
rg -n "toKebab" packages/gunshi/src/utils.ts

Repository: kazupon/gunshi

Length of output: 40


🏁 Script executed:

# Verify the actual test that checks toKebab on lazy commands
cat -n packages/gunshi/src/definition.test.ts | sed -n '290,350p'

Repository: kazupon/gunshi

Length of output: 2389


🏁 Script executed:

# Check how non-lazy commands preserve toKebab
rg -B5 -A5 "Object.assign.*create.*Command" packages/gunshi/src/utils.ts

Repository: kazupon/gunshi

Length of output: 631


🏁 Script executed:

# Critical: Check if toKebab should be copied in the needRunResolving block from loaded
cat -n packages/gunshi/src/utils.ts | sed -n '57,79p'

Repository: kazupon/gunshi

Length of output: 1105


🏁 Script executed:

# Confirm: is there a test that actually exercises resolveLazyCommand and checks toKebab survives?
rg -n "resolveLazyCommand" packages/gunshi/src/cli.test.ts

Repository: kazupon/gunshi

Length of output: 40


🏁 Script executed:

# Check if there's a test for lazy commands with toKebab being used in cli function
rg -B10 -A10 "lazy.*toKebab\|toKebab.*lazy" packages/gunshi/src/cli.test.ts

Repository: kazupon/gunshi

Length of output: 40


🏁 Script executed:

# Check if there are any actual tests that use lazy commands in cli and verify toKebab works
rg -B5 -A15 "lazy\(" packages/gunshi/src/cli.test.ts | grep -A20 -B5 "toKebab"

Repository: kazupon/gunshi

Length of output: 40


🏁 Script executed:

# Search for any test files that test the full cli flow with lazy commands
find packages/gunshi -name "*.test.ts" -o -name "*.spec.ts" | xargs grep -l "lazy" 2>/dev/null

Repository: kazupon/gunshi

Length of output: 171


🏁 Script executed:

# Check the definition.test.ts to see the full test for lazy with toKebab
cat -n packages/gunshi/src/definition.test.ts | sed -n '270,320p'

Repository: kazupon/gunshi

Length of output: 1786


🏁 Script executed:

# Verify the actual behavior by checking what happens when a lazy command with toKebab uses camelCase args
cat -n packages/gunshi/src/definition.test.ts | sed -n '270,316p'

Repository: kazupon/gunshi

Length of output: 1655


🏁 Script executed:

# The test above uses '--foo' which matches the arg name exactly.
# Let's check if there's a test that would fail if toKebab didn't work
# Search for tests that verify kebab case argument conversion
rg -B10 -A10 "foo-bar.*toKebab|toKebab.*foo-bar" packages/gunshi/src/

Repository: kazupon/gunshi

Length of output: 40


🏁 Script executed:

# Check the actual condition: when resolvedCommand.toKebab is used on line 92
# Verify what value it would have for lazy commands
cat -n packages/gunshi/src/utils.ts | sed -n '36,89p' | grep -A60 "if (isLazyCommand"

Repository: kazupon/gunshi

Length of output: 1900


🏁 Script executed:

# Final verification: trace through what toKebab value resolvedCommand would have for a lazy command
# resolveLazyCommand is called with needRunResolving=true at core.ts:81
# Let's confirm that toKebab is missing from both paths in resolveLazyCommand

# Path 1: isLazyCommand(cmd) branch - baseCommand doesn't have toKebab
echo "=== Checking baseCommand properties (should NOT have toKebab) ==="
cat -n packages/gunshi/src/utils.ts | sed -n '43,51p'

echo ""
echo "=== Checking needRunResolving block (should NOT copy toKebab) ==="
cat -n packages/gunshi/src/utils.ts | sed -n '65,72p'

Repository: kazupon/gunshi

Length of output: 915


Fix missing toKebab property in lazy command resolution.

When resolving lazy commands, the toKebab property from the lazy command definition must be copied to the resolved Command object. Currently, it's missing from both the baseCommand object (line 43-51) and the needRunResolving block (line 65-72), causing the toKebab feature to break for all lazy commands.

Add toKebab: cmd.toKebab to baseCommand and toKebab: loaded.toKebab to the needRunResolving block to preserve this property through resolution.

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

In `@packages/gunshi/src/cli/core.ts` around lines 74 - 83, When resolving lazy
commands the toKebab flag is not preserved; update the lazy-resolution code so
the resolved Command includes toKebab copied from the lazy definition: in the
baseCommand construction (where baseCommand is created from the lazy cmd) add
toKebab: cmd.toKebab, and in the needRunResolving/loaded branch (after
resolveLazyCommand returns loaded) include toKebab: loaded.toKebab when building
the final resolvedCommand so toKebab flows through
isLazyCommand/resolveLazyCommand paths.

Comment on lines 90 to 94
const { explicit, values, positionals, rest, error } = resolveArgs(args, tokens, {
shortGrouping: true,
toKebab: command.toKebab,
toKebab: resolvedCommand.toKebab,
skipPositional
})
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

resolvedCommand.toKebab loses the lazy command's toKebab hint — regression.

resolveLazyCommand does not include toKebab in its baseCommand construction, nor does it copy toKebab from the loaded module. Before this change, command.toKebab on the original LazyCommand object correctly reflected the value set via lazy(loader, { toKebab: true }). After this change, resolvedCommand.toKebab is always undefined for lazy commands, silently breaking kebab-casing for any lazy command that specifies toKebab in its definition hint.

Quick fix in cliCore without touching utils.ts:

🐛 Proposed fix to preserve toKebab for lazy commands
  const { explicit, values, positionals, rest, error } = resolveArgs(args, tokens, {
    shortGrouping: true,
-   toKebab: resolvedCommand.toKebab,
+   toKebab: resolvedCommand.toKebab ?? (isLazyCommand<G>(command) ? command.toKebab : undefined),
    skipPositional
  })

A more thorough fix would be to preserve toKebab inside resolveLazyCommand in utils.ts (add toKebab: cmd.toKebab to baseCommand and copy loaded.toKebab in the needRunResolving block), which would also benefit the needRunResolving: false usage path.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const { explicit, values, positionals, rest, error } = resolveArgs(args, tokens, {
shortGrouping: true,
toKebab: command.toKebab,
toKebab: resolvedCommand.toKebab,
skipPositional
})
const { explicit, values, positionals, rest, error } = resolveArgs(args, tokens, {
shortGrouping: true,
toKebab: resolvedCommand.toKebab ?? (isLazyCommand<G>(command) ? command.toKebab : undefined),
skipPositional
})
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/gunshi/src/cli/core.ts` around lines 90 - 94, When calling
resolveArgs in cli/core.ts, ensure the lazy command's toKebab hint is preserved
by copying it from the original LazyCommand into resolvedCommand if missing;
specifically, after resolveLazyCommand returns, check resolvedCommand.toKebab
and if undefined but the original command (the LazyCommand passed into
resolveLazyCommand) has command.toKebab set (e.g. set via lazy(loader, {
toKebab: true })), assign that value onto resolvedCommand.toKebab before using
resolvedCommand in resolveArgs — this avoids changing utils.ts and fixes the
kebab-casing regression for lazy commands.

@pkg-pr-new
Copy link

pkg-pr-new bot commented Feb 23, 2026

Open in StackBlitz

@gunshi/bone

npm i https://pkg.pr.new/@gunshi/bone@500

@gunshi/combinators

npm i https://pkg.pr.new/@gunshi/combinators@500

@gunshi/definition

npm i https://pkg.pr.new/@gunshi/definition@500

@gunshi/docs

npm i https://pkg.pr.new/@gunshi/docs@500

gunshi

npm i https://pkg.pr.new/gunshi@500

@gunshi/plugin

npm i https://pkg.pr.new/@gunshi/plugin@500

@gunshi/plugin-completion

npm i https://pkg.pr.new/@gunshi/plugin-completion@500

@gunshi/plugin-dryrun

npm i https://pkg.pr.new/@gunshi/plugin-dryrun@500

@gunshi/plugin-global

npm i https://pkg.pr.new/@gunshi/plugin-global@500

@gunshi/plugin-i18n

npm i https://pkg.pr.new/@gunshi/plugin-i18n@500

@gunshi/plugin-renderer

npm i https://pkg.pr.new/@gunshi/plugin-renderer@500

@gunshi/resources

npm i https://pkg.pr.new/@gunshi/resources@500

@gunshi/shared

npm i https://pkg.pr.new/@gunshi/shared@500

commit: d8636d5

@kazupon kazupon added the bug Includes new features label Feb 23, 2026
@kazupon
Copy link
Owner

kazupon commented Feb 23, 2026

@cameronhunter
Thank you for your contribution!
And Welcome gunshi contribution!

I've just check your PR!
As coderabbit metion, Could you fix toKebab copy issue? 🙏
And you need to update the e2e snapshot.

Thanks!

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

Labels

bug Includes new features

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants