004. Subcommand-Based CLI Architecture¶
Status¶
Accepted
Context¶
The linter requires a command-line interface that supports multiple operational modes as defined in PRD requirements REQ-006 through REQ-009:
REQ-006: Core linting functionality with file discovery and rule selection
REQ-007: Error reporting with configurable context display
REQ-008: Auto-fix capabilities with safety controls and simulation
REQ-009: Configuration management without destructive file editing
Analysis of CLI design approaches reveals several architectural forces:
Operational Mode Isolation: Different operations have distinct concerns and option sets:
Analysis mode (check): Focus on violation detection and reporting
Fix mode: Requires safety controls, simulation, and change visualization
Configuration mode: Non-destructive configuration management and validation
Documentation mode (describe): Rule discovery and information display
Server modes: Protocol-specific isolation for LSP and MCP implementations
Option Namespace Management: Different modes require different option sets that would conflict in a single namespace:
--contextfor check mode vs.--diff-formatfor fix mode--simulatefor fix mode vs.--validatefor configure modeProtocol-specific options for different server modes
Future Extensibility: The architecture must accommodate planned enhancements:
Language Server Protocol (LSP) implementation
Model Context Protocol (MCP) server mode
Additional output formats and integrations
Enhanced safety classifications for auto-fix
User Experience Consistency:
CLI design should follow established patterns from tools like git, ruff, and mypy that use verb-based subcommand structures for different operational modes.
Decision¶
We will implement a verb-based subcommand architecture with isolated namespaces for different operational modes:
Subcommand Structure:
linter <subcommand> [OPTIONS] [PATHS...]
Subcommands:
- check # Default: Analyze code and report violations
- fix # Apply automated fixes with safety controls
- configure # Manage configuration without file modification
- describe # Display rule information and documentation
- serve # Server modes (lsp, mcp) for protocol integration
Architectural Organization:
# cli.py - Main CLI orchestration
class LinterCLI:
def run(self, args: List[str]) -> int: ...
def dispatch_subcommand(self, subcommand: str, args: List[str]) -> int: ...
# subcommands/ - Isolated subcommand implementations
class CheckCommand:
"""Analysis and violation reporting."""
def configure_parser(self, parser: ArgumentParser) -> None: ...
def execute(self, args: Namespace) -> int: ...
class FixCommand:
"""Automated fixing with safety controls."""
def configure_parser(self, parser: ArgumentParser) -> None: ...
def execute(self, args: Namespace) -> int: ...
class ConfigureCommand:
"""Non-destructive configuration management."""
def configure_parser(self, parser: ArgumentParser) -> None: ...
def execute(self, args: Namespace) -> int: ...
Subcommand-Specific Options:
Check subcommand:
- --context: Show lines around violations
- --select VBL101,VBL201: Rule selection
- --output-format {text,json,structured}: Output formatting
Fix subcommand:
- --simulate: Preview changes without applying
- --diff-format {unified,context}: Diff visualization
- --apply-dangerous: Enable potentially unsafe fixes
- --select VBL101: Apply fixes only for specific rules
Configure subcommand:
- --validate: Check configuration without analysis
- --interactive: Interactive configuration wizard
- Non-destructive TOML snippet generation
Describe subcommand:
- describe rules: List all available rules
- describe rule VBL101: Detailed rule information
Serve subcommand:
- serve lsp [LSP_OPTIONS]: Language Server Protocol mode
- serve mcp [MCP_OPTIONS]: Model Context Protocol mode
Component Integration:
CLI Entry Point
│
▼
Subcommand Router
│
├─── CheckCommand ──── LinterEngine ──── Rules Framework
│
├─── FixCommand ──── FixEngine ──── Safety Classifier
│
├─── ConfigureCommand ──── ConfigurationManager
│
├─── DescribeCommand ──── RuleRegistry
│
└─── ServeCommand ──── Protocol Handlers
Default Behavior:
- linter with no subcommand defaults to linter check
- Maintains backward compatibility with simple usage patterns
- Common options (--help, --version) available at top level
Alternatives¶
Alternative 1: Single Command with Mode Flags
Use flags like --check, --fix, --configure to specify operational mode.
Rejected because:
- Option namespace conflicts: Different modes need conflicting option names
- Poor user experience: Flags like --fix --simulate are less intuitive than fix --simulate
- Scalability limitations: Adding new modes becomes increasingly complex
- Inconsistency: Deviates from established CLI patterns in developer tools
Alternative 2: Separate Executables
Create separate executables: linter-check, linter-fix, linter-configure.
Rejected because: - Installation complexity: Multiple executables complicate packaging and PATH management - Discovery difficulty: Users must know about multiple commands - Maintenance overhead: Separate entry points and argument parsing for each mode - Ecosystem inconsistency: Single-executable subcommand pattern is standard
Alternative 3: Plugin-Based Architecture
Implement subcommands as dynamically loaded plugins.
Rejected because: - Unnecessary complexity: Core operational modes don’t require plugin architecture - Performance overhead: Dynamic loading adds startup cost - Distribution complexity: Plugin discovery and loading mechanisms - Over-engineering: Simple subcommand dispatch suffices for known modes
Consequences¶
Positive Consequences:
Clear separation of concerns: Each subcommand handles a distinct operational mode
Option namespace isolation: No conflicts between mode-specific options
Intuitive user experience: Familiar verb-based CLI pattern matching established tools
Future extensibility: New operational modes (server protocols) can be added easily
Testing isolation: Each subcommand can be tested independently
Maintainability: Subcommand logic is contained and focused
Negative Consequences:
Implementation complexity: Requires subcommand dispatch and separate parsers
Code organization overhead: Multiple command classes vs. single monolithic parser
Potential code duplication: Common options and validation logic across subcommands
Documentation complexity: Help system must cover multiple subcommands
Risks and Mitigations:
Risk: Code duplication across subcommand implementations Mitigation: Shared base classes and utility functions for common functionality
Risk: Inconsistent option naming across subcommands Mitigation: Establish naming conventions and shared option definitions
Risk: Complex help system implementation Mitigation: Use argparse subparser functionality for automatic help generation
Risk: Increased testing surface area Mitigation: Focus on integration tests for CLI dispatch, unit tests for subcommand logic
Implementation Guidelines:
Subcommand base class: Common interface and utilities for all subcommands
Shared options: Common options (paths, verbosity) defined once and reused
Graceful degradation: Helpful error messages for invalid subcommand usage
Consistent exit codes: Standard exit code patterns across all subcommands
Progressive disclosure: Basic usage simple, advanced features discoverable
Architecture Integration:
Configuration system: All subcommands use the same configuration discovery and loading
Rule engine: Check and fix modes share the same rule evaluation infrastructure
Reporting system: Consistent error formatting across operational modes
File discovery: Common file enumeration and filtering logic
Future Enhancements:
Server protocol isolation:
serve lspandserve mcpwith protocol-specific optionsAdditional output formats: Easy extension within existing subcommand structure
Enhanced safety controls: Fix mode can evolve independently of analysis mode
Configuration wizards: Interactive configuration within configure subcommand