Skip to content

Allow command options to be added externally - #165

Open
afomera wants to merge 1 commit into
mainfrom
external-command-params
Open

Allow command options to be added externally#165
afomera wants to merge 1 commit into
mainfrom
external-command-params

Conversation

@afomera

@afomera afomera commented Aug 15, 2026

Copy link
Copy Markdown

Note

Per the Hanakai AI Policy: I used AI to help me put together the code changes with my heavy involvement and direction, but I have reviewed every line of code until it reached the quality bar I set for myself for this work.

At no point was AI running without my direction or supervision.

I believe the comments I've left are useful and the specs feel like they've ensured things are tested properly to me, but it is a lot of them. The commit message + PR description was written by me.

In hanami-cli, generate action declares a --skip-tests flag that nothing in hanami-cli itself uses. It exists only so that hanami-rspec and hanami-minitest, which hook into the command via Hanami::CLI.after "generate action", ..., have a flag their hooks can read. An unparsed flag is a hard error, so there was no other way for those gems to accept one.

Add Registry#command, which returns the registered command, along with Registry#option and Registry#argument as guarded wrappers over it. A third-party gem can now contribute the param its own hook needs

Hanami::CLI.after  "generate action", Commands::Generate::Action
Hanami::CLI.option "generate action", :skip_tests,
                    type: :flag, default: false, desc: "Skip test generation"

Because more than one gem may want the same option, adding it twice is a no-op rather than an error, as long as both declarations agree on :type, :required, :values and :default. Otherwise IncompatibleOptionError names the settings that differ. :cast is not compared, because procs aren't meaningfully comparable.

Note that this extends the command class, so it is not scoped to a single registration, and a subclass defined before the call won't inherit the param.

Registry already had a private #command returning a LookupResult, which is renamed to #lookup.

Commands and callbacks are now also passed only the params their #call declares, so neither side needs a ** catch-all to tolerate params contributed by another gem. Params are passed through untouched unless #call declares keywords and no keyword splat, so this only affects signatures that raise ArgumentError today: a #call declaring no keywords at all (def call(*args), or a callback block) still receives every param, since it reads them from a single positional Hash.

Closes #164

In hanami-cli, `generate action` declares a `--skip-tests` flag that nothing
in hanami-cli itself uses. It exists only so that hanami-rspec and
hanami-minitest, which hook into the command via `Hanami::CLI.after "generate
action", ...`, have a flag their hooks can read. An unparsed flag is a hard
error, so there was no other way for those gems to accept one.

Add `Registry#command`, which returns the registered command, along with
`Registry#option` and `Registry#argument` as guarded wrappers over it. A
third-party gem can now contribute the param its own hook needs

```ruby
Hanami::CLI.after  "generate action", Commands::Generate::Action
Hanami::CLI.option "generate action", :skip_tests,
                    type: :flag, default: false, desc: "Skip test generation"
```

Because more than one gem may want the same option, adding it twice is a
no-op rather than an error, as long as both declarations agree on :type,
:required, :values and :default. Otherwise IncompatibleOptionError
names the settings that differ. :cast is not compared, because procs aren't
meaningfully comparable.

Note that this extends the command class, so it is not scoped to a single
registration, and a subclass defined before the call won't inherit the param.

Registry already had a private #command returning a LookupResult, which
is renamed to #lookup.

Commands and callbacks are now also passed only the params their #call
declares, so neither side needs a ** catch-all to tolerate params
contributed by another gem. Params are passed through untouched unless #call
declares keywords and no keyword splat, so this only affects signatures that
raise ArgumentError today: a #call declaring no keywords at all (def call(*args), or a callback block) still receives every param, since it reads
them from a single positional Hash.
@afomera
afomera force-pushed the external-command-params branch from 28c7546 to 24d165b Compare August 15, 2026 06:38
@afomera
afomera marked this pull request as ready for review August 15, 2026 06:42
@afomera
afomera requested a review from timriley August 15, 2026 06:42
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.

Allow command options to be added externally

1 participant