prvng_core/cli/provisioning

1164 lines
47 KiB
Bash
Executable file
Raw Blame History

#!/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 <number> - 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 <id> - 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 <number> - 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 <id> - 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)
# ═══<E29590><E29590><EFBFBD>════════════════════════════════════════════════════════════════════════════
# 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 <alias> 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 <number>'
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 <cmd> - 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 <module> 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 </dev/null
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
FIRST_ARG="${1:-}"
# CRITICAL: Handle help/version FIRST (avoid Nushell module loading hang)
case "$FIRST_ARG" in
help | h | --help | -h)
_show_help "${2:-}"
exit 0
;;
version | v | --version | -v | -V)
echo "$PROVISIONING_VERS"
exit 0
;;
about | --info | -i)
echo "Provisioning System v$PROVISIONING_VERS"
exit 0
;;
esac
# Expand single-char and short top-level aliases before validation.
# These map directly to canonical command names so the dispatcher and
# _validate_command see the canonical form.
case "$FIRST_ARG" in
s) FIRST_ARG="server" ;;
t) FIRST_ARG="taskserv" ;;
c) FIRST_ARG="component" ;;
e) FIRST_ARG="extension" ;;
w) FIRST_ARG="workflow" ;;
j) FIRST_ARG="job" ;;
b) FIRST_ARG="batch" ;;
o) FIRST_ARG="orchestrator" ;;
a|al) FIRST_ARG="alias" ;;
esac
# Validate command to prevent hanging on invalid commands
# Uses commands-registry.json cache (pure bash grep, no Nu process).
# This will exit immediately with clear error if:
# 1. Command not found in registry
# 2. Command requires daemon but daemon is not available
_validate_command "$FIRST_ARG"
# Don't redirect stdin for infrastructure commands - they may need interactive input
# Only redirect for commands we know are safe
case "$FIRST_ARG" in
status | health | diagnostics)
$NU "${NU_ARGS[@]}" "$PROVISIONING/core/nulib/provisioning-status.nu" $CMD_ARGS </dev/null
;;
workspace | ws)
$NU "${NU_ARGS[@]}" "$PROVISIONING/core/nulib/main_provisioning/workspace.nu" $CMD_ARGS </dev/null
;;
env | allenv | list | ls | l | provider | providers | validate | plugin | plugins | nuinfo)
$NU "${NU_ARGS[@]}" "$PROVISIONING/core/nulib/provisioning-cli.nu" $CMD_ARGS </dev/null
;;
platform | plat | p)
# logs needs interactive stdin for typedialog — keep stdin open.
# All other platform subcommands use the thin entry (~50ms vs ~3s).
case "${2:-}" in
logs | log)
$NU "${NU_ARGS[@]}" "$PROVISIONING/core/nulib/provisioning-cli.nu" $CMD_ARGS
;;
*)
$NU "${NU_ARGS[@]}" "$PROVISIONING/core/nulib/provisioning-platform.nu" $CMD_ARGS </dev/null
;;
esac
;;
batch | bat)
$NU "${NU_ARGS[@]}" "$PROVISIONING/core/nulib/provisioning-batch.nu" $CMD_ARGS </dev/null
;;
bootstrap)
$NU "${NU_ARGS[@]}" "$PROVISIONING/core/nulib/provisioning-bootstrap.nu" $CMD_ARGS </dev/null
;;
taskserv | task)
$NU "${NU_ARGS[@]}" "$PROVISIONING/core/nulib/provisioning-taskserv.nu" $CMD_ARGS </dev/null
;;
component | comp)
$NU "${NU_ARGS[@]}" "$PROVISIONING/core/nulib/provisioning-component.nu" $CMD_ARGS </dev/null
;;
extension | ext)
$NU "${NU_ARGS[@]}" "$PROVISIONING/core/nulib/provisioning-extension.nu" $CMD_ARGS </dev/null
;;
job)
$NU "${NU_ARGS[@]}" "$PROVISIONING/core/nulib/provisioning-job.nu" $CMD_ARGS </dev/null
;;
workflow | wflow)
$NU "${NU_ARGS[@]}" "$PROVISIONING/core/nulib/provisioning-workflow.nu" $CMD_ARGS </dev/null
;;
alias)
$NU "${NU_ARGS[@]}" "$PROVISIONING/core/nulib/provisioning-cli.nu" $CMD_ARGS </dev/null
;;
create | new)
# "prvng create server ..." → "prvng server create ..."
shift
_resource="${1:-}"
[ -n "$_resource" ] && shift
case "$_resource" in
server|s)
$NU "${NU_ARGS[@]}" "$PROVISIONING/core/nulib/provisioning-server.nu" server create "$@"
exit $? ;;
taskserv|task|t)
$NU "${NU_ARGS[@]}" "$PROVISIONING/core/nulib/provisioning-taskserv.nu" taskserv create "$@" </dev/null
exit $? ;;
cluster|cl)
$NU "${NU_ARGS[@]}" "$PROVISIONING/core/nulib/provisioning-cluster.nu" cluster create "$@" </dev/null
exit $? ;;
*)
$NU "${NU_ARGS[@]}" "$PROVISIONING/core/nulib/provisioning-cli.nu" create "$_resource" "$@"
exit $? ;;
esac
;;
server | s)
# Intercept subcommand --help before Nu absorbs it at the top-level main
_srv_sub="${2:-}"
_has_help=false
for _a in "$@"; do [ "$_a" = "--help" ] || [ "$_a" = "-h" ] && _has_help=true && break; done
if [ "$_has_help" = "true" ]; then
case "$_srv_sub" in
delete|d|del)
$NU "${NU_ARGS[@]}" -c "use '$PROVISIONING/core/nulib/servers/delete.nu' *; main delete --help"
exit $? ;;
create|c)
$NU "${NU_ARGS[@]}" -c "use '$PROVISIONING/core/nulib/servers/create.nu' *; main create --help"
exit $? ;;
list|l)
$NU "${NU_ARGS[@]}" -c "use '$PROVISIONING/core/nulib/servers/list.nu' *; main list --help"
exit $? ;;
ssh)
$NU "${NU_ARGS[@]}" -c "use '$PROVISIONING/core/nulib/servers/ssh.nu' *; main ssh --help"
exit $? ;;
esac
fi
$NU "${NU_ARGS[@]}" "$PROVISIONING/core/nulib/provisioning-server.nu" $CMD_ARGS
;;
ssh)
# Shortcut: provisioning ssh <hostname> → provisioning server ssh <hostname> --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 </dev/null
;;
cluster | cl)
$NU "${NU_ARGS[@]}" "$PROVISIONING/core/nulib/provisioning-cluster.nu" $CMD_ARGS </dev/null
;;
volume | vol)
$NU "${NU_ARGS[@]}" "$PROVISIONING/core/nulib/provisioning-volume.nu" "${@:2}" </dev/null
;;
fip | floating-ip)
$NU "${NU_ARGS[@]}" "$PROVISIONING/core/nulib/main_provisioning/fip.nu" "${@:2}"
;;
*)
# All other commands — provisioning-cli.nu is the single fallback entry.
# stdin kept open for interactive commands (delete, update, etc.).
$NU "${NU_ARGS[@]}" "$PROVISIONING/core/nulib/provisioning-cli.nu" $CMD_ARGS
;;
esac
fi
fi