476 lines
18 KiB
Text
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_config_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
|
|
}
|
|
}
|
|
}
|