#!/bin/bash set -euo pipefail # Tree Runner - Execute cookbook trees with checkpoint support # # Usage: # ./tree-runner.sh run [--var-name value]... [--dry-run] # ./tree-runner.sh resume # ./tree-runner.sh only # ./tree-runner.sh status # ./tree-runner.sh teardown # ./tree-runner.sh list # ./tree-runner.sh clean # # Flags: # --dry-run Validate tree and show execution plan without running # --auto-teardown Run teardown on exit (success or failure) # # Examples: # ./tree-runner.sh run landing-page --project-name my-test # ./tree-runner.sh run landing-page --project-name test --dry-run # ./tree-runner.sh resume landing-page # ./tree-runner.sh only landing-page wait-pipeline # ./tree-runner.sh status landing-page # ./tree-runner.sh teardown landing-page # ./tree-runner.sh list # ./tree-runner.sh clean landing-page SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" # Source dependencies source "$SCRIPT_DIR/common.sh" source "$SCRIPT_DIR/lib/checkpoint.sh" source "$SCRIPT_DIR/lib/tree-parser.sh" # Parse global flags from args DRY_RUN="false" ARGS=("$@") for i in "${!ARGS[@]}"; do case "${ARGS[$i]}" in --auto-teardown) AUTO_TEARDOWN="true" unset 'ARGS[$i]' ;; --dry-run) DRY_RUN="true" unset 'ARGS[$i]' ;; esac done ARGS=("${ARGS[@]}") # Re-index array set -- "${ARGS[@]}" # Reset positional params # Parse command COMMAND="${1:-}" if [[ -z "$COMMAND" ]]; then echo "Tree Runner - Execute cookbook trees with checkpoint support" echo "" echo "Usage: $0 [args]" echo "" echo "Commands:" echo " run [--var-name value]... Run a tree from the beginning" echo " resume Resume from last checkpoint" echo " only Run only a specific step" echo " status Show checkpoint status" echo " teardown Run tree's teardown steps" echo " list List available trees" echo " clean Delete checkpoint for a tree" echo "" echo "Global Flags:" echo " --dry-run Validate and show execution plan without running" echo " --auto-teardown Run teardown on exit (success or failure)" echo "" echo "Examples:" echo " $0 run landing-page --project-name my-test-\$(date +%s)" echo " $0 run landing-page --project-name test --dry-run" echo " $0 resume landing-page" echo " $0 only landing-page wait-pipeline" echo " $0 status landing-page" echo " $0 teardown landing-page" echo " $0 list" echo " $0 clean landing-page" echo "" exit 1 fi shift # ============================================================================ # Step Executors # ============================================================================ # Execute an API step # Arguments: step_json # Returns: response JSON execute_api_step() { local step_json="$1" local method endpoint body method=$(echo "$step_json" | jq -r '.method // "GET"') endpoint=$(echo "$step_json" | jq -r '.endpoint') body=$(echo "$step_json" | jq -c '.body // null') local response if [[ "$body" != "null" ]]; then response=$(api_call "$method" "$endpoint" "$body") else response=$(api_call "$method" "$endpoint") fi echo "$response" } # Execute a wait_pipeline step # Arguments: step_json # Returns: 0 on success, 1 on failure execute_wait_pipeline_step() { local step_json="$1" local project_id max_attempts poll_interval project_id=$(echo "$step_json" | jq -r '.project_id') max_attempts=$(echo "$step_json" | jq -r '.max_attempts // 120') poll_interval=$(echo "$step_json" | jq -r '.poll_interval // 5') wait_for_pipeline "$project_id" "$max_attempts" "$poll_interval" } # Execute a wait_site step # Arguments: step_json # Returns: 0 on success, 1 on failure execute_wait_site_step() { local step_json="$1" local domain project_id max_attempts poll_interval domain=$(echo "$step_json" | jq -r '.domain') project_id=$(echo "$step_json" | jq -r '.project_id // ""') max_attempts=$(echo "$step_json" | jq -r '.max_attempts // 30') poll_interval=$(echo "$step_json" | jq -r '.poll_interval // 5') wait_for_site "$domain" "$max_attempts" "$poll_interval" "$project_id" } # Execute a wait_build step # Arguments: step_json # Returns: 0 on success, 1 on failure, 2 on timeout execute_wait_build_step() { local step_json="$1" local build_id max_attempts poll_interval build_id=$(echo "$step_json" | jq -r '.build_id') max_attempts=$(echo "$step_json" | jq -r '.max_attempts // 120') poll_interval=$(echo "$step_json" | jq -r '.poll_interval // 5') if [[ -z "$build_id" || "$build_id" == "null" ]]; then print_error "wait_build: build_id is required" return 1 fi wait_for_build "$build_id" "$max_attempts" "$poll_interval" } # Execute a diagnose step # Arguments: step_json execute_diagnose_step() { local step_json="$1" local diag_type project_id domain diag_type=$(echo "$step_json" | jq -r '.type // "pipeline"') project_id=$(echo "$step_json" | jq -r '.project_id // ""') domain=$(echo "$step_json" | jq -r '.domain // ""') case "$diag_type" in pipeline) diagnose_pipeline_failure "$project_id" ;; site) diagnose_site_failure "$domain" "$project_id" ;; *) print_error "Unknown diagnose type: $diag_type" return 1 ;; esac } # Execute a shell step # Arguments: step_json # Returns: command output execute_shell_step() { local step_json="$1" local command command=$(echo "$step_json" | jq -r '.command') # Use bash -c instead of eval to run command in a subshell # This is safer than eval and still allows shell features bash -c "$command" } # Extract outputs from response # Arguments: response_json output_rules_json # Returns: JSON object of extracted outputs extract_outputs() { local response="$1" local output_rules="$2" if [[ "$output_rules" == "[]" || -z "$output_rules" ]]; then echo "{}" return fi # Extract each output using jq local outputs="{}" while IFS= read -r rule; do local name jq_path value name=$(echo "$rule" | jq -r '.name') jq_path=$(echo "$rule" | jq -r '.jq_path') value=$(echo "$response" | jq -r "$jq_path // \"\"") outputs=$(echo "$outputs" | jq --arg k "$name" --arg v "$value" '. + {($k): $v}') done < <(echo "$output_rules" | jq -c '.[]') echo "$outputs" } # Execute a single step # Arguments: tree_name step_name vars_json outputs_json # Returns: 0 on success, 1 on failure # Updates outputs_json with new outputs execute_step() { local tree_name="$1" local step_name="$2" local vars_json="$3" local outputs_json="$4" # Get and expand step definition local step_raw step_raw=$(tree_get_step "$tree_name" "$step_name") || return 1 local step step=$(tree_expand_step "$step_raw" "$vars_json" "$outputs_json") local action description on_error action=$(tree_step_action "$step") description=$(echo "$step" | jq -r '.description // ""') on_error=$(tree_step_on_error "$step") # Print step header (to stderr so it doesn't mix with JSON output) if [[ -n "$description" ]]; then echo -e "${CYAN}Step: $step_name${NC} - $description" >&2 else echo -e "${CYAN}Step: $step_name${NC}" >&2 fi # Mark step as started checkpoint_step_start "$tree_name" "$step_name" local response="" local step_failed=0 case "$action" in api) response=$(execute_api_step "$step") || step_failed=1 if [[ $step_failed -eq 0 ]]; then # Check for error in response local error error=$(echo "$response" | jq -r '.error // ""') if [[ -n "$error" && "$error" != "null" ]]; then print_error "API error: $error" >&2 step_failed=1 fi fi ;; wait_pipeline) # Redirect status output to stderr so it doesn't pollute JSON return execute_wait_pipeline_step "$step" >&2 || step_failed=1 response="{}" ;; wait_site) # Redirect status output to stderr so it doesn't pollute JSON return execute_wait_site_step "$step" >&2 || step_failed=1 response="{}" ;; wait_build) # Redirect status output to stderr so it doesn't pollute JSON return execute_wait_build_step "$step" >&2 || step_failed=1 response="{}" ;; diagnose) execute_diagnose_step "$step" >&2 response="{}" ;; shell) response=$(execute_shell_step "$step") || step_failed=1 # Try to parse as JSON, otherwise wrap in object if ! echo "$response" | jq -e '.' > /dev/null 2>&1; then response=$(jq -n --arg out "$response" '{output: $out}') fi ;; *) print_error "Unknown action type: $action" >&2 checkpoint_step_fail "$tree_name" "$step_name" "Unknown action: $action" return 1 ;; esac if [[ $step_failed -eq 1 ]]; then checkpoint_step_fail "$tree_name" "$step_name" "Step failed" if [[ "$on_error" == "continue" ]]; then print_warning "Step failed but continuing (on_error: continue)" >&2 checkpoint_step_complete "$tree_name" "$step_name" "{}" echo "{}" # Return empty outputs for caller to merge return 0 fi return 1 fi # Extract outputs if defined local output_rules output_rules=$(tree_step_outputs "$step") local step_outputs step_outputs=$(extract_outputs "$response" "$output_rules") # Save outputs to checkpoint checkpoint_step_complete "$tree_name" "$step_name" "$step_outputs" # Return outputs for use by subsequent steps (this is the only stdout) echo "$step_outputs" print_success "Step completed: $step_name" >&2 return 0 } # Build outputs JSON from checkpoint # Arguments: tree_name # Returns: outputs JSON object build_outputs_from_checkpoint() { local tree_name="$1" local checkpoint checkpoint=$(checkpoint_load "$tree_name") || { echo "{}" return } echo "$checkpoint" | jq ' .steps | to_entries | map(select(.value.status == "completed")) | map({key: .key, value: .value.output}) | from_entries ' } # ============================================================================ # Commands # ============================================================================ # Dry-run: validate tree and show execution plan without running # Arguments: tree_name vars_json cmd_dryrun() { local tree_name="$1" local vars_json="$2" print_header "Dry Run: $tree_name" echo -e "${CYAN}This is a preview. No actions will be taken.${NC}" echo "" # Show tree metadata local meta meta=$(tree_get_meta "$tree_name") echo "Tree: $(echo "$meta" | jq -r '.name')" echo "Description: $(echo "$meta" | jq -r '.description // "No description"')" echo "Version: $(echo "$meta" | jq -r '.version // 1')" echo "" # Show variables echo "Variables:" echo "$vars_json" | jq -r 'to_entries | .[] | " \(.key): \(.value)"' echo "" # Get execution order local execution_order execution_order=$(tree_execution_order "$tree_name") echo "Execution Plan:" local step_num=0 while IFS= read -r step_name; do ((step_num++)) # Get step details - use temp file approach to avoid bash variable corruption local tmpfile tmpfile=$(mktemp) tree_parse "$tree_name" > "$tmpfile" 2>/dev/null local step_json step_json=$(jq --arg step "$step_name" '.steps[$step]' "$tmpfile") rm -f "$tmpfile" local action description deps action=$(echo "$step_json" | jq -r '.action // "unknown"') description=$(echo "$step_json" | jq -r '.description // ""') deps=$(echo "$step_json" | jq -r '(.depends_on // []) | join(", ")') # Format action type with color local action_color case "$action" in api) action_color="${GREEN}api${NC}" ;; shell) action_color="${YELLOW}shell${NC}" ;; wait_pipeline|wait_site|wait_build) action_color="${BLUE}wait${NC}" ;; diagnose) action_color="${RED}diagnose${NC}" ;; *) action_color="$action" ;; esac echo -e " ${step_num}. ${CYAN}$step_name${NC} [$action_color]" if [[ -n "$description" ]]; then echo " $description" fi if [[ -n "$deps" ]]; then echo " depends_on: $deps" fi # Show details for specific action types case "$action" in api) local method endpoint method=$(echo "$step_json" | jq -r '.method // "GET"') endpoint=$(echo "$step_json" | jq -r '.endpoint') echo " → $method $endpoint" ;; shell) local cmd_preview cmd_preview=$(echo "$step_json" | jq -r '.command' | head -1 | cut -c1-60) if [[ ${#cmd_preview} -eq 60 ]]; then cmd_preview="${cmd_preview}..." fi echo " → $cmd_preview" ;; wait_pipeline) echo " → Wait for CI pipeline to complete" ;; wait_site) local domain domain=$(echo "$step_json" | jq -r '.domain // "N/A"') echo " → Wait for https://$domain" ;; wait_build) local build_id_tmpl max_attempts build_id_tmpl=$(echo "$step_json" | jq -r '.build_id // "N/A"') max_attempts=$(echo "$step_json" | jq -r '.max_attempts // 120') echo " → Wait for build $build_id_tmpl (max ${max_attempts} attempts)" ;; esac echo "" done <<< "$execution_order" # Show teardown steps local teardown teardown=$(tree_get_teardown "$tree_name") local teardown_count teardown_count=$(echo "$teardown" | jq 'length') if [[ "$teardown_count" -gt 0 ]]; then echo "Teardown Steps: ($teardown_count steps)" echo "$teardown" | jq -r '.[] | " - \(.action): \(.description // .endpoint // "cleanup")"' echo "" fi print_success "Dry run complete. Tree is valid and ready to execute." echo "" echo "To run for real:" echo " $0 run $tree_name $(echo "$vars_json" | jq -r 'to_entries | map("--\(.key | gsub("_"; "-")) \(.value)") | join(" ")')" } # Auto-teardown handler for tree runner # Called on exit when AUTO_TEARDOWN=true tree_auto_teardown() { local exit_code=$? if [[ -n "$TREE_AUTO_TEARDOWN_NAME" && "$AUTO_TEARDOWN" == "true" ]]; then echo "" echo -e "${CYAN}Auto-teardown: Running teardown for $TREE_AUTO_TEARDOWN_NAME...${NC}" cmd_teardown "$TREE_AUTO_TEARDOWN_NAME" || true fi exit $exit_code } # Track tree name for auto-teardown (set during cmd_run) TREE_AUTO_TEARDOWN_NAME="" # Pre-flight checks before tree execution # Returns: 0 if all checks pass, 1 with error messages if not preflight_check() { local errors=() # Check required environment variables if [[ -z "${RDEV_API_URL:-}" ]]; then errors+=("RDEV_API_URL environment variable is not set") fi if [[ -z "${RDEV_API_KEY:-}" ]]; then errors+=("RDEV_API_KEY environment variable is not set") fi # Check required tools if ! command -v yq &> /dev/null; then errors+=("yq is not installed (brew install yq)") fi if ! command -v jq &> /dev/null; then errors+=("jq is not installed (brew install jq)") fi if ! command -v curl &> /dev/null; then errors+=("curl is not installed") fi # Check API reachability (quick health check, only if env vars are set) if [[ -n "${RDEV_API_URL:-}" && -n "${RDEV_API_KEY:-}" ]]; then local health_response health_response=$(curl -s --max-time 5 "$RDEV_API_URL/health" -H "X-API-Key: $RDEV_API_KEY" 2>/dev/null || echo '{"error":"unreachable"}') if echo "$health_response" | jq -e '.error' > /dev/null 2>&1; then local error_msg error_msg=$(echo "$health_response" | jq -r '.error // "API unreachable"') errors+=("API health check failed: $error_msg (check RDEV_API_URL: $RDEV_API_URL)") fi fi # Report errors if [[ ${#errors[@]} -gt 0 ]]; then echo -e "${RED}Pre-flight checks failed:${NC}" >&2 for err in "${errors[@]}"; do echo " ✗ $err" >&2 done echo "" >&2 echo "Fix these issues before running trees." >&2 return 1 fi return 0 } # Run a tree from the beginning cmd_run() { local tree_name="${1:-}" shift || true if [[ -z "$tree_name" ]]; then echo "Usage: $0 run [--var-name value]..." exit 1 fi # Run pre-flight checks if ! preflight_check; then exit 1 fi # Register auto-teardown trap TREE_AUTO_TEARDOWN_NAME="$tree_name" trap tree_auto_teardown EXIT INT TERM # Validate tree exists if ! tree_parse "$tree_name" > /dev/null 2>&1; then print_error "Tree '$tree_name' not found" echo "" echo "Available trees:" tree_list_detail exit 1 fi # Validate tree structure if ! tree_validate "$tree_name"; then print_error "Tree '$tree_name' has validation errors (see above)" exit 1 fi # Parse variables from args local vars_json vars_json=$(tree_get_default_vars "$tree_name") while [[ $# -gt 0 ]]; do case "$1" in --*) local var_name="${1#--}" var_name="${var_name//-/_}" # Convert dashes to underscores local var_value="${2:-}" if [[ -z "$var_value" ]]; then print_error "Missing value for --$var_name" exit 1 fi vars_json=$(echo "$vars_json" | jq --arg k "$var_name" --arg v "$var_value" '. + {($k): $v}') shift 2 ;; *) print_error "Unknown argument: $1" exit 1 ;; esac done # Check required vars (empty string values) - skip for dry-run to allow preview with placeholders local missing_vars missing_vars=$(echo "$vars_json" | jq -r 'to_entries | .[] | select(.value == "") | .key') if [[ -n "$missing_vars" && "$DRY_RUN" != "true" ]]; then print_error "Missing required variables:" echo "$missing_vars" | sed 's/^/ --/' exit 1 fi # Handle dry-run mode if [[ "$DRY_RUN" == "true" ]]; then cmd_dryrun "$tree_name" "$vars_json" exit 0 fi # Initialize checkpoint local run_id run_id=$(checkpoint_init "$tree_name" "$vars_json") print_header "Running tree: $tree_name" echo "Run ID: $run_id" echo "Variables:" echo "$vars_json" | jq -r 'to_entries | .[] | " \(.key): \(.value)"' echo "" # Get execution order local execution_order execution_order=$(tree_execution_order "$tree_name") # Build outputs as we go local outputs_json="{}" # Execute steps in order while IFS= read -r step_name; do local step_outputs if step_outputs=$(execute_step "$tree_name" "$step_name" "$vars_json" "$outputs_json"); then # Merge step outputs into cumulative outputs outputs_json=$(echo "$outputs_json" | jq --arg step "$step_name" --argjson out "$step_outputs" '. + {($step): $out}') else print_error "Tree execution failed at step: $step_name" echo "" echo "To resume from this point:" echo " $0 resume $tree_name" echo "" echo "To run just this step:" echo " $0 only $tree_name $step_name" echo "" echo "To teardown resources:" echo " $0 teardown $tree_name" exit 1 fi echo "" done <<< "$execution_order" # Mark as completed checkpoint_mark_completed "$tree_name" print_header "Tree completed successfully!" echo "Run ID: $run_id" echo "" echo "To view final state:" echo " $0 status $tree_name" echo "" echo "To teardown resources:" echo " $0 teardown $tree_name" } # Resume from last checkpoint cmd_resume() { local tree_name="${1:-}" if [[ -z "$tree_name" ]]; then echo "Usage: $0 resume " exit 1 fi # Load checkpoint local checkpoint if ! checkpoint=$(checkpoint_load "$tree_name"); then print_error "No checkpoint found for tree: $tree_name" echo "" echo "Run the tree first:" echo " $0 run $tree_name [--var-name value]..." exit 1 fi local status status=$(echo "$checkpoint" | jq -r '.status') if [[ "$status" == "completed" ]]; then print_success "Tree already completed. Nothing to resume." echo "" echo "To run again:" echo " $0 clean $tree_name && $0 run $tree_name ..." exit 0 fi print_header "Resuming tree: $tree_name" echo "Status: $status" # Get vars from checkpoint local vars_json vars_json=$(echo "$checkpoint" | jq '.vars') # Build outputs from completed steps local outputs_json outputs_json=$(build_outputs_from_checkpoint "$tree_name") # Get completed steps local completed_steps completed_steps=$(checkpoint_completed_steps "$tree_name") echo "Completed steps:" if [[ -n "$completed_steps" ]]; then echo "$completed_steps" | sed 's/^/ ✓ /' else echo " (none)" fi echo "" # Get execution order local execution_order execution_order=$(tree_execution_order "$tree_name") # Execute remaining steps while IFS= read -r step_name; do # Skip completed steps if echo "$completed_steps" | grep -q "^${step_name}$"; then continue fi local step_outputs if step_outputs=$(execute_step "$tree_name" "$step_name" "$vars_json" "$outputs_json"); then outputs_json=$(echo "$outputs_json" | jq --arg step "$step_name" --argjson out "$step_outputs" '. + {($step): $out}') else print_error "Tree execution failed at step: $step_name" echo "" echo "To resume again:" echo " $0 resume $tree_name" exit 1 fi echo "" done <<< "$execution_order" checkpoint_mark_completed "$tree_name" print_header "Tree completed successfully!" } # Run only a specific step cmd_only() { local tree_name="${1:-}" local step_name="${2:-}" if [[ -z "$tree_name" || -z "$step_name" ]]; then echo "Usage: $0 only " exit 1 fi # Validate step exists if ! tree_get_step "$tree_name" "$step_name" > /dev/null 2>&1; then print_error "Step '$step_name' not found in tree '$tree_name'" echo "" echo "Available steps:" tree_get_steps "$tree_name" | sed 's/^/ /' exit 1 fi # Load checkpoint (or use empty state) local vars_json="{}" local outputs_json="{}" if checkpoint=$(checkpoint_load "$tree_name" 2>/dev/null); then vars_json=$(echo "$checkpoint" | jq '.vars') outputs_json=$(build_outputs_from_checkpoint "$tree_name") fi print_header "Running single step: $step_name" echo "Tree: $tree_name" echo "" local step_outputs if step_outputs=$(execute_step "$tree_name" "$step_name" "$vars_json" "$outputs_json"); then print_success "Step completed" echo "" echo "Outputs:" echo "$step_outputs" | jq '.' else print_error "Step failed" exit 1 fi } # Show checkpoint status cmd_status() { local tree_name="${1:-}" if [[ -z "$tree_name" ]]; then echo "Usage: $0 status " exit 1 fi checkpoint_status_detail "$tree_name" } # Run teardown steps cmd_teardown() { local tree_name="${1:-}" if [[ -z "$tree_name" ]]; then echo "Usage: $0 teardown " exit 1 fi print_header "Teardown: $tree_name" # Load checkpoint for outputs local vars_json="{}" local outputs_json="{}" if checkpoint=$(checkpoint_load "$tree_name" 2>/dev/null); then vars_json=$(echo "$checkpoint" | jq '.vars') outputs_json=$(build_outputs_from_checkpoint "$tree_name") else print_warning "No checkpoint found - teardown may not have all variables" fi # Get teardown steps local teardown_steps teardown_steps=$(tree_get_teardown "$tree_name") local teardown_count teardown_count=$(echo "$teardown_steps" | jq 'length') if [[ "$teardown_count" -eq 0 ]]; then echo "No teardown steps defined for this tree." return 0 fi echo "Running $teardown_count teardown steps..." echo "" # Execute teardown steps local i=0 while IFS= read -r step_json; do ((i++)) # Expand templates local step step=$(tree_expand_step "$step_json" "$vars_json" "$outputs_json") local action description action=$(echo "$step" | jq -r '.action // "unknown"') description=$(echo "$step" | jq -r ".description // \"Teardown step $i\"") echo -e "${CYAN}Teardown $i:${NC} $description" local response="" case "$action" in api) if response=$(execute_api_step "$step" 2>/dev/null); then # Check for error in response body local api_error api_error=$(echo "$response" | jq -r '.error // empty' 2>/dev/null) if [[ -n "$api_error" ]]; then print_warning "API error (continuing): $api_error" else local api_status api_status=$(echo "$response" | jq -r '.data.status // "ok"' 2>/dev/null) print_success "Done ($api_status)" fi else print_warning "Failed (continuing)" fi ;; shell) if response=$(execute_shell_step "$step" 2>/dev/null); then print_success "Done" else print_warning "Failed (continuing)" fi ;; *) print_warning "Skipping unknown action: $action" ;; esac done < <(echo "$teardown_steps" | jq -c '.[]') echo "" print_success "Teardown complete" # Optionally clean checkpoint echo "" echo "Checkpoint preserved. To remove:" echo " $0 clean $tree_name" } # List available trees cmd_list() { print_header "Available Trees" tree_list_detail echo "" echo "Checkpoints:" local checkpoints checkpoints=$(checkpoint_list) if [[ -n "$checkpoints" ]]; then while IFS= read -r tree; do local status status=$(checkpoint_status "$tree") printf " %-20s %s\n" "$tree" "($status)" done <<< "$checkpoints" else echo " (none)" fi } # Clean checkpoint cmd_clean() { local tree_name="${1:-}" if [[ -z "$tree_name" ]]; then echo "Usage: $0 clean " exit 1 fi if checkpoint_delete "$tree_name"; then print_success "Checkpoint deleted for tree: $tree_name" else echo "No checkpoint found for tree: $tree_name" fi } # ============================================================================ # Main dispatch # ============================================================================ case "$COMMAND" in run) cmd_run "$@" ;; resume) cmd_resume "$@" ;; only) cmd_only "$@" ;; status) cmd_status "$@" ;; teardown) cmd_teardown "$@" ;; list) cmd_list ;; clean) cmd_clean "$@" ;; *) echo "Unknown command: $COMMAND" echo "Valid commands: run, resume, only, status, teardown, list, clean" exit 1 ;; esac