1010# 3. Installs all 6 plugins globally (user scope)
1111# 4. Auto-update is enabled by default — plugins stay current automatically
1212#
13+ # NOTE: The BASH_VERSION check below can be bypassed by setting the BASH_VERSION
14+ # env var before invoking under sh/dash (e.g. BASH_VERSION=x sh install.sh).
15+ # This is a known limitation: the guard is a best-effort hint, not a security boundary.
16+ if [ -z " ${BASH_VERSION:- } " ]; then
17+ 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
18+ exit 1
19+ fi
20+ if [[ " ${BASH_VERSINFO[0]:- 0} " -lt 3 || (" ${BASH_VERSINFO[0]:- 0} " -eq 3 && " ${BASH_VERSINFO[1]:- 0} " -lt 2) ]]; then
21+ printf ' Error: Bash 3.2+ required (found %s)\n' " $BASH_VERSION " >&2
22+ exit 1
23+ fi
1324set -euo pipefail
1425
1526# ── Colors ───────────────────────────────────────────────────────────────────
@@ -24,12 +35,26 @@ info() { echo -e "${GREEN}[✓]${NC} $1"; }
2435warn () { echo -e " ${YELLOW} [!]${NC} $1 " ; }
2536err () { echo -e " ${RED} [✗]${NC} $1 " ; }
2637step () { echo -e " ${BLUE} [→]${NC} ${BOLD} $1 ${NC} " ; }
38+ snapshot_version () { grep -m1 " ^$2 " " $1 " 2> /dev/null | awk ' {print $2}' || true ; }
39+ sanitize_stderr () {
40+ # Strip ANSI color escapes, then drop non-printable control chars.
41+ # Uses bash ANSI-C quoting for a literal ESC so this works under BSD sed (macOS)
42+ # as well as GNU sed. `tr` with octal ranges is POSIX-portable across both.
43+ local esc=$' \033 '
44+ sed " s/${esc} \[[0-9;]*[a-zA-Z]//g" " $1 " | tr -d ' \000-\010\013-\037\177' >&2
45+ }
2746
2847# ── Constants ────────────────────────────────────────────────────────────────
2948MARKETPLACE_SOURCE=" closedloop-ai/claude-plugins"
3049MARKETPLACE_NAME=" closedloop-ai"
3150PLUGINS=(bootstrap code code-review judges platform self-learning)
3251
52+ # ── Per-run working directory ────────────────────────────────────────────────
53+ WORK_DIR=$( mktemp -d " ${TMPDIR:-/ tmp} /closedloop-install.XXXXXX" )
54+ chmod 700 " $WORK_DIR "
55+ _cleanup () { rm -rf " $WORK_DIR " ; }
56+ trap _cleanup EXIT
57+
3358# ── Preflight checks ────────────────────────────────────────────────────────
3459echo
3560echo -e " ${BOLD} ClosedLoop Claude Plugins Installer${NC} "
@@ -49,8 +74,8 @@ info "Claude Code CLI found: $(claude --version 2>/dev/null || echo 'unknown ver
4974# Python 3.11+
5075if command -v python3 & > /dev/null; then
5176 PY_VERSION=$( python3 -c ' import sys; print(f"{sys.version_info.major}.{sys.version_info.minor}")' )
52- PY_MAJOR=$( echo " $ PY_VERSION" | cut -d. -f1 )
53- PY_MINOR=$( echo " $ PY_VERSION" | cut -d. -f2 )
77+ PY_MAJOR=" ${ PY_VERSION%% . * } "
78+ PY_MINOR=" ${ PY_VERSION##* .} "
5479 if [[ " $PY_MAJOR " -gt 3 || ( " $PY_MAJOR " -eq 3 && " $PY_MINOR " -ge 11 ) ]]; then
5580 info " Python $PY_VERSION "
5681 else
6186fi
6287
6388# jq
64- if command -v jq & > /dev/null; then
65- info " jq found"
89+ if ! command -v jq & > /dev/null; then
90+ err " jq is required but not found."
91+ echo " Install: brew install jq (macOS)"
92+ echo " Install: apt install jq (Debian/Ubuntu)"
93+ exit 1
6694else
67- warn " jq not found — some plugin features require it"
68- echo " Install: brew install jq (macOS) / apt install jq (Linux)"
95+ info " jq found"
6996fi
7097
7198echo
7299
73100# ── Add marketplace ─────────────────────────────────────────────────────────
74101step " Registering closedloop-ai marketplace..."
75102
76- if claude plugin marketplace add " $MARKETPLACE_SOURCE " 2> /dev/null; then
77- info " Marketplace registered: $MARKETPLACE_SOURCE "
103+ _MARKETPLACE_LIST=$( claude plugin marketplace list --json 2> /dev/null)
104+ if [[ -n " $_MARKETPLACE_LIST " ]] \
105+ && echo " $_MARKETPLACE_LIST " | jq -e --arg name " $MARKETPLACE_NAME " ' any(.name == $name)' & > /dev/null; then
106+ info " Marketplace already registered: $MARKETPLACE_NAME "
78107else
79- # May already be registered — not a fatal error
80- warn " Marketplace may already be registered (continuing)"
108+ [[ -z " $_MARKETPLACE_LIST " ]] && warn " Could not query marketplace list — attempting add anyway"
109+ if claude plugin marketplace add " $MARKETPLACE_SOURCE " 2> " $WORK_DIR /marketplace_err" ; then
110+ info " Marketplace registered: $MARKETPLACE_SOURCE "
111+ else
112+ warn " Marketplace add failed:"
113+ sanitize_stderr " $WORK_DIR /marketplace_err"
114+ fi
81115fi
82116
83117echo
@@ -86,33 +120,62 @@ echo
86120step " Installing plugins (user scope)..."
87121
88122INSTALLED=0
123+ UPDATED=0
124+ UP_TO_DATE=0
89125FAILED=0
90126
127+ SNAPSHOT_PRE=" $WORK_DIR /snapshot_pre"
128+ SNAPSHOT_POST=" $WORK_DIR /snapshot_post"
129+ STDERR_FILE=" $WORK_DIR /install_err"
130+
131+ claude plugin list --json 2> /dev/null \
132+ | jq -r ' .[] | .id + " " + .version' > " $SNAPSHOT_PRE " 2> /dev/null || true
133+ [[ -s " $SNAPSHOT_PRE " ]] || warn " Could not snapshot installed plugins — state detection will be approximate"
134+
135+ SUCCESSFUL_PLUGINS=()
136+
91137for plugin in " ${PLUGINS[@]} " ; do
92- PLUGIN_REF=" ${plugin} @${MARKETPLACE_NAME} "
93- if claude plugin install " $PLUGIN_REF " --scope user 2> /dev/null; then
94- info " Installed: $plugin "
138+ plugin_ref=" ${plugin} @${MARKETPLACE_NAME} "
139+ if claude plugin install " $plugin_ref " --scope user 2> " $STDERR_FILE " ; then
140+ SUCCESSFUL_PLUGINS+=(" $plugin_ref " )
141+ # Install failed — may already exist; try update instead
142+ elif claude plugin update " $plugin_ref " --scope user 2> " $STDERR_FILE " ; then
143+ SUCCESSFUL_PLUGINS+=(" $plugin_ref " )
144+ else
145+ [[ -s " $STDERR_FILE " ]] && sanitize_stderr " $STDERR_FILE "
146+ warn " Could not install/update: $plugin "
147+ FAILED=$(( FAILED + 1 ))
148+ fi
149+ done
150+
151+ claude plugin list --json 2> /dev/null \
152+ | jq -r ' .[] | .id + " " + .version' > " $SNAPSHOT_POST " 2> /dev/null || true
153+
154+ for plugin_ref in " ${SUCCESSFUL_PLUGINS[@]+" ${SUCCESSFUL_PLUGINS[@]} " } " ; do
155+ plugin=" ${plugin_ref%@* } "
156+ pre_ver=$( snapshot_version " $SNAPSHOT_PRE " " $plugin_ref " )
157+ post_ver=$( snapshot_version " $SNAPSHOT_POST " " $plugin_ref " )
158+ if [[ -z " $pre_ver " || -z " $post_ver " ]]; then
95159 INSTALLED=$(( INSTALLED + 1 ))
160+ info " Installed: $plugin "
161+ elif [[ " $pre_ver " == " $post_ver " ]]; then
162+ UP_TO_DATE=$(( UP_TO_DATE + 1 ))
163+ info " Already up to date: $plugin "
96164 else
97- # May already be installed — try to update instead
98- if claude plugin update " $PLUGIN_REF " --scope user 2> /dev/null; then
99- info " Updated: $plugin "
100- INSTALLED=$(( INSTALLED + 1 ))
101- else
102- warn " Could not install/update: $plugin "
103- FAILED=$(( FAILED + 1 ))
104- fi
165+ UPDATED=$(( UPDATED + 1 ))
166+ info " Updated: $plugin ($pre_ver -> $post_ver )"
105167 fi
106168done
107169
108170echo
109171
110172# ── Summary ──────────────────────────────────────────────────────────────────
173+ TOTAL=$(( INSTALLED + UPDATED + UP_TO_DATE + FAILED))
111174echo " ────────────────────────────────────"
112175if [[ $FAILED -eq 0 ]]; then
113- echo -e " ${GREEN}${BOLD} All $INSTALLED plugins installed successfully! ${NC} "
176+ echo -e " ${GREEN}${BOLD} All $TOTAL plugins ready ( $INSTALLED installed, $UPDATED updated, $UP_TO_DATE already up to date). ${NC} "
114177else
115- echo -e " ${YELLOW}${BOLD} $INSTALLED installed, $FAILED failed${NC} "
178+ echo -e " ${YELLOW}${BOLD} $TOTAL plugins processed: $ INSTALLED installed, $UPDATED updated, $UP_TO_DATE already up to date, $ FAILED failed. ${NC} "
116179fi
117180
118181echo
0 commit comments