Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
109 changes: 86 additions & 23 deletions install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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 ───────────────────────────────────────────────────────────────────
Expand All @@ -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}"
Expand All @@ -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
Expand All @@ -61,23 +86,32 @@ 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

# ── 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
Expand All @@ -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
Expand Down
Loading