Ctrl K
ComunidadGuías…

uninstall.sh

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
#!/usr/bin/env bash
set -euo pipefail

UNINSTALL_SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
# Bare uninstall previews ALL instances and requires their exact names. Keep
# legacy explicit --yes automation default-scoped; --all opts into bulk removal.
if (($# == 0)); then
  exec bash "$UNINSTALL_SCRIPT_DIR/instances.sh" remove --all
fi
for uninstall_option in "$@"; do
  if [[ "$uninstall_option" == "--all" || "$uninstall_option" == "--instance" ]]; then
    exec bash "$UNINSTALL_SCRIPT_DIR/instances.sh" remove "$@"
  fi
done
INSTANCE_NAME="${AGENTS_SERVER_INSTANCE:-default}"
MANAGED_REMOVAL="false"
if [[ "$1" == "--managed-instance" ]]; then
  MANAGED_REMOVAL="true"
  INSTANCE_NAME="${2:?missing instance name}"
  shift 2
fi
if [[ ! "$INSTANCE_NAME" =~ ^[a-z][a-z0-9-]{0,31}$ ]]; then
  echo "Invalid instance name." >&2
  exit 2
fi

INSTALL_ROOT="${AGENTS_SERVER_INSTALL_DIR:-$HOME/.local/share/agents-server}"
CONFIG_ROOT="${AGENTS_SERVER_CONFIG_DIR:-$HOME/.config/agents-server}"
LEGACY_STATE_ROOT="$HOME/.zenithbot-agent"
if [[ -n "${AGENTSDOCK_STATE_DIR:-}" ]]; then
  STATE_ROOT="$AGENTSDOCK_STATE_DIR"
elif [[ -n "${AGENTS_SERVER_STATE_DIR:-}" ]]; then
  STATE_ROOT="$AGENTS_SERVER_STATE_DIR"
elif [[ -n "${ZENITHBOT_AGENT_DIR:-}" ]]; then
  STATE_ROOT="$ZENITHBOT_AGENT_DIR"
else
  STATE_ROOT="$HOME/.agentsdock"
fi
SERVICE_NAME="agents-server"
LABEL="com.agentsdock.server"
INSTANCE_LOG_DIR="$HOME/Library/Logs/AgentsServer"
if [[ "$INSTANCE_NAME" != "default" ]]; then
  instance_bindings="$(bash "$UNINSTALL_SCRIPT_DIR/instances.sh" _bindings "$INSTANCE_NAME")" || exit 2
  eval "$instance_bindings"
fi
OS_NAME="$(uname -s)"

ASSUME_YES="false"
PURGE_STATE="false"
MANAGED_RELEASE_NAME="false"
UNINSTALL_RED=""
UNINSTALL_GREEN=""
UNINSTALL_RESET=""
if [[ -t 1 && "${TERM:-}" != "dumb" && -z "${NO_COLOR+x}" ]]; then
  UNINSTALL_RED=$'\033[31m'
  UNINSTALL_GREEN=$'\033[32m'
  UNINSTALL_RESET=$'\033[0m'
fi

uninstall_status() {
  printf '%s%s%s\n' "$1" "$2" "$UNINSTALL_RESET"
}

normalize_managed_path() {
  local label="$1"
  local candidate="$2"
  local parent leaf physical_parent normalized home_physical

  if [[ -z "$candidate" || "$candidate" != /* || "$candidate" == *$'\n'* || "$candidate" == *$'\r'* ]]; then
    echo "$label must be a non-empty absolute path without control characters." >&2
    return 1
  fi
  while [[ "$candidate" != "/" && "$candidate" == */ ]]; do
    candidate="${candidate%/}"
  done
  case "$candidate/" in
    *"/../"*|*"/./"*|*"//"*)
      echo "$label must not contain '.', '..', or repeated-slash path components: $candidate" >&2
      return 1
      ;;
  esac

  # Resolve existing parent directories without following the final component.
  # Removing a final symlink must remove only that link, while an intermediate
  # symlink still needs to be accounted for by the safety checks below.
  parent="$(dirname -- "$candidate")"
  leaf="$(basename -- "$candidate")"
  while [[ ! -d "$parent" && "$parent" != "/" ]]; do
    leaf="$(basename -- "$parent")/$leaf"
    parent="$(dirname -- "$parent")"
  done
  physical_parent="$(cd -P -- "$parent" 2>/dev/null && pwd -P)" || {
    echo "Could not resolve the parent of $label: $candidate" >&2
    return 1
  }
  if [[ "$physical_parent" == "/" ]]; then
    normalized="/$leaf"
  else
    normalized="$physical_parent/$leaf"
  fi
  home_physical="$(cd -P -- "$HOME" 2>/dev/null && pwd -P)" || {
    echo "Could not resolve HOME safely: $HOME" >&2
    return 1
  }

  case "$normalized" in
    "/"|"/Applications"|"/Library"|"/System"|"/Users"|"/Volumes"|"/bin"|"/etc"|"/home"|"/opt"|"/private"|"/sbin"|"/tmp"|"/usr"|"/var"|\
    "$home_physical"|"$home_physical/.config"|"$home_physical/.local"|"$home_physical/.local/share"|"$home_physical/Library"|"$home_physical/Library/LaunchAgents")
      echo "Refusing unsafe $label target: $normalized" >&2
      return 1
      ;;
  esac
  printf '%s\n' "$normalized"
}

paths_overlap() {
  local first="$1"
  local second="$2"
  [[ "$first" == "$second" || "$first" == "$second/"* || "$second" == "$first/"* ]]
}

INSTALL_ROOT="$(normalize_managed_path AGENTS_SERVER_INSTALL_DIR "$INSTALL_ROOT")" || exit 2
CONFIG_ROOT="$(normalize_managed_path AGENTS_SERVER_CONFIG_DIR "$CONFIG_ROOT")" || exit 2
STATE_ROOT="$(normalize_managed_path AGENTSDOCK_STATE_DIR "$STATE_ROOT")" || exit 2
for managed_root in "$INSTALL_ROOT" "$CONFIG_ROOT" "$STATE_ROOT"; do
  if [[ -L "$managed_root" ]]; then
    echo "Refusing symbolic-link managed root: $managed_root" >&2
    exit 2
  fi
done
if paths_overlap "$INSTALL_ROOT" "$CONFIG_ROOT" || \
  paths_overlap "$INSTALL_ROOT" "$STATE_ROOT" || \
  paths_overlap "$CONFIG_ROOT" "$STATE_ROOT"; then
  echo "Refusing overlapping install, configuration, and state roots; each must be a separate directory." >&2
  exit 2
fi

usage() {
  cat <<USAGE
Usage: ./uninstall.sh [--yes] [--purge-state]

With no arguments: show all current-user instances and confirm their exact names.
  --instance NAME   Remove only NAME (preview + confirmation).
  --all             Remove all instances (preview + confirmation).
  --all --exclude default   Keep the original/default server untouched.
Legacy --yes without --all remains default-scoped for compatibility.

Stops and removes the AgentsServer user service, versioned release runtime,
and generated configuration (including the access token). Chat history, jobs,
files, secure-peer credentials, and terminals under $STATE_ROOT are preserved
by default so a later ./install.sh picks ordinary AgentsServer state back up.
Preserved Team Hub state is not auto-reactivated in this beta; it requires
signed managed recovery or support-assisted restoration.

  --yes           Do not prompt before removing the service, releases, and
                   configuration.
  --purge-state   Also delete $STATE_ROOT (chat history, jobs, files, tokens,
                   and secure-peer credentials). This cannot be undone and
                   always requires typing the exact state path interactively;
                   --yes never bypasses it.
USAGE
}

while (($#)); do
  case "$1" in
    --yes) ASSUME_YES="true"; shift ;;
    --purge-state) PURGE_STATE="true"; shift ;;
    --managed-release-name) MANAGED_RELEASE_NAME="true"; shift ;;
    -h|--help) usage; exit 0 ;;
    *) echo "Unknown option: $1" >&2; usage >&2; exit 2 ;;
  esac
done

# This flag only controls progress text. The manager separately archives state
# after a successful service removal, using the user's affirmative choice.
if [[ "$MANAGED_RELEASE_NAME" == "true" \
  && ( "$MANAGED_REMOVAL" != "true" || "$INSTANCE_NAME" == "default" \
    || "$ASSUME_YES" != "true" || "$PURGE_STATE" == "true" ) ]]; then
  echo "--managed-release-name is only for a confirmed named-instance manager removal." >&2
  exit 2
fi

INSTALL_LOCK_DIR="$INSTALL_ROOT/.install-lock"
UNINSTALL_LOCK_HELD="false"
INSTALL_ROOT_CREATED_FOR_LOCK="false"

release_operation_lock() {
  [[ "$UNINSTALL_LOCK_HELD" == "true" ]] || return 0
  if [[ "$(cat "$INSTALL_LOCK_DIR/pid" 2>/dev/null || true)" == "$$" ]]; then
    rm -rf "$INSTALL_LOCK_DIR"
  fi
  UNINSTALL_LOCK_HELD="false"
  if [[ "$INSTALL_ROOT_CREATED_FOR_LOCK" == "true" ]]; then
    rmdir "$INSTALL_ROOT" 2>/dev/null || true
  fi
}

acquire_operation_lock() {
  if [[ ! -d "$INSTALL_ROOT" ]]; then
    mkdir -p "$INSTALL_ROOT"
    INSTALL_ROOT_CREATED_FOR_LOCK="true"
  fi
  if ! mkdir "$INSTALL_LOCK_DIR" 2>/dev/null; then
    local owner_pid=""
    owner_pid="$(cat "$INSTALL_LOCK_DIR/pid" 2>/dev/null || true)"
    if [[ "$owner_pid" =~ ^[0-9]+$ ]] && kill -0 "$owner_pid" >/dev/null 2>&1; then
      echo "Refusing to uninstall while an AgentsServer install/update is active (PID $owner_pid)." >&2
      return 1
    fi
    local stale_lock="$INSTALL_ROOT/.uninstall-stale-lock-$$"
    if ! mv "$INSTALL_LOCK_DIR" "$stale_lock" 2>/dev/null; then
      echo "An AgentsServer install/update started while uninstall was checking the operation lock." >&2
      return 1
    fi
    rm -rf "$stale_lock"
    if ! mkdir "$INSTALL_LOCK_DIR" 2>/dev/null; then
      echo "An AgentsServer install/update is already running." >&2
      return 1
    fi
  fi
  printf '%s\n' "$$" > "$INSTALL_LOCK_DIR/pid"
  chmod 700 "$INSTALL_LOCK_DIR"
  UNINSTALL_LOCK_HELD="true"
}

acquire_operation_lock || exit 1
trap release_operation_lock EXIT
trap 'exit 130' HUP INT TERM

confirm() {
  local prompt="$1"
  [[ "$ASSUME_YES" != "true" ]] || return 0
  if [[ ! -t 0 ]]; then
    echo "Refusing to $prompt without --yes on a non-interactive run." >&2
    return 1
  fi
  local reply=""
  read -r -p "$prompt [y/N] " reply || return 1
  case "$reply" in
    y|Y|yes|YES) return 0 ;;
    *) return 1 ;;
  esac
}

confirm_state_purge() {
  local reply=""
  if [[ ! -t 0 ]]; then
    echo "Refusing to purge state without an interactive terminal; --yes never bypasses the state guard." >&2
    return 1
  fi
  echo "Permanent deletion requested for chat history, jobs, files, and tokens."
  read -r -p "Type the exact state path to confirm ($STATE_ROOT): " reply || return 1
  if [[ "$reply" != "$STATE_ROOT" ]]; then
    echo "State path did not match; nothing was changed." >&2
    return 1
  fi
}

if [[ "$PURGE_STATE" == "true" ]] && ! confirm_state_purge; then
  exit 1
fi

echo "This will stop and remove the AgentsServer service, release runtime at"
echo "$INSTALL_ROOT, and configuration at $CONFIG_ROOT (including the access token)."
if confirm "Proceed?"; then
  :
else
  echo "Aborted; nothing was changed." >&2
  exit 1
fi
echo

case "$OS_NAME" in
  Darwin)
    LABEL="${LABEL:-com.agentsdock.server}"
    PLIST="$HOME/Library/LaunchAgents/$LABEL.plist"
    SERVICE_TARGET="gui/$(id -u)/$LABEL"
    if launchctl print "$SERVICE_TARGET" >/dev/null 2>&1; then
      uninstall_status "$UNINSTALL_RED" "Stopping $LABEL"
      if ! launchctl bootout "$SERVICE_TARGET" >/dev/null 2>&1 && \
        launchctl print "$SERVICE_TARGET" >/dev/null 2>&1; then
        echo "Could not stop $LABEL; no files were removed." >&2
        exit 1
      fi
      stopped="false"
      for ((_attempt = 1; _attempt <= 50; _attempt++)); do
        if ! launchctl print "$SERVICE_TARGET" >/dev/null 2>&1; then
          stopped="true"
          break
        fi
        sleep 0.1
      done
      if [[ "$stopped" != "true" ]]; then
        echo "$LABEL did not stop; no files were removed." >&2
        exit 1
      fi
    fi
    if [[ -f "$PLIST" ]]; then
      uninstall_status "$UNINSTALL_RED" "Removing $PLIST"
      rm -f "$PLIST"
    fi
    LOG_DIR="$INSTANCE_LOG_DIR"
    if [[ -d "$LOG_DIR" ]]; then
      uninstall_status "$UNINSTALL_RED" "Removing $LOG_DIR"
      rm -rf "$LOG_DIR"
    fi
    ;;
  Linux)
    SERVICE_FILE="$HOME/.config/systemd/user/$SERVICE_NAME.service"
    if ! systemctl --user show-environment >/dev/null 2>&1; then
      echo "The systemd user service manager is unavailable; no files were removed." >&2
      exit 1
    fi
    if [[ -f "$SERVICE_FILE" ]] || systemctl --user is-active --quiet "$SERVICE_NAME.service"; then
      uninstall_status "$UNINSTALL_RED" "Stopping $SERVICE_NAME.service"
      if ! systemctl --user disable --now "$SERVICE_NAME.service" >/dev/null 2>&1 && \
        systemctl --user is-active --quiet "$SERVICE_NAME.service"; then
        echo "Could not stop $SERVICE_NAME.service; no files were removed." >&2
        exit 1
      fi
      stopped="false"
      for ((_attempt = 1; _attempt <= 50; _attempt++)); do
        if ! systemctl --user is-active --quiet "$SERVICE_NAME.service"; then
          stopped="true"
          break
        fi
        sleep 0.1
      done
      if [[ "$stopped" != "true" ]]; then
        echo "$SERVICE_NAME.service did not stop; no files were removed." >&2
        exit 1
      fi
    fi
    if [[ -f "$SERVICE_FILE" ]]; then
      uninstall_status "$UNINSTALL_RED" "Removing $SERVICE_FILE"
      rm -f "$SERVICE_FILE"
      systemctl --user daemon-reload || true
    fi
    ;;
  *)
    echo "Unsupported host OS: $OS_NAME; no files were removed." >&2
    exit 1
    ;;
esac

if [[ -d "$INSTALL_ROOT" ]]; then
  uninstall_status "$UNINSTALL_RED" "Removing release runtime at $INSTALL_ROOT"
  shopt -s dotglob nullglob
  for install_entry in "$INSTALL_ROOT"/*; do
    [[ "$install_entry" == "$INSTALL_LOCK_DIR" ]] || rm -rf -- "$install_entry"
  done
  shopt -u dotglob nullglob
fi

if [[ -d "$CONFIG_ROOT" ]]; then
  uninstall_status "$UNINSTALL_RED" "Removing configuration at $CONFIG_ROOT"
  rm -rf "$CONFIG_ROOT"
fi

if [[ "$PURGE_STATE" == "true" ]]; then
  if [[ -e "$STATE_ROOT" || -L "$LEGACY_STATE_ROOT" ]]; then
    if [[ "$INSTANCE_NAME" == "default" ]]; then
      [[ ! -L "$LEGACY_STATE_ROOT" ]] || rm -f "$LEGACY_STATE_ROOT"
    fi
    [[ ! -e "$STATE_ROOT" ]] || rm -rf "$STATE_ROOT"
    uninstall_status "$UNINSTALL_RED" "Deleted $STATE_ROOT"
  fi
elif [[ -e "$STATE_ROOT" && "$MANAGED_RELEASE_NAME" != "true" ]]; then
  echo
  uninstall_status "$UNINSTALL_GREEN" "Preserved chat history, jobs, files, tokens, and secure-peer credentials at $STATE_ROOT."
  echo
  if [[ "$INSTANCE_NAME" == "default" ]]; then
    echo "Re-running ./install.sh will pick ordinary AgentsServer state back up. Pass --purge-state to also delete it."
  else
    echo "To reuse this named history, run ./install.sh --instance $INSTANCE_NAME --port PORT (choose an available port)."
    echo "Do not use bare ./install.sh for this instance; it targets the default server. Pass --purge-state to also delete history."
  fi
  if [[ -e "$STATE_ROOT/team-hub/team-hub.sqlite3" || -L "$STATE_ROOT/team-hub/team-hub.sqlite3" ]]; then
    echo
    echo "Preserved Team Hub state is not auto-reactivated in this beta; use a signed managed recovery or support-assisted restoration."
  fi
fi

# Keep the shared install/update lock alive until every requested removal has
# completed. Removing the install root while the lock still lives inside it
# would let a new installer race with configuration or state deletion.
release_operation_lock
if [[ -d "$INSTALL_ROOT" ]] && ! rmdir "$INSTALL_ROOT"; then
  echo "Could not remove the now-unlocked install directory at $INSTALL_ROOT." >&2
  exit 1
fi

echo
if [[ "$INSTANCE_NAME" == "default" ]]; then
  echo "Note: any persistent chat terminals (tmux sessions named zd_*) keep running"
  echo "independently and are not touched by this script. List them with: tmux ls"
else
  echo "Note: persistent chat terminals for $INSTANCE_NAME keep running independently and are not touched by this script."
  echo "List only this instance's terminals with: tmux -L agents-server-$INSTANCE_NAME ls"
fi
if [[ "$MANAGED_REMOVAL" != "true" ]]; then
  echo
  uninstall_status "$UNINSTALL_GREEN" "Successful!"
fi