Adding CLI Commands¶
This guide provides step-by-step instructions for adding new commands to the Hatch CLI, following the established modular architecture.
Prerequisites¶
Before adding a new command, familiarize yourself with:
- CLI Architecture: Understand the overall design
- Component Architecture: Understand how CLI integrates with managers
- Existing handler implementations in
hatch/cli/cli_*.py
Step-by-Step Process¶
1. Determine Command Category¶
Identify which handler module your command belongs to:
cli_env.py: Environment lifecycle and Python environment operationscli_package.py: Package installation, removal, and synchronizationcli_mcp.py: MCP host configuration, discovery, and backupcli_system.py: System-level operations (package creation, validation)
Decision Criteria:
- Does it manage environment state? → cli_env.py
- Does it install/remove packages? → cli_package.py
- Does it configure MCP hosts? → cli_mcp.py
- Does it operate on packages outside environments? → cli_system.py
2. Add Argument Parser Setup¶
In hatch/cli/__main__.py, add a parser setup function or extend an existing one:
For new top-level commands:
def _setup_mycommand_command(subparsers):
"""Set up 'hatch mycommand' command parser."""
mycommand_parser = subparsers.add_parser(
"mycommand", help="Brief description of command"
)
mycommand_parser.add_argument("required_arg", help="Required argument")
mycommand_parser.add_argument(
"--optional-flag", action="store_true", help="Optional flag"
)
mycommand_parser.add_argument(
"--dry-run", action="store_true", help="Preview changes without execution"
)
For subcommands under existing commands:
def _setup_env_commands(subparsers):
# ... existing code ...
# Add new subcommand
env_newcmd_parser = env_subparsers.add_parser(
"newcmd", help="New environment subcommand"
)
env_newcmd_parser.add_argument("name", help="Environment name")
env_newcmd_parser.add_argument(
"--dry-run", action="store_true", help="Preview changes without execution"
)
Standard Arguments to Include:
- --dry-run: For mutation commands (preview without execution)
- --auto-approve: For destructive operations (skip confirmation)
- --json: For list/show commands (JSON output format)
- --env or -e: For commands that operate on environments
Call the setup function in main():
def main():
# ... existing code ...
_setup_mycommand_command(subparsers) # Add this line
# ... rest of main ...
3. Implement Handler Function¶
In the appropriate handler module (cli_env.py, cli_package.py, etc.), implement the handler:
Handler Template:
def handle_mycommand(args: Namespace) -> int:
"""Handle 'hatch mycommand' command.
Args:
args: Namespace with:
- env_manager: HatchEnvironmentManager instance
- mcp_manager: MCPHostConfigurationManager instance (if needed)
- required_arg: Description of required argument
- optional_flag: Description of optional flag
- dry_run: Preview changes without execution
Returns:
Exit code (0 for success, 1 for error)
"""
from hatch.cli.cli_utils import (
EXIT_SUCCESS,
EXIT_ERROR,
ResultReporter,
ConsequenceType,
request_confirmation,
format_info,
)
# Extract arguments
env_manager = args.env_manager
required_arg = args.required_arg
optional_flag = getattr(args, "optional_flag", False)
dry_run = getattr(args, "dry_run", False)
# Create reporter for unified output
reporter = ResultReporter("hatch mycommand", dry_run=dry_run)
# Add consequences (actions to be performed)
reporter.add(ConsequenceType.CREATE, f"Resource '{required_arg}'")
# Handle dry-run
if dry_run:
reporter.report_result()
return EXIT_SUCCESS
# Show prompt and request confirmation (for mutation commands)
prompt = reporter.report_prompt()
if prompt:
print(prompt)
if not request_confirmation("Proceed?"):
format_info("Operation cancelled")
return EXIT_SUCCESS
# Execute operation
try:
# Call manager methods to perform actual work
success = env_manager.some_operation(required_arg)
if success:
reporter.report_result()
return EXIT_SUCCESS
else:
reporter.report_error(f"Failed to perform operation on '{required_arg}'")
return EXIT_ERROR
except Exception as e:
reporter.report_error(
"Operation failed",
details=[f"Reason: {str(e)}"]
)
return EXIT_ERROR
Handler Patterns by Command Type:
Mutation Commands (Create, Update, Delete)¶
# 1. Build consequences
reporter.add(ConsequenceType.CREATE, "Resource 'name'")
# 2. Handle dry-run early
if dry_run:
reporter.report_result()
return EXIT_SUCCESS
# 3. Show prompt and confirm
prompt = reporter.report_prompt()
if prompt:
print(prompt)
if not request_confirmation("Proceed?", auto_approve):
format_info("Operation cancelled")
return EXIT_SUCCESS
# 4. Execute
success = manager.operation()
# 5. Report results
if success:
reporter.report_result()
return EXIT_SUCCESS
else:
reporter.report_error("Operation failed")
return EXIT_ERROR
List Commands¶
from hatch.cli.cli_utils import TableFormatter, ColumnDef
# Get data
items = manager.list_items()
# JSON output (if requested)
if getattr(args, 'json', False):
import json
print(json.dumps({"items": items}, indent=2))
return EXIT_SUCCESS
# Table output
print("Items:")
columns = [
ColumnDef(name="Name", width=20),
ColumnDef(name="Status", width=10),
ColumnDef(name="Count", width="auto", align="right"),
]
formatter = TableFormatter(columns)
for item in items:
formatter.add_row([item.name, item.status, str(item.count)])
print(formatter.render())
return EXIT_SUCCESS
Show Commands (Detailed Views)¶
from hatch.cli.cli_utils import highlight
# Get detailed data
item = manager.get_item(name)
if not item:
format_validation_error(ValidationError(
f"Item '{name}' not found",
field="name",
suggestion="Use 'hatch list' to see available items"
))
return EXIT_ERROR
# Hierarchical output
separator = "═" * 79
print(separator)
print(f"Item: {highlight(item.name)}")
print(f" Status: {item.status}")
print(f" Created: {item.created_at}")
print()
print(f" Details ({len(item.details)}):")
for detail in item.details:
print(f" {highlight(detail.name)}")
print(f" Value: {detail.value}")
print()
return EXIT_SUCCESS
4. Add Routing Logic¶
In hatch/cli/__main__.py, add routing for your command:
For new top-level commands:
def main():
# ... existing code ...
# Route commands
if args.command == "mycommand":
from hatch.cli.cli_system import handle_mycommand
return handle_mycommand(args)
# ... existing routes ...
For subcommands:
def _route_env_command(args):
"""Route environment commands to handlers."""
from hatch.cli.cli_env import (
# ... existing imports ...
handle_env_newcmd, # Add new handler
)
# ... existing routes ...
elif args.env_command == "newcmd":
return handle_env_newcmd(args)
# ... rest of routing ...
5. Choose Appropriate ConsequenceType¶
Select the correct ConsequenceType for your operations:
Constructive (Green):
- CREATE: Creating new resources
- ADD: Adding items to collections
- CONFIGURE: Setting up configurations
- INSTALL: Installing dependencies
- INITIALIZE: Initializing environments
Recovery (Blue):
- RESTORE: Restoring from backups
Destructive (Red):
- REMOVE: Removing items from collections
- DELETE: Deleting resources permanently
- CLEAN: Cleaning up old data
Modification (Yellow):
- SET: Setting values
- UPDATE: Updating existing resources
Transfer (Magenta):
- SYNC: Synchronizing between systems
Informational (Cyan):
- VALIDATE: Validating data
No-op (Gray):
- SKIP: Skipping operations
- EXISTS: Resource already exists
- UNCHANGED: No changes needed
6. Handle Nested Consequences (Optional)¶
For field-level details under resource-level actions:
# Resource-level consequence with field-level children
children = [
Consequence(ConsequenceType.UPDATE, "field1: 'old' → 'new'"),
Consequence(ConsequenceType.SKIP, "field2: unsupported by host"),
Consequence(ConsequenceType.UNCHANGED, "field3: 'value'"),
]
reporter.add(
ConsequenceType.CONFIGURE,
"Server 'my-server' on 'claude-desktop'",
children=children
)
7. Add Error Handling¶
Use structured error reporting:
from hatch.cli.cli_utils import (
ValidationError,
format_validation_error,
)
# Validation errors
try:
host_type = MCPHostType(host)
except ValueError:
format_validation_error(ValidationError(
f"Invalid host '{host}'",
field="--host",
suggestion=f"Supported hosts: {', '.join(h.value for h in MCPHostType)}"
))
return EXIT_ERROR
# Operation errors
if not success:
reporter.report_error(
"Operation failed",
details=[
f"Resource: {resource_name}",
f"Reason: {error_message}"
]
)
return EXIT_ERROR
# Partial success
reporter.report_partial_success(
"Partial operation",
successes=["item1", "item2"],
failures=[("item3", "reason"), ("item4", "reason")]
)
8. Test Your Command¶
Manual Testing¶
# Test help output
hatch mycommand --help
# Test dry-run mode
hatch mycommand arg --dry-run
# Test actual execution
hatch mycommand arg
# Test error cases
hatch mycommand invalid-arg
# Test JSON output (if applicable)
hatch mycommand --json
# Test with NO_COLOR
NO_COLOR=1 hatch mycommand arg
Unit Testing¶
Create tests in tests/unit/cli/ or tests/regression/cli/:
def test_handle_mycommand_success(mock_env_manager):
"""Test successful command execution."""
args = Namespace(
env_manager=mock_env_manager,
required_arg="test",
optional_flag=False,
dry_run=False,
)
result = handle_mycommand(args)
assert result == EXIT_SUCCESS
mock_env_manager.some_operation.assert_called_once_with("test")
def test_handle_mycommand_dry_run(mock_env_manager):
"""Test dry-run mode."""
args = Namespace(
env_manager=mock_env_manager,
required_arg="test",
dry_run=True,
)
result = handle_mycommand(args)
assert result == EXIT_SUCCESS
mock_env_manager.some_operation.assert_not_called()
9. Update Documentation¶
After implementing your command:
- CLI Reference: Add command documentation to
docs/articles/users/CLIReference.md - Tutorials: Add usage examples if appropriate
- Changelog: Document the new command in
CHANGELOG.md
Common Patterns and Gotchas¶
Pattern: Accessing Optional Arguments¶
Always use getattr() with defaults for optional arguments:
dry_run = getattr(args, "dry_run", False)
auto_approve = getattr(args, "auto_approve", False)
env_name = getattr(args, "env", None)
Pattern: Environment Name Resolution¶
Many commands default to the current environment:
Pattern: Regex Pattern Filtering¶
For list commands with pattern filtering:
import re
pattern = getattr(args, 'pattern', None)
if pattern:
try:
regex = re.compile(pattern)
items = [item for item in items if regex.search(item.name)]
except re.error as e:
format_validation_error(ValidationError(
f"Invalid regex pattern: {e}",
field="--pattern",
suggestion="Use a valid Python regex pattern"
))
return EXIT_ERROR
Gotcha: Manager Initialization¶
Managers are initialized in main() and attached to args. Don't create new manager instances in handlers:
# ✅ Correct
env_manager = args.env_manager
# ❌ Wrong
env_manager = HatchEnvironmentManager() # Creates new instance!
Gotcha: Exit Codes¶
Always return EXIT_SUCCESS or EXIT_ERROR, never raw integers:
Gotcha: Confirmation Prompts¶
Always check auto_approve before prompting:
# ✅ Correct
if not request_confirmation("Proceed?", auto_approve):
format_info("Operation cancelled")
return EXIT_SUCCESS
# ❌ Wrong
if not request_confirmation("Proceed?"): # Ignores auto_approve!
return EXIT_SUCCESS
Gotcha: Dry-Run Handling¶
Handle dry-run AFTER building consequences but BEFORE execution:
# ✅ Correct
reporter.add(ConsequenceType.CREATE, "Resource")
if dry_run:
reporter.report_result()
return EXIT_SUCCESS
# ... execute operation ...
# ❌ Wrong
if dry_run:
return EXIT_SUCCESS # Consequences not shown!
reporter.add(ConsequenceType.CREATE, "Resource")
Examples from Codebase¶
Simple Mutation Command¶
See handle_env_use() in hatch/cli/cli_env.py:
- Single consequence
- No confirmation needed (non-destructive)
- Simple success/error reporting
Complex Mutation Command¶
See handle_package_sync() in hatch/cli/cli_package.py:
- Multiple consequences (packages + dependencies)
- Confirmation required
- Nested consequences from conversion reports
- Partial success handling
List Command with Filtering¶
See handle_env_list() in hatch/cli/cli_env.py:
- Regex pattern filtering
- JSON output support
- Table formatting with auto-width columns
Show Command with Hierarchy¶
See handle_mcp_show_hosts() in hatch/cli/cli_mcp.py:
- Hierarchical output with separators
- Entity highlighting with highlight()
- Sensitive data masking
Related Documentation¶
- CLI Architecture: Overall design and components
- Testing Standards: Testing requirements
- CLI Reference: User-facing command documentation