Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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:

DomainHandlerResponsibility
infrastructure.nuServer/taskserv/cluster/infra lifecycle
orchestration.nuWorkflow/batch operations, orchestrator control
development.nuModule discovery, layers, versions, packaging
workspace.nuWorkspace and template management
configuration.nuEnvironment, settings, initialization
utilities.nuSSH, SOPS, cache, providers, utilities
generation.nuGenerate 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 FieldDescription
show_versionVersion display (--version, -v)
show_infoInfo display (--info, -i)
show_aboutAbout display (--about, -a)
debug_modeDebug mode (--debug, -x)
check_modeCheck mode (--check, -c)
auto_confirmAuto-confirm (--yes, -y)
waitWait for completion (--wait, -w)
keep_storageKeep storage (--keepstorage)
infraInfrastructure name (--infra)
outfileOutput file (--outfile)
output_formatOutput format (--out)
templateTemplate name (--template)
selectSelection (--select)
settingsSettings file (--settings)
new_infraNew 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