prvng_core/nulib/main_provisioning/dispatcher.nu

476 lines
18 KiB
Text

# 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 <category>" - 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 <cmd> help" → convert to "provisioning help <cmd>"
# 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 <task>" 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
}
}
}