2026-04-17 21:42:20 +01:00
|
|
|
#!/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)
|
|
|
|
|
}
|
2026-04-17 22:14:40 +01:00
|
|
|
|
|
|
|
|
# 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)
|
|
|
|
|
}
|
2026-04-17 21:42:20 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# 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)
|
2026-04-17 22:14:40 +01:00
|
|
|
if not ($env.PROVISIONING_DEBUG? | default false) { end_run "" }
|
2026-04-17 21:42:20 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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
|
2026-04-17 22:14:40 +01:00
|
|
|
if not ($env.PROVISIONING_DEBUG? | default false) { end_run "" }
|
2026-04-17 21:42:20 +01:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# -mod <module> mode: bash wrapper extracts `-mod <name>` 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 <command> [options]"
|
|
|
|
|
print ""
|
|
|
|
|
print "Commands:"
|
|
|
|
|
print " create <role> - Build snapshot for role"
|
|
|
|
|
print " list - Show all role states"
|
|
|
|
|
print " update <role> - Rebuild stale snapshot"
|
|
|
|
|
print " delete <role> - Remove snapshot + state"
|
|
|
|
|
print " state - List all state files"
|
|
|
|
|
print " watch - Monitor role freshness"
|
|
|
|
|
print ""
|
|
|
|
|
print "Options:"
|
|
|
|
|
print " --infra <path> - 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 "" }
|
|
|
|
|
}
|