#!/usr/bin/env bash set -euo pipefail ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" CTRL_DATA_DIR="${CTRL_DATA_DIR:-${XDG_DATA_HOME:-$HOME/.local/share}/ctrl}" CTRL_ECOSYSTEM_DIR="${CTRL_ECOSYSTEM_DIR:-$CTRL_DATA_DIR/ecosystems}" SHOW_ALL="${PM2_MANAGER_SHOW_ALL:-0}" declare -a APP_NAMES=() declare -a APP_DIRS=() declare -a APP_ECOSYSTEMS=() declare -a APP_SOURCES=() declare -A SEEN_SERVICES=() trim() { local s="$1" s="${s#"${s%%[![:space:]]*}"}" s="${s%"${s##*[![:space:]]}"}" printf "%s" "$s" } extract_app_name() { local ecosystem_file="$1" local name name="$(grep -E 'name:[[:space:]]*"[^"]+"' "$ecosystem_file" | head -n1 | sed -E 's/.*name:[[:space:]]*"([^"]+)".*/\1/' || true)" name="$(trim "$name")" if [ -z "$name" ]; then name="$(basename "$(dirname "$ecosystem_file")")" fi printf "%s" "$name" } extract_app_cwd() { local ecosystem_file="$1" local cwd cwd="$(grep -E 'cwd:[[:space:]]*"[^"]+"' "$ecosystem_file" | head -n1 | sed -E 's/.*cwd:[[:space:]]*"([^"]+)".*/\1/' || true)" cwd="$(trim "$cwd")" if [ -z "$cwd" ]; then cwd="$(dirname "$ecosystem_file")" fi printf "%s" "$cwd" } add_ecosystem() { local file="$1" local source="$2" local app_name project_dir key app_name="$(extract_app_name "$file")" project_dir="$(extract_app_cwd "$file")" key="${app_name}|${project_dir}" if [ "$SHOW_ALL" != "1" ] && [ -n "${SEEN_SERVICES[$key]:-}" ]; then return fi SEEN_SERVICES[$key]=1 APP_NAMES+=("$app_name") APP_DIRS+=("$project_dir") APP_ECOSYSTEMS+=("$file") APP_SOURCES+=("$source") } discover_ecosystems() { local file if [ -d "$CTRL_ECOSYSTEM_DIR" ]; then while IFS= read -r file; do add_ecosystem "$file" "ctrl" done < <( find "$CTRL_ECOSYSTEM_DIR" -maxdepth 1 -type f -name "*.config.cjs" | sort ) fi while IFS= read -r file; do add_ecosystem "$file" "repo" done < <( find "$ROOT_DIR" -maxdepth 4 \ \( -path "$ROOT_DIR/gitea" -o -path "*/node_modules" -o -path "*/.next" -o -path "*/.git" \) -prune -o \ -type f \ \( -name "ecosystem.config.cjs" -o -name "ecosystem.cron.config.js" \) \ -print \ | sort ) } print_services() { local i rel cwd_rel if [ "${#APP_NAMES[@]}" -eq 0 ]; then echo "No ecosystem config files found under $ROOT_DIR" return fi echo "Discovered services:" for i in "${!APP_NAMES[@]}"; do if [[ "${APP_ECOSYSTEMS[$i]}" == "$ROOT_DIR/"* ]]; then rel="${APP_ECOSYSTEMS[$i]#"$ROOT_DIR/"}" else rel="$(basename "${APP_ECOSYSTEMS[$i]}")" fi if [[ "${APP_DIRS[$i]}" == "$ROOT_DIR/"* ]]; then cwd_rel="${APP_DIRS[$i]#"$ROOT_DIR/"}" else cwd_rel="${APP_DIRS[$i]}" fi printf " %2d) %-16s %-4s %-24s %s\n" \ "$((i + 1))" "${APP_NAMES[$i]}" "${APP_SOURCES[$i]}" "$cwd_rel" "$rel" done if [ "$SHOW_ALL" != "1" ]; then echo echo "Showing preferred configs only. Use '$0 list-all' to include repo-local duplicates." fi } run_action() { local action="$1" local app_name="$2" local project_dir="$3" local ecosystem_file="$4" cd "$project_dir" case "$action" in recreate) echo "Recreating '$app_name' from '$ecosystem_file'..." pm2 delete "$app_name" >/dev/null 2>&1 || true pm2 start "$ecosystem_file" pm2 save ;; reload) echo "Reloading '$app_name'..." pm2 reload "$app_name" --update-env pm2 save ;; restart) echo "Restarting '$app_name'..." pm2 restart "$app_name" --update-env pm2 save ;; delete) echo "Deleting '$app_name'..." pm2 delete "$app_name" pm2 save ;; *) echo "Unknown action: $action" exit 1 ;; esac } interactive_mode() { local action_pick action idx print_services echo echo "Choose action:" echo " 1) Recreate from ecosystem (best for .env/config changes)" echo " 2) Reload" echo " 3) Restart" echo " 4) Delete" read -r -p "Action [1]: " action_pick action_pick="${action_pick:-1}" case "$action_pick" in 1) action="recreate" ;; 2) action="reload" ;; 3) action="restart" ;; 4) action="delete" ;; *) echo "Invalid action"; exit 1 ;; esac echo read -r -p "Service number: " idx if ! [[ "$idx" =~ ^[0-9]+$ ]]; then echo "Invalid service number" exit 1 fi idx=$((idx - 1)) if [ "$idx" -lt 0 ] || [ "$idx" -ge "${#APP_NAMES[@]}" ]; then echo "Service number out of range" exit 1 fi echo run_action "$action" "${APP_NAMES[$idx]}" "${APP_DIRS[$idx]}" "${APP_ECOSYSTEMS[$idx]}" echo pm2 ls } usage() { cat < Actions: recreate | reload | restart | delete Service can be: - numeric index from "list" - app name (e.g. zap, zap-cleanup, caddy-manager, api) Ctrl-managed configs are discovered from: ${CTRL_ECOSYSTEM_DIR} EOF } resolve_service_index() { local target="$1" local i if [[ "$target" =~ ^[0-9]+$ ]]; then i=$((target - 1)) if [ "$i" -ge 0 ] && [ "$i" -lt "${#APP_NAMES[@]}" ]; then echo "$i" return 0 fi return 1 fi for i in "${!APP_NAMES[@]}"; do if [ "${APP_NAMES[$i]}" = "$target" ]; then echo "$i" return 0 fi done return 1 } main() { if [ "${1:-}" = "-h" ] || [ "${1:-}" = "--help" ]; then usage exit 0 fi if [ "${1:-}" = "list-all" ]; then SHOW_ALL=1 fi discover_ecosystems if [ "${1:-}" = "list" ] || [ "${1:-}" = "list-all" ]; then print_services exit 0 fi if [ $# -eq 0 ]; then interactive_mode exit 0 fi if [ $# -ne 2 ]; then usage exit 1 fi local action="$1" local target="$2" local idx if ! idx="$(resolve_service_index "$target")"; then echo "Unknown service: $target" echo print_services exit 1 fi run_action "$action" "${APP_NAMES[$idx]}" "${APP_DIRS[$idx]}" "${APP_ECOSYSTEMS[$idx]}" } main "$@"