provisioning/docs/src/development/command-handler-guide.md
2026-01-12 04:42:18 +00:00

17 KiB

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

  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 infrastructure.nu Server/taskserv/cluster/infra lifecycle
orchestration orchestration.nu Workflow/batch operations, orchestrator control
development development.nu Module discovery, layers, versions, packaging
workspace workspace.nu Workspace and template management
configuration configuration.nu Environment, settings, initialization
utilities utilities.nu SSH, SOPS, cache, providers, utilities
generation 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:

  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

# 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:

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_server manages all server operations
  • Bad: handle_server also 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 --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:

# ✅ 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

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