Command Handler Developer Guide
Target Audience: Developers working on the provisioning CLI Last Updated: 2025-09-30 Related: ADR-006 CLI Refactoring
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
- Separation of Concerns: Routing, flag parsing, and business logic are separated
- Domain-Driven Design: Commands organized by domain (infrastructure, orchestration, etc.)
- DRY (Don’t Repeat Yourself): Centralized flag handling eliminates code duplication
- Single Responsibility: Each module has one clear purpose
- 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:
# 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:
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:
def handle_taskserv [ops: string, flags: record] {
let args = build_module_args $flags $ops
run_module $args "taskserv" --exec
}
After:
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:
# 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:
- Update main
provisioningfile to accept the flag - Update
flags.nu:parse_common_flagsto normalize it - Update
flags.nu:build_module_argsto pass it to modules
Example: Adding --timeout flag
# 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 (
sfor server,wsfor workspace) - 3-4 letters: Abbreviations (
orchfor orchestrator,tmplfor template) - Aliases: Alternative names (
taskfor taskserv,flowfor workflow)
Example: Adding a New Shortcut
Edit provisioning/core/nulib/main_provisioning/dispatcher.nu:
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
# 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:
# 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
# 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
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
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)
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
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_servermanages all server operations - ❌ Bad:
handle_serveralso manages clusters and taskservs
2. Use Descriptive Error Messages
# ❌ 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:
# ❌ 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
--checkflag -
Test with
--debugflag -
Test help: both
provisioning cmd helpandprovisioning help cmd - Test shortcuts
Troubleshooting
Issue: “Module not found”
Cause: Incorrect import path in handler
Fix: Use relative imports with .nu extension:
# ✅ 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:
# ✅ 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:
"myshortcut" => "domain command"
Issue: “Flags not being passed”
Cause: Not using build_module_args
Fix: Use centralized flag builder:
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
# 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
# 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 - Complete architectural decision record
- Project Structure - Overall project organization
- Workflow Development - Workflow system architecture
- Development Integration - Integration patterns
Contributing
When contributing command handler changes:
- Follow existing patterns - Use the patterns in this guide
- Update documentation - Keep docs in sync with code
- Add tests - Cover your new functionality
- Run test suite - Ensure nothing breaks
- 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