provisioning/docs/src/development/COMMAND_HANDLER_GUIDE.md

602 lines
17 KiB
Markdown
Raw Normal View History

# Command Handler Developer Guide
**Target Audience**: Developers working on the provisioning CLI
**Last Updated**: 2025-09-30
**Related**: [ADR-006 CLI Refactoring](../architecture/adr/ADR-006-provisioning-cli-refactoring.md)
## Overview
The provisioning CLI uses a **modular, domain-driven architecture** that separates concerns into focused command handlers. This guide shows you how to work with this architecture.
### Key Architecture Principles
1. **Separation of Concerns**: Routing, flag parsing, and business logic are separated
2. **Domain-Driven Design**: Commands organized by domain (infrastructure, orchestration, etc.)
3. **DRY (Don't Repeat Yourself)**: Centralized flag handling eliminates code duplication
4. **Single Responsibility**: Each module has one clear purpose
5. **Open/Closed Principle**: Easy to extend, no need to modify core routing
### Architecture Components
```
provisioning/core/nulib/
├── provisioning (211 lines) - Main entry point
├── main_provisioning/
│ ├── flags.nu (139 lines) - Centralized flag handling
│ ├── dispatcher.nu (264 lines) - Command routing
│ ├── help_system.nu - Categorized help system
│ └── commands/ - Domain-focused handlers
│ ├── infrastructure.nu (117 lines) - Server, taskserv, cluster, infra
│ ├── orchestration.nu (64 lines) - Workflow, batch, orchestrator
│ ├── development.nu (72 lines) - Module, layer, version, pack
│ ├── workspace.nu (56 lines) - Workspace, template
│ ├── generation.nu (78 lines) - Generate commands
│ ├── utilities.nu (157 lines) - SSH, SOPS, cache, providers
│ └── configuration.nu (316 lines) - Env, show, init, validate
```
## Adding New Commands
### Step 1: Choose the Right Domain Handler
Commands are organized by domain. Choose the appropriate handler:
| Domain | Handler | Responsibility |
|--------|---------|----------------|
| `infrastructure.nu` | Server/taskserv/cluster/infra lifecycle |
| `orchestration.nu` | Workflow/batch operations, orchestrator control |
| `development.nu` | Module discovery, layers, versions, packaging |
| `workspace.nu` | Workspace and template management |
| `configuration.nu` | Environment, settings, initialization |
| `utilities.nu` | SSH, SOPS, cache, providers, utilities |
| `generation.nu` | Generate commands (server, taskserv, etc.) |
### Step 2: Add Command to Handler
**Example: Adding a new server command `server status`**
Edit `provisioning/core/nulib/main_provisioning/commands/infrastructure.nu`:
```nushell
# Add to the handle_infrastructure_command match statement
export def handle_infrastructure_command [
command: string
ops: string
flags: record
] {
set_debug_env $flags
match $command {
"server" => { handle_server $ops $flags }
"taskserv" | "task" => { handle_taskserv $ops $flags }
"cluster" => { handle_cluster $ops $flags }
"infra" | "infras" => { handle_infra $ops $flags }
_ => {
print $"❌ Unknown infrastructure command: ($command)"
print ""
print "Available infrastructure commands:"
print " server - Server operations (create, delete, list, ssh, status)" # Updated
print " taskserv - Task service management"
print " cluster - Cluster operations"
print " infra - Infrastructure management"
print ""
print "Use 'provisioning help infrastructure' for more details"
exit 1
}
}
}
# Add the new command handler
def handle_server [ops: string, flags: record] {
let args = build_module_args $flags $ops
run_module $args "server" --exec
}
```
**That's it!** The command is now available as `provisioning server status`.
### Step 3: Add Shortcuts (Optional)
If you want shortcuts like `provisioning s status`:
Edit `provisioning/core/nulib/main_provisioning/dispatcher.nu`:
```nushell
export def get_command_registry []: nothing -> record {
{
# Infrastructure commands
"s" => "infrastructure server" # Already exists
"server" => "infrastructure server" # Already exists
# Your new shortcut (if needed)
# Example: "srv-status" => "infrastructure server status"
# ... rest of registry
}
}
```
**Note**: Most shortcuts are already configured. You only need to add new shortcuts if you're creating completely new command categories.
## Modifying Existing Handlers
### Example: Enhancing the `taskserv` Command
Let's say you want to add better error handling to the taskserv command:
**Before:**
```nushell
def handle_taskserv [ops: string, flags: record] {
let args = build_module_args $flags $ops
run_module $args "taskserv" --exec
}
```
**After:**
```nushell
def handle_taskserv [ops: string, flags: record] {
# Validate taskserv name if provided
let first_arg = ($ops | split row " " | get -o 0)
if ($first_arg | is-not-empty) and $first_arg not-in ["create", "delete", "list", "generate", "check-updates", "help"] {
# Check if taskserv exists
let available_taskservs = (^$env.PROVISIONING_NAME module discover taskservs | from json)
if $first_arg not-in $available_taskservs {
print $"❌ Unknown taskserv: ($first_arg)"
print ""
print "Available taskservs:"
$available_taskservs | each { |ts| print $" • ($ts)" }
exit 1
}
}
let args = build_module_args $flags $ops
run_module $args "taskserv" --exec
}
```
## Working with Flags
### Using Centralized Flag Handling
The `flags.nu` module provides centralized flag handling:
```nushell
# Parse all flags into normalized record
let parsed_flags = (parse_common_flags {
version: $version, v: $v, info: $info,
debug: $debug, check: $check, yes: $yes,
wait: $wait, infra: $infra, # ... etc
})
# Build argument string for module execution
let args = build_module_args $parsed_flags $ops
# Set environment variables based on flags
set_debug_env $parsed_flags
```
### Available Flag Parsing
The `parse_common_flags` function normalizes these flags:
| Flag Record Field | Description |
|-------------------|-------------|
| `show_version` | Version display (`--version`, `-v`) |
| `show_info` | Info display (`--info`, `-i`) |
| `show_about` | About display (`--about`, `-a`) |
| `debug_mode` | Debug mode (`--debug`, `-x`) |
| `check_mode` | Check mode (`--check`, `-c`) |
| `auto_confirm` | Auto-confirm (`--yes`, `-y`) |
| `wait` | Wait for completion (`--wait`, `-w`) |
| `keep_storage` | Keep storage (`--keepstorage`) |
| `infra` | Infrastructure name (`--infra`) |
| `outfile` | Output file (`--outfile`) |
| `output_format` | Output format (`--out`) |
| `template` | Template name (`--template`) |
| `select` | Selection (`--select`) |
| `settings` | Settings file (`--settings`) |
| `new_infra` | New infra name (`--new`) |
### Adding New Flags
If you need to add a new flag:
1. **Update main `provisioning` file** to accept the flag
2. **Update `flags.nu:parse_common_flags`** to normalize it
3. **Update `flags.nu:build_module_args`** to pass it to modules
**Example: Adding `--timeout` flag**
```nushell
# 1. In provisioning main file (parameter list)
def main [
# ... existing parameters
--timeout: int = 300 # Timeout in seconds
# ... rest of parameters
] {
# ... existing code
let parsed_flags = (parse_common_flags {
# ... existing flags
timeout: $timeout
})
}
# 2. In flags.nu:parse_common_flags
export def parse_common_flags [flags: record]: nothing -> record {
{
# ... existing normalizations
timeout: ($flags.timeout? | default 300)
}
}
# 3. In flags.nu:build_module_args
export def build_module_args [flags: record, extra: string = ""]: nothing -> string {
# ... existing code
let str_timeout = if ($flags.timeout != 300) { $"--timeout ($flags.timeout) " } else { "" }
# ... rest of function
$"($extra) ($use_check)($use_yes)($use_wait)($str_timeout)..."
}
```
## Adding New Shortcuts
### Shortcut Naming Conventions
- **1-2 letters**: Ultra-short for common commands (`s` for server, `ws` for workspace)
- **3-4 letters**: Abbreviations (`orch` for orchestrator, `tmpl` for template)
- **Aliases**: Alternative names (`task` for taskserv, `flow` for workflow)
### Example: Adding a New Shortcut
Edit `provisioning/core/nulib/main_provisioning/dispatcher.nu`:
```nushell
export def get_command_registry []: nothing -> record {
{
# ... existing shortcuts
# Add your new shortcut
"db" => "infrastructure database" # New: db command
"database" => "infrastructure database" # Full name
# ... rest of registry
}
}
```
**Important**: After adding a shortcut, update the help system in `help_system.nu` to document it.
## Testing Your Changes
### Running the Test Suite
```bash
# Run comprehensive test suite
nu tests/test_provisioning_refactor.nu
```
### Test Coverage
The test suite validates:
- ✅ Main help display
- ✅ Category help (infrastructure, orchestration, development, workspace)
- ✅ Bi-directional help routing
- ✅ All command shortcuts
- ✅ Category shortcut help
- ✅ Command routing to correct handlers
### Adding Tests for Your Changes
Edit `tests/test_provisioning_refactor.nu`:
```nushell
# Add your test function
export def test_my_new_feature [] {
print "\n🧪 Testing my new feature..."
let output = (run_provisioning "my-command" "test")
assert_contains $output "Expected Output" "My command works"
}
# Add to main test runner
export def main [] {
# ... existing tests
let results = [
# ... existing test calls
(try { test_my_new_feature; "passed" } catch { "failed" })
]
# ... rest of main
}
```
### Manual Testing
```bash
# Test command execution
provisioning/core/cli/provisioning my-command test --check
# Test with debug mode
provisioning/core/cli/provisioning --debug my-command test
# Test help
provisioning/core/cli/provisioning my-command help
provisioning/core/cli/provisioning help my-command # Bi-directional
```
## Common Patterns
### Pattern 1: Simple Command Handler
**Use Case**: Command just needs to execute a module with standard flags
```nushell
def handle_simple_command [ops: string, flags: record] {
let args = build_module_args $flags $ops
run_module $args "module_name" --exec
}
```
### Pattern 2: Command with Validation
**Use Case**: Need to validate input before execution
```nushell
def handle_validated_command [ops: string, flags: record] {
# Validate
let first_arg = ($ops | split row " " | get -o 0)
if ($first_arg | is-empty) {
print "❌ Missing required argument"
print "Usage: provisioning command <arg>"
exit 1
}
# Execute
let args = build_module_args $flags $ops
run_module $args "module_name" --exec
}
```
### Pattern 3: Command with Subcommands
**Use Case**: Command has multiple subcommands (like `server create`, `server delete`)
```nushell
def handle_complex_command [ops: string, flags: record] {
let subcommand = ($ops | split row " " | get -o 0)
let rest_ops = ($ops | split row " " | skip 1 | str join " ")
match $subcommand {
"create" => { handle_create $rest_ops $flags }
"delete" => { handle_delete $rest_ops $flags }
"list" => { handle_list $rest_ops $flags }
_ => {
print "❌ Unknown subcommand: $subcommand"
print "Available: create, delete, list"
exit 1
}
}
}
```
### Pattern 4: Command with Flag-Based Routing
**Use Case**: Command behavior changes based on flags
```nushell
def handle_flag_routed_command [ops: string, flags: record] {
if $flags.check_mode {
# Dry-run mode
print "🔍 Check mode: simulating command..."
let args = build_module_args $flags $ops
run_module $args "module_name" # No --exec, returns output
} else {
# Normal execution
let args = build_module_args $flags $ops
run_module $args "module_name" --exec
}
}
```
## Best Practices
### 1. Keep Handlers Focused
Each handler should do **one thing well**:
- ✅ Good: `handle_server` manages all server operations
- ❌ Bad: `handle_server` also manages clusters and taskservs
### 2. Use Descriptive Error Messages
```nushell
# ❌ Bad
print "Error"
# ✅ Good
print "❌ Unknown taskserv: kubernetes-invalid"
print ""
print "Available taskservs:"
print " • kubernetes"
print " • containerd"
print " • cilium"
print ""
print "Use 'provisioning taskserv list' to see all available taskservs"
```
### 3. Leverage Centralized Functions
Don't repeat code - use centralized functions:
```nushell
# ❌ Bad: Repeating flag handling
def handle_bad [ops: string, flags: record] {
let use_check = if $flags.check_mode { "--check " } else { "" }
let use_yes = if $flags.auto_confirm { "--yes " } else { "" }
let str_infra = if ($flags.infra | is-not-empty) { $"--infra ($flags.infra) " } else { "" }
# ... 10 more lines of flag handling
run_module $"($ops) ($use_check)($use_yes)($str_infra)..." "module" --exec
}
# ✅ Good: Using centralized function
def handle_good [ops: string, flags: record] {
let args = build_module_args $flags $ops
run_module $args "module" --exec
}
```
### 4. Document Your Changes
Update relevant documentation:
- **ADR-006**: If architectural changes
- **CLAUDE.md**: If new commands or shortcuts
- **help_system.nu**: If new categories or commands
- **This guide**: If new patterns or conventions
### 5. Test Thoroughly
Before committing:
- [ ] Run test suite: `nu tests/test_provisioning_refactor.nu`
- [ ] Test manual execution
- [ ] Test with `--check` flag
- [ ] Test with `--debug` flag
- [ ] Test help: both `provisioning cmd help` and `provisioning help cmd`
- [ ] Test shortcuts
## Troubleshooting
### Issue: "Module not found"
**Cause**: Incorrect import path in handler
**Fix**: Use relative imports with `.nu` extension:
```nushell
# ✅ Correct
use ../flags.nu *
use ../../lib_provisioning *
# ❌ Wrong
use ../main_provisioning/flags *
use lib_provisioning *
```
### Issue: "Parse mismatch: expected colon"
**Cause**: Missing type signature format
**Fix**: Use proper Nushell 0.107 type signature:
```nushell
# ✅ Correct
export def my_function [param: string]: nothing -> string {
"result"
}
# ❌ Wrong
export def my_function [param: string] -> string {
"result"
}
```
### Issue: "Command not routing correctly"
**Cause**: Shortcut not in command registry
**Fix**: Add to `dispatcher.nu:get_command_registry`:
```nushell
"myshortcut" => "domain command"
```
### Issue: "Flags not being passed"
**Cause**: Not using `build_module_args`
**Fix**: Use centralized flag builder:
```nushell
let args = build_module_args $flags $ops
run_module $args "module" --exec
```
## Quick Reference
### File Locations
```
provisioning/core/nulib/
├── provisioning - Main entry, flag definitions
├── main_provisioning/
│ ├── flags.nu - Flag parsing (parse_common_flags, build_module_args)
│ ├── dispatcher.nu - Routing (get_command_registry, dispatch_command)
│ ├── help_system.nu - Help (provisioning-help, help-*)
│ └── commands/ - Domain handlers (handle_*_command)
tests/
└── test_provisioning_refactor.nu - Test suite
docs/
├── architecture/
│ └── ADR-006-provisioning-cli-refactoring.md - Architecture docs
└── development/
└── COMMAND_HANDLER_GUIDE.md - This guide
```
### Key Functions
```nushell
# In flags.nu
parse_common_flags [flags: record]: nothing -> record
build_module_args [flags: record, extra: string = ""]: nothing -> string
set_debug_env [flags: record]
get_debug_flag [flags: record]: nothing -> string
# In dispatcher.nu
get_command_registry []: nothing -> record
dispatch_command [args: list, flags: record]
# In help_system.nu
provisioning-help [category?: string]: nothing -> string
help-infrastructure []: nothing -> string
help-orchestration []: nothing -> string
# ... (one for each category)
# In commands/*.nu
handle_*_command [command: string, ops: string, flags: record]
# Example: handle_infrastructure_command, handle_workspace_command
```
### Testing Commands
```bash
# Run full test suite
nu tests/test_provisioning_refactor.nu
# Test specific command
provisioning/core/cli/provisioning my-command test --check
# Test with debug
provisioning/core/cli/provisioning --debug my-command test
# Test help
provisioning/core/cli/provisioning help my-command
provisioning/core/cli/provisioning my-command help # Bi-directional
```
## Further Reading
- **[ADR-006: CLI Refactoring](../architecture/adr/ADR-006-provisioning-cli-refactoring.md)** - Complete architectural decision record
- **[Project Structure](project-structure.md)** - Overall project organization
- **[Workflow Development](workflow.md)** - Workflow system architecture
- **[Development Integration](integration.md)** - Integration patterns
## Contributing
When contributing command handler changes:
1. **Follow existing patterns** - Use the patterns in this guide
2. **Update documentation** - Keep docs in sync with code
3. **Add tests** - Cover your new functionality
4. **Run test suite** - Ensure nothing breaks
5. **Update CLAUDE.md** - Document new commands/shortcuts
For questions or issues, refer to ADR-006 or ask the team.
---
*This guide is part of the provisioning project documentation. Last updated: 2025-09-30*