#!/usr/bin/env bash # Info: Script to run Provisioning # Author: Jesus Perez Lorenzo # Release: 3.0.11 # Date: 2026-01-14 set +o errexit set +o pipefail # Debug: log startup [ "${PROVISIONING_DEBUG_STARTUP:-false}" = "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 # Show provisioning logo/banner by default (can be overridden by env var) export PROVISIONING_NO_TITLES=${PROVISIONING_NO_TITLES:-true} set +o allexport export PROVISIONING=${PROVISIONING:-/usr/local/provisioning} # For development: search upward from script location to find provisioning directory if [ ! -d "$PROVISIONING/resources" ]; then SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" current="$SCRIPT_DIR" # Search up to 5 levels up from script directory for _ in {1..5}; do if [ -d "$current/provisioning/resources" ]; then export PROVISIONING="$current/provisioning" break fi parent="$(dirname "$current")" [ "$parent" = "$current" ] && break # Stop at filesystem root current="$parent" done fi export PROVISIONING_RESOURCES=${PROVISIONING_RESOURCES:-"$PROVISIONING/resources"} PROVIISONING_WKPATH=${PROVIISONING_WKPATH:-/tmp/tmp.} RUNNER="provisioning-cli.nu" PROVISIONING_MODULE="" PROVISIONING_MODULE_TASK="" # Main help function (defined early for early help detection) _show_help() { local category="${1:-}" # If help cache available and fresh, use it for speed if [ -n "$HELP_CACHE_DIR" ] && [ -f "$HELP_CACHE_DIR/main.txt" ]; then local cache_age=$(($(date +%s) - $(stat -f %m "$HELP_CACHE_DIR/main.txt" 2>/dev/null || echo 0))) if [ "$cache_age" -lt "$HELP_CACHE_TTL" ]; then cat "$HELP_CACHE_DIR/main.txt" return 0 fi fi # 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) _workflow_help() { echo "Workflow Management Commands" echo "" echo "Available commands:" echo " l | list - List workflows" echo " s | status - Show workflow status" echo " m | monitor - Monitor workflow progress" echo " st | stats - Show workflow statistics" echo " c | cleanup - Clean up old workflows" echo " b | browse - Browse workflows" echo " o | orchestrator - Show orchestrator health" echo "" echo "Usage:" echo " provisioning workflow [command] [arguments]" echo " provisioning workflow - List with limit" echo "" echo "Examples:" echo " provisioning wf l - List workflows" echo " provisioning wf 5 - List last 5 workflows" echo " provisioning wf st - Show statistics" echo " provisioning wf s - Show status of specific task" } # ════════════════════════════════════════════════════════════════════════════════ # Daemon Routing Helpers - Route operations to provisioning-daemon (port 9095) # ════════════════════════════════════════════════════════════════════════════════ # Get daemon port from user configuration (or default to 9095) # Reads from: ~/.config/provisioning/daemon.conf or PROVISIONING_DAEMON_PORT env var _get_daemon_port() { local port # Priority 1: Environment variable if [ -n "${PROVISIONING_DAEMON_PORT:-}" ]; then echo "$PROVISIONING_DAEMON_PORT" return fi # Priority 2: User config file local config_file="${HOME}/.config/provisioning/daemon.conf" if [ -f "$config_file" ]; then port=$(grep "^DAEMON_PORT=" "$config_file" | cut -d'=' -f2 | tr -d '[:space:]') if [ -n "$port" ]; then echo "$port" return fi fi # Default port echo "9095" } DAEMON_PORT=$(_get_daemon_port) DAEMON_ENDPOINT="http://127.0.0.1:${DAEMON_PORT}" DAEMON_EXECUTE_ENDPOINT="${DAEMON_ENDPOINT}/api/v1/execute" DAEMON_TIMEOUT_FAST="0.5" # Help/quick operations: 500ms DAEMON_TIMEOUT_NORMAL="1.0" # Template rendering: 1s DAEMON_TIMEOUT_BATCH="5.0" # Batch operations: 5s # Cache directory for help and other CLI outputs HELP_CACHE_DIR="${XDG_CACHE_HOME:-${HOME}/.cache}/provisioning/help" HELP_CACHE_TTL=86400 # 24 hours in seconds # ════════════════════════════════════════════════════════════════════════════════ # Help Cache Functions - Instant help output (after first run) # ════════════════════════════════════════════════════════════════════════════════ # Get cache file path for a help category _get_cache_path() { echo "${HELP_CACHE_DIR}/$1.txt" } # Check if cache is valid (not expired) _is_cache_valid() { local cache_file="$1" local now local mtime local age [ ! -f "$cache_file" ] && return 1 now=$(date +%s) mtime=$(stat -f%m "$cache_file" 2>/dev/null || stat -c%Y "$cache_file" 2>/dev/null || echo 0) age=$((now - mtime)) [ $age -lt $HELP_CACHE_TTL ] && return 0 return 1 } # Store help output in cache (handle special characters safely) _cache_help() { local category="$1" local content="$2" mkdir -p "$HELP_CACHE_DIR" # Use printf to safely handle newlines and special characters printf '%s\n' "$content" >"$(_get_cache_path "$category")" } # Get help from cache (if valid) or fetch fresh _get_help_cached() { local category="$1" local cache_file cache_file="$(_get_cache_path "$category")" # Try cache first (instant!) if _is_cache_valid "$cache_file"; then cat "$cache_file" return 0 fi # Cache miss or expired - fetch fresh from daemon or Nushell return 1 } # Try daemon first with timeout, fall back to direct execution # Usage: _route_daemon_or_fallback "command_name" "timeout" "fallback_cmd" _route_daemon_or_fallback() { local cmd_name="$1" local timeout="$2" local fallback_cmd="$3" shift 3 local cmd_args=("$@") local response local json_args if command -v timeout &>/dev/null && command -v curl &>/dev/null; then # Build JSON payload for daemon json_args=$(printf '%s\n' "${cmd_args[@]}" | jq -R . | jq -s .) payload="{\"command\": \"$cmd_name\", \"args\": $json_args}" # Try daemon with timeout response=$(timeout "$timeout" curl -s -m "$timeout" -X POST "$DAEMON_ENDPOINT" \ -H "Content-Type: application/json" \ -d "$payload" 2>/dev/null) if [ -n "$response" ] && [ "$response" != "null" ] && [ "$response" != "{}" ]; then echo "$response" return 0 fi fi # Fallback: execute directly eval "$fallback_cmd" } # Daemon render wrapper for tera templates # Usage: _daemon_render "template_path" "context_json_file" _daemon_render() { local template_path="$1" local context_file="$2" local context local payload context=$(cat "$context_file" 2>/dev/null) payload="{\"command\": \"tera-render\", \"template\": \"$(cat "$template_path")\", \"context\": $context}" if command -v timeout &>/dev/null && command -v curl &>/dev/null; then timeout "$DAEMON_TIMEOUT_NORMAL" curl -s -m "$DAEMON_TIMEOUT_NORMAL" -X POST "$DAEMON_ENDPOINT" \ -H "Content-Type: application/json" \ -d "$payload" 2>/dev/null return $? fi return 1 } # Safe argument handling - use default empty value if unbound [ "${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 # ════════════════════════════════════════════════════════════════════════════════ # EARLY DETECTION - Avoid expensive parsing for no-args and workflow help # ════════════════════════════════════════════════════════════════════════════════ # No arguments at all - show quick usage (don't load Nushell) if [ -z "$1" ]; then echo "Usage: provisioning [command] [options]" echo "" echo "Use 'provisioning help' for available commands" exit 0 fi # Job help detection (before expensive parsing) — "job" is the orchestrator job command case "$1" in job|j) case "$2" in help|-h|--help|-help) _workflow_help exit 0 ;; esac ;; esac # ════════════════════════════════════════════════════════════════════════════════ # FLOW-AWARE TTY COMMAND FILTER # Manages three execution flows: exit (standalone), pipe (inter-command), continue (Nushell) # Registry: provisioning/core/cli/tty-commands.conf # Filter: provisioning/core/cli/tty-filter.sh # ════════════════════════════════════════════════════════════════════════════════ if [ -f "$PROVISIONING/core/cli/tty-filter.sh" ]; then # Source filter function # shellcheck source=/dev/null source "$PROVISIONING/core/cli/tty-filter.sh" # Try to filter TTY command (full command line as single string) # Return codes: # - filter_tty_command returns 0: flow=continue case handled, continue to Nushell with $TTY_OUTPUT # - filter_tty_command exits: flow=exit/pipe case completed (already exited) # - filter returns 1: not a TTY command, continue to normal processing if filter_tty_command "$@"; then # Flow=continue: TTY wrapper executed, output in $TTY_OUTPUT, bypass daemon # $env.PROVISIONING_BYPASS_DAEMON and $env.TTY_OUTPUT available to Nushell : # Continue to Nushell dispatcher below fi fi 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:-false}" = "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" PROVISIONING_USER_PLATFORM="$HOME/.config/provisioning/platform" ;; darwin) PROVISIONING_USER_CONFIG="$HOME/Library/Application Support/provisioning/nushell" PROVISIONING_CONTEXT_PATH="$HOME/Library/Application Support/provisioning/$DEFAULT_CONTEXT_TEMPLATE" PROVISIONING_USER_PLATFORM="$HOME/Library/Application Support/provisioning/platform" ;; *) PROVISIONING_USER_CONFIG="$HOME/.config/provisioning/nushell" PROVISIONING_CONTEXT_PATH="$HOME/.config/provisioning/$DEFAULT_CONTEXT_TEMPLATE" PROVISIONING_USER_PLATFORM="$HOME/.config/provisioning/platform" ;; esac # ════════════════════════════════════════════════════════════════════════════════ # Workflow help function (DRY) - defined early for use in global help handler _workflow_help() { echo "Workflow Management Commands" echo "" echo "Available commands:" echo " l | list - List workflows" echo " s | status - Show workflow status" echo " m | monitor - Monitor workflow progress" echo " st | stats - Show workflow statistics" echo " c | cleanup - Clean up old workflows" echo " b | browse - Browse workflows" echo " o | orchestrator - Show orchestrator health" echo "" echo "Usage:" echo " provisioning workflow [command] [arguments]" echo " provisioning workflow - List with limit" echo "" echo "Examples:" echo " provisioning wf l - List workflows" echo " provisioning wf 5 - List last 5 workflows" echo " provisioning wf st - Show statistics" echo " provisioning wf s - Show status of specific task" } # DAEMON ROUTING - Try daemon for all commands (except setup/help/interactive) # Falls back to traditional handlers if daemon unavailable # ════════════════════════════════════════════════════════════════════════════════ # NOTE: DAEMON_ENDPOINT is already defined above as http://127.0.0.1:9095 # Do NOT redefine it here # Function to execute command via daemon execute_via_daemon() { local cmd="$1" shift local cwd_json local response # 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]" cwd_json=$(printf '%s' "$PWD" | sed 's/\\/\\\\/g; s/"/\\"/g') # 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 response=$(curl -s -m $timeout -X POST "$DAEMON_EXECUTE_ENDPOINT" \ -H "Content-Type: application/json" \ -d "{\"command\":\"$cmd\",\"args\":$args_json,\"cwd\":\"$cwd_json\",\"timeout_ms\":30000}" 2>/dev/null) if [ -z "$response" ] || [ "$response" = "null" ] || [ "$response" = "{}" ]; then return 1 fi if command -v jq >/dev/null 2>&1; then printf '%s' "$response" | jq -r '.stdout // empty' else printf '%s' "$response" | sed -n 's/.*"stdout":"\(.*\)","execution.*/\1/p' | sed 's/\\n/\n/g' fi } # Intercept: server volume → volume (avoids loading full server module) if [ "${1:-}" = "server" ] || [ "${1:-}" = "s" ]; then if [ "${2:-}" = "volume" ] || [ "${2:-}" = "vol" ]; then shift 2 exec "$0" volume "$@" fi fi # Try daemon ONLY for lightweight commands (list, show, status) # Skip daemon for heavy commands (create, delete, update) because bash wrapper is slow # ALSO skip daemon for flow=continue commands (need stdin for TTY interaction) if [ "${PROVISIONING_BYPASS_DAEMON:-}" != "true" ] && [ "${PROVISIONING_NO_DAEMON:-}" != "true" ] && ([ "${1:-}" = "server" ] || [ "${1:-}" = "s" ]); then if [ "${2:-}" = "list" ] || [ "${2:-}" = "ls" ] || [ "${2:-}" = "l" ] || [ -z "${2:-}" ]; then # Light command - try daemon [ -n "${PROVISIONING_DEBUG:-}" ] && [ "${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 [ -n "${PROVISIONING_DEBUG:-}" ] && [ "${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 fast-path (uses help_minimal.nu) # Detects "help" in ANY argument position, not just first # Normalize help category aliases to canonical names _normalize_help_category() { local category="$1" case "$category" in # Infrastructure aliases s | server | infra | i) echo "infrastructure" ;; # Orchestration aliases wf | flow | workflow | orch | orchestrator | bat | batch) echo "orchestration" ;; # Development aliases mod | module | lyr | layer | pack | dev) echo "development" ;; # Workspace aliases ws | workspace | tpl | tmpl | template) echo "workspace" ;; # Platform aliases p | plat | platform) echo "platform" ;; # Setup aliases st | setup | config) echo "setup" ;; # Authentication aliases auth | authentication) echo "authentication" ;; # Plugin aliases plugin | plugins) echo "plugins" ;; # Utilities aliases utils | utilities | cache) echo "utilities" ;; # Diagnostics aliases diag | diagnostics | status | health) echo "diagnostics" ;; # Other categories orchestration | development | workspace | authentication | mfa | plugins | utilities | tools | vm | diagnostics | concepts | guides | integrations | build | infrastructure | setup) echo "$category" ;; # Unknown - return as-is *) echo "$category" ;; esac } help_category="" help_found=false help_subcmd="" # subcommand after the main command (e.g. "delete" in "server delete --help") _pos_count=0 # count of positional (non-flag, non-help) args # Check if first arg is empty (no args provided) - treat as help request if [ -z "${1:-}" ]; then help_found=true else # Loop through all arguments to find help variant and extract category for arg in "$@"; do case "$arg" in help | h | -h | --help | --helpinfo) help_found=true ;; -*) # Skip flags (like -x, -xm, -i, -v, etc.) ;; *) _pos_count=$((_pos_count + 1)) if [ "$help_category" = "" ]; then help_category="$(_normalize_help_category "$arg")" elif [ "$help_subcmd" = "" ]; then help_subcmd="$arg" # second positional = subcommand fi ;; esac done fi # If help was requested for a SUBCOMMAND (e.g. "server delete --help"), # clear help_found so the fast-path is skipped and the Nu module handles --help. if [ "$help_found" = true ] && [ -n "$help_subcmd" ]; then help_found=false fi # Execute help fast-path if help was requested if [ "$help_found" = true ]; then # List of known help categories - if not in this list, let command handle --help case "$help_category" in infrastructure | orchestration | development | workspace | setup | platform | authentication | mfa | plugins | utilities | tools | vm | diagnostics | concepts | guides | integrations | build) # TIER 1: Try local cache first (instant! <1ms) if _get_help_cached "$help_category"; then exit 0 fi # TIER 2: Try daemon next - DISABLED (daemon not critical for help) # The daemon is optional - help can be generated directly via Nushell # TIER 3: Fall back to Nushell (slower ~2-3s) export LANG # Execute Nushell help and capture output HELP_OUTPUT=$($NU -n -c "source '$PROVISIONING/core/nulib/help_minimal.nu'; provisioning-help '$help_category' | print") # Cache the output for next time (if not empty) if [ -n "$HELP_OUTPUT" ]; then _cache_help "$help_category" "$HELP_OUTPUT" echo "$HELP_OUTPUT" exit 0 else # If output is empty, exit gracefully exit 1 fi ;; "") # No category specified - show main help with all categories # TIER 1: Try local cache for main help if _get_help_cached "main"; then exit 0 fi # TIER 2: Try daemon next if command -v timeout &>/dev/null && command -v curl &>/dev/null; then DAEMON_OUTPUT=$(timeout 0.5 curl -s -m 0.5 -X POST "$DAEMON_ENDPOINT" \ -H "Content-Type: application/json" \ -d "{\"command\": \"help\", \"args\": []}" 2>/dev/null) if [ -n "$DAEMON_OUTPUT" ] && [ "$DAEMON_OUTPUT" != "null" ] && [ "$DAEMON_OUTPUT" != "{}" ]; then # Store in cache for next time _cache_help "main" "$DAEMON_OUTPUT" echo "$DAEMON_OUTPUT" exit 0 fi fi # TIER 3: Fall back to Nushell export LANG HELP_OUTPUT=$($NU -n -c "source '$PROVISIONING/core/nulib/help_minimal.nu'; provisioning-help | print") if [ -n "$HELP_OUTPUT" ]; then _cache_help "main" "$HELP_OUTPUT" echo "$HELP_OUTPUT" exit 0 else exit 1 fi ;; *) # Unknown category/command - let the main dispatcher handle it # Don't process help here, just continue to normal flow # The dispatcher will pass --help to the command for handling unset help_found ;; esac fi # ════════════════════════════════════════════════════════════════════════════════ # Commands requiring arguments - Fast-path: serve cached help when run without args # ════════════════════════════════════════════════════════════════════════════════ # Map command to help category (for commands that require arguments) # Get help category from Nickel schema registry _get_help_category_for_command() { local cmd="$1" local schema_file="$PROVISIONING/core/nulib/commands-registry.ncl" if [ ! -f "$schema_file" ]; then return 1 fi # Use external Nushell script for better maintainability $NU "$PROVISIONING/core/nulib/scripts/get-help-category.nu" "$schema_file" "$cmd" 2>/dev/null } # Execute Nushell command with minimal lib (fast-path commands) _nu_minimal() { local nu_command="$1" $NU -n -c "source '$PROVISIONING/core/nulib/lib_minimal.nu'; $nu_command" 2>/dev/null } # Execute Nushell command with full user config (workflow commands) _nu_with_config() { local nu_command="$1" $NU --config "$PROVISIONING_USER_CONFIG/config.nu" --env-config "$PROVISIONING_USER_CONFIG/env.nu" -c "$nu_command" } # Check if first arg is a command that requires arguments and has no second arg if [ -n "${1:-}" ] && [ -z "${2:-}" ]; then help_cat=$(_get_help_category_for_command "${1}") if [ -n "$help_cat" ]; then # Command requires arguments but none provided - serve cached help if _get_help_cached "$help_cat"; then exit 0 fi # Fallback to normal help system if cache miss PROVISIONING_HELP_CATEGORY="$help_cat" export PROVISIONING_HELP_CATEGORY fi fi # workspace fast-path removed (ADR-025 Phase 4 — single-route principle). # All workspace subcommands now route to main_provisioning/workspace.nu via # the main dispatch case. --help still intercepts before full load. if [ "${1:-}" = "workspace" ] || [ "${1:-}" = "ws" ]; then case "${2:-}" in "-help" | "h" | "help") exec "$0" "${1}" --help ;; esac fi # Status/Health check (fast-path) - DISABLED to fix dispatcher loop # Use normal dispatcher path instead of fast-path with lib_minimal.nu # if [ "$1" = "status" ] || [ "$1" = "health" ]; then # $NU -n -c "source '$PROVISIONING/core/nulib/lib_minimal.nu'; status-quick | table" 2>/dev/null # exit $? # fi # env fast-path removed (ADR-025 Phase 4 — single-route principle). # env/allenv now route to the full dispatcher via the *) default case. # Alias list fast-path — reads JSON cache directly in bash, no Nu process if [ "${1:-}" = "alias" ] || [ "${1:-}" = "a" ] || [ "${1:-}" = "al" ]; then _ALIAS_CACHE="${XDG_CACHE_HOME:-$HOME/.cache}/provisioning/commands-registry.json" echo "" echo "ALIASES" echo "════════════════════════════════════════════════════" if [ -f "$_ALIAS_CACHE" ]; then # Single awk pass: extract all command→aliases pairs, then filter by category _alias_table=$(awk ' BEGIN { cmd=""; als=""; in_al=0 } /"command": *"[^"]*"/ { match($0, /"command": *"[^"]*"/) s = substr($0, RSTART, RLENGTH) gsub(/"command": *"|"$/, "", s); gsub(/"/, "", s) cmd = s } /"aliases": *\[/ { in_al=1; als=""; next } in_al && /^ *"[^"]*"/ { match($0, /"[^"]*"/) a = substr($0, RSTART+1, RLENGTH-2) if (a != "") als = als (als==""?"":" ") a } /^ *\]/ && in_al { in_al=0 } /^ *\}/ && cmd != "" && als != "" { print cmd "|" als; cmd=""; als="" } ' "$_ALIAS_CACHE") echo "" echo "INFRASTRUCTURE" echo "$_alias_table" | grep -E "^(server|taskserv|component|extension)\|" | \ awk -F'|' '{ printf " %-14s → %s\n", $2, $1 }' echo "" echo "ORCHESTRATION" echo "$_alias_table" | grep -E "^(job|workflow|batch|orchestrator)\|" | \ awk -F'|' '{ printf " %-14s → %s\n", $2, $1 }' echo "" echo "OTHER" echo "$_alias_table" | grep -E "^(alias|workspace|platform|build|validate|help)\|" | \ awk -F'|' '{ printf " %-14s → %s\n", $2, $1 }' unset _alias_table else echo "" echo " s → server" echo " t task → taskserv" echo " c comp → component" echo " e ext → extension" echo " w wflow → workflow" echo " j → job" echo " b bat → batch" echo " o orch → orchestrator" echo " a al → alias" fi echo "" echo "════════════════════════════════════════════════════" echo "Tip: prvng help → subcommand details" echo "" exit 0 fi # Job commands fast-path (orchestrator jobs — was "workflow") if [ "${1:-}" = "job" ] || [ "${1:-}" = "j" ]; then WORKFLOW_CMD="${2:-list}" ARG="${3:-}" # Handle help commands (matches -h, -help, h, ?) case "$WORKFLOW_CMD" in -h|-help|h|\?) _workflow_help exit 0 ;; esac # Expand short command aliases case "$WORKFLOW_CMD" in l) WORKFLOW_CMD="list" ;; s) WORKFLOW_CMD="status" ;; m) WORKFLOW_CMD="monitor" ;; st) WORKFLOW_CMD="stats" ;; b) WORKFLOW_CMD="browse" ;; c) WORKFLOW_CMD="cleanup" ;; o) WORKFLOW_CMD="orchestrator" ;; help) WORKFLOW_CMD="h" ;; -help) WORKFLOW_CMD="h" ;; --help) WORKFLOW_CMD="h" ;; esac # If WORKFLOW_CMD is a number, treat it as 'list ' if [ -n "$WORKFLOW_CMD" ] && [ "$WORKFLOW_CMD" -ge 0 ] 2>/dev/null; then ARG="$WORKFLOW_CMD" WORKFLOW_CMD="list" fi # Use minimal config for quick execution case "$WORKFLOW_CMD" in list) # Note: No < /dev/null here to allow interactive typedialog if [ -z "$ARG" ]; then _nu_with_config "use workflows/management.nu *; workflow list" else _nu_with_config "use workflows/management.nu *; workflow list $ARG" fi exit $? ;; status) if [ -z "$ARG" ]; then echo "❌ Error: workflow status requires a task ID" exit 1 fi _nu_with_config "use workflows/management.nu *; workflow status '$ARG'" exit $? ;; monitor) if [ -z "$ARG" ]; then echo "❌ Error: workflow monitor requires a task ID" exit 1 fi _nu_with_config "use workflows/management.nu *; workflow monitor '$ARG'" exit $? ;; stats) _nu_with_config "use workflows/management.nu *; workflow stats" exit $? ;; *) echo "❌ Error: unknown workflow command '$WORKFLOW_CMD'" echo "" _workflow_help exit 1 ;; esac fi # provider fast-path removed (ADR-025 Phase 4 — single-route principle). # Falls through to main dispatch case. # Fast-paths removed (ADR-025 Phase 4 — single-route principle). # taskserv/server/cluster `list` now route to their thin handlers which invoke # the full semantic path (middleware + live provider state). Daemon routing # (for server list/ls/l) is preserved further down in the dispatch case. # infra fast-path removed (ADR-025 Phase 4 — single-route principle). # Falls through to main dispatch case. Help with no args still shows help menu. if [ "${1:-}" = "infra" ] || [ "${1:-}" = "inf" ]; then if [ -z "${2:-}" ]; then provisioning help infrastructure exit 0 fi fi # validate fast-path removed (ADR-025 Phase 4 — single-route principle). # Falls through to main dispatch case. 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} # Suppress repetitive config export output during initialization export PROVISIONING_QUIET_EXPORT="true" # 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" # Export NICKEL_IMPORT_PATH so all nickel invocations resolve schemas/ and extensions/ without --import-path per call export NICKEL_IMPORT_PATH="$PROVISIONING" # ============================================================================ # COMMAND VALIDATION - Fast-fail for invalid commands + daemon check # ============================================================================ # Read command-registry.txt and validate commands BEFORE invoking Nushell. # This prevents hanging on invalid commands (like "prvng ps"). # # Registry format: command|aliases|requires_daemon|requires_services|uses_cache|description # Validation checks: # 1. Command exists in registry (command or alias) # 2. If requires_daemon=true, verify daemon is listening on port # Fail-fast: Exit immediately with clear error if validation fails # _validate_command() { local cmd="$1" local registry_file="$PROVISIONING/core/nulib/commands-registry.ncl" # Skip validation for empty command or help flags if [ -z "$cmd" ] || [[ "$cmd" =~ ^(--help|--info|-i|-v|--version|-h|-V)$ ]]; then return 0 fi # Check if Nickel registry exists if [ ! -f "$registry_file" ]; then echo "ERROR: commands-registry.ncl not found at $registry_file" >&2 return 1 fi # Cache: ~/.cache/provisioning/commands-registry.json # Rebuilt via nickel export only when registry source changes (mtime check). # Validated in pure bash using grep — no Nu process launched for validation. local cache_dir="${XDG_CACHE_HOME:-$HOME/.cache}/provisioning" local cache_file="$cache_dir/commands-registry.json" # Rebuild cache if stale or missing if [ ! -f "$cache_file" ] || [ "$registry_file" -nt "$cache_file" ]; then mkdir -p "$cache_dir" nickel export --format json --import-path "$PROVISIONING" "$registry_file" \ > "$cache_file" 2>/dev/null || rm -f "$cache_file" fi local found=false local requires_daemon=false if [ -f "$cache_file" ]; then # Pure bash grep: find the entry whose "command" or "aliases" contains $cmd. # Extract all command names and alias values as a line-per-name list, then check. local all_names all_names=$(grep -o '"[a-zA-Z0-9_\-\+\.]*"' "$cache_file" | tr -d '"') if echo "$all_names" | grep -qx "$cmd"; then found=true # Check requires_daemon for this specific command block. # Strategy: find the block containing our cmd, check its requires_daemon value. # Simple grep: look for "requires_daemon": true in the same JSON object as $cmd. # We extract the 30-line window around the match and check for requires_daemon true. local window window=$(grep -n "\"$cmd\"" "$cache_file" | head -1 | cut -d: -f1) if [ -n "$window" ]; then local block block=$(sed -n "$((window > 10 ? window - 10 : 1)),$((window + 15))p" "$cache_file") if echo "$block" | grep -q '"requires_daemon": *true'; then requires_daemon=true fi fi else found=false fi else # No cache and nickel failed — fall back to Nu script (slow, one-time) local validate_script="$PROVISIONING/core/nulib/scripts/validate-command.nu" local query_result query_result=$($NU -n "$validate_script" "$cmd" 2>&1) if [[ "$query_result" == "NOT_FOUND" ]]; then found=false elif [[ "$query_result" =~ ^FOUND\|(true|false)$ ]]; then found=true requires_daemon="${BASH_REMATCH[1]}" fi fi # ERROR 1: Command not found in registry if [ "$found" = "false" ]; then echo "" >&2 echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" >&2 echo "❌ Unknown command: $cmd" >&2 echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" >&2 echo "" >&2 echo "This command is not recognized by the provisioning system." >&2 echo "" >&2 echo "To see available commands:" >&2 echo " provisioning help" >&2 echo " prvng help # short alias" >&2 echo "" >&2 echo "Common commands:" >&2 echo " provisioning help - Show help" >&2 echo " provisioning platform - Manage platform services" >&2 echo " provisioning workspace - Workspace management" >&2 echo " provisioning create - Create resources" >&2 echo "" >&2 exit 1 fi # ERROR 2: Command requires daemon but daemon is not available if [ "$requires_daemon" = "true" ]; then # Check if daemon is listening on port (using lsof) if ! lsof -i :"$DAEMON_PORT" -P -n 2>/dev/null | grep -q LISTEN; then echo "" >&2 echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" >&2 echo "❌ CRITICAL: provisioning_daemon not available" >&2 echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" >&2 echo "" >&2 echo "The provisioning daemon is required for operation: $cmd" >&2 echo "Daemon is not listening on port $DAEMON_PORT" >&2 echo "" >&2 echo "The daemon is a CRITICAL component - all operations require it." >&2 echo "" >&2 echo "To check daemon status:" >&2 echo " provisioning platform status" >&2 echo " prvng plat st # short alias" >&2 echo "" >&2 echo "To start the daemon:" >&2 echo " provisioning platform start provisioning_daemon" >&2 echo " prvng plat start provisioning_daemon # short alias" >&2 echo "" >&2 echo "Allowed operations without daemon:" >&2 echo " • help / -h / --help - View help" >&2 echo " • platform - Manage platform services" >&2 echo " • setup - Initial setup" >&2 echo "" >&2 exit 1 fi fi return 0 } # ============================================================================ # 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 # -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 → provisioning server ssh --run shift $NU "${NU_ARGS[@]}" "$PROVISIONING/core/nulib/provisioning-server.nu" server ssh "$@" --run ;; state | st) $NU "${NU_ARGS[@]}" "$PROVISIONING/core/nulib/provisioning-state.nu" $CMD_ARGS