feat(cli): add PATH-based plugin discovery for external subcommands
Nobody has claimed this yet.
- Dominant language
- Rust
- Stars
- 8.7k
- Forks
- 1.3k
- Avg merge
- 2d 7h
- Merged PRs (30d)
- 243
Description
Problem Statement
OpenShell CLI should be extensible via third-party plugins, following the same pattern used by git, kubectl, docker, and helm. If a binary named openshell-<name> exists on $PATH, then openshell <name> [args...] should discover and execute it, forwarding all remaining arguments. This enables the ecosystem to grow beyond built-in commands without requiring changes to the core CLI.
Technical Context
The CLI binary is named openshell (defined in crates/openshell-cli/Cargo.toml). It uses clap 4.x with #[derive(Parser)] and a Commands enum with 14 subcommand variants. Today, unknown subcommands produce a clap error (unrecognized subcommand) before any custom code runs. Clap 4.x has first-class support for external subcommands via allow_external_subcommands and an #[command(external_subcommand)] enum variant, making this a well-supported pattern.
Affected Components
| Component | Key Files | Role |
|---|---|---|
| CLI entry point | crates/openshell-cli/src/main.rs |
Parser struct, Commands enum, dispatch match |
| Help template | crates/openshell-cli/src/main.rs:197-233 |
Custom grouped help text |
| Process exec patterns | crates/openshell-cli/src/ssh.rs:218-242 |
Existing exec_or_wait pattern to reuse |
| CLI tests | crates/openshell-cli/src/main.rs:3124-4575 |
~90 parse/completion unit tests |
Technical Investigation
Architecture Overview
main()
├── rustls crypto provider install
├── CompleteEnv::with_factory(Cli::command).complete() // shell completions
├── Cli::parse() // clap parses args
├── TlsOptions setup from global flags
├── tracing_subscriber init from --verbose
└── match cli.command {
Some(Commands::Gateway { .. }) => ...
Some(Commands::Sandbox { .. }) => ...
...14 variants...
None => print root help ◄── insertion point
}
Key clap configuration:
command: Option<Commands>— subcommand is optionaldisable_help_subcommand = true,disable_help_flag = true— help manually managedallow_external_subcommandsis not currently set- Custom
HELP_TEMPLATEgroups commands into "SANDBOX COMMANDS", "GATEWAY COMMANDS", "ADDITIONAL COMMANDS"
Code References
| Location | Description |
|---|---|
crates/openshell-cli/Cargo.toml:14 |
Binary name: openshell |
crates/openshell-cli/src/main.rs:356-408 |
Cli struct with #[derive(Parser)] |
crates/openshell-cli/src/main.rs:410-580 |
Commands enum with #[derive(Subcommand)] — 14 variants |
crates/openshell-cli/src/main.rs:197-233 |
HELP_TEMPLATE — custom grouped help |
crates/openshell-cli/src/main.rs:1887-3106 |
Main dispatch: match cli.command |
crates/openshell-cli/src/main.rs:3100-3102 |
None arm — prints help for no subcommand |
crates/openshell-cli/src/ssh.rs:218-242 |
exec_or_wait() — Unix process replacement pattern |
crates/openshell-cli/src/ssh.rs:1472-1492 |
launch_editor_command() — spawn with NotFound handling |
crates/openshell-cli/src/main.rs:3124-4575 |
Test module — ~90 parse/completion tests |
Current Behavior
When a user types openshell foobar:
Cli::parse()calls clap's parser- Clap sees
foobardoesn't match anyCommandsvariant - Clap prints
error: unrecognized subcommand 'foobar'with suggestions and exits code 2 - Custom code in
main()is never reached
What Would Need to Change
1. Clap configuration (Cli struct):
- Add
allow_external_subcommands = trueto the#[command(...)]attribute - Add an
#[command(external_subcommand)] External(Vec<OsString>)variant to theCommandsenum
2. Plugin dispatch (new match arm in main()):
- Extract the subcommand name and remaining args from the
External(Vec<OsString>)variant - Look up
openshell-<subcommand>on$PATH(viawhichcrate orCommand::new+ error matching) - Set environment variables for global CLI context (gateway name, endpoint, verbose level)
- On Unix: use
exec()to replace the process (matches existingexec_or_waitpattern) - On non-Unix: spawn and forward exit code
3. Internal binary blocklist:
- The workspace ships binaries like
openshell-gateway,openshell-sandbox,openshell-driver-kubernetes, etc. - These are on
$PATHin deployment scenarios but are NOT plugins - Need a blocklist to prevent
openshell driver-kubernetesfrom exec'ing the driver binary
4. Help template update:
- Add a "PLUGINS" section to
HELP_TEMPLATEexplaining thatopenshell-<name>binaries on PATH are available asopenshell <name>
Alternative Approaches Considered
| Approach | Pros | Cons | Verdict |
|---|---|---|---|
A. allow_external_subcommands |
Built into clap, minimal code, established pattern | Changes error messages for genuine typos | Recommended |
| B. Manual pre-parse | No clap changes, preserves error messages | Must handle global flags manually, fragile | Not recommended |
C. try_parse + fallback |
Preserves clap error messages for typos | More complex; must re-handle parse failures | Good fallback if typo UX matters |
Patterns to Follow
- Process replacement: Reuse the
exec_or_waitpattern fromssh.rs:218-242— Unixexec()for clean signal handling, Windows spawn+wait fallback - Error handling: Reuse the
NotFoundmatching pattern fromlaunch_editor_commandinssh.rs:1472-1492 - Env var conventions: Follow existing
OPENSHELL_*env var naming (gateway, gateway_endpoint, gateway_insecure)
Proposed Approach
Use clap's built-in allow_external_subcommands + #[command(external_subcommand)] enum variant. When an unknown subcommand is encountered, look up openshell-<name> on $PATH, propagate global CLI state as environment variables, and exec() the plugin on Unix (spawn+wait on Windows). Add a blocklist for internal binaries (openshell-gateway, openshell-sandbox, etc.) that are not plugins. Update the help template with a "PLUGINS" section.
Scope Assessment
- Complexity: Low
- Confidence: High — clear path, well-understood pattern, clap has first-class support
- Estimated files to change: 2-3 (main.rs, Cargo.toml for
whichdep, possibly a new test file) - Issue type:
feat
Risks & Open Questions
- Internal binary collision: Binaries like
openshell-gatewayandopenshell-sandboxare on PATH in deployments. A blocklist is needed, but the exact list should be reviewed — should it be hardcoded or derived from workspaceCargo.toml? - Auth token forwarding: Should plugins receive auth tokens via environment variables? This widens the trust boundary. Recommendation: do NOT pass tokens; let plugins resolve auth via
openshell-bootstrapAPIs. Needs human decision. - Shell completions: External subcommands won't appear in shell completions automatically. The
clap_completedynamic engine won't suggest plugin names. Acceptable for v1, but worth noting as a follow-up. - Error UX for typos: With
allow_external_subcommands, clap no longer shows "did you mean?" suggestions for typos. The plugin-not-found error should include available built-in commands or suggest--help. Alternatively, use thetry_parsefallback approach (Option C). - Security (accepted risk): Running arbitrary binaries from PATH is the same threat model as git/kubectl plugins — the user controls their PATH. No shell interpolation is used (
Command::newis safe).
Test Considerations
- Unit tests: Add parse tests verifying (1) external subcommands are captured in the
Externalvariant, (2) built-in commands take precedence, (3) global flags work with external subcommands - Integration test: Create a temporary
openshell-test-pluginscript on PATH, runopenshell test-plugin --check, verify exit code and environment variables are forwarded - Existing tests: The ~90 existing parse/completion tests in
main.rs::testsmust continue to pass. Thecompletions_engine_returns_candidatestest should verify external subcommands don't leak into normal completion - Blocklist test: Verify that
openshell driver-kubernetes(internal binary name) produces an error, not a plugin exec
Created by spike investigation. Use build-from-issue to plan and implement.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start in crates/openshell-cli/src/main.rs, reviewing the Cli and Commands definitions, main dispatch, HELP_TEMPLATE, and existing parse/completion tests. Compare process handling with ssh.rs:218-242 and ssh.rs:1472-1492. Done means external subcommands are safely discovered and dispatched with the required blocklist, help text, environment behavior, and tests while existing tests continue to pass.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- rust
- Domain
- cli
- Issue type
- Feature
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 48/100