#!/usr/bin/env bash # Info: Script to run Provisioning # Author: JesusPerezLorenzo # Release: 1.0.11 # Date: 2026-01-14 set +o errexit set +o pipefail # Debug: log startup [ "$PROVISIONING_DEBUG_STARTUP" = "true" ] && echo "[DEBUG] Wrapper started with args: $@" >&2 export NU=$(type -P nu) _release() { grep "^# Release:" "$0" | sed "s/# Release: //g" } export PROVISIONING_VERS=$(_release) set -o allexport ## shellcheck disable=SC1090 [ -n "$PROVISIONING_ENV" ] && [ -r "$PROVISIONING_ENV" ] && source "$PROVISIONING_ENV" [ -r "../env-provisioning" ] && source ../env-provisioning [ -r "env-provisioning" ] && source ./env-provisioning #[ -r ".env" ] && source .env set # Disable provisioning logo/banner output export PROVISIONING_NO_TITLES=true set +o allexport export PROVISIONING=${PROVISIONING:-/usr/local/provisioning} PROVIISONING_WKPATH=${PROVIISONING_WKPATH:-/tmp/tmp.} RUNNER="provisioning" [ "$1" == "" ] && shift [ -z "$NU" ] || [ "$1" == "install" ] || [ "$1" == "reinstall" ] || [ "$1" == "mode" ] && exec bash $PROVISIONING/core/bin/install_nu.sh $PROVISIONING $1 $2 [ "$1" == "rmwk" ] && rm -rf "$PROVIISONING_WKPATH"* && echo "$PROVIISONING_WKPATH deleted" && exit [ "$1" == "-x" ] && debug=-x && export PROVISIONING_DEBUG=true && shift [ "$1" == "-xm" ] && export PROVISIONING_METADATA=true && shift [ "$1" == "nu" ] && export PROVISIONING_DEBUG=true [ "$1" == "--x" ] && set -x && debug=-x && export PROVISIONING_DEBUG=true && shift [ "$1" == "-i" ] || [ "$2" == "-i" ] && echo "$(basename "$0") $(grep "^# Info:" "$0" | sed "s/# Info: //g") " && exit [ "$1" == "-v" ] || [ "$1" == "--version" ] || [ "$2" == "-v" ] && _release && exit CMD_ARGS=$@ # Note: Flag ordering is handled by Nushell's reorder_args function # which automatically reorders flags before positional arguments. # Flags can be placed anywhere on the command line. case "$1" in # Note: "setup" is now handled by the main provisioning CLI dispatcher # No special module handling needed -mod) PROVISIONING_MODULE=$(echo "$2" | sed 's/ //g' | cut -f1 -d"|") PROVISIONING_MODULE_TASK=$(echo "$2" | sed 's/ //g' | cut -f2 -d"|") [ "$PROVISIONING_MODULE" == "$PROVISIONING_MODULE_TASK" ] && PROVISIONING_MODULE_TASK="" shift 2 CMD_ARGS=$@ [ "$PROVISIONING_DEBUG_STARTUP" = "true" ] && echo "[DEBUG] -mod detected: MODULE=$PROVISIONING_MODULE, TASK=$PROVISIONING_MODULE_TASK, CMD_ARGS=$CMD_ARGS" >&2 ;; esac NU_ARGS="" DEFAULT_CONTEXT_TEMPLATE="default_context.yaml" case "$(uname | tr '[:upper:]' '[:lower:]')" in linux) PROVISIONING_USER_CONFIG="$HOME/.config/provisioning/nushell" PROVISIONING_CONTEXT_PATH="$HOME/.config/provisioning/$DEFAULT_CONTEXT_TEMPLATE" ;; darwin) PROVISIONING_USER_CONFIG="$HOME/Library/Application Support/provisioning/nushell" PROVISIONING_CONTEXT_PATH="$HOME/Library/Application Support/provisioning/$DEFAULT_CONTEXT_TEMPLATE" ;; *) PROVISIONING_USER_CONFIG="$HOME/.config/provisioning/nushell" PROVISIONING_CONTEXT_PATH="$HOME/.config/provisioning/$DEFAULT_CONTEXT_TEMPLATE" ;; esac # ════════════════════════════════════════════════════════════════════════════════ # DAEMON ROUTING - Try daemon for all commands (except setup/help/interactive) # Falls back to traditional handlers if daemon unavailable # ════════════════════════════════════════════════════════════════════════════════ DAEMON_ENDPOINT="http://127.0.0.1:9091/execute" # Function to execute command via daemon execute_via_daemon() { local cmd="$1" shift # Build JSON array of arguments (simple bash) local args_json="[" local first=1 for arg in "$@"; do [ $first -eq 0 ] && args_json="$args_json," args_json="$args_json\"$(echo "$arg" | sed 's/"/\\"/g')\"" first=0 done args_json="$args_json]" # Determine timeout based on command type # Heavy commands (create, delete, update) get longer timeout local timeout=0.5 case "$cmd" in create|delete|update|setup|init) timeout=5 ;; *) timeout=0.2 ;; esac # Make request and extract stdout curl -s -m $timeout -X POST "$DAEMON_ENDPOINT" \ -H "Content-Type: application/json" \ -d "{\"command\":\"$cmd\",\"args\":$args_json,\"timeout_ms\":30000}" 2>/dev/null | \ sed -n 's/.*"stdout":"\(.*\)","execution.*/\1/p' | \ sed 's/\\n/\n/g' } # Try daemon ONLY for lightweight commands (list, show, status) # Skip daemon for heavy commands (create, delete, update) because bash wrapper is slow if [ "$1" = "server" ] || [ "$1" = "s" ]; then if [ "$2" = "list" ] || [ -z "$2" ]; then # Light command - try daemon [ "$PROVISIONING_DEBUG" = "true" ] && echo "⚡ Attempting daemon execution..." >&2 DAEMON_OUTPUT=$(execute_via_daemon "$@" 2>/dev/null) if [ -n "$DAEMON_OUTPUT" ]; then echo "$DAEMON_OUTPUT" exit 0 fi [ "$PROVISIONING_DEBUG" = "true" ] && echo "⚠️ Daemon unavailable, using traditional handlers..." >&2 fi # NOTE: Command reordering (server create -> create server) has been removed. # The Nushell dispatcher in provisioning/core/nulib/main_provisioning/dispatcher.nu # handles command routing correctly and expects "server create" format. # The reorder_args function in provisioning script handles any flag reordering needed. fi # ════════════════════════════════════════════════════════════════════════════════ # FAST-PATH: Commands that don't need full config loading or platform bootstrap # These commands use lib_minimal.nu for <100ms execution # (ONLY REACHED if daemon is not available) # ════════════════════════════════════════════════════════════════════════════════ # Help commands (uses help_minimal.nu) if [ -z "$1" ] || [ "$1" = "help" ] || [ "$1" = "-h" ] || [ "$1" = "--help" ] || [ "$1" = "--helpinfo" ]; then category="${2:-}" # Export LANG explicitly to ensure locale detection works in nu subprocess export LANG $NU -n -c "source '$PROVISIONING/core/nulib/help_minimal.nu'; provisioning-help '$category' | print" 2>/dev/null exit $? fi # Workspace operations (fast-path) if [ "$1" = "workspace" ] || [ "$1" = "ws" ]; then case "$2" in "list"|"") $NU -n -c "source '$PROVISIONING/core/nulib/lib_minimal.nu'; workspace-list | table" 2>/dev/null exit $? ;; "active") $NU -n -c "source '$PROVISIONING/core/nulib/lib_minimal.nu'; workspace-active" 2>/dev/null exit $? ;; "info") if [ -n "$3" ]; then $NU -n -c "source '$PROVISIONING/core/nulib/lib_minimal.nu'; workspace-info '$3'" 2>/dev/null else $NU -n -c "source '$PROVISIONING/core/nulib/lib_minimal.nu'; workspace-active | workspace-info \$in" 2>/dev/null fi exit $? ;; esac # Other workspace commands (switch, register, etc.) fall through to full loading fi # Status/Health check (fast-path) if [ "$1" = "status" ] || [ "$1" = "health" ]; then $NU -n -c "source '$PROVISIONING/core/nulib/lib_minimal.nu'; status-quick | table" 2>/dev/null exit $? fi # Environment display (fast-path) if [ "$1" = "env" ] || [ "$1" = "allenv" ]; then $NU -n -c "source '$PROVISIONING/core/nulib/lib_minimal.nu'; env-quick | table" 2>/dev/null exit $? fi # Provider list (lightweight - reads filesystem only, no module loading) if [ "$1" = "provider" ] || [ "$1" = "providers" ]; then if [ "$2" = "list" ] || [ -z "$2" ]; then $NU -n -c " source '$PROVISIONING/core/nulib/lib_minimal.nu' let provisioning = (\$env.PROVISIONING | default '/usr/local/provisioning') let providers_base = (\$provisioning | path join 'extensions' | path join 'providers') if not (\$providers_base | path exists) { print 'PROVIDERS list: (none found)' return } # Discover all providers from directories let all_providers = ( ls \$providers_base | where type == 'dir' | each {|prov_dir| let prov_name = (\$prov_dir.name | path basename) if \$prov_name != 'prov_lib' { {name: \$prov_name, type: 'providers', version: '0.0.1'} } else { null } } | compact ) if (\$all_providers | length) == 0 { print 'PROVIDERS list: (none found)' } else { print 'PROVIDERS list: ' print '' \$all_providers | table } " 2>/dev/null exit $? fi fi # Taskserv list (fast-path) - avoid full system load if [ "$1" = "taskserv" ] || [ "$1" = "task" ]; then if [ "$2" = "list" ] || [ -z "$2" ]; then $NU -n -c " # Direct implementation of taskserv discovery (no dependency loading) # Taskservs are nested: extensions/taskservs/{category}/{name}/kcl/ let provisioning = (\$env.PROVISIONING | default '/usr/local/provisioning') let taskservs_base = (\$provisioning | path join 'extensions' | path join 'taskservs') if not (\$taskservs_base | path exists) { print '📦 Available Taskservs: (none found)' return null } # Discover all taskservs from nested categories let all_taskservs = ( ls \$taskservs_base | where type == 'dir' | each {|cat_dir| let category = (\$cat_dir.name | path basename) let cat_path = (\$taskservs_base | path join \$category) if (\$cat_path | path exists) { ls \$cat_path | where type == 'dir' | each {|ts| let ts_name = (\$ts.name | path basename) {task: \$ts_name, mode: \$category, info: ''} } } else { [] } } | flatten ) if (\$all_taskservs | length) == 0 { print '📦 Available Taskservs: (none found)' } else { print '📦 Available Taskservs:' print '' \$all_taskservs | each {|ts| print \$\" • (\$ts.task) [(\$ts.mode)]\" } | ignore } " 2>/dev/null exit $? fi fi # Server list (lightweight - reads filesystem only, no config loading) if [ "$1" = "server" ] || [ "$1" = "s" ]; then if [ "$2" = "list" ] || [ -z "$2" ]; then # Extract --infra flag from remaining args INFRA_FILTER="" shift [ "$1" = "list" ] && shift while [ $# -gt 0 ]; do case "$1" in --infra|-i) INFRA_FILTER="$2"; shift 2 ;; *) shift ;; esac done $NU -n -c " source '$PROVISIONING/core/nulib/lib_minimal.nu' # Get active workspace let active_ws = (workspace-active) if (\$active_ws | is-empty) { print 'No active workspace' return } # Get workspace path from config let user_config_path = if (\$env.HOME | path exists) { ( \$env.HOME | path join 'Library' | path join 'Application Support' | path join 'provisioning' | path join 'user_config.yaml' ) } else { '' } if not (\$user_config_path | path exists) { print 'Config not found' return } let config = (open \$user_config_path) let workspaces = (\$config | get --optional workspaces | default []) let ws = (\$workspaces | where { \$in.name == \$active_ws } | first) if (\$ws | is-empty) { print 'Workspace not found' return } let ws_path = \$ws.path let infra_path = (\$ws_path | path join 'infra') if not (\$infra_path | path exists) { print 'No infrastructures found' return } # Filter by infrastructure if specified let infra_filter = \"$INFRA_FILTER\" # List server definitions from infrastructure (filtered if --infra specified) let servers = ( ls \$infra_path | where type == 'dir' | each {|infra| let infra_name = (\$infra.name | path basename) # Skip if filter is specified and doesn't match if ((\$infra_filter | is-not-empty) and (\$infra_name != \$infra_filter)) { [] } else { let servers_file = (\$infra_path | path join \$infra_name | path join 'defs' | path join 'servers.k') if (\$servers_file | path exists) { # Parse the KCL servers.k file to extract server names let content = (open \$servers_file --raw) # Extract hostnames from hostname = "..." patterns by splitting on quotes let hostnames = ( \$content | split row \"\\n\" | where {|line| \$line | str contains \"hostname = \\\"\" } | each {|line| # Split by quotes to extract hostname value let parts = (\$line | split row \"\\\"\") if (\$parts | length) >= 2 { \$parts | get 1 } else { \"\" } } | where {|h| (\$h | is-not-empty) } ) \$hostnames | each {|srv_name| { name: \$srv_name infrastructure: \$infra_name path: \$servers_file } } } else { [] } } } | flatten ) if (\$servers | length) == 0 { print '📦 Available Servers: (none configured)' } else { print '📦 Available Servers:' print '' \$servers | each {|srv| print \$\" • (\$srv.name) [(\$srv.infrastructure)]\" } | ignore } " 2>/dev/null exit $? fi fi # Cluster list (lightweight - reads filesystem only) if [ "$1" = "cluster" ] || [ "$1" = "cl" ]; then if [ "$2" = "list" ] || [ -z "$2" ]; then $NU -n -c " source '$PROVISIONING/core/nulib/lib_minimal.nu' # Get active workspace let active_ws = (workspace-active) if (\$active_ws | is-empty) { print 'No active workspace' return } # Get workspace path from config let user_config_path = ( \$env.HOME | path join 'Library' | path join 'Application Support' | path join 'provisioning' | path join 'user_config.yaml' ) if not (\$user_config_path | path exists) { print 'Config not found' return } let config = (open \$user_config_path) let workspaces = (\$config | get --optional workspaces | default []) let ws = (\$workspaces | where { \$in.name == \$active_ws } | first) if (\$ws | is-empty) { print 'Workspace not found' return } let ws_path = \$ws.path # List all clusters from workspace let clusters = ( if ((\$ws_path | path join '.clusters') | path exists) { let clusters_path = (\$ws_path | path join '.clusters') ls \$clusters_path | where type == 'dir' | each {|cl| let cl_name = (\$cl.name | path basename) { name: \$cl_name path: \$cl.name } } } else { [] } ) if (\$clusters | length) == 0 { print '🗂️ Available Clusters: (none found)' } else { print '🗂️ Available Clusters:' print '' \$clusters | each {|cl| print \$\" • (\$cl.name)\" } | ignore } " 2>/dev/null exit $? fi fi # Infra list (lightweight - reads filesystem only) if [ "$1" = "infra" ] || [ "$1" = "inf" ]; then if [ "$2" = "list" ] || [ -z "$2" ]; then $NU -n -c " source '$PROVISIONING/core/nulib/lib_minimal.nu' # Get active workspace let active_ws = (workspace-active) if (\$active_ws | is-empty) { print 'No active workspace' return } # Get workspace path from config let user_config_path = ( \$env.HOME | path join 'Library' | path join 'Application Support' | path join 'provisioning' | path join 'user_config.yaml' ) if not (\$user_config_path | path exists) { print 'Config not found' return } let config = (open \$user_config_path) let workspaces = (\$config | get --optional workspaces | default []) let ws = (\$workspaces | where { \$in.name == \$active_ws } | first) if (\$ws | is-empty) { print 'Workspace not found' return } let ws_path = \$ws.path let infra_path = (\$ws_path | path join 'infra') if not (\$infra_path | path exists) { print '📁 Available Infrastructures: (none configured)' return } # List all infrastructures let infras = ( ls \$infra_path | where type == 'dir' | each {|inf| let inf_name = (\$inf.name | path basename) let inf_full_path = (\$infra_path | path join \$inf_name) let has_config = ((\$inf_full_path | path join 'settings.k') | path exists) { name: \$inf_name configured: \$has_config modified: \$inf.modified } } ) if (\$infras | length) == 0 { print '📁 Available Infrastructures: (none found)' } else { print '📁 Available Infrastructures:' print '' \$infras | each {|inf| let status = if \$inf.configured { '✓' } else { '○' } let output = \" [\" + \$status + \"] \" + \$inf.name print \$output } | ignore } " 2>/dev/null exit $? fi fi # Config validation (lightweight - validates config structure without full load) if [ "$1" = "validate" ]; then if [ "$2" = "config" ] || [ -z "$2" ]; then $NU -n -c " source '$PROVISIONING/core/nulib/lib_minimal.nu' try { # Get active workspace let active_ws = (workspace-active) if (\$active_ws | is-empty) { print '❌ Error: No active workspace' return } # Get workspace path from config let user_config_path = ( \$env.HOME | path join 'Library' | path join 'Application Support' | path join 'provisioning' | path join 'user_config.yaml' ) if not (\$user_config_path | path exists) { print '❌ Error: User config not found at' \$user_config_path return } let config = (open \$user_config_path) let workspaces = (\$config | get --optional workspaces | default []) let ws = (\$workspaces | where { \$in.name == \$active_ws } | first) if (\$ws | is-empty) { print '❌ Error: Workspace' \$active_ws 'not found in config' return } let ws_path = \$ws.path # Validate workspace structure let required_dirs = ['infra', 'config', '.clusters'] let infra_path = (\$ws_path | path join 'infra') let config_path = (\$ws_path | path join 'config') let missing_dirs = \$required_dirs | where { not ((\$ws_path | path join \$in) | path exists) } if (\$missing_dirs | length) > 0 { print '⚠️ Warning: Missing directories:' (\$missing_dirs | str join ', ') } # Validate infrastructures have required files if (\$infra_path | path exists) { let infras = (ls \$infra_path | where type == 'dir') let invalid_infras = ( \$infras | each {|inf| let inf_name = (\$inf.name | path basename) let inf_full_path = (\$infra_path | path join \$inf_name) if not ((\$inf_full_path | path join 'settings.k') | path exists) { \$inf_name } else { null } } | compact ) if (\$invalid_infras | length) > 0 { print '⚠️ Warning: Infrastructures missing settings.k:' (\$invalid_infras | str join ', ') } } # Validate user config structure let has_active = ((\$config | get --optional active_workspace) != null) let has_workspaces = ((\$config | get --optional workspaces) != null) let has_preferences = ((\$config | get --optional preferences) != null) if not \$has_active { print '⚠️ Warning: Missing active_workspace in user config' } if not \$has_workspaces { print '⚠️ Warning: Missing workspaces list in user config' } if not \$has_preferences { print '⚠️ Warning: Missing preferences in user config' } # Summary print '' print '✓ Configuration validation complete for workspace:' \$active_ws print ' Path:' \$ws_path print ' Status: Valid (with warnings, if any listed above)' } catch {|err| print '❌ Validation error:' \$err } " 2>/dev/null exit $? fi fi if [ ! -d "$PROVISIONING_USER_CONFIG" ] || [ ! -r "$PROVISIONING_CONTEXT_PATH" ] ; then [ ! -x "$PROVISIONING/core/nulib/provisioning setup" ] && echo "$PROVISIONING/core/nulib/provisioning setup not found" && exit 1 cd "$PROVISIONING/core/nulib" ./"provisioning setup" echo "" read -p "Use [enter] to continue or [ctrl-c] to cancel" fi [ ! -r "$PROVISIONING_USER_CONFIG/config.nu" ] && echo "$PROVISIONING_USER_CONFIG/config.nu not found" && exit 1 [ ! -r "$PROVISIONING_USER_CONFIG/env.nu" ] && echo "$PROVISIONING_USER_CONFIG/env.nu not found" && exit 1 NU_ARGS=(--config "$PROVISIONING_USER_CONFIG/config.nu" --env-config "$PROVISIONING_USER_CONFIG/env.nu") export PROVISIONING_ARGS="$CMD_ARGS" NU_ARGS="$NU_ARGS" #export NU_ARGS=${NU_ARGS//Application Support/Application\\ Support} # Export NU_LIB_DIRS so Nushell can find modules during parsing export NU_LIB_DIRS="$PROVISIONING/core/nulib:/opt/provisioning/core/nulib:/usr/local/provisioning/core/nulib" # ============================================================================ # DAEMON ROUTING - ENABLED (Phase 3.7: CLI Daemon Integration) # ============================================================================ # Redesigned daemon with pre-loaded Nushell environment (no CLI callback). # Routes eligible commands to HTTP daemon for <100ms execution. # Gracefully falls back to full load if daemon unavailable. # # ARCHITECTURE: # 1. Check daemon health (curl with 5ms timeout) # 2. Route eligible commands to daemon via HTTP POST # 3. Fall back to full load if daemon unavailable # 4. Zero breaking changes (graceful degradation) # # PERFORMANCE: # - With daemon: <100ms for ALL commands # - Without daemon: ~430ms (normal behavior) # - 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 else # Only redirect stdin for non-interactive commands (nu command needs interactive stdin) if [ "$1" = "nu" ]; then # For interactive mode, start nu with provisioning environment export PROVISIONING_CONFIG="$PROVISIONING_USER_CONFIG" # Start nu interactively - it will use the config and env from NU_ARGS $NU "${NU_ARGS[@]}" else # Don't redirect stdin for infrastructure commands - they may need interactive input # Only redirect for commands we know are safe case "$1" in help|h|--help|--info|-i|-v|--version|env|allenv|status|health|list|ls|l|workspace|ws|provider|providers|validate|plugin|plugins|nuinfo) # Safe commands - can use /dev/null $NU "${NU_ARGS[@]}" "$PROVISIONING/core/nulib/$RUNNER" $CMD_ARGS < /dev/null ;; *) # All other commands (create, delete, server, taskserv, etc.) - keep stdin open # NOTE: PROVISIONING_MODULE is automatically inherited by Nushell from bash environment $NU "${NU_ARGS[@]}" "$PROVISIONING/core/nulib/$RUNNER" $CMD_ARGS ;; esac fi fi