#!/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) } # Bash exports booleans as strings — normalize before any module code runs. let _coerce = {|raw| $raw == "true" or $raw == "1" } let raw_no_titles = ($env.PROVISIONING_NO_TITLES? | default "") if ($raw_no_titles | describe) == "string" and ($raw_no_titles | is-not-empty) { $env.PROVISIONING_NO_TITLES = (do $_coerce $raw_no_titles) } let raw_no_terminal = ($env.PROVISIONING_NO_TERMINAL? | default "") if ($raw_no_terminal | describe) == "string" and ($raw_no_terminal | is-not-empty) { $env.PROVISIONING_NO_TERMINAL = (do $_coerce $raw_no_terminal) } let raw_titles_shown = ($env.PROVISIONING_TITLES_SHOWN? | default "") if ($raw_titles_shown | describe) == "string" and ($raw_titles_shown | is-not-empty) { $env.PROVISIONING_TITLES_SHOWN = (do $_coerce $raw_titles_shown) } let raw_debug = ($env.PROVISIONING_DEBUG? | default "") if ($raw_debug | describe) == "string" and ($raw_debug | is-not-empty) { $env.PROVISIONING_DEBUG = (do $_coerce $raw_debug) } } # 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? | default false) { 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? | default false) { 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 "" } }