diff --git a/CHANGELOG.md b/CHANGELOG.md index 6030702..8d5ee1d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,38 @@ All notable changes to the Leo Claude Marketplace will be documented in this fil The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). +## [Unreleased] + +### Added + +#### Plugin Installation Scripts +New scripts for installing marketplace plugins into consumer projects: + +- **`scripts/install-plugin.sh`** — Install a plugin to a consumer project + - Adds MCP server entry to target's `.mcp.json` (if plugin has MCP server) + - Appends integration snippet to target's `CLAUDE.md` + - Idempotent: safe to run multiple times + - Validates plugin exists and target path is valid + +- **`scripts/uninstall-plugin.sh`** — Remove a plugin from a consumer project + - Removes MCP server entry from `.mcp.json` + - Removes integration section from `CLAUDE.md` + +- **`scripts/list-installed.sh`** — Show installed plugins in a project + - Lists fully installed, partially installed, and available plugins + - Shows plugin versions and descriptions + +**Usage:** +```bash +./scripts/install-plugin.sh data-platform ~/projects/personal-portfolio +./scripts/list-installed.sh ~/projects/personal-portfolio +./scripts/uninstall-plugin.sh data-platform ~/projects/personal-portfolio +``` + +**Documentation:** `docs/CONFIGURATION.md` updated with "Installing Plugins to Consumer Projects" section. + +--- + ## [5.8.0] - 2026-02-02 ### Added diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md index 51b9201..abf7c6d 100644 --- a/docs/CONFIGURATION.md +++ b/docs/CONFIGURATION.md @@ -415,6 +415,87 @@ The command auto-detects that system config exists and runs quick project setup. --- +## Installing Plugins to Consumer Projects + +The marketplace provides scripts to install plugins into consumer projects. This sets up the MCP server connections and adds CLAUDE.md integration snippets. + +### Install a Plugin + +```bash +cd /path/to/leo-claude-mktplace +./scripts/install-plugin.sh +``` + +**Examples:** +```bash +# Install data-platform to a portfolio project +./scripts/install-plugin.sh data-platform ~/projects/personal-portfolio + +# Install multiple plugins +./scripts/install-plugin.sh viz-platform ~/projects/personal-portfolio +./scripts/install-plugin.sh projman ~/projects/personal-portfolio +``` + +**What it does:** +1. Validates the plugin exists in the marketplace +2. Adds MCP server entry to target's `.mcp.json` (if plugin has MCP server) +3. Appends integration snippet to target's `CLAUDE.md` +4. Reports changes and lists available commands + +**After installation:** Restart your Claude Code session for MCP tools to become available. + +### Uninstall a Plugin + +```bash +./scripts/uninstall-plugin.sh +``` + +Removes the MCP server entry and CLAUDE.md integration section. + +### List Installed Plugins + +```bash +./scripts/list-installed.sh +``` + +Shows which marketplace plugins are installed, partially installed, or available. + +**Output example:** +``` +✓ Fully Installed: + PLUGIN VERSION DESCRIPTION + ------ ------- ----------- + data-platform 1.3.0 pandas, PostgreSQL, and dbt integration... + viz-platform 1.1.0 DMC validation, Plotly charts, and theming... + +○ Available (not installed): + projman 3.4.0 Sprint planning and project management... +``` + +### Plugins with MCP Servers + +Not all plugins have MCP servers. The install script handles this automatically: + +| Plugin | Has MCP Server | Notes | +|--------|---------------|-------| +| data-platform | ✓ | pandas, PostgreSQL, dbt tools | +| viz-platform | ✓ | DMC validation, chart, theme tools | +| contract-validator | ✓ | Plugin compatibility validation | +| cmdb-assistant | ✓ (via netbox) | NetBox CMDB tools | +| projman | ✓ (via gitea) | Issue, wiki, PR tools | +| pr-review | ✓ (via gitea) | PR review tools | +| git-flow | ✗ | Commands only | +| doc-guardian | ✗ | Commands and hooks only | +| code-sentinel | ✗ | Commands and hooks only | +| clarity-assist | ✗ | Commands only | + +### Script Requirements + +- **jq** must be installed (`sudo apt install jq`) +- Scripts are idempotent (safe to run multiple times) + +--- + ## Automatic Validation Features ### API Validation diff --git a/scripts/install-plugin.sh b/scripts/install-plugin.sh new file mode 100755 index 0000000..7740d5f --- /dev/null +++ b/scripts/install-plugin.sh @@ -0,0 +1,337 @@ +#!/usr/bin/env bash +# ============================================================================= +# install-plugin.sh - Install marketplace plugin to a consumer project +# ============================================================================= +# +# Usage: ./scripts/install-plugin.sh +# +# This script: +# 1. Validates plugin exists in the marketplace +# 2. Updates target project's .mcp.json with MCP server entry (if applicable) +# 3. Appends CLAUDE.md integration snippet to target project +# 4. Is idempotent (safe to run multiple times) +# +# Examples: +# ./scripts/install-plugin.sh data-platform ~/projects/personal-portfolio +# ./scripts/install-plugin.sh projman /home/user/my-project +# +# ============================================================================= +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(dirname "$SCRIPT_DIR")" + +# --- Color Definitions --- +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +CYAN='\033[0;36m' +NC='\033[0m' # No Color + +# --- Logging Functions --- +log_info() { echo -e "${BLUE}[INFO]${NC} $1"; } +log_success() { echo -e "${GREEN}[OK]${NC} $1"; } +log_skip() { echo -e "${YELLOW}[SKIP]${NC} $1"; } +log_error() { echo -e "${RED}[ERROR]${NC} $1"; } +log_warning() { echo -e "${YELLOW}[WARN]${NC} $1"; } + +# --- Track Changes --- +CHANGES_MADE=() +SKIPPED=() + +# --- Usage --- +usage() { + echo "Usage: $0 " + echo "" + echo "Install a marketplace plugin to a consumer project." + echo "" + echo "Arguments:" + echo " plugin-name Name of the plugin (e.g., data-platform, viz-platform, projman)" + echo " target-project-path Path to the target project (absolute or relative)" + echo "" + echo "Available plugins:" + for dir in "$REPO_ROOT"/plugins/*/; do + if [[ -d "$dir" ]]; then + basename "$dir" + fi + done + echo "" + echo "Examples:" + echo " $0 data-platform ~/projects/personal-portfolio" + echo " $0 projman /home/user/my-project" + exit 1 +} + +# --- Prerequisite Check --- +check_prerequisites() { + if ! command -v jq &> /dev/null; then + log_error "jq is required but not installed." + echo "Install with: sudo apt install jq" + exit 1 + fi +} + +# --- Validate Plugin Exists --- +validate_plugin() { + local plugin_name="$1" + local plugin_dir="$REPO_ROOT/plugins/$plugin_name" + + if [[ ! -d "$plugin_dir" ]]; then + log_error "Plugin '$plugin_name' not found in $REPO_ROOT/plugins/" + echo "" + echo "Available plugins:" + for dir in "$REPO_ROOT"/plugins/*/; do + if [[ -d "$dir" ]]; then + echo " - $(basename "$dir")" + fi + done + exit 1 + fi + + if [[ ! -f "$plugin_dir/.claude-plugin/plugin.json" ]]; then + log_error "Plugin '$plugin_name' missing .claude-plugin/plugin.json" + exit 1 + fi + + log_success "Plugin '$plugin_name' found" +} + +# --- Validate Target Project --- +validate_target() { + local target_path="$1" + + if [[ ! -d "$target_path" ]]; then + log_error "Target project path does not exist: $target_path" + exit 1 + fi + + log_success "Target project found: $target_path" + + # Warn if no CLAUDE.md + if [[ ! -f "$target_path/CLAUDE.md" ]]; then + log_warning "Target project has no CLAUDE.md - will create one" + fi +} + +# --- Check if MCP Server Exists for Plugin --- +has_mcp_server() { + local plugin_name="$1" + local mcp_dir="$REPO_ROOT/mcp-servers/$plugin_name" + + if [[ -d "$mcp_dir" && -f "$mcp_dir/run.sh" ]]; then + return 0 + fi + return 1 +} + +# --- Update .mcp.json --- +update_mcp_json() { + local plugin_name="$1" + local target_path="$2" + local mcp_json="$target_path/.mcp.json" + local mcp_server_path="$REPO_ROOT/mcp-servers/$plugin_name/run.sh" + + # Check if plugin has MCP server + if ! has_mcp_server "$plugin_name"; then + log_skip "Plugin '$plugin_name' has no MCP server - skipping .mcp.json update" + SKIPPED+=(".mcp.json: No MCP server for $plugin_name") + return 0 + fi + + # Create .mcp.json if it doesn't exist + if [[ ! -f "$mcp_json" ]]; then + log_info "Creating new .mcp.json" + echo '{"mcpServers":{}}' > "$mcp_json" + CHANGES_MADE+=("Created .mcp.json") + fi + + # Check if entry already exists + if jq -e ".mcpServers[\"$plugin_name\"]" "$mcp_json" > /dev/null 2>&1; then + log_skip "MCP server '$plugin_name' already in .mcp.json" + SKIPPED+=(".mcp.json: $plugin_name already present") + return 0 + fi + + # Add MCP server entry + log_info "Adding MCP server '$plugin_name' to .mcp.json" + local tmp_file=$(mktemp) + jq ".mcpServers[\"$plugin_name\"] = {\"command\": \"$mcp_server_path\", \"args\": []}" "$mcp_json" > "$tmp_file" + mv "$tmp_file" "$mcp_json" + + CHANGES_MADE+=("Added $plugin_name to .mcp.json") + log_success "Added MCP server entry for '$plugin_name'" +} + +# --- Update CLAUDE.md --- +update_claude_md() { + local plugin_name="$1" + local target_path="$2" + local target_claude_md="$target_path/CLAUDE.md" + local integration_file="$REPO_ROOT/plugins/$plugin_name/claude-md-integration.md" + + # Check if integration file exists + if [[ ! -f "$integration_file" ]]; then + log_skip "No claude-md-integration.md for plugin '$plugin_name'" + SKIPPED+=("CLAUDE.md: No integration snippet for $plugin_name") + return 0 + fi + + # Create CLAUDE.md if it doesn't exist + if [[ ! -f "$target_claude_md" ]]; then + log_info "Creating new CLAUDE.md" + cat > "$target_claude_md" << 'EOF' +# CLAUDE.md + +This file provides guidance to Claude Code when working with code in this repository. + +EOF + CHANGES_MADE+=("Created CLAUDE.md") + fi + + # Read integration content + local integration_content + integration_content=$(cat "$integration_file") + + # Extract the first line (# header) to use as marker for detection + local header_marker + header_marker=$(head -1 "$integration_file") + + # Check if already integrated - look for the integration file's header + # Handles both formats: "# {name} Plugin - CLAUDE.md Integration" and "# {name} CLAUDE.md Integration" + if grep -qE "^# ${plugin_name}( Plugin)? -? ?CLAUDE\.md Integration" "$target_claude_md" 2>/dev/null; then + log_skip "Plugin '$plugin_name' integration already in CLAUDE.md" + SKIPPED+=("CLAUDE.md: $plugin_name already present") + return 0 + fi + + # Check for or create Marketplace Plugin Integration section + local section_header="## Marketplace Plugin Integration" + + if ! grep -qF "$section_header" "$target_claude_md"; then + log_info "Creating '$section_header' section" + echo "" >> "$target_claude_md" + echo "$section_header" >> "$target_claude_md" + echo "" >> "$target_claude_md" + echo "The following plugins are installed from the leo-claude-mktplace:" >> "$target_claude_md" + echo "" >> "$target_claude_md" + fi + + # Append integration content + log_info "Adding '$plugin_name' integration to CLAUDE.md" + echo "" >> "$target_claude_md" + echo "---" >> "$target_claude_md" + echo "" >> "$target_claude_md" + echo "$integration_content" >> "$target_claude_md" + + CHANGES_MADE+=("Added $plugin_name integration to CLAUDE.md") + log_success "Added CLAUDE.md integration for '$plugin_name'" +} + +# --- Get Commands for Plugin --- +get_plugin_commands() { + local plugin_name="$1" + local commands_dir="$REPO_ROOT/plugins/$plugin_name/commands" + + if [[ ! -d "$commands_dir" ]]; then + return + fi + + for cmd_file in "$commands_dir"/*.md; do + if [[ -f "$cmd_file" ]]; then + local cmd_name + cmd_name=$(basename "$cmd_file" .md) + echo " /$cmd_name" + fi + done +} + +# --- Print Summary --- +print_summary() { + local plugin_name="$1" + local target_path="$2" + + echo "" + echo "==============================================" + echo -e "${GREEN}Installation Summary${NC}" + echo "==============================================" + echo "" + echo -e "${CYAN}Plugin:${NC} $plugin_name" + echo -e "${CYAN}Target:${NC} $target_path" + echo "" + + if [[ ${#CHANGES_MADE[@]} -gt 0 ]]; then + echo -e "${GREEN}Changes Made:${NC}" + for change in "${CHANGES_MADE[@]}"; do + echo " ✓ $change" + done + echo "" + fi + + if [[ ${#SKIPPED[@]} -gt 0 ]]; then + echo -e "${YELLOW}Skipped (already present or N/A):${NC}" + for skip in "${SKIPPED[@]}"; do + echo " - $skip" + done + echo "" + fi + + # Show available commands + echo -e "${CYAN}Commands Now Available:${NC}" + local commands + commands=$(get_plugin_commands "$plugin_name") + if [[ -n "$commands" ]]; then + echo "$commands" + else + echo " (No commands - this plugin may be hooks-only)" + fi + echo "" + + # MCP tools reminder + if has_mcp_server "$plugin_name"; then + echo -e "${CYAN}MCP Tools:${NC}" + echo " This plugin includes MCP server tools. Use ToolSearch to discover them." + echo "" + fi + + # Important reminder + echo -e "${YELLOW}⚠️ IMPORTANT:${NC}" + echo " Restart your Claude Code session for changes to take effect." + echo " The .mcp.json changes require a session restart to load MCP servers." + echo "" +} + +# ============================================================================= +# Main Execution +# ============================================================================= + +# Check arguments +if [[ $# -lt 2 ]]; then + usage +fi + +PLUGIN_NAME="$1" +TARGET_PATH="$2" + +# Resolve target path to absolute +TARGET_PATH=$(cd "$TARGET_PATH" 2>/dev/null && pwd || echo "$TARGET_PATH") + +echo "" +echo "==============================================" +echo -e "${BLUE}Installing Plugin: $PLUGIN_NAME${NC}" +echo "==============================================" +echo "" + +# Run checks +check_prerequisites +validate_plugin "$PLUGIN_NAME" +validate_target "$TARGET_PATH" + +echo "" + +# Perform installation +update_mcp_json "$PLUGIN_NAME" "$TARGET_PATH" +update_claude_md "$PLUGIN_NAME" "$TARGET_PATH" + +# Print summary +print_summary "$PLUGIN_NAME" "$TARGET_PATH" diff --git a/scripts/list-installed.sh b/scripts/list-installed.sh new file mode 100755 index 0000000..0233c16 --- /dev/null +++ b/scripts/list-installed.sh @@ -0,0 +1,293 @@ +#!/usr/bin/env bash +# ============================================================================= +# list-installed.sh - Show installed marketplace plugins in a project +# ============================================================================= +# +# Usage: ./scripts/list-installed.sh +# +# This script: +# 1. Checks .mcp.json for MCP server entries from this marketplace +# 2. Checks CLAUDE.md for plugin integration sections +# 3. Reports which plugins are installed +# +# Examples: +# ./scripts/list-installed.sh ~/projects/personal-portfolio +# ./scripts/list-installed.sh . +# +# ============================================================================= +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(dirname "$SCRIPT_DIR")" + +# --- Color Definitions --- +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +CYAN='\033[0;36m' +NC='\033[0m' # No Color + +# --- Logging Functions --- +log_info() { echo -e "${BLUE}[INFO]${NC} $1"; } +log_success() { echo -e "${GREEN}[OK]${NC} $1"; } +log_error() { echo -e "${RED}[ERROR]${NC} $1"; } +log_warning() { echo -e "${YELLOW}[WARN]${NC} $1"; } + +# --- Usage --- +usage() { + echo "Usage: $0 " + echo "" + echo "Show which marketplace plugins are installed in a project." + echo "" + echo "Arguments:" + echo " target-project-path Path to the target project (absolute or relative)" + echo "" + echo "Examples:" + echo " $0 ~/projects/personal-portfolio" + echo " $0 ." + exit 1 +} + +# --- Prerequisite Check --- +check_prerequisites() { + if ! command -v jq &> /dev/null; then + log_error "jq is required but not installed." + echo "Install with: sudo apt install jq" + exit 1 + fi +} + +# --- Get Available Plugins --- +get_available_plugins() { + for dir in "$REPO_ROOT"/plugins/*/; do + if [[ -d "$dir" ]]; then + basename "$dir" + fi + done +} + +# --- Get Plugins with MCP Servers --- +get_mcp_plugins() { + for dir in "$REPO_ROOT"/mcp-servers/*/; do + if [[ -d "$dir" && -f "$dir/run.sh" ]]; then + basename "$dir" + fi + done +} + +# --- Check MCP Installation --- +check_mcp_installed() { + local plugin_name="$1" + local target_path="$2" + local mcp_json="$target_path/.mcp.json" + + if [[ ! -f "$mcp_json" ]]; then + return 1 + fi + + # Check if this plugin's MCP server is referenced + if jq -e ".mcpServers[\"$plugin_name\"]" "$mcp_json" > /dev/null 2>&1; then + return 0 + fi + + # Also check if any entry points to this marketplace's mcp-servers + if grep -q "leo-claude-mktplace.*mcp-servers/$plugin_name" "$mcp_json" 2>/dev/null || \ + grep -q "claude-plugins-work.*mcp-servers/$plugin_name" "$mcp_json" 2>/dev/null; then + return 0 + fi + + return 1 +} + +# --- Check CLAUDE.md Integration --- +check_claude_md_installed() { + local plugin_name="$1" + local target_path="$2" + local target_claude_md="$target_path/CLAUDE.md" + + if [[ ! -f "$target_claude_md" ]]; then + return 1 + fi + + # Look for plugin-specific headers that install-plugin.sh adds + # Formats vary by plugin: + # "# {plugin-name} Plugin - CLAUDE.md Integration" + # "# {plugin-name} CLAUDE.md Integration" + # Use regex to match both patterns + if grep -qE "^# ${plugin_name}( Plugin)? -? ?CLAUDE\.md Integration" "$target_claude_md" 2>/dev/null; then + return 0 + fi + + return 1 +} + +# --- Get Plugin Version --- +get_plugin_version() { + local plugin_name="$1" + local plugin_json="$REPO_ROOT/plugins/$plugin_name/.claude-plugin/plugin.json" + + if [[ -f "$plugin_json" ]]; then + jq -r '.version // "unknown"' "$plugin_json" + else + echo "unknown" + fi +} + +# --- Get Plugin Description --- +get_plugin_description() { + local plugin_name="$1" + local plugin_json="$REPO_ROOT/plugins/$plugin_name/.claude-plugin/plugin.json" + + if [[ -f "$plugin_json" ]]; then + jq -r '.description // "No description"' "$plugin_json" | cut -c1-60 + else + echo "No description" + fi +} + +# ============================================================================= +# Main Execution +# ============================================================================= + +# Check arguments +if [[ $# -lt 1 ]]; then + usage +fi + +TARGET_PATH="$1" + +# Resolve target path to absolute +if [[ -d "$TARGET_PATH" ]]; then + TARGET_PATH=$(cd "$TARGET_PATH" && pwd) +else + log_error "Target project path does not exist: $TARGET_PATH" + exit 1 +fi + +check_prerequisites + +echo "" +echo "==============================================" +echo -e "${BLUE}Installed Plugins: $(basename "$TARGET_PATH")${NC}" +echo "==============================================" +echo -e "${CYAN}Target:${NC} $TARGET_PATH" +echo "" + +# Collect results +declare -A INSTALLED_MCP +declare -A INSTALLED_CLAUDE_MD +INSTALLED_PLUGINS=() +PARTIAL_PLUGINS=() +NOT_INSTALLED=() + +# Check each available plugin +for plugin in $(get_available_plugins); do + mcp_installed=false + claude_installed=false + has_mcp_server=false + + # Check if plugin has MCP server + if [[ -d "$REPO_ROOT/mcp-servers/$plugin" ]]; then + has_mcp_server=true + fi + + # Check MCP installation (only if plugin has MCP server) + if $has_mcp_server && check_mcp_installed "$plugin" "$TARGET_PATH"; then + mcp_installed=true + INSTALLED_MCP[$plugin]=true + fi + + # Check CLAUDE.md integration + if check_claude_md_installed "$plugin" "$TARGET_PATH"; then + claude_installed=true + INSTALLED_CLAUDE_MD[$plugin]=true + fi + + # Categorize + if $mcp_installed || $claude_installed; then + if $has_mcp_server; then + if $mcp_installed && $claude_installed; then + INSTALLED_PLUGINS+=("$plugin") + else + PARTIAL_PLUGINS+=("$plugin") + fi + else + # Plugins without MCP servers just need CLAUDE.md + if $claude_installed; then + INSTALLED_PLUGINS+=("$plugin") + fi + fi + else + NOT_INSTALLED+=("$plugin") + fi +done + +# Print fully installed plugins +if [[ ${#INSTALLED_PLUGINS[@]} -gt 0 ]]; then + echo -e "${GREEN}✓ Fully Installed:${NC}" + echo "" + printf " %-24s %-10s %s\n" "PLUGIN" "VERSION" "DESCRIPTION" + printf " %-24s %-10s %s\n" "------" "-------" "-----------" + for plugin in "${INSTALLED_PLUGINS[@]}"; do + version=$(get_plugin_version "$plugin") + desc=$(get_plugin_description "$plugin") + printf " %-24s %-10s %s\n" "$plugin" "$version" "$desc" + done + echo "" +fi + +# Print partially installed plugins +if [[ ${#PARTIAL_PLUGINS[@]} -gt 0 ]]; then + echo -e "${YELLOW}⚠ Partially Installed:${NC}" + echo "" + for plugin in "${PARTIAL_PLUGINS[@]}"; do + version=$(get_plugin_version "$plugin") + echo " $plugin (v$version)" + if [[ -v INSTALLED_MCP[$plugin] ]]; then + echo " ✓ MCP server configured in .mcp.json" + else + echo " ✗ MCP server NOT in .mcp.json" + fi + if [[ -v INSTALLED_CLAUDE_MD[$plugin] ]]; then + echo " ✓ Integration in CLAUDE.md" + else + echo " ✗ Integration NOT in CLAUDE.md" + fi + echo "" + done + echo " Run install-plugin.sh to complete installation." + echo "" +fi + +# Print available but not installed +if [[ ${#NOT_INSTALLED[@]} -gt 0 ]]; then + echo -e "${BLUE}○ Available (not installed):${NC}" + echo "" + for plugin in "${NOT_INSTALLED[@]}"; do + version=$(get_plugin_version "$plugin") + desc=$(get_plugin_description "$plugin") + printf " %-24s %-10s %s\n" "$plugin" "$version" "$desc" + done + echo "" +fi + +# Summary +echo "----------------------------------------------" +total_available=$(get_available_plugins | wc -l) +total_installed=${#INSTALLED_PLUGINS[@]} +total_partial=${#PARTIAL_PLUGINS[@]} + +echo -e "Total: ${GREEN}$total_installed installed${NC}" +if [[ $total_partial -gt 0 ]]; then + echo -e " ${YELLOW}$total_partial partial${NC}" +fi +echo " $((total_available - total_installed - total_partial)) available" +echo "" + +# Install hint +if [[ ${#NOT_INSTALLED[@]} -gt 0 ]]; then + echo "To install a plugin:" + echo " $SCRIPT_DIR/install-plugin.sh $TARGET_PATH" + echo "" +fi diff --git a/scripts/uninstall-plugin.sh b/scripts/uninstall-plugin.sh new file mode 100755 index 0000000..8949779 --- /dev/null +++ b/scripts/uninstall-plugin.sh @@ -0,0 +1,266 @@ +#!/usr/bin/env bash +# ============================================================================= +# uninstall-plugin.sh - Remove marketplace plugin from a consumer project +# ============================================================================= +# +# Usage: ./scripts/uninstall-plugin.sh +# +# This script: +# 1. Removes MCP server entry from target project's .mcp.json +# 2. Removes CLAUDE.md integration section for the plugin +# 3. Is idempotent (safe to run multiple times) +# +# Examples: +# ./scripts/uninstall-plugin.sh data-platform ~/projects/personal-portfolio +# ./scripts/uninstall-plugin.sh projman /home/user/my-project +# +# ============================================================================= +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(dirname "$SCRIPT_DIR")" + +# --- Color Definitions --- +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +CYAN='\033[0;36m' +NC='\033[0m' # No Color + +# --- Logging Functions --- +log_info() { echo -e "${BLUE}[INFO]${NC} $1"; } +log_success() { echo -e "${GREEN}[OK]${NC} $1"; } +log_skip() { echo -e "${YELLOW}[SKIP]${NC} $1"; } +log_error() { echo -e "${RED}[ERROR]${NC} $1"; } +log_warning() { echo -e "${YELLOW}[WARN]${NC} $1"; } + +# --- Track Changes --- +CHANGES_MADE=() +SKIPPED=() + +# --- Usage --- +usage() { + echo "Usage: $0 " + echo "" + echo "Remove a marketplace plugin from a consumer project." + echo "" + echo "Arguments:" + echo " plugin-name Name of the plugin (e.g., data-platform, viz-platform, projman)" + echo " target-project-path Path to the target project (absolute or relative)" + echo "" + echo "Examples:" + echo " $0 data-platform ~/projects/personal-portfolio" + echo " $0 projman /home/user/my-project" + exit 1 +} + +# --- Prerequisite Check --- +check_prerequisites() { + if ! command -v jq &> /dev/null; then + log_error "jq is required but not installed." + echo "Install with: sudo apt install jq" + exit 1 + fi +} + +# --- Validate Target Project --- +validate_target() { + local target_path="$1" + + if [[ ! -d "$target_path" ]]; then + log_error "Target project path does not exist: $target_path" + exit 1 + fi + + log_success "Target project found: $target_path" +} + +# --- Remove from .mcp.json --- +remove_from_mcp_json() { + local plugin_name="$1" + local target_path="$2" + local mcp_json="$target_path/.mcp.json" + + # Check if .mcp.json exists + if [[ ! -f "$mcp_json" ]]; then + log_skip "No .mcp.json found - nothing to remove" + SKIPPED+=(".mcp.json: File does not exist") + return 0 + fi + + # Check if entry exists + if ! jq -e ".mcpServers[\"$plugin_name\"]" "$mcp_json" > /dev/null 2>&1; then + log_skip "MCP server '$plugin_name' not in .mcp.json" + SKIPPED+=(".mcp.json: $plugin_name not present") + return 0 + fi + + # Remove MCP server entry + log_info "Removing MCP server '$plugin_name' from .mcp.json" + local tmp_file=$(mktemp) + jq "del(.mcpServers[\"$plugin_name\"])" "$mcp_json" > "$tmp_file" + mv "$tmp_file" "$mcp_json" + + CHANGES_MADE+=("Removed $plugin_name from .mcp.json") + log_success "Removed MCP server entry for '$plugin_name'" +} + +# --- Remove from CLAUDE.md --- +remove_from_claude_md() { + local plugin_name="$1" + local target_path="$2" + local target_claude_md="$target_path/CLAUDE.md" + + # Check if CLAUDE.md exists + if [[ ! -f "$target_claude_md" ]]; then + log_skip "No CLAUDE.md found - nothing to remove" + SKIPPED+=("CLAUDE.md: File does not exist") + return 0 + fi + + # Look for the plugin section header (handles multiple formats) + # Formats: "# {plugin-name} Plugin - CLAUDE.md Integration" or "# {plugin-name} CLAUDE.md Integration" + local section_header + section_header=$(grep -E "^# ${plugin_name}( Plugin)? -? ?CLAUDE\.md Integration" "$target_claude_md" 2>/dev/null | head -1) + + if [[ -z "$section_header" ]]; then + log_skip "Plugin '$plugin_name' section not found in CLAUDE.md" + SKIPPED+=("CLAUDE.md: $plugin_name section not found") + return 0 + fi + + log_info "Removing '$plugin_name' section from CLAUDE.md" + + # Create temp file and use awk to remove section + # Remove from header to next "---" divider or next plugin header + local tmp_file=$(mktemp) + + awk -v header="$section_header" ' + BEGIN { skip = 0; found = 0; in_code_block = 0 } + { + # Track code blocks (``` markers) + if (/^```/) { + in_code_block = !in_code_block + } + + # Check if this is the section header we want to remove + if ($0 == header) { + skip = 1 + found = 1 + next + } + + # Check if this is a horizontal rule (---) - only count if not in code block + is_hr = /^---[[:space:]]*$/ && !in_code_block + + # Check if this is a new plugin section header (only outside code blocks) + # Patterns: "# {name} Plugin - CLAUDE.md Integration" or "# {name} CLAUDE.md Integration" + is_new_plugin_section = /^# [a-z-]+( Plugin)? -? ?CLAUDE\.md Integration/ && !in_code_block && $0 != header + + if (skip) { + # Stop skipping when we hit --- (outside code block) or a new plugin section + if (is_hr) { + # This --- ends the section we are removing, skip it too + skip = 0 + next + } + if (is_new_plugin_section) { + # New plugin section starts, stop skipping and print it + skip = 0 + print + } + next + } + + # Not skipping - print the line + print + } + END { if (!found) exit 1 } + ' "$target_claude_md" > "$tmp_file" 2>/dev/null + + if [[ $? -eq 0 ]]; then + # Clean up multiple consecutive blank lines + awk 'NF{blank=0} !NF{blank++} blank<=2' "$tmp_file" > "${tmp_file}.clean" + mv "${tmp_file}.clean" "$target_claude_md" + rm -f "$tmp_file" + CHANGES_MADE+=("Removed $plugin_name section from CLAUDE.md") + log_success "Removed CLAUDE.md section for '$plugin_name'" + else + rm -f "$tmp_file" + log_skip "Could not locate exact section boundaries in CLAUDE.md" + log_warning "You may need to manually remove the $plugin_name section" + SKIPPED+=("CLAUDE.md: Manual removal may be needed") + fi +} + +# --- Print Summary --- +print_summary() { + local plugin_name="$1" + local target_path="$2" + + echo "" + echo "==============================================" + echo -e "${GREEN}Uninstallation Summary${NC}" + echo "==============================================" + echo "" + echo -e "${CYAN}Plugin:${NC} $plugin_name" + echo -e "${CYAN}Target:${NC} $target_path" + echo "" + + if [[ ${#CHANGES_MADE[@]} -gt 0 ]]; then + echo -e "${GREEN}Changes Made:${NC}" + for change in "${CHANGES_MADE[@]}"; do + echo " ✓ $change" + done + echo "" + fi + + if [[ ${#SKIPPED[@]} -gt 0 ]]; then + echo -e "${YELLOW}Skipped (not present or N/A):${NC}" + for skip in "${SKIPPED[@]}"; do + echo " - $skip" + done + echo "" + fi + + if [[ ${#CHANGES_MADE[@]} -gt 0 ]]; then + echo -e "${YELLOW}⚠️ IMPORTANT:${NC}" + echo " Restart your Claude Code session for changes to take effect." + echo "" + fi +} + +# ============================================================================= +# Main Execution +# ============================================================================= + +# Check arguments +if [[ $# -lt 2 ]]; then + usage +fi + +PLUGIN_NAME="$1" +TARGET_PATH="$2" + +# Resolve target path to absolute +TARGET_PATH=$(cd "$TARGET_PATH" 2>/dev/null && pwd || echo "$TARGET_PATH") + +echo "" +echo "==============================================" +echo -e "${BLUE}Uninstalling Plugin: $PLUGIN_NAME${NC}" +echo "==============================================" +echo "" + +# Run checks +check_prerequisites +validate_target "$TARGET_PATH" + +echo "" + +# Perform uninstallation +remove_from_mcp_json "$PLUGIN_NAME" "$TARGET_PATH" +remove_from_claude_md "$PLUGIN_NAME" "$TARGET_PATH" + +# Print summary +print_summary "$PLUGIN_NAME" "$TARGET_PATH"