diff --git a/scripts/lib/install-plugins.sh b/scripts/lib/install-plugins.sh new file mode 100644 index 0000000..df8866b --- /dev/null +++ b/scripts/lib/install-plugins.sh @@ -0,0 +1,74 @@ +#!/usr/bin/env bash +# Install Claude Code plugins from brain manifest +# Adds marketplaces, installs plugins, warns on SHA mismatch. +# +# CORRECTION B: real Claude CLI subcommand shape is +# `claude plugin marketplace add ` (NOT `claude marketplace add`) +# `claude plugin marketplace list` +# `claude plugin install ` +# `claude plugin info --json` +# Phase 0 discovery confirmed: `marketplace` is a child of `plugin`, not a +# top-level subcommand. The plan originally had the wrong shape. +# +# Usage: install-plugins.sh --marketplaces= --manifest= +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +source "$SCRIPT_DIR/common.sh" + +marketplaces="" +manifest="" + +while [ $# -gt 0 ]; do + case "$1" in + --marketplaces=*) marketplaces="${1#--marketplaces=}" ;; + --manifest=*) manifest="${1#--manifest=}" ;; + *) log_error "Unknown arg: $1"; exit 1 ;; + esac + shift +done + +[ -f "$marketplaces" ] || { log_error "Marketplaces file: $marketplaces not found"; exit 1; } +[ -f "$manifest" ] || { log_error "Manifest file: $manifest not found"; exit 1; } + +require_cmd jq || exit 1 + +# Fallback: if `claude` CLI is not in PATH (common on this Windows machine where +# the VSCode-extension binary lives outside %PATH%), log instructions and exit 0 +# so installer can continue with manual JSON edits. +if ! command -v claude >/dev/null 2>&1; then + log_warn "claude CLI not found — fallback: edit JSON configs manually and restart" + log_info "Required edits:" + log_info " 1. Copy $marketplaces -> ~/.claude/plugins/known_marketplaces.json" + log_info " 2. Copy $manifest -> ~/.claude/plugins/installed_plugins.json" + log_info " 3. Update ~/.claude/settings.json:enabledPlugins" + log_info " 4. Restart Claude Code" + exit 0 +fi + +# Add marketplaces (CORRECTION B: `claude plugin marketplace ...`) +for mp in $(jq -r 'keys[]' "$marketplaces"); do + repo=$(jq -r ".[\"$mp\"].source.repo" "$marketplaces") + if claude plugin marketplace list 2>/dev/null | grep -q "$mp"; then + log_info "Marketplace $mp already added" + else + log_info "Adding marketplace: $repo" + claude plugin marketplace add "$repo" || { log_error "plugin marketplace add failed: $repo"; exit 6; } + fi +done + +# Install plugins +for plugin in $(jq -r '.plugins | keys[]' "$manifest"); do + expected_sha=$(jq -r ".plugins[\"$plugin\"][0].gitCommitSha" "$manifest") + expected_ver=$(jq -r ".plugins[\"$plugin\"][0].version" "$manifest") + + log_info "Installing $plugin (pinned v$expected_ver, SHA $expected_sha)" + claude plugin install "$plugin" || { log_error "plugin install failed: $plugin"; exit 6; } + + actual_sha=$(claude plugin info "$plugin" --json 2>/dev/null | jq -r '.gitCommitSha // "unknown"') + if [ "$actual_sha" = "$expected_sha" ]; then + log_info " SHA match: $actual_sha" + else + log_warn " SHA mismatch (expected $expected_sha, got $actual_sha) — marketplace may have updated" + fi +done diff --git a/scripts/tests/install-plugins-test.sh b/scripts/tests/install-plugins-test.sh new file mode 100644 index 0000000..d2be9b5 --- /dev/null +++ b/scripts/tests/install-plugins-test.sh @@ -0,0 +1,83 @@ +#!/usr/bin/env bash +set -u +SCRIPT_DIR="$(cd "$(dirname "$0")/.." && pwd)" +source "$SCRIPT_DIR/lib/common.sh" + +FAILURES=0 +assert_eq() { + if [ "$1" = "$2" ]; then echo "PASS: $3"; else echo "FAIL: $3 (expected '$1', got '$2')"; FAILURES=$((FAILURES + 1)); fi +} + +# Setup: temp dir with fake `claude` CLI +# CORRECTION B: real subcommand structure is `claude plugin marketplace add` and +# `claude plugin install` (NOT `claude marketplace add`). Mock matches real shape. +tmpdir=$(mktemp -d) +mkdir "$tmpdir/bin" +cat > "$tmpdir/bin/claude" <<'CLAUDE_EOF' +#!/usr/bin/env bash +# Mock claude CLI: records calls to /tmp/claude-calls.log, then dispatches. +# Real subcommand shapes used by install-plugins.sh: +# claude plugin marketplace list -> stdout: existing marketplaces +# claude plugin marketplace add -> exit 0 on success +# claude plugin install -> exit 0 on success +# claude plugin info --json -> stdout: JSON with gitCommitSha +echo "claude $*" >> /tmp/claude-calls.log + +# Dispatch on first 3 args (plugin/marketplace family) +if [ "$1" = "plugin" ] && [ "$2" = "marketplace" ]; then + case "$3" in + list) ;; # print nothing (empty marketplace list) + add) exit 0 ;; # marketplace add success + esac +elif [ "$1" = "plugin" ] && [ "$2" = "install" ]; then + exit 0 # plugin install success +elif [ "$1" = "plugin" ] && [ "$2" = "info" ]; then + # Expect: claude plugin info --json + echo '{"gitCommitSha": "f2cbfbefebbfef77321e4c9abc9e949826bea9d7"}' +fi +exit 0 +CLAUDE_EOF +chmod +x "$tmpdir/bin/claude" + +# Create fixtures +cat > "$tmpdir/marketplaces.json" < "$tmpdir/plugins-manifest.json" <