# Module: Command Dispatcher # Purpose: Main command router: dispatches CLI commands to appropriate handlers (infra, tools, workspace, etc.). # Dependencies: All command modules # Command Dispatcher # Central routing logic for all provisioning commands # Command module imports are lazy — loaded inside wrapper functions at dispatch time. # Only load lib_provisioning helpers required for routing logic in dispatch_command itself. # # ADR-025 Phase 4: narrowed from stars to selective imports. The two prior # imports `commands/traits.nu *` (20 exports) and `utils/command-registry.nu *` # (3 exports) were fully DEAD here — zero symbol uses — and have been removed. # `enforcement.nu` and `metadata_handler.nu` are narrowed to the single symbol # each that dispatcher actually calls (`check-and-enforce`, `validate-and-prepare`). use ../lib_provisioning/utils/undefined.nu [invalid_task] use ../lib_provisioning/workspace/enforcement.nu [check-and-enforce] use ./metadata_handler.nu [validate-and-prepare] use ../lib_provisioning/utils/nickel_processor.nu [ncl-eval-soft, default-ncl-paths] # Helper to run module commands def run_module [ args: string module: string option?: string --exec ] { let use_debug = if ($env.PROVISIONING_DEBUG? | default false) { "-x" } else { "" } if $exec { exec $"($env.PROVISIONING_NAME)" $use_debug -mod $module ($option | default "") $args } else { ^$"($env.PROVISIONING_NAME)" $use_debug -mod $module ($option | default "") $args } } # Lazy dispatch wrappers — each module is loaded only when its domain is actually invoked. def _dispatch_infrastructure [cmd: string, ops: string, flags: record] { use commands/infrastructure.nu * handle_infrastructure_command $cmd $ops $flags } def _dispatch_orchestration [cmd: string, ops: string, flags: record] { use commands/orchestration.nu * handle_orchestration_command $cmd $ops $flags } def _dispatch_development [cmd: string, ops: string, flags: record] { use commands/development.nu * handle_development_command $cmd $ops $flags } def _dispatch_workspace [cmd: string, ops: string, flags: record] { use commands/workspace.nu * handle_workspace_command $cmd $ops $flags } def _dispatch_config [cmd: string, ops: string, flags: record] { use commands/configuration.nu * handle_configuration_command $cmd $ops $flags } def _dispatch_utilities [cmd: string, ops: string, flags: record] { use commands/utilities/mod.nu * handle_utility_command $cmd $ops $flags } def _dispatch_generation [cmd: string, ops: string, flags: record] { use commands/generation.nu * handle_generation_command $cmd $ops $flags } def _dispatch_guides [cmd: string, ops: string, flags: record] { use commands/guides.nu * handle_guide_command $cmd $ops $flags } def _dispatch_authentication [cmd: string, ops: string, flags: record] { use commands/authentication.nu * handle_authentication_command $cmd $ops $flags } def _dispatch_diagnostics [cmd: string, ops: string, flags: record] { use commands/diagnostics.nu * handle_diagnostics_command $cmd $ops $flags } def _dispatch_vm [cmd: string, ops: string, flags: record] { use commands/vm_domain.nu * handle_vm_command $cmd $ops $flags } def _dispatch_platform [cmd: string, ops: string, flags: record] { use commands/platform.nu * handle_platform_command $cmd $ops $flags } def _dispatch_secretumvault [cmd: string, ops: string, flags: record] { use commands/secretumvault.nu * handle_secretumvault_command $cmd $ops $flags } def _dispatch_build [cmd: string, ops: string, flags: record] { use commands/build.nu * handle_build_command $cmd $ops $flags } def _dispatch_state [cmd: string, ops: string, flags: record] { use commands/state.nu * handle_state_command $cmd $ops $flags } # Command registry with shortcuts and aliases # Maps short forms and aliases to their canonical command domain export def get_command_registry [] { # Read commands registry from Nickel configuration let registry_file = ($env.PROVISIONING | path join "core/nulib/commands-registry.ncl") let prov = ($env.PROVISIONING? | default "/usr/local/provisioning") let registry_data = (ncl-eval-soft $registry_file (default-ncl-paths "") {}) if ($registry_data | is-empty) or ($registry_data == {}) { print "Error loading command registry" return {} } let commands = $registry_data.commands # Build registry record mapping commands and aliases to "category command" format let entries = ( $commands | each {|cmd| let help_cat = $cmd.help_category let cmd_name = $cmd.command let cmd_value = $"($help_cat) ($cmd_name)" # Create entries for command and all its aliases let command_entry = {($cmd_name): $cmd_value} let alias_entries = ($cmd.aliases | each {|alias| {($alias): $cmd_value}}) # Merge all entries [$command_entry] | append $alias_entries | reduce {|it, acc| $acc | merge $it} } | reduce {|it, acc| $acc | merge $it} ) $entries } # Commands that require arguments are defined in commands-registry.ncl (Nickel config file) # Use utils/command-registry.nu module to query the registry via JSON export # Note: This is loaded dynamically when needed, not at dispatcher load time # Main command dispatcher # Routes commands to appropriate domain handlers export def dispatch_command [ args: list flags: record ] { use flags.nu * # Find first non-flag argument as the task # (flags have already been parsed by main function, but reorder_args may have moved them) let matches = ($args | enumerate | where {|item| not ($item.item | str starts-with "-") and ($item.item | is-not-empty) }) let task_result = if ($matches | length) > 0 { $matches | first } else { null } let task = if ($task_result | is-not-empty) { $task_result.item } else { "" } # DEBUG if ($env.PROVISIONING_DEBUG? | default false) { print $"DEBUG dispatcher: task = '($task)'" >&2 } let task_index = if ($task_result | is-not-empty) { $task_result.index } else { 0 } # Get remaining args after task let ops_list = if $task_index < ($args | length) { ($args | skip ($task_index + 1)) } else { [] } let ops_str = ($ops_list | str join " ") # Handle empty command if ($task | is-empty) { print "Use 'provisioning help' for available commands" exit } # NOTE: Bash wrapper validates commands via command registry # Direct Nushell invocations will fail later with invalid_task if command is unknown # Handle "provisioning help " - DON'T dispatch, let main script handle it # The main script has "main help" function that Nushell will automatically route to # Using exec here creates infinite loop (calls bash wrapper → calls Nushell → calls exec → repeat) if $task in ["help" "h"] { # Don't dispatch help - it's handled by "export def main help" in provisioning script # Just exit dispatcher and let Nushell's built-in command routing handle it return } # Intercept bi-directional help: "provisioning help" → convert to "provisioning help " # Then exit dispatcher so main script's "main help" function handles it let first_op = if ($ops_list | length) > 0 { ($ops_list | get 0) } else { "" } if $first_op in ["help" "h"] { # Bi-directional help detected: convert args and exit dispatcher # The main script will see "help " and route to "main help" return } # Resolve command through registry let registry = get_command_registry let resolved = if ($task in ($registry | columns)) { $registry | get $task } else { $task } # Split into domain, command, and optional subcommand args let parts = ($resolved | split row " ") let domain = if ($parts | length) > 1 { ($parts | get 0) } else { "special" } let command = if ($parts | length) > 1 { ($parts | get 1) } else { $task } # Extract any additional parts as pre-filled ops (for compound shortcuts like "dt" → "discover taskservs") let extra_ops = if ($parts | length) > 2 { ($parts | skip 2 | str join " ") } else { "" } # Combine extra_ops with user-provided ops let final_ops = if ($extra_ops | is-not-empty) and ($ops_str | is-not-empty) { $"($extra_ops) ($ops_str)" } else if ($extra_ops | is-not-empty) { $extra_ops } else { $ops_str } # Handle workspace override from flags let workspace_context = (extract-workspace-infra-from-flags $flags) # Set temporary workspace context if specified if ($workspace_context.workspace | is-not-empty) { $env.TEMP_WORKSPACE = $workspace_context.workspace } # Update infra flag if parsed from workspace:infra notation let updated_flags = if ($workspace_context.infra | is-not-empty) { $flags | merge { infra: $workspace_context.infra } } else { $flags } # WORKSPACE ENFORCEMENT - Check workspace requirement before processing # This enforces that most commands require an active workspace let enforcement_allowed = (check-and-enforce $task $args) if not $enforcement_allowed { # Enforcement failed - error already displayed by check-and-enforce exit 1 } # METADATA VALIDATION - Check command requirements and handle interactive forms # Build canonical command name for metadata lookup let canonical_name = if ($domain == "special") { $command } else if ($domain != "") { $"($domain) ($command)" } else { $command } # Validate command and prepare execution (handles interactive forms) let prep_result = (validate-and-prepare $canonical_name $updated_flags) if not $prep_result.proceed { # Validation failed - error already displayed by validate-and-prepare exit 1 } # Set environment based on flags set_debug_env $updated_flags # Ensure PROVISIONING_INFRA is explicitly set if infra flag was provided # This ensures context-aware filtering works with --infra flag let infra_flag = ($updated_flags | get --optional infra) if ($infra_flag | is-not-empty) { $env.PROVISIONING_INFRA = $infra_flag } # Dispatch to domain handler if ($env.PROVISIONING_DEBUG? | default false) { print $"DEBUG: Dispatching to domain='($domain)' command='($command)' final_ops='($final_ops)'" >&2 } # Handler registry - maps domain to handler closure # To add a new command category: # 1. Add to commands-registry.ncl with help_category # 2. Add handler closure here # 3. Create handle_CATEGORY_command function in commands/ module let handlers = { infrastructure: {|cmd, ops, flags| _dispatch_infrastructure $cmd $ops $flags} orchestration: {|cmd, ops, flags| _dispatch_orchestration $cmd $ops $flags} development: {|cmd, ops, flags| _dispatch_development $cmd $ops $flags} workspace: {|cmd, ops, flags| _dispatch_workspace $cmd $ops $flags} config: {|cmd, ops, flags| _dispatch_config $cmd $ops $flags} utils: {|cmd, ops, flags| _dispatch_utilities $cmd $ops $flags} generation: {|cmd, ops, flags| _dispatch_generation $cmd $ops $flags} guides: {|cmd, ops, flags| _dispatch_guides $cmd $ops $flags} authentication: {|cmd, ops, flags| _dispatch_authentication $cmd $ops $flags} secretumvault: {|cmd, ops, flags| _dispatch_secretumvault $cmd $ops $flags} diagnostics: {|cmd, ops, flags| _dispatch_diagnostics $cmd $ops $flags} integrations: {|cmd, ops, flags| handle_integrations_command $cmd $ops $flags} platform: {|cmd, ops, flags| _dispatch_platform $cmd $ops $flags} vm: {|cmd, ops, flags| _dispatch_vm $cmd $ops $flags} build: {|cmd, ops, flags| _dispatch_build $cmd $ops $flags} state: {|cmd, ops, flags| _dispatch_state $cmd $ops $flags} special: {|cmd, ops, flags| handle_special_command $cmd $ops $flags} test: {|cmd, ops, flags| handle_test_command $cmd $ops $flags} help: {|cmd, ops, flags| exec $"($env.PROVISIONING_NAME)" help $cmd --notitles} } # Dynamic dispatch based on domain if ($domain in ($handlers | columns)) { let handler = ($handlers | get $domain) do $handler $command $final_ops $updated_flags } else { print $"❌ Error: No handler registered for domain '($domain)'" print $" Command: ($task)" print $" Available handlers: ($handlers | columns | str join ', ')" print "" print "To fix: Add handler closure to dispatcher.nu handlers record" invalid_task "" $task --end exit 1 } # Clean up temporary workspace context if ($workspace_context.workspace | is-not-empty) { hide-env TEMP_WORKSPACE } } # Integrations command handler (prov-ecosystem + provctl) def handle_integrations_command [command: string, ops: string, flags: record] { use commands/integrations/mod.nu * let args_list = if ($ops | is-not-empty) { $ops | split row " " | where { |x| ($x | is-not-empty) } } else { [] } let check_mode = if $flags.check_mode { "--check" } else { "" } # Parse command - could be "integrations integrations" or just the subcommand let subcommand = if $command == "integrations" { ($args_list | get 0?) } else { $command } # Get remaining args let remaining_args = if $command == "integrations" { ($args_list | skip 1) } else { $args_list } # Call the integrations handler with parsed arguments if ($subcommand == null) { cmd-integrations "help" $remaining_args --check=($check_mode | is-not-empty) } else { cmd-integrations $subcommand $remaining_args --check=($check_mode | is-not-empty) } } # Test command handler def handle_test_command [command: string, ops: string, flags: record] { let args = if ($ops | is-not-empty) { $ops } else { "" } run_module $args "test" --exec } # Special command handler (create, delete, update, deploy, etc.) def handle_special_command [command: string, ops: string, flags: record] { match $command { "create" | "c" => { let use_debug = if $flags.debug_mode or ($env.PROVISIONING_DEBUG? | default false) { "-x" } else { "" } let use_check = if $flags.check_mode { "--check " } else { "" } let str_infra = if ($flags.infra | is-not-empty) { $"--infra ($flags.infra) " } else { "" } let str_out = if ($flags.outfile | is-not-empty) { $"--outfile ($flags.outfile) " } else { "" } exec $"($env.PROVISIONING_NAME)" $use_debug "create" $ops $use_check $str_infra $str_out --notitles } "delete" | "d" => { let use_debug = if $flags.debug_mode { "-x" } else { "" } let use_check = if $flags.check_mode { "--check " } else { "" } let use_yes = if $flags.auto_confirm { "--yes " } else { "" } let use_keepstorage = if $flags.keep_storage { "--keepstorage " } else { "" } let str_infra = if ($flags.infra | is-not-empty) { $"--infra ($flags.infra) " } else { "" } exec $"($env.PROVISIONING_NAME)" "delete" $ops $use_check $use_yes $use_keepstorage $str_infra --notitles } "update" | "u" => { let use_debug = if $flags.debug_mode { "-x" } else { "" } let use_check = if $flags.check_mode { "--check " } else { "" } let str_infra = if ($flags.infra | is-not-empty) { $"--infra ($flags.infra) " } else { "" } exec $"($env.PROVISIONING_NAME)" "update" $ops $use_check $str_infra --notitles } "price" | "prices" | "cost" | "costs" => { use commands/infrastructure.nu * handle_price_command $ops $flags } "create-server-task" | "cst" | "csts" | "create-servers-tasks" => { use commands/infrastructure.nu * handle_create_server_task $ops $flags } "new" => { let str_new = ($flags.new_infra | default "") print $"\n (_ansi yellow)New Infra ($str_new)(_ansi reset)" } "ai" => { let str_infra = if ($flags.infra | is-not-empty) { $"--infra ($flags.infra) " } else { "" } let str_settings = if ($flags.settings | is-not-empty) { $"--settings ($flags.settings) " } else { "" } let str_out = if ($flags.output_format | is-not-empty) { $"--out ($flags.output_format) " } else { "" } run_module $"($ops) ($str_infra) ($str_settings) ($str_out)" "ai" --exec } "context" | "ctx" => { ^$"($env.PROVISIONING_NAME)" "context" $ops --notitles run_module $ops "" --exec } "setup" | "st" | "config" => { # Route to full setup command handler use commands/setup.nu * let args_list = if ($ops | is-not-empty) { $ops | split row " " | where { |x| ($x | is-not-empty) } } else { [] } let command = if ($args_list | length) > 0 { ($args_list | get 0) } else { "help" } let remaining_args = if ($args_list | length) > 1 { ($args_list | skip 1) } else { [] } cmd-setup $command $remaining_args --check=$flags.check_mode --verbose=$flags.debug_mode --yes=$flags.auto_confirm } "control-center" => { run_module $ops "control-center" --exec } "mcp-server" => { run_module $ops "mcp-server" --exec } "volume" | "vol" => { use ../provisioning-volume.nu * let vol_args = if ($ops | is-not-empty) { $ops | split row " " | where { $in | is-not-empty } } else { [] } let subcmd = ($vol_args | get 0? | default "list") let rest = if ($vol_args | length) > 1 { $vol_args | skip 1 } else { [] } match $subcmd { "list" | "l" => { main list --infra $flags.infra } "create" | "c" => { main create ($rest | get 0? | default "") --yes=$flags.auto_confirm } "attach" | "a" => { main attach ($rest | get 0? | default "") --server ($rest | get 1? | default "") --yes=$flags.auto_confirm } "detach" | "d" => { main detach ($rest | get 0? | default "") --yes=$flags.auto_confirm } "delete" | "rm" => { main delete ($rest | get 0? | default "") --yes=$flags.auto_confirm } _ => { main list --infra $flags.infra } } } _ => { print $"❌ Unknown command: ($command)" print "Use 'provisioning help' for available commands" exit 1 } } }