diff --git a/cli/provisioning b/cli/provisioning index c3fb78a..f0f7d32 100755 --- a/cli/provisioning +++ b/cli/provisioning @@ -51,7 +51,7 @@ fi export PROVISIONING_RESOURCES=${PROVISIONING_RESOURCES:-"$PROVISIONING/resources"} PROVIISONING_WKPATH=${PROVIISONING_WKPATH:-/tmp/tmp.} -RUNNER="provisioning" +RUNNER="provisioning-cli.nu" PROVISIONING_MODULE="" PROVISIONING_MODULE_TASK="" @@ -68,8 +68,8 @@ _show_help() { fi fi - # Fallback: Call Nushell for help (will use daemon if available) - $NU "${NU_ARGS[@]}" "$PROVISIONING/core/nulib/$RUNNER" help $category + # Fallback: Call Nushell for help via single CLI entry + $NU "${NU_ARGS[@]}" "$PROVISIONING/core/nulib/provisioning-cli.nu" help $category } # Workflow help function (defined early for early help detection) @@ -995,11 +995,9 @@ _validate_command() { # - Daemon fallback: Automatic, user sees no difference if [ -n "$PROVISIONING_MODULE" ]; then - if [[ -x $PROVISIONING/core/nulib/$RUNNER\ $PROVISIONING_MODULE ]]; then - $NU "${NU_ARGS[@]}" "$PROVISIONING/core/nulib/$RUNNER $PROVISIONING_MODULE" $CMD_ARGS - else - echo "Error \"$PROVISIONING/core/nulib/$RUNNER $PROVISIONING_MODULE\" not found" - fi + # -mod mode: provisioning-cli.nu reads PROVISIONING_MODULE from env + # and dispatches to the module's main function directly. + $NU "${NU_ARGS[@]}" "$PROVISIONING/core/nulib/provisioning-cli.nu" $CMD_ARGS list { - $args -} - -# Help on provisioning commands -export def "main help" [ - ...args: string # Optional category: infrastructure, orchestration, development, workspace, concepts - --notitles # not titles - --out: string # Print Output format: json, yaml, text (default) -] { - if $notitles == null or not $notitles { show_titles } - if ($out | is-not-empty) { $env.PROVISIONING_NO_TERMINAL = false } - # Use only the first argument, ignore any extras (e.g., "orch status" -> "orch") - let category = if ($args | length) > 0 { ($args | get 0) } else { "" } - print (provisioning_options $category) - if not $env.PROVISIONING_DEBUG { end_run "" } -} - -def main [ - ...args: string # Other options, use help to get info - --infra (-i): string # Cloud directory - --settings (-s): string # Settings path - --serverpos (-p): int # Server position in settings - --outfile (-o): string # Output file - --template(-t): string # Template path or name in PROVISION_KLOUDS_PATH - --check (-c) # Only check mode no servers will be created - --upload (-u) # Upload scripts to server for inspection without executing (use with --check) - --yes (-y) # confirm task - --wait # Wait servers to be created - --keepstorage # keep storage - --select: string # Select with task as option - --onsel: string # On selection: e (edit) | v (view) | l (list) | t (tree) - --infras: string # Infra list names separated by commas - --new (-n): string # New infrastructure name - --debug (-x) # Use Debug mode - --xm # Debug with PROVISIONING_METADATA - --xc # Debug for task and services locally PROVISIONING_DEBUG_CHECK - --xr # Debug for remote servers PROVISIONING_DEBUG_REMOTE - --xld # Log level with DEBUG PROVISIONING_LOG_LEVEL=debug - --nc # Not clean working settings - --metadata # Error with metadata (-xm) - --notitles # not tittles - --environment: string # Environment override (dev/test/prod) - --dep-option: string # Workspace dependency option: workspace-home, home-package, git-package, publish-repo - --dep-url: string # Dependency URL for git-package or publish-repo - --dry-run # Show what would be done without doing it (pack command) - --force (-f) # Skip confirmation prompts (pack/delete commands) - --all # Process all items (pack clean command) - --keep-latest: int # Keep N latest versions (pack clean command) - --workspace (-w): string # Workspace name (for bootstrap, cluster deploy, etc.) - --activate # Activate workspace as default (workspace commands) - --interactive # Interactive workspace creation wizard - --org: string # Organization name (for detect/complete commands) - --apply # Apply changes (for complete command) - --verbose # Verbose output (for detect/complete/workflow commands) - --pretty # Pretty-print JSON/YAML output (for detect/complete commands) - -v # Show version - --version (-V) # Show version with title - --info # Show Info with title - --about # Show About - --helpinfo (-h) # For more details use options "help" (no dashes) - --out: string # Print Output format: json, yaml, text (default) - --view # Print with highlight - --inputfile: string # Input format: json, yaml, text (default) - --include_notuse # Include servers not use - --services: string # Platform services set: core, all, custom (for platform start) -]: nothing -> nothing { - # Reorder arguments: move flags to the beginning - # This allows: provisioning workspace update --yes - let reordered_args = (reorder_args $args) - - # Extract flags from reordered args (for flags that came after positional args) - let has_yes_in_args = ($reordered_args | any {|x| $x == "--yes" or $x == "-y"}) - let has_check_in_args = ($reordered_args | any {|x| $x == "--check" or $x == "-c"}) - let has_upload_in_args = ($reordered_args | any {|x| $x == "--upload" or $x == "-u"}) - let has_force_in_args = ($reordered_args | any {|x| $x == "--force" or $x == "-f"}) - let has_verbose_in_args = ($reordered_args | any {|x| $x == "--verbose" or $x == "-v"}) - let has_wait_in_args = ($reordered_args | any {|x| $x == "--wait"}) - - # Combine with already-parsed flags (take OR - if either parsed or in args, then true) - let final_yes = ($yes or $has_yes_in_args) - let final_check = ($check or $has_check_in_args) - let final_upload = ($upload or $has_upload_in_args) - let final_force = ($force or $has_force_in_args) - let final_verbose = ($verbose or $has_verbose_in_args) - let final_wait = ($wait or $has_wait_in_args) - - # Initialize provisioning system - provisioning_init $helpinfo "" $reordered_args - - # Parse all flags into normalized structure - let parsed_flags = (parse_common_flags { - version: $version, v: $v, info: $info, about: $about, - debug: $debug, metadata: $metadata, xc: $xc, xr: $xr, xld: $xld, - check: $final_check, upload: $final_upload, yes: $final_yes, wait: $final_wait, keepstorage: $keepstorage, - nc: $nc, include_notuse: $include_notuse, - out: $out, notitles: $notitles, view: $view, - infra: $infra, infras: $infras, settings: $settings, outfile: $outfile, - template: $template, select: $select, onsel: $onsel, serverpos: $serverpos, - new: $new, environment: $environment, - dep_option: $dep_option, dep_url: $dep_url, - dry_run: $dry_run, force: $final_force, all: $all, keep_latest: $keep_latest, - activate: $activate, interactive: $interactive, - org: $org, apply: $apply, verbose: $final_verbose, pretty: $pretty, - services: $services, workspace: $workspace - }) - - # Handle version, info, about flags - if $parsed_flags.show_version { ^$env.PROVISIONING_NAME -v ; exit } - if $parsed_flags.show_info { ^$env.PROVISIONING_NAME -i ; exit } - if $parsed_flags.show_about { _print (get_about_info) ; exit } - - # Bootstrap platform services (only if running actual commands, not help/info) - # Skip bootstrap for help-like, guide, setup, discovery/info, and utility commands - # Updated for Phase 1: Fast-Path Expansion - Include read-only workspace commands - let is_help_command = ( - ($reordered_args | length) == 0 or - ($reordered_args | get 0) in [ - # Help and guides - "help", "-h", "--help", - "sc", "shortcuts", "quickstart", "quick", - "from-scratch", "scratch", - "customize", "custom", - "guide", "guides", "howto", - # Setup - "setup", "st", - # Workspace commands (read-only, fast-path) - "workspace", "ws", - # Discovery and module commands - "mod", "module", "discover", "disc", - "dt", "dp", "dc", - "discover-taskservs", "disc-t", - "discover-providers", "disc-p", - "discover-clusters", "disc-c", - # Development info - "lyr", "layer", "version", "pack", - # Utilities and info - "nuinfo", "env", "allenv", - "validate", "val", "show", "config-template", - "cache", - "list", "l", "ls", - "plugin", "plugins", - "qr", "ssh", "sops", - "providers", - # Diagnostics commands (workspace-agnostic) - "status", "health", "diagnostics", "next", "phase" - ] - ) - - # Check if this is a command that doesn't need platform bootstrap - # VM commands and infrastructure commands can work without bootstrap - # Also skip bootstrap if --check flag is present (validation mode, no execution needed) - let skip_bootstrap = ( - (($reordered_args | length) > 0 and - ($reordered_args | get 0) in [ - # Interactive Nushell session (no bootstrap needed) - "nu", - # Platform commands (don't need bootstrap) - "platform", "plat", "p", - # VM commands (info/list only, no bootstrap needed) - "vm", "vmi", "vmh", "vml", - # Infrastructure commands can work offline - "server", "s", - "taskserv", "task", "t", - "cluster", "cl", - "bootstrap", - # Create command (with various targets) - "create", "c", - # Delete command - "delete", "d", - # Update command - "update", "u", - # Build commands (image management, doesn't need orchestrator) - "build", "b", "bi", "build-image" - ]) or - # Skip bootstrap if in check mode (validation/dry-run, no execution needed) - $final_check - ) - - if (not $is_help_command) and (not $skip_bootstrap) { - # Load bootstrap module dynamically when needed - use lib_provisioning/platform/bootstrap.nu * - let bootstrap_result = (bootstrap-platform --auto-start --timeout=60 --verbose=($final_verbose)) - if not $bootstrap_result.all_healthy { - _print "" - _print $"(_ansi red)❌ Platform services not healthy(_ansi reset)" - _print "" - _print "Failed services:" - for service in ($bootstrap_result.services | where {|s| $s.status != "healthy"}) { - _print $" - ($service.name): ($service.action)" - } - _print "" - _print "To start services manually:" - _print " cd provisioning/platform && docker-compose up -d" - _print "" - exit 1 - } - } - - # DEBUG - if ($env.PROVISIONING_DEBUG? | default false) { - print $"DEBUG provisioning: reordered_args = ($reordered_args)" >&2 - print $"DEBUG provisioning: parsed_flags.infra = (($parsed_flags | get -o infra | default 'MISSING'))" >&2 - } - - # Handle help command BEFORE dispatcher to avoid infinite loop - # The dispatcher used to call "exec provisioning help" which created infinite recursion - if (($reordered_args | length) > 0) and (($reordered_args | get 0) in ["help", "h"]) { - if ($env.PROVISIONING_DEBUG? | default false) { - print $"DEBUG: Help command detected, args=($reordered_args)" >&2 - } - let category = if ($reordered_args | length) > 1 { ($reordered_args | get 1) } else { "" } - print (provisioning_options $category) - if not ($env.PROVISIONING_DEBUG? | default false) { end_run "" } - return - } - - # For info/discovery/utility commands, dispatch directly without going through workspace enforcement - # These commands don't need workspace context - if (($reordered_args | length) > 0) and (($reordered_args | get 0) in [ - # Guide commands - "guide", "guides", "sc", "howto", "shortcuts", "quickstart", "quick", - "from-scratch", "scratch", "customize", "custom", - # Discovery/info commands - "mod", "module", "discover", "disc", - "dt", "dp", "dc", - "discover-taskservs", "disc-t", - "discover-providers", "disc-p", - "discover-clusters", "disc-c", - "lyr", "layer", "version", - "nuinfo", "env", "allenv", - "validate", "val", "show", "cache", - # Utility commands (these are informational) - "plugin", "plugins", - "qr", "nuinfo", - # Diagnostics commands (workspace-agnostic) - "status", "health", "diagnostics", "next", "phase" - ]) { - dispatch_command $reordered_args $parsed_flags - if not $env.PROVISIONING_DEBUG { end_run "" } - return - } - - # Check if we're in module mode (invoked with -mod flag from bash wrapper) - # If so, bypass dispatcher and call the module directly - if ($env.PROVISIONING_MODULE? | default "" | is-not-empty) { - let module = $env.PROVISIONING_MODULE - # At this point, $reordered_args contains [create, ...] or whatever the user provided after -mod - # We need to invoke the module's main function - - match $module { - "server" => { - use servers/create.nu * - # Ensure tera plugin is loaded for template rendering - let tera_available = ((plugin list | where name == "tera" | length) > 0) - if $tera_available { - if ($env.PROVISIONING_DEBUG? | default false) { - _print "DEBUG: Loading tera plugin (-mod server)..." >&2 - } - (plugin use tera) - if ($env.PROVISIONING_DEBUG? | default false) { - _print "DEBUG: Tera plugin loaded for -mod server" >&2 - } - } - # Call server create module main function - # $reordered_args now has ["create"] or ["delete"] or ["list"] etc. - main ...$reordered_args --check=$final_check --wait=$final_wait --infra=($infra | default "") --settings=($settings | default "") --outfile=($outfile | default "") --debug=$debug --xm=$xm --xc=$xc --xr=$xr --xld=$xld --metadata=$metadata --notitles=$notitles --out=($out | default "") - } - "taskserv" | "task" => { - use taskservs/create.nu * - main ...$reordered_args --check=$final_check --upload=$final_upload --wait=$final_wait --debug=$debug - } - "cluster" => { - use clusters/create.nu * - main ...$reordered_args --check=$final_check --debug=$debug - } - "images" => { - use images/create.nu * - use images/list.nu * - use images/update.nu * - use images/delete.nu * - use images/state.nu * - use images/watch.nu * - # $reordered_args now has ["create", "cp", "--infra", "..."] or similar - let subcommand = if ($reordered_args | length) > 0 { $reordered_args | get 0 } else { "help" } - match $subcommand { - "create" | "c" => { - let role = if ($reordered_args | length) > 1 { $reordered_args | get 1 } else { "" } - let infra_arg = if ($infra | is-not-empty) { $infra } else { "" } - image-create $role --infra=$infra_arg --check=$final_check - } - "list" | "l" => { - let provider = if ($infra | is-not-empty) { $infra } else { "" } - image-list --provider=$provider - } - "update" | "u" => { - let role = if ($reordered_args | length) > 1 { $reordered_args | get 1 } else { "" } - let infra_arg = if ($infra | is-not-empty) { $infra } else { "" } - image-update $role --infra=$infra_arg --check=$final_check - } - "delete" | "d" => { - let role = if ($reordered_args | length) > 1 { $reordered_args | get 1 } else { "" } - image-delete $role --yes=$final_yes - } - "state" | "s" => { - image-state-list --provider=$infra - } - "watch" | "w" => { - let interval = if ($reordered_args | length) > 1 { $reordered_args | get 1 } else { "30" } - image-watch --interval=($interval | into int) - } - "help" | "h" | _ => { - print "Image Management Commands" - print "=======================" - print "" - print "Usage: provisioning build image [options]" - print "" - print "Commands:" - print " create - Build snapshot for role" - print " list - Show all role states" - print " update - Rebuild stale snapshot" - print " delete - Remove snapshot + state" - print " state - List all state files" - print " watch - Monitor role freshness" - print "" - print "Options:" - print " --infra - Infrastructure directory" - print " --check - Validate without executing" - print " --yes - Skip confirmation" - print "" - } - } - } - _ => { - print $"Unknown module: ($module)" - exit 1 - } - } - } else { - # Normal command dispatch through dispatcher - dispatch_command $reordered_args $parsed_flags - } - - # End run if not in debug mode - if not ($env.PROVISIONING_DEBUG? | default false) { end_run "" } -} - -export def get_show_info [ - ops: list - curr_settings: record - out: string -]: nothing -> record { - match ($ops | get -o 0 | default "") { - "set" |"setting" | "settings" => $curr_settings, - "def" | "defs" |"defsetting" | "defsettings" => { - let src = ($curr_settings | get -o src | default ""); - let src_path = ($curr_settings | get -o src_path | default ""); - let def_settings = if ($src_path | path join $src | path exists) { - open -r ($src_path | path join $src) - } else { "" } - let main_path = ($env.PROVISIONING | path join "kcl" | path join "settings.k") - let src_main_settings = if ($main_path | path exists) { - open -r $main_path - } else { "" } - { - def: $src, - def_path: $src_path, - infra: ($curr_settings | get -o infra | default ""), - infra_path: ($curr_settings | get -o infra_path | default ""), - def_settings: $def_settings, - main_path: $main_path, - main_settings: $src_main_settings, - } - }, - "server" |"servers" | "s" => { - let servers = ($curr_settings | get -o data | get -o servers | default {}) - let item = ($ops | get -o 1 | default "") - if ($item | is-empty) { - $servers - } else { - let server = (find_server $item $servers ($out | default "")) - let def_target = ($ops | get -o 2 | default "") - match $def_target { - "t" | "task" | "taskserv" => { - let task = ($ops | get -o 3 | default "") - (find_taskserv $curr_settings $server $task ($out | default "")) - }, - _ => $server, - } - } - }, - "serverdefs" |"serversdefs" | "sd" => { - (find_serversdefs $curr_settings) - }, - "provgendefs" |"provgendef" | "pgd" => { - (find_provgendefs) - }, - "taskservs" |"taskservs" | "ts" => { - #(list_taskservs $curr_settings) - let list_taskservs = (taskservs_list) - if ($list_taskservs | length) == 0 { - _print $"🛑 no items found for (_ansi cyan)taskservs list(_ansi reset)" - return - } - $list_taskservs - }, - "taskservsgendefs" |"taskservsgendef" | "tsd" => { - let defs_path = ($env.PROVISIONING_TASKSERVS_PATH | path join $env.PROVISIONING_GENERATE_DIRPATH | path join $env.PROVISIONING_GENERATE_DEFSFILE) - if ($defs_path | path exists) { - open $defs_path - } - }, - "cost" | "costs" | "c" | "price" | "prices" | "p" => { - (servers_walk_by_costs $curr_settings "" false false "stdout") - }, - "alldata" => ($curr_settings | get -o data | default {} - | merge { costs: (servers_walk_by_costs $curr_settings "" false false "stdout") } - ), - "data" | _ => { - if ($out | is-not-empty) { - ($curr_settings | get -o data | default {}) - } else { - print ($" (_ansi cyan_bold)($curr_settings | get -o data | get -o main_name | default '')" - + $"(_ansi reset): (_ansi yellow_bold)($curr_settings | get -o data | get -o main_title | default '') (_ansi reset)" - ) - print ($curr_settings | get -o data | default {} | merge { servers: ''}) - ($curr_settings | get -o data | default {} | get -o servers | each {|item| - print $"\n server: (_ansi cyan_bold)($item.hostname | default '') (_ansi reset)" - print $item - }) - "" - } - }, - } -} diff --git a/nulib/provisioning-cli.nu b/nulib/provisioning-cli.nu new file mode 100644 index 0000000..c60e32d --- /dev/null +++ b/nulib/provisioning-cli.nu @@ -0,0 +1,351 @@ +#!/usr/bin/env nu +# Single CLI entry — replaces legacy nulib/provisioning runner (ADR-025 Phase 4). +# +# Single-route architecture: every command goes through dispatch_command, which +# lazy-loads per-domain handlers on demand. The star-imports that dominated +# cold-start in the legacy runner are gone; only the dispatcher surface + a +# handful of init helpers are parsed on startup. +# +# Daemon and cache become orthogonal concerns applied INSIDE handlers (or their +# lazy dependencies), not separate routes. + +export-env { + let lib_dirs_raw = ($env.NU_LIB_DIRS? | default "") + let current_lib_dirs = if ($lib_dirs_raw | type) == "string" { + if ($lib_dirs_raw | is-empty) { + [] + } else { + ($lib_dirs_raw | split row ":") + } + } else { + $lib_dirs_raw + } + + let default_paths = [ + "/opt/provisioning/core/nulib" + "/usr/local/provisioning/core/nulib" + ] + + $env.NU_LIB_DIRS = ($default_paths | append $current_lib_dirs) + + if ( (version).installed_plugins | str contains "tera" ) { + (plugin use tera) + } +} + +# ADR-025 Phase 4 perf insight: Nushell selective imports (`use x [sym]`) still +# parse the entire source module. To actually defer parse cost we must move +# `use` statements INSIDE function bodies — they're then evaluated only when +# the function is called, not at file-parse time. Parsing this file itself +# only sees two `def` headers and one `export-env` block. + +# Pass-through: Nushell parameter parsing handles interleaved flags, so we +# just return args as-is. Preserved as a seam for future normalization. +def reorder_args [args: list]: nothing -> list { $args } + +export def "main help" [ + ...args: string + --notitles + --out: string +] { + use lib_provisioning/utils/init.nu [show_titles] + use lib_provisioning/utils/interface.nu [end_run] + use main_provisioning/ops.nu [provisioning_options] + + if $notitles == null or not $notitles { show_titles } + if ($out | is-not-empty) { $env.PROVISIONING_NO_TERMINAL = false } + let category = if ($args | length) > 0 { ($args | get 0) } else { "" } + print (provisioning_options $category) + if not $env.PROVISIONING_DEBUG { end_run "" } +} + +def main [ + ...args: string + --infra (-i): string + --settings (-s): string + --serverpos (-p): int + --outfile (-o): string + --template(-t): string + --check (-c) + --upload (-u) + --yes (-y) + --wait + --keepstorage + --select: string + --onsel: string + --infras: string + --new (-n): string + --debug (-x) + --xm + --xc + --xr + --xld + --nc + --metadata + --notitles + --environment: string + --dep-option: string + --dep-url: string + --dry-run + --force (-f) + --all + --keep-latest: int + --workspace (-w): string + --activate + --interactive + --org: string + --apply + --verbose + --pretty + -v + --version (-V) + --info + --about + --helpinfo (-h) + --out: string + --view + --inputfile: string + --include_notuse + --services: string +]: nothing -> nothing { + # Function-local imports: parsed only when main() is called, not at + # file-parse time. Keeps cold-start for help-like shortcuts minimal. + use lib_provisioning/utils/interface.nu [_ansi _print end_run] + use lib_provisioning/utils/init.nu [provisioning_init] + use lib_provisioning/defs/about.nu [about_info] + use main_provisioning/flags.nu [parse_common_flags] + use main_provisioning/ops.nu [provisioning_options] + use main_provisioning/dispatcher.nu [dispatch_command] + + let reordered_args = (reorder_args $args) + + let has_yes_in_args = ($reordered_args | any {|x| $x == "--yes" or $x == "-y"}) + let has_check_in_args = ($reordered_args | any {|x| $x == "--check" or $x == "-c"}) + let has_upload_in_args = ($reordered_args | any {|x| $x == "--upload" or $x == "-u"}) + let has_force_in_args = ($reordered_args | any {|x| $x == "--force" or $x == "-f"}) + let has_verbose_in_args = ($reordered_args | any {|x| $x == "--verbose" or $x == "-v"}) + let has_wait_in_args = ($reordered_args | any {|x| $x == "--wait"}) + + let final_yes = ($yes or $has_yes_in_args) + let final_check = ($check or $has_check_in_args) + let final_upload = ($upload or $has_upload_in_args) + let final_force = ($force or $has_force_in_args) + let final_verbose = ($verbose or $has_verbose_in_args) + let final_wait = ($wait or $has_wait_in_args) + + provisioning_init $helpinfo "" $reordered_args + + let parsed_flags = (parse_common_flags { + version: $version, v: $v, info: $info, about: $about, + debug: $debug, metadata: $metadata, xc: $xc, xr: $xr, xld: $xld, + check: $final_check, upload: $final_upload, yes: $final_yes, wait: $final_wait, keepstorage: $keepstorage, + nc: $nc, include_notuse: $include_notuse, + out: $out, notitles: $notitles, view: $view, + infra: $infra, infras: $infras, settings: $settings, outfile: $outfile, + template: $template, select: $select, onsel: $onsel, serverpos: $serverpos, + new: $new, environment: $environment, + dep_option: $dep_option, dep_url: $dep_url, + dry_run: $dry_run, force: $final_force, all: $all, keep_latest: $keep_latest, + activate: $activate, interactive: $interactive, + org: $org, apply: $apply, verbose: $final_verbose, pretty: $pretty, + services: $services, workspace: $workspace + }) + + if $parsed_flags.show_version { ^$env.PROVISIONING_NAME -v ; exit } + if $parsed_flags.show_info { ^$env.PROVISIONING_NAME -i ; exit } + if $parsed_flags.show_about { _print (about_info) ; exit } + + let is_help_command = ( + ($reordered_args | length) == 0 or + ($reordered_args | get 0) in [ + "help", "-h", "--help", + "sc", "shortcuts", "quickstart", "quick", + "from-scratch", "scratch", + "customize", "custom", + "guide", "guides", "howto", + "setup", "st", + "workspace", "ws", + "mod", "module", "discover", "disc", + "dt", "dp", "dc", + "discover-taskservs", "disc-t", + "discover-providers", "disc-p", + "discover-clusters", "disc-c", + "lyr", "layer", "version", "pack", + "nuinfo", "env", "allenv", + "validate", "val", "show", "config-template", + "cache", + "list", "l", "ls", + "plugin", "plugins", + "qr", "ssh", "sops", + "providers", + "status", "health", "diagnostics", "next", "phase" + ] + ) + + let skip_bootstrap = ( + (($reordered_args | length) > 0 and + ($reordered_args | get 0) in [ + "nu", + "platform", "plat", "p", + "vm", "vmi", "vmh", "vml", + "server", "s", + "taskserv", "task", "t", + "cluster", "cl", + "bootstrap", + "create", "c", + "delete", "d", + "update", "u", + "build", "b", "bi", "build-image" + ]) or + $final_check + ) + + if (not $is_help_command) and (not $skip_bootstrap) { + use lib_provisioning/platform/bootstrap.nu * + let bootstrap_result = (bootstrap-platform --auto-start --timeout=60 --verbose=($final_verbose)) + if not $bootstrap_result.all_healthy { + _print "" + _print $"(_ansi red)❌ Platform services not healthy(_ansi reset)" + _print "" + _print "Failed services:" + for service in ($bootstrap_result.services | where {|s| $s.status != "healthy"}) { + _print $" - ($service.name): ($service.action)" + } + _print "" + _print "To start services manually:" + _print " cd provisioning/platform && docker-compose up -d" + _print "" + exit 1 + } + } + + if ($env.PROVISIONING_DEBUG? | default false) { + print $"DEBUG provisioning-cli: reordered_args = ($reordered_args)" >&2 + print $"DEBUG provisioning-cli: parsed_flags.infra = (($parsed_flags | get -o infra | default 'MISSING'))" >&2 + } + + # Help: short-circuit before dispatcher to avoid recursive exec loops. + if (($reordered_args | length) > 0) and (($reordered_args | get 0) in ["help" "h"]) { + let category = if ($reordered_args | length) > 1 { ($reordered_args | get 1) } else { "" } + print (provisioning_options $category) + if not ($env.PROVISIONING_DEBUG? | default false) { end_run "" } + return + } + + # Info/discovery/utility commands bypass workspace enforcement. + if (($reordered_args | length) > 0) and (($reordered_args | get 0) in [ + "guide", "guides", "sc", "howto", "shortcuts", "quickstart", "quick", + "from-scratch", "scratch", "customize", "custom", + "mod", "module", "discover", "disc", + "dt", "dp", "dc", + "discover-taskservs", "disc-t", + "discover-providers", "disc-p", + "discover-clusters", "disc-c", + "lyr", "layer", "version", + "nuinfo", "env", "allenv", + "validate", "val", "show", "cache", + "plugin", "plugins", + "qr", "nuinfo", + "status", "health", "diagnostics", "next", "phase" + ]) { + dispatch_command $reordered_args $parsed_flags + if not $env.PROVISIONING_DEBUG { end_run "" } + return + } + + # -mod mode: bash wrapper extracts `-mod ` into + # PROVISIONING_MODULE and forwards remaining args. We invoke that module's + # `main` directly, bypassing the dispatcher. + if ($env.PROVISIONING_MODULE? | default "" | is-not-empty) { + let module = $env.PROVISIONING_MODULE + + match $module { + "server" => { + use servers/create.nu * + let tera_available = ((plugin list | where name == "tera" | length) > 0) + if $tera_available { + if ($env.PROVISIONING_DEBUG? | default false) { + _print "DEBUG: Loading tera plugin (-mod server)..." >&2 + } + (plugin use tera) + if ($env.PROVISIONING_DEBUG? | default false) { + _print "DEBUG: Tera plugin loaded for -mod server" >&2 + } + } + main ...$reordered_args --check=$final_check --wait=$final_wait --infra=($infra | default "") --settings=($settings | default "") --outfile=($outfile | default "") --debug=$debug --xm=$xm --xc=$xc --xr=$xr --xld=$xld --metadata=$metadata --notitles=$notitles --out=($out | default "") + } + "taskserv" | "task" => { + use taskservs/create.nu * + main ...$reordered_args --check=$final_check --upload=$final_upload --wait=$final_wait --debug=$debug + } + "cluster" => { + use clusters/create.nu * + main ...$reordered_args --check=$final_check --debug=$debug + } + "images" => { + use images/create.nu * + use images/list.nu * + use images/update.nu * + use images/delete.nu * + use images/state.nu * + use images/watch.nu * + let subcommand = if ($reordered_args | length) > 0 { $reordered_args | get 0 } else { "help" } + match $subcommand { + "create" | "c" => { + let role = if ($reordered_args | length) > 1 { $reordered_args | get 1 } else { "" } + let infra_arg = if ($infra | is-not-empty) { $infra } else { "" } + image-create $role --infra=$infra_arg --check=$final_check + } + "list" | "l" => { + let provider = if ($infra | is-not-empty) { $infra } else { "" } + image-list --provider=$provider + } + "update" | "u" => { + let role = if ($reordered_args | length) > 1 { $reordered_args | get 1 } else { "" } + let infra_arg = if ($infra | is-not-empty) { $infra } else { "" } + image-update $role --infra=$infra_arg --check=$final_check + } + "delete" | "d" => { + let role = if ($reordered_args | length) > 1 { $reordered_args | get 1 } else { "" } + image-delete $role --yes=$final_yes + } + "state" | "s" => { + image-state-list --provider=$infra + } + "watch" | "w" => { + let interval = if ($reordered_args | length) > 1 { $reordered_args | get 1 } else { "30" } + image-watch --interval=($interval | into int) + } + "help" | "h" | _ => { + print "Image Management Commands" + print "=======================" + print "" + print "Usage: provisioning build image [options]" + print "" + print "Commands:" + print " create - Build snapshot for role" + print " list - Show all role states" + print " update - Rebuild stale snapshot" + print " delete - Remove snapshot + state" + print " state - List all state files" + print " watch - Monitor role freshness" + print "" + print "Options:" + print " --infra - Infrastructure directory" + print " --check - Validate without executing" + print " --yes - Skip confirmation" + print "" + } + } + } + _ => { + print $"Unknown module: ($module)" + exit 1 + } + } + } else { + dispatch_command $reordered_args $parsed_flags + } + + if not ($env.PROVISIONING_DEBUG? | default false) { end_run "" } +}