ADR 008: Introduce A Plugin-Based CLI Pre-Dispatch Layer With Structured Command Responses
Status
Accepted
Context
The OntoBDC CLI entrypoint in wip/src/ontobdc/cli/__init__.py historically relied on a monolithic command dispatch flow based on:
- raw
sys.argvinspection - a large
if/elifcommand tree - direct rendering side effects such as
print(),message_box.sh, or shell delegation
That design has been operationally useful, but it creates several structural limitations:
- top-level CLI behavior is tightly coupled to one file
- command-specific parsing and rendering concerns accumulate inside the entrypoint
- introducing new command-like behaviors requires editing the central dispatcher
- root CLI flags such as
--helpdo not have a first-class command abstraction - command handlers do not return a stable response model that can be rendered consistently
At the same time, the project already uses plugin-oriented discovery successfully in other runtime layers, including:
- capability discovery
- parameter strategy discovery
The CLI is now evolving toward the same extensibility model.
The current codebase already contains the first working pieces of that transition:
CliCommandRunAdapterinwip/src/ontobdc/cli/adapter/command.pyCliCommandPortandCliCommandMetadatainwip/src/ontobdc/cli/domain/port/command.py- structured response dataclasses in
wip/src/ontobdc/cli/domain/resource/command.py - a base command plugin in
wip/src/ontobdc/cli/plugin/command/base.py - pre-dispatch invocation from
wip/src/ontobdc/cli/__init__.py
This design shift should therefore be recorded explicitly.
Decision
OntoBDC adopts a plugin-based CLI pre-dispatch layer that runs before the legacy monolithic CLI dispatcher.
The CLI root entrypoint now supports the following architecture:
- raw CLI arguments are first normalized by
CliCommandRunAdapter - the adapter resolves an appropriate command plugin through
CommandLoader - the resolved plugin implements
CliCommandPort - the plugin returns a structured response object rather than rendering directly
- the CLI root entrypoint renders that response through a dedicated rendering step
- if no plugin command handles the input, the legacy CLI flow remains available as fallback
This decision introduces a new architectural layer, not a full replacement of the legacy dispatcher in a single step.
Rationale
This decision exists to establish a cleaner command architecture for the CLI root while preserving compatibility with the existing command surface.
First-Class Command Abstractions
The CLI root should be able to treat command-like behaviors as explicit objects rather than ad hoc inline branches.
That includes behaviors such as:
- root help
- root flags
- future CLI-level command handlers
Extensibility
The project already uses plugin discovery successfully in other parts of the runtime.
Applying a similar pattern to the CLI makes it possible to:
- introduce new handlers incrementally
- reduce central dispatcher growth
- attach metadata to command handlers
- scope handlers by logical component
Structured Responses
Commands should return structured response objects rather than coupling execution directly to one rendering method.
This enables:
- consistent rendering
- easier testing
- future support for multiple render modes
- clearer separation between command execution and response presentation
Incremental Migration
The legacy dispatcher still carries most of the CLI behavior.
The pre-dispatch plugin layer provides a safe migration path:
- add one command handler at a time
- keep the old flow for everything else
- validate behavior incrementally
Consequences
Positive
- the CLI root gains a formal command abstraction
- command discovery becomes plugin-oriented rather than fully hardcoded
- root-level behaviors such as
--helpcan be expressed as command plugins - command execution and command rendering become more clearly separated
- structured responses provide a better base for future renderers and tests
- the migration can happen incrementally without rewriting the full CLI at once
Negative
- the CLI now contains two dispatch models during the transition period
- plugin loading introduces additional runtime complexity
- partial or broken command plugins can interfere with discovery if the loader scans too broadly
- the current
CommandLoaderstill has uneven maturity compared with capability and parameter loaders - contributors must understand ports, adapters, loader behavior, and legacy fallback together
Neutral
- the legacy
if/elifdispatcher is not immediately removed - command plugins currently coexist with the historical CLI implementation
- early response rendering may still be simple, including plain JSON-style output
Alternatives Considered
Keep All CLI Dispatch In cli/__init__.py
Rejected because the central dispatcher would continue to accumulate parsing, execution, and rendering responsibilities in one place.
Replace The Legacy Dispatcher Immediately
Rejected because the current CLI surface is broad and operationally important.
A direct rewrite would raise migration risk and make it harder to validate behaviors incrementally.
Add More Helper Functions But Keep No Command Plugin Layer
Rejected because helper extraction alone does not create:
- a discoverable command model
- plugin-oriented extensibility
- a stable response contract
Use Plugins Only For Business Commands, Not For CLI Root Flags
Rejected because root CLI behaviors such as --help are precisely the kind of command-like behavior that benefits from first-class modeling in the new architecture.
Implementation Notes
The current implementation reflects this decision through the following elements.
Pre-Dispatch In The CLI Root
wip/src/ontobdc/cli/__init__.py now attempts to resolve and run a command plugin before entering the legacy CLI flow.
The current sequence is:
- Build
incoming_args. - Determine the render mode.
- Resolve a command through
CliCommandRunAdapter.make(...). - If
check()succeeds: - run the command
- render the structured response
- exit early
- If command resolution raises
CliCommandArgumentException, fall through to the legacy dispatcher.
Command Request Model
CliCommandRequest in wip/src/ontobdc/cli/adapter/command.py normalizes the incoming request information passed to command plugins.
Its current shape includes:
logical_componentcomponent_actioncommand_args
Command Port
CliCommandPort defines the minimum command contract:
check() -> boolrun()
The port also carries:
METADATA: CliCommandMetadata
Metadata Model
CliCommandMetadata introduces a metadata contract for CLI command plugins.
The current minimum fields are:
idlogical_componentdescription
Response Model
The CLI now has structured response dataclasses in:
wip/src/ontobdc/cli/domain/resource/command.py
The current model includes:
CommandResponseHelpCommandResponse
Base CLI Command Plugin
wip/src/ontobdc/cli/plugin/command/base.py is the first concrete CLI command plugin in this architecture.
It currently acts as the handler for root help-like arguments:
--help-h
It returns a HelpCommandResponse instead of printing directly from the command.
Component Adoption
The architecture is actively expanding to other core components. For instance, the storage component now uses this plugin-based model for command handlers such as:
baseenablelistcreatedelete
These handlers return structured CommandResponse subclasses, while lower-level storage consistency logic remains in Python adapters and storage check/hotfix modules below the CLI command surface.
Command Argument Exception
The CLI now includes a dedicated exception type:
CliCommandArgumentException
This exception is used to signal that a command plugin could not accept the provided CLI arguments and that resolution should fall back safely.
Transitional Notes
This decision is intentionally transitional.
The new architecture is active, but not yet fully generalized.
Known transitional properties include:
- the base CLI command plugin and the
storageplugins are implemented, but a broader family of legacy CLI commands are still waiting to be migrated - the
storagecomponent already combines plugin-dispatched commands with internal repository and integrity helpers, which shows the intended separation between CLI command surface and component internals CommandLoaderstill scans command plugins across components- incomplete command plugins outside
clican still surface warnings during discovery - the legacy dispatcher remains the operational fallback for most commands
This is acceptable under the current migration strategy.
Risks And Constraints
The current design introduces several risks that must be acknowledged explicitly.
Cross-Component Discovery Noise
Because command discovery is loader-based, incomplete command plugins in other components can produce warnings or empty discovery results during root CLI execution.
Import Graph Sensitivity
The command loader and CLI adapter layers are sensitive to import ordering and circular dependencies while the architecture is still settling.
Uneven Loader Contracts
The command loader is newer and less mature than the capability and parameter loaders.
This means contributors should treat command plugin discovery as an active architectural area rather than a fully stabilized contract.
Future Direction
This ADR supports future work such as:
- introducing additional CLI command plugins beyond the base help handler
- narrowing command loader discovery by logical component
- improving loader diagnostics and failure visibility
- supporting richer render modes for
CommandResponse - progressively moving command-specific logic out of the legacy dispatcher
- eventually reducing or removing the monolithic
if/elifcommand tree once plugin coverage becomes sufficient