diff --git a/install.sh b/install.sh index 88d78608..55ca08e6 100755 --- a/install.sh +++ b/install.sh @@ -10,6 +10,17 @@ # 3. Installs all 6 plugins globally (user scope) # 4. Auto-update is enabled by default — plugins stay current automatically # +# NOTE: The BASH_VERSION check below can be bypassed by setting the BASH_VERSION +# env var before invoking under sh/dash (e.g. BASH_VERSION=x sh install.sh). +# This is a known limitation: the guard is a best-effort hint, not a security boundary. +if [ -z "${BASH_VERSION:-}" ]; then + printf 'Error: This script requires bash. Run: bash install.sh\n or: curl -fsSL https://raw.githubusercontent.com/closedloop-ai/claude-plugins/main/install.sh | bash\n' >&2 + exit 1 +fi +if [[ "${BASH_VERSINFO[0]:-0}" -lt 3 || ("${BASH_VERSINFO[0]:-0}" -eq 3 && "${BASH_VERSINFO[1]:-0}" -lt 2) ]]; then + printf 'Error: Bash 3.2+ required (found %s)\n' "$BASH_VERSION" >&2 + exit 1 +fi set -euo pipefail # ── Colors ─────────────────────────────────────────────────────────────────── @@ -24,12 +35,26 @@ info() { echo -e "${GREEN}[✓]${NC} $1"; } warn() { echo -e "${YELLOW}[!]${NC} $1"; } err() { echo -e "${RED}[✗]${NC} $1"; } step() { echo -e "${BLUE}[→]${NC} ${BOLD}$1${NC}"; } +snapshot_version() { grep -m1 "^$2 " "$1" 2>/dev/null | awk '{print $2}' || true; } +sanitize_stderr() { + # Strip ANSI color escapes, then drop non-printable control chars. + # Uses bash ANSI-C quoting for a literal ESC so this works under BSD sed (macOS) + # as well as GNU sed. `tr` with octal ranges is POSIX-portable across both. + local esc=$'\033' + sed "s/${esc}\[[0-9;]*[a-zA-Z]//g" "$1" | tr -d '\000-\010\013-\037\177' >&2 +} # ── Constants ──────────────────────────────────────────────────────────────── MARKETPLACE_SOURCE="closedloop-ai/claude-plugins" MARKETPLACE_NAME="closedloop-ai" PLUGINS=(bootstrap code code-review judges platform self-learning) +# ── Per-run working directory ──────────────────────────────────────────────── +WORK_DIR=$(mktemp -d "${TMPDIR:-/tmp}/closedloop-install.XXXXXX") +chmod 700 "$WORK_DIR" +_cleanup() { rm -rf "$WORK_DIR"; } +trap _cleanup EXIT + # ── Preflight checks ──────────────────────────────────────────────────────── echo echo -e "${BOLD}ClosedLoop Claude Plugins Installer${NC}" @@ -49,8 +74,8 @@ info "Claude Code CLI found: $(claude --version 2>/dev/null || echo 'unknown ver # Python 3.11+ if command -v python3 &>/dev/null; then PY_VERSION=$(python3 -c 'import sys; print(f"{sys.version_info.major}.{sys.version_info.minor}")') - PY_MAJOR=$(echo "$PY_VERSION" | cut -d. -f1) - PY_MINOR=$(echo "$PY_VERSION" | cut -d. -f2) + PY_MAJOR="${PY_VERSION%%.*}" + PY_MINOR="${PY_VERSION##*.}" if [[ "$PY_MAJOR" -gt 3 || ( "$PY_MAJOR" -eq 3 && "$PY_MINOR" -ge 11 ) ]]; then info "Python $PY_VERSION" else @@ -61,11 +86,13 @@ else fi # jq -if command -v jq &>/dev/null; then - info "jq found" +if ! command -v jq &>/dev/null; then + err "jq is required but not found." + echo " Install: brew install jq (macOS)" + echo " Install: apt install jq (Debian/Ubuntu)" + exit 1 else - warn "jq not found — some plugin features require it" - echo " Install: brew install jq (macOS) / apt install jq (Linux)" + info "jq found" fi echo @@ -73,11 +100,18 @@ echo # ── Add marketplace ───────────────────────────────────────────────────────── step "Registering closedloop-ai marketplace..." -if claude plugin marketplace add "$MARKETPLACE_SOURCE" 2>/dev/null; then - info "Marketplace registered: $MARKETPLACE_SOURCE" +_MARKETPLACE_LIST=$(claude plugin marketplace list --json 2>/dev/null) +if [[ -n "$_MARKETPLACE_LIST" ]] \ + && echo "$_MARKETPLACE_LIST" | jq -e --arg name "$MARKETPLACE_NAME" 'any(.name == $name)' &>/dev/null; then + info "Marketplace already registered: $MARKETPLACE_NAME" else - # May already be registered — not a fatal error - warn "Marketplace may already be registered (continuing)" + [[ -z "$_MARKETPLACE_LIST" ]] && warn "Could not query marketplace list — attempting add anyway" + if claude plugin marketplace add "$MARKETPLACE_SOURCE" 2>"$WORK_DIR/marketplace_err"; then + info "Marketplace registered: $MARKETPLACE_SOURCE" + else + warn "Marketplace add failed:" + sanitize_stderr "$WORK_DIR/marketplace_err" + fi fi echo @@ -86,33 +120,62 @@ echo step "Installing plugins (user scope)..." INSTALLED=0 +UPDATED=0 +UP_TO_DATE=0 FAILED=0 +SNAPSHOT_PRE="$WORK_DIR/snapshot_pre" +SNAPSHOT_POST="$WORK_DIR/snapshot_post" +STDERR_FILE="$WORK_DIR/install_err" + +claude plugin list --json 2>/dev/null \ + | jq -r '.[] | .id + " " + .version' > "$SNAPSHOT_PRE" 2>/dev/null || true +[[ -s "$SNAPSHOT_PRE" ]] || warn "Could not snapshot installed plugins — state detection will be approximate" + +SUCCESSFUL_PLUGINS=() + for plugin in "${PLUGINS[@]}"; do - PLUGIN_REF="${plugin}@${MARKETPLACE_NAME}" - if claude plugin install "$PLUGIN_REF" --scope user 2>/dev/null; then - info "Installed: $plugin" + plugin_ref="${plugin}@${MARKETPLACE_NAME}" + if claude plugin install "$plugin_ref" --scope user 2>"$STDERR_FILE"; then + SUCCESSFUL_PLUGINS+=("$plugin_ref") + # Install failed — may already exist; try update instead + elif claude plugin update "$plugin_ref" --scope user 2>"$STDERR_FILE"; then + SUCCESSFUL_PLUGINS+=("$plugin_ref") + else + [[ -s "$STDERR_FILE" ]] && sanitize_stderr "$STDERR_FILE" + warn "Could not install/update: $plugin" + FAILED=$((FAILED + 1)) + fi +done + +claude plugin list --json 2>/dev/null \ + | jq -r '.[] | .id + " " + .version' > "$SNAPSHOT_POST" 2>/dev/null || true + +for plugin_ref in "${SUCCESSFUL_PLUGINS[@]+"${SUCCESSFUL_PLUGINS[@]}"}"; do + plugin="${plugin_ref%@*}" + pre_ver=$(snapshot_version "$SNAPSHOT_PRE" "$plugin_ref") + post_ver=$(snapshot_version "$SNAPSHOT_POST" "$plugin_ref") + if [[ -z "$pre_ver" || -z "$post_ver" ]]; then INSTALLED=$((INSTALLED + 1)) + info "Installed: $plugin" + elif [[ "$pre_ver" == "$post_ver" ]]; then + UP_TO_DATE=$((UP_TO_DATE + 1)) + info "Already up to date: $plugin" else - # May already be installed — try to update instead - if claude plugin update "$PLUGIN_REF" --scope user 2>/dev/null; then - info "Updated: $plugin" - INSTALLED=$((INSTALLED + 1)) - else - warn "Could not install/update: $plugin" - FAILED=$((FAILED + 1)) - fi + UPDATED=$((UPDATED + 1)) + info "Updated: $plugin ($pre_ver -> $post_ver)" fi done echo # ── Summary ────────────────────────────────────────────────────────────────── +TOTAL=$((INSTALLED + UPDATED + UP_TO_DATE + FAILED)) echo "────────────────────────────────────" if [[ $FAILED -eq 0 ]]; then - echo -e "${GREEN}${BOLD}All $INSTALLED plugins installed successfully!${NC}" + echo -e "${GREEN}${BOLD}All $TOTAL plugins ready ($INSTALLED installed, $UPDATED updated, $UP_TO_DATE already up to date).${NC}" else - echo -e "${YELLOW}${BOLD}$INSTALLED installed, $FAILED failed${NC}" + echo -e "${YELLOW}${BOLD}$TOTAL plugins processed: $INSTALLED installed, $UPDATED updated, $UP_TO_DATE already up to date, $FAILED failed.${NC}" fi echo