diff --git a/CHANGELOG.md b/CHANGELOG.md index 00744f5..eb7a1c7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Content Versions +- **v53**: Rewrite `/claude-code-setup` to 3-phase flow ([Record 040](docs/records/040-setup-command-ux.md)) + - Discovery script (`lib/setup-status.sh`) replaces 8-12 Bash calls with single JSON output + - New flags: `--remove-skill` and `--remove-mcp` for non-interactive module removal + - Any scenario completes in 2 permission prompts (down from ~12-18) - **v52**: Generalize `/delegate` to support any task type - Documentation clarified: works for coding, research, analysis, information gathering, documentation - Examples updated to include non-development tasks (sports analysis, competitor pricing, academic research) diff --git a/CLAUDE.md b/CLAUDE.md index 0131a03..a6ed822 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -98,6 +98,7 @@ The installer is a modular Bash script. `install.sh` is the entry point, sourcin | `statusline.sh` | ccstatusline configuration | | `hooks.sh` | Claude Code hooks setup | | `agent-teams.sh` | Agent Teams env var toggle in settings.json | +| `setup-status.sh` | Discovery script for `/claude-code-setup` — outputs JSON status (standalone, NOT sourced by install.sh) | **Install flow:** detect OS → select modules (interactive toggle) → copy commands to `~/.claude/commands/` → install MCP configs to `~/.claude.json` → copy skills to `~/.claude/skills/` → build global CLAUDE.md from template + dynamic tables → install external plugins → configure statusline/hooks/agent-teams. diff --git a/README.md b/README.md index b220200..564c3f9 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ [![macOS](https://img.shields.io/badge/platform-macOS-blue.svg)](https://www.apple.com/macos/) [![Linux](https://img.shields.io/badge/platform-Linux-blue.svg)](https://www.linux.org/) [![WSL](https://img.shields.io/badge/platform-WSL-blue.svg)](https://docs.microsoft.com/en-us/windows/wsl/) -[![Content v52](https://img.shields.io/badge/content-v52-blue.svg)](CHANGELOG.md) +[![Content v53](https://img.shields.io/badge/content-v53-blue.svg)](CHANGELOG.md) **Persistent memory for Claude Code via Markdown files.** diff --git a/commands/claude-code-setup.md b/commands/claude-code-setup.md index d948d50..2e2ed16 100644 --- a/commands/claude-code-setup.md +++ b/commands/claude-code-setup.md @@ -1,355 +1,287 @@ # Claude Code Setup -Manage your claude-code-setup installation: check status, upgrade, and install modules. - -## Tasks - -### Phase 1: Check Status - -1. **Check current version** - - Read `content_version` from `~/.claude/installed.json` - - If file doesn't exist, inform user to run install.sh first - -2. **Fetch latest version** - ```bash - curl -fsSL https://raw.githubusercontent.com/b33eep/claude-code-setup/main/templates/VERSION - ``` +Manage your claude-code-setup installation: check status, upgrade, install, and remove modules. -3. **Clone repo to temp** (needed for module discovery) - ```bash - temp_dir=$(mktemp -d /tmp/claude-setup-XXXXXX) - git clone --depth 1 https://github.com/b33eep/claude-code-setup.git "$temp_dir" - ``` - -4. **Discover available modules** - ```bash - # Available skills - ls -1 "$temp_dir/skills/" - - # Available MCP servers - ls -1 "$temp_dir/mcp/" - - # Available external plugins - jq -r '.plugins[].id' "$temp_dir/external-plugins.json" - ``` - -5. **Get installed modules** - ```bash - jq -r '.skills[]' ~/.claude/installed.json 2>/dev/null || echo "(none)" - jq -r '.mcp[]' ~/.claude/installed.json 2>/dev/null || echo "(none)" - jq -r '.external_plugins[]' ~/.claude/installed.json 2>/dev/null || echo "(none)" - - # Also check what plugins are actually installed via claude CLI - claude plugin list 2>/dev/null || echo "(claude CLI not available)" - ``` - -6. **Check for new modules** and compare versions - - Find modules NOT in installed.json (delta) - - Determine if upgrade is needed (current < latest) - - Fetch CHANGELOG.md from GitHub to show changes - -7. **Check Agent Teams status** - ```bash - jq -e '.env.CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS' ~/.claude/settings.json 2>/dev/null - ``` - - If key exists → Agent Teams already configured - - If key missing or file missing → Agent Teams not configured (can be offered) - -8. **Check custom repo** (if exists) - - If `~/.claude/custom` exists: - ```bash - # Fetch latest from remote - git -C ~/.claude/custom fetch origin 2>/dev/null - - # Get local VERSION - local_version=$(cat ~/.claude/custom/VERSION 2>/dev/null || echo "0") - - # Get remote VERSION - remote_version=$(git -C ~/.claude/custom show origin/main:VERSION 2>/dev/null || echo "0") - ``` - - Compare local vs remote VERSION - - If remote > local: custom update available - - Read `custom_version` from installed.json for comparison - - List uninstalled custom modules (custom:* not in installed.json) - -### Phase 2: Show Status & Ask User - -9. **Present findings to user** - - Show a summary like this: - ``` - claude-code-setup status: - - Base: v8 installed, v9 available - - Custom: v1 installed, v2 available - - Agent Teams: not configured - - Modules available to install: - Skills: - - skill-creator (Create custom skills) - - custom:standards-java (Java standards) - - MCP Servers: - - brave-search (Web search via Brave) - - External Plugins: - - code-review-ai (AI-powered architectural review) - - What would you like to do? - ``` - - Include Agent Teams line: - - `Agent Teams: enabled` (if key exists in settings.json) - - `Agent Teams: not configured` (if key missing) - -10. **STOP and ask user** (use AskUserQuestion tool) - - Options depend on what's available: - - "Upgrade base" (if base update available) - - "Upgrade custom" (if custom update available) - - "Install modules" (if uninstalled modules exist) - - "Enable Agent Teams" (if not configured — required for /with-advisor and /delegate) - - "Remove modules" (if modules are installed) - - "Nothing" - - Combine options as appropriate (e.g., "Upgrade base + custom + install modules") - -### Phase 3: Execute User's Choice - -11. **Perform base upgrade** (if requested) - ```bash - cd "$temp_dir" && ./install.sh --update --yes - ``` - -12. **Perform custom upgrade** (if requested) - ```bash - git -C ~/.claude/custom pull - # Update custom_version in installed.json - if [[ -f ~/.claude/custom/VERSION ]] && [[ -f ~/.claude/installed.json ]]; then - new_version=$(cat ~/.claude/custom/VERSION 2>/dev/null || echo "0") - jq --arg v "$new_version" '.custom_version = ($v | tonumber)' \ - ~/.claude/installed.json > ~/.claude/installed.json.tmp && mv ~/.claude/installed.json.tmp ~/.claude/installed.json - fi - ``` - -13. **Install new modules** (if requested) - - Ask which specific modules to install - - For Skills, use `--add-skill `: - ```bash - "$temp_dir/install.sh" --add-skill standards-kotlin - "$temp_dir/install.sh" --add-skill custom:my-skill # for custom skills - ``` - - For MCP servers, use `--add-mcp `: - ```bash - "$temp_dir/install.sh" --add-mcp pdf-reader - "$temp_dir/install.sh" --add-mcp custom:my-mcp # for custom MCP - ``` - - These commands are non-interactive and handle tracking automatically - - **Install external plugins** (if requested) - External plugins CANNOT be installed via install.sh --add (stdin issues). - Install them directly via claude CLI: - - ```bash - # 1. Get plugin info from external-plugins.json - plugin_id="code-review-ai" - marketplace=$(jq -r --arg id "$plugin_id" '.plugins[] | select(.id == $id) | .marketplace' "$temp_dir/external-plugins.json") - repo=$(jq -r --arg m "$marketplace" '.marketplaces[$m].repo' "$temp_dir/external-plugins.json") - - # 2. Add marketplace (if not already registered) - if ! claude plugin marketplace list 2>/dev/null | grep -q "❯ $marketplace"; then - claude plugin marketplace add "$repo" - fi - - # 3. Install the plugin - claude plugin install "$plugin_id@$marketplace" - - # 4. Track in installed.json - jq --arg p "$plugin_id@$marketplace" '.external_plugins = ((.external_plugins // []) + [$p] | unique)' \ - ~/.claude/installed.json > ~/.claude/installed.json.tmp && mv ~/.claude/installed.json.tmp ~/.claude/installed.json - ``` - -14. **Enable Agent Teams** (if requested) - ```bash - # Ensure settings.json exists - if [[ ! -f ~/.claude/settings.json ]]; then - echo '{}' > ~/.claude/settings.json - fi - - # Add env var - jq '.env = (.env // {}) | .env.CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS = "1"' \ - ~/.claude/settings.json > ~/.claude/settings.json.tmp && mv ~/.claude/settings.json.tmp ~/.claude/settings.json - ``` - Show confirmation: - ``` - Agent Teams enabled. - Required for /with-advisor and /delegate commands. - - ⚠️ IMPORTANT: Restart Claude Code now. - After restart, run /catchup to reload context. - ``` - -15. **Remove modules** (if requested) - - Show installed modules from `~/.claude/installed.json` - - Ask which specific modules to remove - - Run install.sh --remove: - ```bash - "$temp_dir/install.sh" --remove - ``` - - Or remove manually: - - MCP: Remove from `~/.claude.json` using jq - - Skills: Remove directory from `~/.claude/skills/` - - Plugins: Run `claude plugin remove ` - - Update `installed.json` accordingly - -16. **Cleanup** - ```bash - rm -rf "$temp_dir" - ``` +**Goal:** Any scenario completes in 2 permission prompts (1 discover + 1 execute). -## IMPORTANT +## Phase 1: Discovery (1 Bash call) -- **Always clone first** - needed to discover available modules -- **Always ask user before taking action** - never auto-upgrade without consent -- **Cleanup LAST** - only after all operations complete +Clone repo and run setup-status.sh in a single Bash call: -## Output Examples +```bash +temp=$(mktemp -d /tmp/claude-setup-XXXXXX) && \ +git clone --depth 1 https://github.com/b33eep/claude-code-setup.git "$temp" 2>/dev/null && \ +"$temp/lib/setup-status.sh" +``` + +The script reads `installed.json` and `templates/VERSION`, compares modules, and outputs JSON. + +Parse the JSON output. Handle errors: + +- **Bash call fails** (no network, git clone fails): + ``` + Unable to reach GitHub. -### Status presentation (before asking): + Manual upgrade: + cd /path/to/claude-code-setup + git pull + ./install.sh --update + ``` + Stop here. + +- **JSON has `"error": "not_installed"`** (no installed.json): + ``` + claude-code-setup is not installed. Run install.sh first. + ``` + Clean up: `rm -rf "$temp"` → Stop. + +- **Success**: Parse JSON fields. The `temp_dir` field contains the repo path. Continue to Phase 2. + +### JSON structure reference + +```json +{ + "temp_dir": "/tmp/claude-setup-XXXXXX", + "base": { "installed": 50, "available": 52, "update_available": true }, + "custom": { "configured": true, "installed": 1, "available": 2, "update_available": true }, + "new_modules": { "skills": ["name"], "mcp": ["name"], "plugins": ["id@marketplace"] }, + "installed_modules": { "skills": ["name"], "mcp": ["name"], "plugins": ["id@marketplace"] }, + "agent_teams": { "enabled": true } +} ``` -claude-code-setup status: -- Base: v8 installed, v9 available -- Custom: v1 (up-to-date) -- Agent Teams: not configured -Modules available to install: - Skills: - - skill-creator (Create custom skills) +## Phase 2: Present + Ask (0 Bash calls) - MCP Servers: - (all installed) +### Show status + +Present a summary from the JSON: - External Plugins: - - code-review-ai (AI-powered architectural review) ``` +claude-code-setup status: +- Base: v{base.installed} installed, v{base.available} available +- Custom: v{custom.installed} installed, v{custom.available} available +- Agent Teams: enabled / not configured -### After upgrade: +Modules available to install: + Skills: {new_modules.skills} + MCP Servers: {new_modules.mcp} + External Plugins: {new_modules.plugins} ``` -Upgraded: -- Base: v8 → v9 -- Custom: v1 → v2 -Changes (base): -- v9: Add /skill-creator command skill +Version line variants: +- Update available: `v50 installed, v52 available` +- Up-to-date: `v52 (up-to-date)` +- Custom not configured: `(not configured)` — add tip: `Use /add-custom to add company modules.` -Changes (custom): -- v2: Add standards-kotlin skill +Agent Teams line: +- `Agent Teams: enabled` if `agent_teams.enabled == true` +- `Agent Teams: not configured` if `agent_teams.enabled == false` -⚠️ IMPORTANT: Restart Claude Code now. - Tools (Read, Bash, etc.) may not work until restart. - After restart, run /catchup to reload context. +If no new modules in any category: `All modules installed.` + +### Read CHANGELOG (if upgrade available) + +If `base.update_available` is true, use the Read tool on `{temp_dir}/CHANGELOG.md`. +Show relevant entries from content_version `base.installed` to `base.available`. + +### Check for new modules that require API keys + +For each MCP in `new_modules.mcp`, read `{temp_dir}/mcp/{name}.json` using the Read tool. +Check `requiresApiKey` field. Note which MCPs need API keys for Phase 3 handling. + +### AskUserQuestion (multiSelect: true) + +Build options dynamically from JSON. Only include options where the condition is met: + +| Condition | Option label | +|-----------|-------------| +| `base.update_available == true` | "Upgrade base (vX → vY)" | +| `custom.update_available == true` | "Upgrade custom (vX → vY)" | +| `new_modules.skills` or `new_modules.mcp` has entries | "Install new skills/MCP" | +| `new_modules.plugins` has entries | "Install plugins" | +| any `installed_modules` array is non-empty | "Remove modules" | +| `agent_teams.enabled == false` | "Enable Agent Teams" | + +If everything is up-to-date, no new modules, and Agent Teams already configured: +→ Show `All up-to-date.` → Clean up with a Bash call: `rm -rf "$temp"` + (This path uses 2 prompts total: 1 discover + 1 cleanup. No Phase 3.) + +### Follow-up questions + +**If user selected "Install new skills/MCP":** +→ Second AskUserQuestion (multiSelect) listing each module from `new_modules.skills` and `new_modules.mcp`. + +**If user selected "Remove modules":** +→ Second AskUserQuestion (multiSelect) listing installed modules from `installed_modules.skills`, `installed_modules.mcp`, and `installed_modules.plugins`. + +**If user selected "Install plugins":** +→ Second AskUserQuestion (multiSelect) listing each plugin from `new_modules.plugins`. + +## Phase 3: Execute (1 Bash call) + +Build ONE chained Bash command from all user selections. **Cleanup always uses `;`** (ensures temp dir is removed even if a command in the chain fails). + +### Execution chains + +| Action | Command segment | +|--------|----------------| +| Upgrade base | `cd "$temp" && ./install.sh --update --yes` | +| Upgrade custom | `git -C ~/.claude/custom pull && new_v=$(cat ~/.claude/custom/VERSION 2>/dev/null \|\| echo "0") && jq --arg v "$new_v" '.custom_version = ($v \| tonumber)' ~/.claude/installed.json > ~/.claude/installed.json.tmp && mv ~/.claude/installed.json.tmp ~/.claude/installed.json` | +| Install skill | `cd "$temp" && ./install.sh --add-skill ` | +| Install MCP (no API key) | `cd "$temp" && ./install.sh --add-mcp ` | +| Install MCP (API key) | See "MCP with API key" section below | +| Install plugin | `claude plugin marketplace add 2>/dev/null; claude plugin install @ && jq --arg p "@" '.external_plugins = ((.external_plugins // []) + [$p] \| unique)' ~/.claude/installed.json > ~/.claude/installed.json.tmp && mv ~/.claude/installed.json.tmp ~/.claude/installed.json` | +| Remove skill | `cd "$temp" && ./install.sh --remove-skill ` | +| Remove MCP | `cd "$temp" && ./install.sh --remove-mcp ` | +| Remove plugin | `claude plugin remove && jq '.external_plugins = (.external_plugins // [] \| map(select(. != "@")))' ~/.claude/installed.json > ~/.claude/installed.json.tmp && mv ~/.claude/installed.json.tmp ~/.claude/installed.json` | +| Enable Agent Teams | `[[ -f ~/.claude/settings.json ]] \|\| echo '{}' > ~/.claude/settings.json; jq '.env = (.env // {}) \| .env.CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS = "1"' ~/.claude/settings.json > ~/.claude/settings.json.tmp && mv ~/.claude/settings.json.tmp ~/.claude/settings.json` | +| **Cleanup** (always last) | `; rm -rf "$temp"` | + +### Combining actions + +Chain selected actions with `&&`, always end with `; rm -rf "$temp"`: + +```bash +cd "$temp" && ./install.sh --update --yes && ./install.sh --add-skill standards-kotlin && ./install.sh --remove-mcp brave-search ; rm -rf "$temp" ``` -### MCP with API key (insert with placeholder): - -If an MCP server requires an API key, the install.sh script cannot set it non-interactively. -Instead of just showing a snippet, **insert the config directly into ~/.claude.json** with a placeholder: - -1. **Read the MCP config** from the JSON file: - ```bash - # For base MCP: - cat "$temp_dir/mcp/.json" - # For custom MCP: - cat ~/.claude/custom/mcp/.json - ``` - -2. **Insert into ~/.claude.json** with placeholder (use jq or Edit tool): - ```bash - # Example: Add brave-search with placeholder - jq '.mcpServers["brave-search"] = { - "type": "stdio", - "command": "npx", - "args": ["-y", "@brave/brave-search-mcp-server"], - "env": { - "BRAVE_API_KEY": "YOUR_API_KEY_HERE" - } - }' ~/.claude.json > ~/.claude.json.tmp && mv ~/.claude.json.tmp ~/.claude.json - ``` - -3. **Show simple instructions** to the user: - ``` - MCP "brave-search" configured with placeholder. - - Replace YOUR_API_KEY_HERE in ~/.claude.json with your actual key. - - To get your API key: - 1. Visit: https://brave.com/search/api/ - 2. Sign up for 'Data for AI' plan - 3. Create an API key (free tier: 2000 queries/month) - - ⚠️ IMPORTANT: Restart Claude Code now. - Tools (Read, Bash, etc.) may not work until restart. - ``` - -**Key point:** User only needs to replace the placeholder value, not copy/paste the entire config block. - -### Already current, modules available to install: +More examples: + +| User selections | Full chain | +|-----------------|------------| +| Upgrade base only | `cd "$temp" && ./install.sh --update --yes ; rm -rf "$temp"` | +| Install skill + remove MCP | `cd "$temp" && ./install.sh --add-skill X && ./install.sh --remove-mcp Y ; rm -rf "$temp"` | +| Enable Agent Teams only | `[[ -f ~/.claude/settings.json ]] \|\| echo '{}' > ~/.claude/settings.json; jq '.env = (.env // {}) \| .env.CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS = "1"' ~/.claude/settings.json > ~/.claude/settings.json.tmp && mv ~/.claude/settings.json.tmp ~/.claude/settings.json ; rm -rf "$temp"` | +| Upgrade + install + remove | `cd "$temp" && ./install.sh --update --yes && ./install.sh --add-skill X && ./install.sh --remove-skill Y ; rm -rf "$temp"` | + +### MCP with API key (special handling) + +When an MCP has `requiresApiKey: true` in its config file (checked in Phase 2): + +1. Read the MCP config JSON from `{temp_dir}/mcp/.json` +2. Build a jq command that inserts the config into `~/.claude.json` with `YOUR_API_KEY_HERE` replacing all `{{PLACEHOLDER}}` values +3. Track in installed.json in the same chain +4. After execution, use the Edit tool to add the MCP to the MCP_TABLE in `~/.claude/CLAUDE.md` + +Example for brave-search (single API key): +```bash +jq '.mcpServers["brave-search"] = {"type":"stdio","command":"npx","args":["-y","@brave/brave-search-mcp-server"],"env":{"BRAVE_API_KEY":"YOUR_API_KEY_HERE"}}' ~/.claude.json > ~/.claude.json.tmp && mv ~/.claude.json.tmp ~/.claude.json && jq '.mcp = ((.mcp // []) + ["brave-search"] | unique)' ~/.claude/installed.json > ~/.claude/installed.json.tmp && mv ~/.claude/installed.json.tmp ~/.claude/installed.json ``` -claude-code-setup status: -- Base: v9 (up-to-date) -- Custom: v2 (up-to-date) -- Agent Teams: enabled -Modules available to install: - Skills: - - custom:standards-kotlin (Kotlin standards) +For MCPs with multiple API keys (e.g., google-search with `GOOGLE_API_KEY` and `GOOGLE_CSE_ID`), replace ALL `{{PLACEHOLDER}}` values with `YOUR_API_KEY_HERE` (or distinct placeholders like `YOUR_GOOGLE_API_KEY_HERE`), and list each key the user needs to replace in the post-execution message. - MCP Servers: - - brave-search (Web search via Brave) +Show instructions after execution: +``` +MCP "brave-search" configured with placeholder. - External Plugins: - - code-review-ai (AI-powered architectural review) +Replace YOUR_API_KEY_HERE in ~/.claude.json with your actual key. -Would you like to install any modules? +To get your API key: +{apiKeyInstructions from the MCP config JSON} ``` -### After plugin installation: +Note: Since this bypasses install.sh, the MCP_TABLE in `~/.claude/CLAUDE.md` is NOT automatically updated. After the Bash call completes, use the Edit tool to add the MCP entry to the MCP_TABLE section. This is a tool call (not Bash), so it does not add a permission prompt. + +### Plugin install details + +Get marketplace repo from `{temp_dir}/external-plugins.json`: +```bash +# Read from external-plugins.json (already in cloned repo) +repo=$(jq -r --arg m "" '.marketplaces[$m].repo' "$temp/external-plugins.json") ``` -Installing external plugin code-review-ai... - Adding marketplace claude-code-workflows... - ✓ Marketplace claude-code-workflows added - Installing code-review-ai... - ✓ code-review-ai installed +Chain: marketplace add (idempotent) → plugin install → tracking update. + +### After execution + +Show summary of completed actions. + +If anything was installed, upgraded, removed, or Agent Teams was enabled: +``` ⚠️ IMPORTANT: Restart Claude Code now. - Tools (Read, Bash, etc.) may not work until restart. After restart, run /catchup to reload context. ``` -### Already current, all modules installed: +## IMPORTANT + +- **Phase 1 = 1 Bash call** — clone + setup-status.sh +- **Phase 2 = 0 Bash calls** — parse JSON + show status + AskUserQuestion +- **Phase 3 = 1 Bash call** — all operations chained + cleanup +- **Always ask before acting** — never auto-upgrade +- **Cleanup uses `;`** — temp dir removed even if chain fails +- **MCP with API keys** — insert placeholder via jq, don't use --add-mcp (it prompts interactively) + +## Output Examples + +### Status with upgrade available: ``` claude-code-setup status: -- Base: v9 (up-to-date) +- Base: v50 installed, v52 available +- Custom: v1 (up-to-date) +- Agent Teams: enabled + +Modules available to install: + Skills: standards-kotlin + MCP Servers: brave-search + External Plugins: document-skills + +Changes in v51-v52: +- v52: Add /skill-creator command skill +- v51: Add standards-kotlin coding standards +``` + +### All up-to-date: +``` +claude-code-setup status: +- Base: v52 (up-to-date) - Custom: v2 (up-to-date) - Agent Teams: enabled -All available modules are installed. +All modules installed. ``` -### No custom repo configured: +### No custom repo: ``` claude-code-setup status: -- Base: v9 (up-to-date) +- Base: v52 (up-to-date) - Custom: (not configured) - Agent Teams: not configured -All available modules are installed. +Modules available to install: + Skills: standards-kotlin Tip: Use /add-custom to add company modules. ``` +### After upgrade + install: +``` +Completed: +- Upgraded base: v50 → v52 +- Installed skill: standards-kotlin +- Enabled Agent Teams + +⚠️ IMPORTANT: Restart Claude Code now. + After restart, run /catchup to reload context. +``` + +### MCP with API key: +``` +MCP "brave-search" configured with placeholder. + +Replace YOUR_API_KEY_HERE in ~/.claude.json with your actual key. + +To get your API key: +1. Visit: https://brave.com/search/api/ +2. Sign up for 'Data for AI' plan +3. Create an API key (free tier: 2000 queries/month) + +⚠️ IMPORTANT: Restart Claude Code now. + After restart, run /catchup to reload context. +``` + ### Error: ``` -Upgrade failed: {reason} +Unable to reach GitHub. Manual upgrade: cd /path/to/claude-code-setup diff --git a/docs/records/040-setup-command-ux.md b/docs/records/040-setup-command-ux.md new file mode 100644 index 0000000..65310f7 --- /dev/null +++ b/docs/records/040-setup-command-ux.md @@ -0,0 +1,480 @@ +# Record 040: /claude-code-setup UX — Reduce Permission Prompts + +## Status + +Done + +--- + +## Problem + +`/claude-code-setup` makes ~12-18 individual Bash calls during the discovery phase (curl, git clone, ls, jq reads, etc.). Each triggers a permission prompt. With a custom repo configured, the count is even higher (git fetch, cat VERSION, git show, ls custom/skills, ls custom/mcp, jq custom_version). + +The user clicks through a wall of approvals for purely read-only operations before anything useful happens. This makes the central management command frustrating to use, pushing users toward manual `./install.sh` instead. + +**Goal:** Reduce to ~2 permission prompts regardless of scenario (1 discover + 1 execute). + +## Options Considered + +Evaluated Script vs Subagent for both Discovery and Execution phases. All subagent variants add complexity without reducing prompt count below 2. Discovery Script wins (testbar, debuggable, fits `lib/` pattern). Execution doesn't need a script since existing install.sh commands are chainable — except `--remove` which is interactive. Solution: add `--remove-skill`/`--remove-mcp` flags. + +### Decision + +Discovery Script + non-interactive remove flags. Simplest approach with maximum impact. + +## Solution + +### Overview + +Three changes: + +1. **`lib/setup-status.sh`** — Discovery script, outputs JSON (replaces 8-12 Bash calls with 1) +2. **`--remove-skill X` / `--remove-mcp X`** — Non-interactive removal flags for install.sh +3. **Updated `commands/claude-code-setup.md`** — Uses new script + chainable commands + +### New Flow (2 prompts) + +``` +Prompt 1: Clone + setup-status.sh → JSON + Claude parses JSON, shows status + AskUserQuestion (multiSelect) → User picks actions +Prompt 2: Chained execution + cleanup +``` + +### Part 1: `lib/setup-status.sh` + +Single script that gathers ALL discovery info and outputs JSON to stdout. + +**Important:** `setup-status.sh` is a standalone executable invoked by the command markdown. It is NOT sourced by `install.sh` and should NOT be added to the source chain in `install.sh`. + +**Invocation** (from the command markdown — 1 Bash call): +```bash +temp=$(mktemp -d /tmp/claude-setup-XXXXXX) && \ +git clone --depth 1 https://github.com/b33eep/claude-code-setup.git "$temp" 2>/dev/null && \ +"$temp/lib/setup-status.sh" +``` + +**Variable initialization:** The script runs from within the cloned repo but needs access to the user's installation. It must set these variables before sourcing any helpers: +```bash +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +CLAUDE_DIR="${CLAUDE_DIR:-$HOME/.claude}" +CUSTOM_DIR="$CLAUDE_DIR/custom" +INSTALLED_FILE="$CLAUDE_DIR/installed.json" +CONTENT_VERSION_FILE="$SCRIPT_DIR/templates/VERSION" +MCP_CONFIG_FILE="$HOME/.claude.json" +``` + +**What it does:** +1. Read `~/.claude/installed.json` (installed version, modules) +2. Read `templates/VERSION` from cloned repo (available version) +3. Compare installed vs available skills/MCP → find new (uninstalled) modules +4. Read `external-plugins.json` + `installed.json` → find available/installed plugins (does NOT call `claude` CLI — uses tracking data only) +5. Check `~/.claude/settings.json` for Agent Teams status +6. Check custom repo (if `~/.claude/custom` exists): `git fetch`, compare VERSION +7. Output JSON to stdout + +**Note on module discovery:** The script should NOT source `lib/modules.sh` (which pulls in `interactive_select` and terminal control code). Instead, it implements module discovery directly — listing directories/files and comparing against `installed.json` via jq. + +**Note on CHANGELOG:** The script does NOT parse CHANGELOG.md. Claude reads the CHANGELOG directly from the cloned repo (`$temp/CHANGELOG.md`) after receiving the JSON, if an upgrade is available. This avoids fragile text parsing in bash. + +**Output JSON:** +```json +{ + "temp_dir": "/tmp/claude-setup-XXXXXX", + "base": { + "installed": 50, + "available": 52, + "update_available": true + }, + "custom": { + "configured": true, + "installed": 1, + "available": 2, + "update_available": true + }, + "new_modules": { + "skills": ["standards-kotlin"], + "mcp": ["brave-search"], + "plugins": ["document-skills"] + }, + "installed_modules": { + "skills": ["standards-python", "standards-shell"], + "mcp": ["pdf-reader", "google-search"], + "plugins": ["code-review-ai@claude-code-workflows"] + }, + "agent_teams": { + "enabled": true + } +} +``` + +**Error cases:** +- No network (clone fails) → script isn't reached, Bash call fails, command shows manual fallback +- No `installed.json` → output `{"error": "not_installed"}` +- jq not found → output plain text error (shouldn't happen, jq is a dependency) +- Custom repo configured but unreachable → `custom.configured: true`, `custom.update_available: false`, no error thrown +- `claude` CLI not available → does not affect discovery (uses installed.json tracking only) + +**Implementation notes:** +- Sources `lib/helpers.sh` for `get_installed`, `get_content_version`, `is_installed` etc. +- Uses jq to build output JSON (no manual string concatenation) +- Script is NOT standalone — runs from within the cloned repo (has access to SCRIPT_DIR) +- stderr for diagnostic messages, stdout reserved for JSON only + +### Part 2: `--remove-skill X` / `--remove-mcp X` + +Add to `install.sh` to make removal non-interactive. Mirrors existing `--add-skill`/`--add-mcp` pattern. + +**New CLI:** +```bash +./install.sh --remove-skill standards-kotlin +./install.sh --remove-mcp brave-search +./install.sh --remove-skill custom:my-skill +./install.sh --remove-mcp custom:my-mcp +``` + +**`do_remove_skill()` — placed in `install.sh`** (alongside `do_add_skill()`, following the `do_*` pattern): +1. Validate skill exists in installed.json (or filesystem) +2. Call existing `uninstall_skill()` from `lib/uninstall.sh` (removes dir + tracking) +3. Call `build_claude_md()` (rebuilds tables without removed skill) + +**`do_remove_mcp()` — placed in `install.sh`** (alongside `do_add_mcp()`): +1. Validate MCP exists in installed.json (or `~/.claude.json`) +2. Call existing `uninstall_mcp()` from `lib/uninstall.sh` (removes from claude.json + tracking) +3. Call `build_claude_md()` (rebuilds tables without removed MCP) + +**Arg parsing** (in `main()`): +```bash +--remove-skill) + action="remove-skill" + shift + if [[ $# -eq 0 ]]; then + print_error "--remove-skill requires a skill name" + echo "Usage: ./install.sh --remove-skill " + exit 1 + fi + remove_skill_name="$1" + ;; +--remove-mcp) + action="remove-mcp" + shift + if [[ $# -eq 0 ]]; then + print_error "--remove-mcp requires an MCP server name" + echo "Usage: ./install.sh --remove-mcp " + exit 1 + fi + remove_mcp_name="$1" + ;; +``` + +**Note on multiple removes in one chain:** Each `--remove-skill`/`--remove-mcp` call triggers `build_claude_md()`. When chained (`--remove-skill A && --remove-skill B`), the rebuild runs twice. This is idempotent and harmless. Optimization (e.g., `--skip-rebuild` flag) is not needed now. + +**Plugin removal:** Not adding `--remove-plugin` — Claude chains `claude plugin remove X` + jq tracking update directly. Plugin removal is rare and already non-interactive. The explicit jq filter for tracking: +```bash +jq '.external_plugins = (.external_plugins // [] | map(select(. != "PLUGIN_ID@MARKETPLACE")))' \ + ~/.claude/installed.json > ~/.claude/installed.json.tmp && \ + mv ~/.claude/installed.json.tmp ~/.claude/installed.json +``` + +**Disable Agent Teams:** Intentionally excluded from this feature. Users who want to disable Agent Teams can edit `~/.claude/settings.json` directly. The command does not offer a toggle-off option. + +### Part 3: Updated Command Markdown + +The command becomes three clean phases. + +**Phase 1 — Discovery (1 Bash call):** +```bash +temp=$(mktemp -d /tmp/claude-setup-XXXXXX) && \ +git clone --depth 1 https://github.com/b33eep/claude-code-setup.git "$temp" 2>/dev/null && \ +"$temp/lib/setup-status.sh" +``` +Parse JSON output. If error → show message, exit. + +If `base.update_available` is true, also read the CHANGELOG for relevant entries: +```bash +# Claude reads this from the cloned repo (already available, no extra Bash call needed) +# Read $temp/CHANGELOG.md using the Read tool +``` + +**Phase 2 — Present + Ask (0 Bash calls):** + +Show status summary (same format as current command). Then AskUserQuestion with multiSelect. Options generated dynamically based on JSON: +- "Upgrade base (vX → vY)" — if `base.update_available` +- "Upgrade custom (vX → vY)" — if `custom.update_available` +- "Install [module-name]" — for each entry in `new_modules.*` +- "Remove modules" — if installed modules exist +- "Enable Agent Teams" — if `agent_teams.enabled == false` + +If "Remove modules" selected → second AskUserQuestion listing installed modules from JSON. +If "Install modules" and >4 available → second AskUserQuestion for selection. + +**Phase 3 — Execute (1 Bash call):** + +Build a single chained command based on user selections. + +**Important: Cleanup uses `;` not `&&`** — ensures temp dir is removed even if a command in the chain fails: +```bash +cd $temp && ./install.sh --update --yes && ./install.sh --add-skill X ; rm -rf $temp +``` + +Execution chains for all scenarios: + +| User Choice | Execution Chain | +|-------------|----------------| +| Upgrade base only | `cd $temp && ./install.sh --update --yes ; rm -rf $temp` | +| Upgrade custom only | `git -C ~/.claude/custom pull && new_v=$(cat ~/.claude/custom/VERSION 2>/dev/null \|\| echo "0") && jq --arg v "$new_v" '.custom_version = ($v \| tonumber)' ~/.claude/installed.json > ~/.claude/installed.json.tmp && mv ~/.claude/installed.json.tmp ~/.claude/installed.json ; rm -rf $temp` | +| Upgrade base + custom | Base chain `&&` custom chain `;` cleanup | +| Install skill | `cd $temp && ./install.sh --add-skill X ; rm -rf $temp` | +| Install MCP (no key) | `cd $temp && ./install.sh --add-mcp X ; rm -rf $temp` | +| Install MCP (API key) | Claude inserts config directly into `~/.claude.json` with `YOUR_API_KEY_HERE` placeholder via jq (bypasses `--add-mcp` which prompts interactively) `;` cleanup | +| Remove skill | `cd $temp && ./install.sh --remove-skill X ; rm -rf $temp` | +| Remove MCP | `cd $temp && ./install.sh --remove-mcp X ; rm -rf $temp` | +| Upgrade + install | `cd $temp && ./install.sh --update --yes && ./install.sh --add-skill X ; rm -rf $temp` | +| Upgrade + remove | `cd $temp && ./install.sh --update --yes && ./install.sh --remove-mcp X ; rm -rf $temp` | +| Multiple installs + removes | `cd $temp && ./install.sh --update --yes && ./install.sh --add-skill X && ./install.sh --remove-skill Y ; rm -rf $temp` | +| Enable Agent Teams only | `jq '.env = (.env // {}) | .env.CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS = "1"' ~/.claude/settings.json > ~/.claude/settings.json.tmp && mv ~/.claude/settings.json.tmp ~/.claude/settings.json ; rm -rf $temp` | +| Install plugin | `claude plugin marketplace add REPO 2>/dev/null; claude plugin install ID@MP && jq '.external_plugins = ((.external_plugins // []) + ["ID@MP"] \| unique)' ~/.claude/installed.json > ~/.claude/installed.json.tmp && mv ~/.claude/installed.json.tmp ~/.claude/installed.json ; rm -rf $temp` | +| Remove plugin | `claude plugin remove ID && jq '.external_plugins = (.external_plugins // [] \| map(select(. != "ID@MP")))' ~/.claude/installed.json > ~/.claude/installed.json.tmp && mv ~/.claude/installed.json.tmp ~/.claude/installed.json ; rm -rf $temp` | + +**Note on `do_update()` exit behavior:** `do_update()` in `lib/update.sh` uses `exit 0` (not `return 0`) when already up-to-date. This is safe for chains because exit 0 is success — the next `&&` command still runs. But this is fragile if `do_update()` ever changes to exit non-zero for "nothing to update." Worth noting for future maintenance. + +### Prompt Count Summary + +| Scenario | Before | After | +|----------|--------|-------| +| Check status (no action) | ~10 | 1 | +| Upgrade only | ~12 | 2 | +| Upgrade + install module | ~14 | 2 | +| Install module only | ~12 | 2 | +| Remove module | ~12 | 2 | +| Upgrade + remove | ~14 | 2 | +| Enable Agent Teams only | ~12 | 2 | +| Everything combined | ~18 | 2 | +| With custom repo | ~18 | 2 | + +### Files to Create/Modify + +| File | Action | Description | +|------|--------|-------------| +| `lib/setup-status.sh` | Create | Discovery script (standalone, NOT sourced by install.sh) | +| `install.sh` | Modify | Add `do_remove_skill()`, `do_remove_mcp()` + arg parsing for `--remove-skill`, `--remove-mcp` | +| `commands/claude-code-setup.md` | Rewrite | New 3-phase flow | +| `tests/scenarios/XX-setup-status.sh` | Create | Test for discovery script | +| `tests/scenarios/XX-remove-direct.sh` | Create | Test for new remove flags | + +## User Stories + +### Story 1: Discovery Script + +**As a** setup user, +**I want** all status checks to complete in a single operation, +**So that** I see my installation status without approving 12+ permission prompts. + +**Acceptance Criteria:** + +``` +Given a user has claude-code-setup installed (installed.json exists), +When the setup-status.sh script runs from a cloned repo, +Then it outputs valid JSON to stdout with: + - .base.installed matching content_version from installed.json + - .base.available matching templates/VERSION from the repo + - .base.update_available == true when installed < available + - .installed_modules.skills containing each skill from installed.json + - .installed_modules.mcp containing each MCP from installed.json + - .new_modules.skills containing skills in repo but NOT in installed.json + - .new_modules.mcp containing MCP servers in repo but NOT in installed.json + - .agent_teams.enabled matching CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS in settings.json +``` + +``` +Given a user has a custom repo configured (~/.claude/custom exists with a git remote), +When setup-status.sh runs, +Then the JSON includes custom.configured: true, custom.installed, custom.available, and custom.update_available. +``` + +``` +Given a user has no custom repo (~/.claude/custom does not exist), +When setup-status.sh runs, +Then the JSON contains custom.configured: false and omits custom version fields. +``` + +``` +Given a custom repo is configured but the remote is unreachable, +When setup-status.sh runs, +Then the JSON contains custom.configured: true, custom.update_available: false, and no error is thrown. +``` + +``` +Given installed.json does not exist, +When setup-status.sh runs, +Then it outputs {"error": "not_installed"}. +``` + +``` +Given the script runs, +When it writes diagnostic messages, +Then diagnostics go to stderr and only valid JSON goes to stdout. +``` + +**Priority:** High +**Status:** Done + +**Test coverage:** Scenario 26 (31 assertions). Custom repo positive/unreachable cases not tested (require git remote and network mock). + +--- + +### Story 2: Non-Interactive Remove Flags + +**As a** setup user, +**I want** to remove a specific module by name via command line, +**So that** module removal can be chained with other install.sh operations without interactive prompts. + +**Acceptance Criteria:** + +``` +Given a skill "standards-kotlin" is installed, +When I run ./install.sh --remove-skill standards-kotlin, +Then the skill directory is removed from ~/.claude/skills/, installed.json is updated, and CLAUDE.md is rebuilt without the skill. +``` + +``` +Given an MCP server "brave-search" is installed, +When I run ./install.sh --remove-mcp brave-search, +Then the server is removed from ~/.claude.json, installed.json is updated, and CLAUDE.md is rebuilt without the server. +``` + +``` +Given a custom skill "custom:my-skill" is installed, +When I run ./install.sh --remove-skill custom:my-skill, +Then it handles the custom: prefix correctly and removes the skill. +``` + +``` +Given a skill "nonexistent" is NOT installed, +When I run ./install.sh --remove-skill nonexistent, +Then it shows a warning and exits with 0 (idempotent, matches --add-skill/--add-mcp pattern). +``` + +``` +Given --remove-skill is called without a name argument, +When install.sh parses arguments, +Then it shows a usage error and exits. +``` + +``` +Given I chain --update --yes and --remove-skill as separate calls (./install.sh --update --yes && ./install.sh --remove-skill X), +When both commands execute sequentially, +Then upgrade completes first, then removal completes, both successfully. +``` + +**Priority:** High +**Status:** Done + +**Test coverage:** Scenario 27 (20 assertions). Custom prefix removal (custom:my-skill) not tested (requires custom repo fixture). + +--- + +### Story 3: Command Markdown Rewrite + +**As a** setup user, +**I want** `/claude-code-setup` to use the discovery script and chainable commands, +**So that** any scenario (upgrade, install, remove, or combination) completes in 2 permission prompts. + +**Acceptance Criteria:** + +``` +Given the command runs Phase 1 (discovery), +When Claude executes the clone + setup-status.sh, +Then it is a single Bash call that produces parseable JSON. +``` + +``` +Given the JSON shows an available upgrade, +When Claude presents the status and the user selects "Upgrade base", +Then Claude builds a single chained Bash command (cd $temp && ./install.sh --update --yes ; rm -rf $temp) as one permission prompt. +``` + +``` +Given the user selects multiple actions (e.g., upgrade + install skill + enable Agent Teams), +When Claude builds the execution chain, +Then all install.sh operations are chained into one Bash call with ; rm -rf $temp for cleanup. +``` + +``` +Given the user selects "Remove modules", +When Claude asks which modules to remove, +Then it uses AskUserQuestion (not a Bash call) listing installed modules from the JSON. +``` + +``` +Given the user selects only "Enable Agent Teams", +When Claude executes, +Then it runs a jq command + cleanup without needing cd $temp or install.sh. +``` + +``` +Given the user selects "Nothing" or all is up-to-date, +When Phase 2 completes, +Then Claude cleans up the temp dir and shows "All up-to-date". +``` + +``` +Given the user selects an MCP server that requires an API key (e.g., brave-search), +When Claude builds the execution chain, +Then it uses jq to insert the config into ~/.claude.json with YOUR_API_KEY_HERE placeholder instead of calling --add-mcp (which would prompt interactively). +``` + +``` +Given any execution chain fails midway, +When the Bash call completes, +Then the temp dir is still cleaned up (cleanup uses ; not &&). +``` + +**Priority:** High +**Status:** Done + +**Test coverage:** Scenario 09 (42 assertions). + +--- + +### Story 4: Tests for New Functionality + +**As a** developer, +**I need** automated tests for setup-status.sh and --remove-skill/--remove-mcp, +**So that** regressions are caught before release. + +**Acceptance Criteria:** + +``` +Given a test environment with installed modules, +When tests/scenarios/XX-setup-status.sh runs, +Then setup-status.sh outputs valid JSON where: + - .base.installed matches content_version from installed.json + - .base.available matches templates/VERSION + - .base.update_available is correct (true or false) + - .installed_modules.skills lists tracked skills + - .new_modules.skills lists uninstalled skills from the repo + - .agent_teams.enabled matches settings.json state + - .temp_dir is a valid directory path +``` + +``` +Given a test environment with a skill installed, +When tests/scenarios/XX-remove-direct.sh runs --remove-skill, +Then the skill directory no longer exists, installed.json no longer contains it, and CLAUDE.md is rebuilt. +``` + +``` +Given a test environment with an MCP server installed, +When tests/scenarios/XX-remove-direct.sh runs --remove-mcp, +Then the server is removed from both ~/.claude.json and installed.json, and CLAUDE.md is rebuilt. +``` + +``` +Given a test calls --remove-skill with a nonexistent skill, +When the test runs, +Then it verifies the command exits with non-zero status and shows a warning. +``` + +**Priority:** Medium +**Status:** Done diff --git a/install.sh b/install.sh index 12c8985..40b9314 100755 --- a/install.sh +++ b/install.sh @@ -70,13 +70,17 @@ show_usage() { echo " --help Show this help message" echo "" echo "Direct Installation (non-interactive):" - echo " --add-skill Install a specific skill by name" - echo " --add-mcp Install a specific MCP server by name" + echo " --add-skill Install a specific skill by name" + echo " --add-mcp Install a specific MCP server by name" + echo " --remove-skill Remove a specific skill by name" + echo " --remove-mcp Remove a specific MCP server by name" echo "" echo "Examples:" echo " ./install.sh --update --yes Non-interactive update" echo " ./install.sh --add-skill standards-kotlin" echo " ./install.sh --add-mcp brave-search" + echo " ./install.sh --remove-skill standards-kotlin" + echo " ./install.sh --remove-mcp brave-search" echo " ./install.sh --remove Remove installed modules" echo "" echo "Custom Modules:" @@ -404,6 +408,108 @@ do_add_mcp() { echo "" } +# ============================================ +# DIRECT REMOVAL (non-interactive) +# ============================================ + +# Remove a single skill by name +# Usage: do_remove_skill +# skill_name can be "skill-name" or "custom:skill-name" +do_remove_skill() { + local skill_name=$1 + + echo "" + echo "Claude Code Setup - Remove Skill" + echo "=================================" + + # Check dependencies + detect_os + check_package_manager + install_jq + + # Check for installed.json + if [[ ! -f "$INSTALLED_FILE" ]]; then + print_error "No installation found. Nothing to remove." + return 1 + fi + + # Check if skill is installed (tracking or filesystem) + local display_name="${skill_name#custom:}" + if ! is_installed "skills" "$skill_name"; then + print_warning "Skill '$skill_name' is not installed" + return 0 + fi + + # Remove skill + print_header "Removing Skill: $display_name" + + if uninstall_skill "$skill_name"; then + print_success "$display_name removed" + else + print_error "Failed to remove $display_name" + return 1 + fi + + # Rebuild CLAUDE.md without removed skill + print_header "Rebuilding CLAUDE.md" + build_claude_md + print_success "CLAUDE.md updated" + + echo "" + print_success "Done! Skill '$display_name' has been removed." + echo "" +} + +# Remove a single MCP server by name +# Usage: do_remove_mcp +# mcp_name can be "mcp-name" or "custom:mcp-name" +do_remove_mcp() { + local mcp_name=$1 + + echo "" + echo "Claude Code Setup - Remove MCP Server" + echo "=======================================" + + # Check dependencies + detect_os + check_package_manager + install_jq + + # Check for installed.json + if [[ ! -f "$INSTALLED_FILE" ]]; then + print_error "No installation found. Nothing to remove." + return 1 + fi + + # Check if MCP is installed (tracking or filesystem) + local display_name="${mcp_name#custom:}" + if ! is_installed "mcp" "$mcp_name"; then + print_warning "MCP server '$mcp_name' is not installed" + return 0 + fi + + # Remove MCP + print_header "Removing MCP: $display_name" + + if uninstall_mcp "$mcp_name"; then + print_success "$display_name removed" + else + print_error "Failed to remove $display_name" + return 1 + fi + + # Rebuild CLAUDE.md without removed MCP + print_header "Rebuilding CLAUDE.md" + build_claude_md + print_success "CLAUDE.md updated" + + echo "" + print_success "Done! MCP server '$display_name' has been removed." + echo "" + echo "IMPORTANT: Restart Claude Code to deactivate the removed MCP server." + echo "" +} + # ============================================ # MAIN # ============================================ @@ -412,6 +518,8 @@ main() { local action="" local add_skill_name="" local add_mcp_name="" + local remove_skill_name="" + local remove_mcp_name="" # Parse arguments while [[ $# -gt 0 ]]; do @@ -450,6 +558,26 @@ main() { fi add_mcp_name="$1" ;; + --remove-skill) + action="remove-skill" + shift + if [[ $# -eq 0 ]]; then + print_error "--remove-skill requires a skill name" + echo "Usage: ./install.sh --remove-skill " + exit 1 + fi + remove_skill_name="$1" + ;; + --remove-mcp) + action="remove-mcp" + shift + if [[ $# -eq 0 ]]; then + print_error "--remove-mcp requires an MCP server name" + echo "Usage: ./install.sh --remove-mcp " + exit 1 + fi + remove_mcp_name="$1" + ;; --update|-u) action="update" ;; @@ -483,6 +611,12 @@ main() { "add-mcp") do_add_mcp "$add_mcp_name" ;; + "remove-skill") + do_remove_skill "$remove_skill_name" + ;; + "remove-mcp") + do_remove_mcp "$remove_mcp_name" + ;; "update") do_update ;; diff --git a/lib/setup-status.sh b/lib/setup-status.sh new file mode 100755 index 0000000..eff04c5 --- /dev/null +++ b/lib/setup-status.sh @@ -0,0 +1,224 @@ +#!/bin/bash +set -euo pipefail + +# Discovery script for /claude-code-setup command +# Outputs JSON to stdout with installation status +# All diagnostics go to stderr +# +# NOT sourced by install.sh — standalone executable invoked by the command markdown + +# ============================================ +# VARIABLE INITIALIZATION +# ============================================ + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +CLAUDE_DIR="${CLAUDE_DIR:-$HOME/.claude}" +CUSTOM_DIR="$CLAUDE_DIR/custom" +INSTALLED_FILE="$CLAUDE_DIR/installed.json" +# shellcheck disable=SC2034 # Used by sourced helpers.sh (get_content_version) +CONTENT_VERSION_FILE="$SCRIPT_DIR/templates/VERSION" +# Env override needed for tests (Record 040 spec uses $HOME/.claude.json) +MCP_CONFIG_FILE="${MCP_CONFIG_FILE:-$HOME/.claude.json}" + +# ============================================ +# SOURCE HELPERS (only helpers.sh) +# ============================================ + +# shellcheck source=lib/helpers.sh +source "$SCRIPT_DIR/lib/helpers.sh" + +# ============================================ +# ERROR CASES +# ============================================ + +# jq must be available (user has a working install) +if ! command -v jq &>/dev/null; then + echo "Error: jq not found. Please install jq first." >&2 + exit 1 +fi + +# No installed.json → not installed +if [[ ! -f "$INSTALLED_FILE" ]]; then + echo '{"error":"not_installed"}' + exit 0 +fi + +# ============================================ +# GATHER DATA +# ============================================ + +# 1. Base versions (validate numeric to prevent set -e abort on malformed data) +installed_version=$(get_installed_content_version) +available_version=$(get_content_version) +[[ "$installed_version" =~ ^[0-9]+$ ]] || installed_version=0 +[[ "$available_version" =~ ^[0-9]+$ ]] || available_version=0 +if [[ "$installed_version" -lt "$available_version" ]]; then + update_available=true +else + update_available=false +fi + +# 2. Installed modules from installed.json +installed_skills_json=$(jq -c '.skills // []' "$INSTALLED_FILE") +installed_mcp_json=$(jq -c '.mcp // []' "$INSTALLED_FILE") +installed_plugins_json=$(jq -c '.external_plugins // []' "$INSTALLED_FILE") + +# 3. New skills (in repo but NOT tracked in installed.json) +# Use is_tracked instead of is_installed to avoid filesystem fallback +# which can find untracked modules left by partial installs +new_skills=() +for d in "$SCRIPT_DIR/skills/"*/; do + [[ -d "$d" ]] || continue + name=$(basename "$d") + if ! is_tracked "skills" "$name"; then + new_skills+=("$name") + fi +done + +# New MCP servers +new_mcp=() +for f in "$SCRIPT_DIR/mcp/"*.json; do + [[ -f "$f" ]] || continue + name=$(basename "$f" .json) + if ! is_tracked "mcp" "$name"; then + new_mcp+=("$name") + fi +done + +# 4. New plugins (compare external-plugins.json against installed.json) +new_plugins=() +plugins_config="$SCRIPT_DIR/external-plugins.json" +if [[ -f "$plugins_config" ]]; then + while IFS= read -r plugin_line; do + [[ -n "$plugin_line" ]] || continue + plugin_id=$(echo "$plugin_line" | jq -r '.id') + marketplace=$(echo "$plugin_line" | jq -r '.marketplace') + full_id="${plugin_id}@${marketplace}" + # Check if tracked in installed.json + if ! jq -e --arg id "$full_id" '.external_plugins // [] | index($id)' "$INSTALLED_FILE" > /dev/null 2>&1; then + new_plugins+=("$full_id") + fi + done < <(jq -c '.plugins[]' "$plugins_config") +fi + +# 5. Custom repo status +custom_configured=false +custom_installed=0 +custom_available=0 +custom_update_available=false + +if [[ -d "$CUSTOM_DIR" ]] && [[ -d "$CUSTOM_DIR/.git" ]]; then + custom_configured=true + custom_installed=$(jq -r '.custom_version // 0' "$INSTALLED_FILE" 2>/dev/null || echo "0") + [[ "$custom_installed" =~ ^[0-9]+$ ]] || custom_installed=0 + + # Try git fetch to check for updates + if git -C "$CUSTOM_DIR" fetch --quiet 2>/dev/null; then + custom_available=$(cat "$CUSTOM_DIR/VERSION" 2>/dev/null || echo "0") + [[ "$custom_available" =~ ^[0-9]+$ ]] || custom_available=0 + # Check if remote has newer version + remote_version=$(git -C "$CUSTOM_DIR" show "origin/$(git -C "$CUSTOM_DIR" rev-parse --abbrev-ref HEAD):VERSION" 2>/dev/null || echo "$custom_available") + [[ "$remote_version" =~ ^[0-9]+$ ]] || remote_version=0 + if [[ "$remote_version" -gt "$custom_installed" ]]; then + custom_available="$remote_version" + custom_update_available=true + fi + else + # Remote unreachable — use local VERSION, no update available + custom_available=$(cat "$CUSTOM_DIR/VERSION" 2>/dev/null || echo "0") + [[ "$custom_available" =~ ^[0-9]+$ ]] || custom_available=0 + fi + + # Also discover custom skills/MCP not yet installed + if [[ -d "$CUSTOM_DIR/skills" ]]; then + for d in "$CUSTOM_DIR/skills/"*/; do + [[ -d "$d" ]] || continue + name=$(basename "$d") + if ! is_installed "skills" "custom:$name"; then + new_skills+=("custom:$name") + fi + done + fi + + if [[ -d "$CUSTOM_DIR/mcp" ]]; then + for f in "$CUSTOM_DIR/mcp/"*.json; do + [[ -f "$f" ]] || continue + name=$(basename "$f" .json) + if ! is_installed "mcp" "custom:$name"; then + new_mcp+=("custom:$name") + fi + done + fi +fi + +# 6. Agent Teams status (only "1" or "true" count as enabled) +agent_teams_enabled=false +claude_settings="$CLAUDE_DIR/settings.json" +if [[ -f "$claude_settings" ]]; then + teams_val=$(jq -r '.env.CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS // ""' "$claude_settings" 2>/dev/null || echo "") + if [[ "$teams_val" = "1" || "$teams_val" = "true" ]]; then + agent_teams_enabled=true + fi +fi + +# ============================================ +# BUILD JSON OUTPUT +# ============================================ + +# Convert bash array to JSON array (empty-safe) +array_to_json() { + if [[ $# -gt 0 ]]; then + printf '%s\n' "$@" | jq -Rn '[inputs | select(. != "")]' + else + echo '[]' + fi +} + +new_skills_json=$(array_to_json "${new_skills[@]+"${new_skills[@]}"}") +new_mcp_json=$(array_to_json "${new_mcp[@]+"${new_mcp[@]}"}") +new_plugins_json=$(array_to_json "${new_plugins[@]+"${new_plugins[@]}"}") + +# Build final JSON +jq -n \ + --arg temp_dir "$SCRIPT_DIR" \ + --argjson installed_ver "$installed_version" \ + --argjson available_ver "$available_version" \ + --argjson update_available "$update_available" \ + --argjson custom_configured "$custom_configured" \ + --argjson custom_installed "$custom_installed" \ + --argjson custom_available "$custom_available" \ + --argjson custom_update "$custom_update_available" \ + --argjson installed_skills "$installed_skills_json" \ + --argjson installed_mcp "$installed_mcp_json" \ + --argjson installed_plugins "$installed_plugins_json" \ + --argjson new_skills "$new_skills_json" \ + --argjson new_mcp "$new_mcp_json" \ + --argjson new_plugins "$new_plugins_json" \ + --argjson agent_teams "$agent_teams_enabled" \ + '{ + temp_dir: $temp_dir, + base: { + installed: $installed_ver, + available: $available_ver, + update_available: $update_available + }, + custom: { + configured: $custom_configured, + installed: $custom_installed, + available: $custom_available, + update_available: $custom_update + }, + new_modules: { + skills: $new_skills, + mcp: $new_mcp, + plugins: $new_plugins + }, + installed_modules: { + skills: $installed_skills, + mcp: $installed_mcp, + plugins: $installed_plugins + }, + agent_teams: { + enabled: $agent_teams + } + }' diff --git a/templates/VERSION b/templates/VERSION index 0691f67..59343b0 100644 --- a/templates/VERSION +++ b/templates/VERSION @@ -1 +1 @@ -52 +53 diff --git a/tests/scenarios/09-claude-code-setup.sh b/tests/scenarios/09-claude-code-setup.sh index ff5c2ad..a531ba3 100755 --- a/tests/scenarios/09-claude-code-setup.sh +++ b/tests/scenarios/09-claude-code-setup.sh @@ -1,6 +1,6 @@ #!/bin/bash -# Scenario: /claude-code-setup command structure +# Scenario: /claude-code-setup command structure (3-phase flow) set -e @@ -19,25 +19,32 @@ scenario "claude-code-setup.md command exists" assert_file_exists "$PROJECT_DIR/commands/claude-code-setup.md" "claude-code-setup.md exists" -scenario "Command has required sections" +scenario "Command has 3-phase structure" -assert_file_contains "$PROJECT_DIR/commands/claude-code-setup.md" "installed.json" "References installed.json" -assert_file_contains "$PROJECT_DIR/commands/claude-code-setup.md" "content_version" "Checks content_version" -assert_file_contains "$PROJECT_DIR/commands/claude-code-setup.md" "templates/VERSION" "Fetches VERSION" -assert_file_contains "$PROJECT_DIR/commands/claude-code-setup.md" "git clone" "Has clone instruction" -assert_file_contains "$PROJECT_DIR/commands/claude-code-setup.md" "install.sh --update" "Runs install.sh --update" -assert_file_contains "$PROJECT_DIR/commands/claude-code-setup.md" "CHANGELOG" "References CHANGELOG" +assert_file_contains "$PROJECT_DIR/commands/claude-code-setup.md" "Phase 1" "Has Phase 1 (Discovery)" +assert_file_contains "$PROJECT_DIR/commands/claude-code-setup.md" "Phase 2" "Has Phase 2 (Present + Ask)" +assert_file_contains "$PROJECT_DIR/commands/claude-code-setup.md" "Phase 3" "Has Phase 3 (Execute)" +assert_file_contains "$PROJECT_DIR/commands/claude-code-setup.md" "setup-status.sh" "Uses setup-status.sh discovery script" +assert_file_contains "$PROJECT_DIR/commands/claude-code-setup.md" "AskUserQuestion" "Uses AskUserQuestion for user input" +assert_file_contains "$PROJECT_DIR/commands/claude-code-setup.md" "multiSelect" "Supports multi-select" -scenario "Command has correct URLs" +scenario "Command has discovery phase" -assert_file_contains "$PROJECT_DIR/commands/claude-code-setup.md" "claude-code-setup" "Uses correct repo name" +assert_file_contains "$PROJECT_DIR/commands/claude-code-setup.md" "git clone" "Has clone instruction" +assert_file_contains "$PROJECT_DIR/commands/claude-code-setup.md" "installed.json" "References installed.json" +assert_file_contains "$PROJECT_DIR/commands/claude-code-setup.md" "content_version" "References content_version" +assert_file_contains "$PROJECT_DIR/commands/claude-code-setup.md" "templates/VERSION" "References templates/VERSION" -scenario "Command has error handling" +scenario "Command has status presentation" assert_file_contains "$PROJECT_DIR/commands/claude-code-setup.md" "up-to-date" "Has up-to-date message" -assert_file_contains "$PROJECT_DIR/commands/claude-code-setup.md" "Manual upgrade" "Has manual fallback" +assert_file_contains "$PROJECT_DIR/commands/claude-code-setup.md" "Modules available to install" "Shows available modules output" +assert_file_contains "$PROJECT_DIR/commands/claude-code-setup.md" "External Plugins" "Shows External Plugins in status" +assert_file_contains "$PROJECT_DIR/commands/claude-code-setup.md" "CHANGELOG" "References CHANGELOG for upgrade notes" -scenario "Command uses --yes for non-interactive mode" +scenario "Command has execution chains" + +assert_file_contains "$PROJECT_DIR/commands/claude-code-setup.md" "install.sh --update" "Runs install.sh --update" # Use grep -F for fixed string match (avoids -- being interpreted as option) if grep -qF -- "--update --yes" "$PROJECT_DIR/commands/claude-code-setup.md"; then @@ -46,22 +53,21 @@ else fail "Should use --yes flag for non-interactive update" fi -scenario "Command checks for new modules" - -assert_file_contains "$PROJECT_DIR/commands/claude-code-setup.md" "Check for new modules" "Has new modules check step" -assert_file_contains "$PROJECT_DIR/commands/claude-code-setup.md" "install.sh --add" "Can install new modules" -assert_file_contains "$PROJECT_DIR/commands/claude-code-setup.md" "Modules available to install" "Shows available modules output" +assert_file_contains "$PROJECT_DIR/commands/claude-code-setup.md" "install.sh --add-skill" "Can install skills via --add-skill" +assert_file_contains "$PROJECT_DIR/commands/claude-code-setup.md" "install.sh --add-mcp" "Can install MCP via --add-mcp" +assert_file_contains "$PROJECT_DIR/commands/claude-code-setup.md" "install.sh --remove-skill" "Can remove skills via --remove-skill" +assert_file_contains "$PROJECT_DIR/commands/claude-code-setup.md" "install.sh --remove-mcp" "Can remove MCP via --remove-mcp" scenario "Command has custom repo support" -assert_file_contains "$PROJECT_DIR/commands/claude-code-setup.md" "Check custom repo" "Has custom repo check step" assert_file_contains "$PROJECT_DIR/commands/claude-code-setup.md" "custom_version" "Checks custom_version" +assert_file_contains "$PROJECT_DIR/commands/claude-code-setup.md" "custom.update_available" "Checks custom update status from JSON" assert_file_contains "$PROJECT_DIR/commands/claude-code-setup.md" "Upgrade custom" "Has upgrade custom option" assert_file_contains "$PROJECT_DIR/commands/claude-code-setup.md" "not configured" "Has no-custom-repo message" scenario "Command has Agent Teams support" -assert_file_contains "$PROJECT_DIR/commands/claude-code-setup.md" "Check Agent Teams status" "Has Agent Teams status check step" +assert_file_contains "$PROJECT_DIR/commands/claude-code-setup.md" "agent_teams.enabled" "Checks Agent Teams status from JSON" assert_file_contains "$PROJECT_DIR/commands/claude-code-setup.md" "Enable Agent Teams" "Has Enable Agent Teams option" assert_file_contains "$PROJECT_DIR/commands/claude-code-setup.md" "CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS" "References Agent Teams env var" assert_file_contains "$PROJECT_DIR/commands/claude-code-setup.md" "Agent Teams: enabled" "Has enabled status example" @@ -70,10 +76,26 @@ assert_file_contains "$PROJECT_DIR/commands/claude-code-setup.md" "Agent Teams: scenario "Command has external plugins support" assert_file_contains "$PROJECT_DIR/commands/claude-code-setup.md" "external-plugins.json" "References external-plugins.json" -assert_file_contains "$PROJECT_DIR/commands/claude-code-setup.md" "External Plugins" "Shows External Plugins in status" assert_file_contains "$PROJECT_DIR/commands/claude-code-setup.md" "claude plugin marketplace" "Has marketplace add instruction" assert_file_contains "$PROJECT_DIR/commands/claude-code-setup.md" "claude plugin install" "Has plugin install instruction" assert_file_contains "$PROJECT_DIR/commands/claude-code-setup.md" "Restart Claude Code" "Has restart hint" +scenario "Command has MCP API key handling" + +assert_file_contains "$PROJECT_DIR/commands/claude-code-setup.md" "requiresApiKey" "Checks for API key requirement" +assert_file_contains "$PROJECT_DIR/commands/claude-code-setup.md" "YOUR_API_KEY_HERE" "Uses placeholder for API keys" + +scenario "Command has cleanup and constraints" + +assert_file_contains "$PROJECT_DIR/commands/claude-code-setup.md" "; rm -rf" "Cleanup uses ; separator (not &&)" +assert_file_contains "$PROJECT_DIR/commands/claude-code-setup.md" "0 Bash calls" "Phase 2 specifies zero Bash calls" +assert_file_contains "$PROJECT_DIR/commands/claude-code-setup.md" "claude plugin remove" "Has plugin remove instruction" + +scenario "Command has error handling" + +assert_file_contains "$PROJECT_DIR/commands/claude-code-setup.md" "Manual upgrade" "Has manual fallback" +assert_file_contains "$PROJECT_DIR/commands/claude-code-setup.md" "not_installed" "Handles not-installed error" +assert_file_contains "$PROJECT_DIR/commands/claude-code-setup.md" "rm -rf" "Cleans up temp dir" + # Print summary print_summary diff --git a/tests/scenarios/26-setup-status.sh b/tests/scenarios/26-setup-status.sh new file mode 100755 index 0000000..9ac0cbf --- /dev/null +++ b/tests/scenarios/26-setup-status.sh @@ -0,0 +1,363 @@ +#!/bin/bash + +# Scenario: Discovery script (setup-status.sh) outputs valid JSON status + +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +export PROJECT_DIR="${1:-$(cd "$SCRIPT_DIR/../.." && pwd)}" + +# Source helpers +# shellcheck source=../helpers.sh +source "$SCRIPT_DIR/../helpers.sh" + +# Setup test environment +setup_test_env +trap cleanup_test_env EXIT + +# Helper: run setup-status.sh in test environment +run_setup_status() { + HOME="$TEST_DIR" \ + CLAUDE_DIR="$CLAUDE_DIR" \ + MCP_CONFIG_FILE="$MCP_CONFIG_FILE" \ + "$PROJECT_DIR/lib/setup-status.sh" 2>/dev/null +} + +# ============================================ +# SCENARIO: No installed.json → error output +# ============================================ + +scenario "No installed.json outputs error" + +output=$(run_setup_status) + +# Must be valid JSON +if echo "$output" | jq . > /dev/null 2>&1; then + pass "Output is valid JSON" +else + fail "Output should be valid JSON (got: $output)" +fi + +# Must contain error field +if echo "$output" | jq -e '.error == "not_installed"' > /dev/null 2>&1; then + pass "Error is 'not_installed'" +else + fail "Should output error: not_installed" +fi + +# ============================================ +# SCENARIO: After minimal install +# ============================================ + +scenario "Setup status after minimal install" + +# Do a minimal install (no MCP, no skills) +run_install_expect ' + deselect_all_mcp + deselect_all_skills + decline_statusline + decline_agent_teams +' > /dev/null + +assert_file_exists "$INSTALLED_FILE" "installed.json exists after install" + +# Run setup-status.sh +output=$(run_setup_status) + +# Must be valid JSON +if echo "$output" | jq . > /dev/null 2>&1; then + pass "Output is valid JSON after install" +else + fail "Output should be valid JSON after install (got: $output)" +fi + +# ============================================ +# SCENARIO: Base version checks +# ============================================ + +scenario "Base version fields" + +available_version=$(cat "$PROJECT_DIR/templates/VERSION") +installed_version=$(jq -r '.content_version' "$INSTALLED_FILE") + +# .base.installed matches installed.json +actual_installed=$(echo "$output" | jq '.base.installed') +if [[ "$actual_installed" = "$installed_version" ]]; then + pass ".base.installed matches installed.json ($installed_version)" +else + fail ".base.installed should be $installed_version (got: $actual_installed)" +fi + +# .base.available matches templates/VERSION +actual_available=$(echo "$output" | jq '.base.available') +if [[ "$actual_available" = "$available_version" ]]; then + pass ".base.available matches templates/VERSION ($available_version)" +else + fail ".base.available should be $available_version (got: $actual_available)" +fi + +# .base.update_available should be false (same version) +actual_update=$(echo "$output" | jq '.base.update_available') +if [[ "$actual_update" = "false" ]]; then + pass ".base.update_available is false (same version)" +else + fail ".base.update_available should be false (got: $actual_update)" +fi + +# ============================================ +# SCENARIO: Update available detection +# ============================================ + +scenario "Detects update available" + +# Artificially lower the installed version +jq '.content_version = 1' "$INSTALLED_FILE" > "$INSTALLED_FILE.tmp" && mv "$INSTALLED_FILE.tmp" "$INSTALLED_FILE" + +output=$(run_setup_status) + +actual_update=$(echo "$output" | jq '.base.update_available') +if [[ "$actual_update" = "true" ]]; then + pass ".base.update_available is true when installed < available" +else + fail ".base.update_available should be true (got: $actual_update)" +fi + +actual_installed=$(echo "$output" | jq '.base.installed') +if [[ "$actual_installed" = "1" ]]; then + pass ".base.installed reflects lowered version (1)" +else + fail ".base.installed should be 1 (got: $actual_installed)" +fi + +# Restore version for subsequent tests +jq --argjson v "$available_version" '.content_version = $v' "$INSTALLED_FILE" > "$INSTALLED_FILE.tmp" && mv "$INSTALLED_FILE.tmp" "$INSTALLED_FILE" + +# ============================================ +# SCENARIO: Installed modules +# ============================================ + +scenario "Installed modules (empty after minimal install)" + +output=$(run_setup_status) + +installed_skills_count=$(echo "$output" | jq '.installed_modules.skills | length') +if [[ "$installed_skills_count" = "0" ]]; then + pass ".installed_modules.skills is empty (no skills installed)" +else + fail ".installed_modules.skills should be empty (got $installed_skills_count)" +fi + +installed_mcp_count=$(echo "$output" | jq '.installed_modules.mcp | length') +if [[ "$installed_mcp_count" = "0" ]]; then + pass ".installed_modules.mcp is empty (no MCP installed)" +else + fail ".installed_modules.mcp should be empty (got $installed_mcp_count)" +fi + +# ============================================ +# SCENARIO: New modules detection +# ============================================ + +scenario "New + installed modules equal repo total" + +# Count available skills in the repo +repo_skills_count=0 +for d in "$PROJECT_DIR/skills/"*/; do + [[ -d "$d" ]] && repo_skills_count=$((repo_skills_count + 1)) +done +new_skills_count=$(echo "$output" | jq '.new_modules.skills | length') +installed_skills_count=$(echo "$output" | jq '.installed_modules.skills | length') +total_skills=$((new_skills_count + installed_skills_count)) +if [[ "$total_skills" = "$repo_skills_count" ]]; then + pass "new + installed skills = repo total ($repo_skills_count)" +else + fail "new ($new_skills_count) + installed ($installed_skills_count) should equal $repo_skills_count (got: $total_skills)" +fi + +# Count available MCP in the repo +repo_mcp_count=0 +for f in "$PROJECT_DIR/mcp/"*.json; do + [[ -f "$f" ]] && repo_mcp_count=$((repo_mcp_count + 1)) +done +new_mcp_count=$(echo "$output" | jq '.new_modules.mcp | length') +installed_mcp_count=$(echo "$output" | jq '.installed_modules.mcp | length') +total_mcp=$((new_mcp_count + installed_mcp_count)) +if [[ "$total_mcp" = "$repo_mcp_count" ]]; then + pass "new + installed MCP = repo total ($repo_mcp_count)" +else + fail "new ($new_mcp_count) + installed ($installed_mcp_count) should equal $repo_mcp_count (got: $total_mcp)" +fi + +# ============================================ +# SCENARIO: After installing a skill +# ============================================ + +scenario "Reflects installed skill" + +# Install one skill directly +HOME="$TEST_DIR" CLAUDE_DIR="$CLAUDE_DIR" MCP_CONFIG_FILE="$MCP_CONFIG_FILE" \ + SKIP_SKILL_DEPS=1 "$PROJECT_DIR/install.sh" --add-skill standards-python > /dev/null 2>&1 + +output=$(run_setup_status) + +# standards-python should be in installed_modules.skills +if echo "$output" | jq -e '.installed_modules.skills | index("standards-python")' > /dev/null 2>&1; then + pass "standards-python in .installed_modules.skills" +else + fail "standards-python should be in .installed_modules.skills" +fi + +# standards-python should NOT be in new_modules.skills +if echo "$output" | jq -e '.new_modules.skills | index("standards-python")' > /dev/null 2>&1; then + fail "standards-python should NOT be in .new_modules.skills" +else + pass "standards-python not in .new_modules.skills" +fi + +# new + installed should still equal repo total +actual_new=$(echo "$output" | jq '.new_modules.skills | length') +actual_installed=$(echo "$output" | jq '.installed_modules.skills | length') +actual_total=$((actual_new + actual_installed)) +if [[ "$actual_total" = "$repo_skills_count" ]]; then + pass "new + installed skills still = repo total after add ($repo_skills_count)" +else + fail "new ($actual_new) + installed ($actual_installed) should equal $repo_skills_count (got: $actual_total)" +fi + +# ============================================ +# SCENARIO: After installing an MCP +# ============================================ + +scenario "Reflects installed MCP" + +HOME="$TEST_DIR" CLAUDE_DIR="$CLAUDE_DIR" MCP_CONFIG_FILE="$MCP_CONFIG_FILE" \ + "$PROJECT_DIR/install.sh" --add-mcp pdf-reader > /dev/null 2>&1 + +output=$(run_setup_status) + +if echo "$output" | jq -e '.installed_modules.mcp | index("pdf-reader")' > /dev/null 2>&1; then + pass "pdf-reader in .installed_modules.mcp" +else + fail "pdf-reader should be in .installed_modules.mcp" +fi + +if echo "$output" | jq -e '.new_modules.mcp | index("pdf-reader")' > /dev/null 2>&1; then + fail "pdf-reader should NOT be in .new_modules.mcp" +else + pass "pdf-reader not in .new_modules.mcp" +fi + +# ============================================ +# SCENARIO: Agent Teams status +# ============================================ + +scenario "Agent Teams disabled by default" + +output=$(run_setup_status) + +actual_teams=$(echo "$output" | jq '.agent_teams.enabled') +if [[ "$actual_teams" = "false" ]]; then + pass ".agent_teams.enabled is false" +else + fail ".agent_teams.enabled should be false (got: $actual_teams)" +fi + +scenario "Agent Teams enabled" + +# Enable agent teams in settings.json +mkdir -p "$CLAUDE_DIR" +echo '{}' > "$CLAUDE_DIR/settings.json" +jq '.env = (.env // {}) | .env.CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS = "1"' \ + "$CLAUDE_DIR/settings.json" > "$CLAUDE_DIR/settings.json.tmp" && \ + mv "$CLAUDE_DIR/settings.json.tmp" "$CLAUDE_DIR/settings.json" + +output=$(run_setup_status) + +actual_teams=$(echo "$output" | jq '.agent_teams.enabled') +if [[ "$actual_teams" = "true" ]]; then + pass ".agent_teams.enabled is true" +else + fail ".agent_teams.enabled should be true (got: $actual_teams)" +fi + +# ============================================ +# SCENARIO: Custom repo not configured +# ============================================ + +scenario "Custom repo not configured" + +output=$(run_setup_status) + +actual_custom=$(echo "$output" | jq '.custom.configured') +if [[ "$actual_custom" = "false" ]]; then + pass ".custom.configured is false" +else + fail ".custom.configured should be false (got: $actual_custom)" +fi + +# ============================================ +# SCENARIO: temp_dir in output +# ============================================ + +scenario "temp_dir in output" + +output=$(run_setup_status) + +actual_temp=$(echo "$output" | jq -r '.temp_dir') +if [[ "$actual_temp" = "$PROJECT_DIR" ]]; then + pass ".temp_dir matches SCRIPT_DIR parent ($PROJECT_DIR)" +else + fail ".temp_dir should be $PROJECT_DIR (got: $actual_temp)" +fi + +# ============================================ +# SCENARIO: stdout is valid JSON (no stderr leakage) +# ============================================ + +scenario "No stderr leakage to stdout" + +# Run and capture stdout only (stderr goes to /dev/null) +stdout_output=$(run_setup_status) + +# Validate entire stdout is valid JSON +if echo "$stdout_output" | jq . > /dev/null 2>&1; then + pass "stdout is clean JSON (no stderr leakage)" +else + fail "stdout should be valid JSON without stderr content" +fi + +# Verify output has all expected top-level keys +for key in temp_dir base custom new_modules installed_modules agent_teams; do + if echo "$stdout_output" | jq -e "has(\"$key\")" > /dev/null 2>&1; then + pass "Has required key: $key" + else + fail "Missing required key: $key" + fi +done + +# ============================================ +# SCENARIO: Plugins discovery +# ============================================ + +scenario "Plugins discovery" + +output=$(run_setup_status) + +# Should have new plugins listed (none installed) +new_plugins_count=$(echo "$output" | jq '.new_modules.plugins | length') +if [[ "$new_plugins_count" -gt 0 ]]; then + pass ".new_modules.plugins has entries ($new_plugins_count)" +else + fail ".new_modules.plugins should have entries (got: $new_plugins_count)" +fi + +# Check that plugin IDs contain @ separator (id@marketplace format) +first_plugin=$(echo "$output" | jq -r '.new_modules.plugins[0]') +if [[ "$first_plugin" == *"@"* ]]; then + pass "Plugin IDs use id@marketplace format ($first_plugin)" +else + fail "Plugin IDs should use id@marketplace format (got: $first_plugin)" +fi + +# Print summary +print_summary diff --git a/tests/scenarios/27-remove-direct.sh b/tests/scenarios/27-remove-direct.sh new file mode 100755 index 0000000..ae8e137 --- /dev/null +++ b/tests/scenarios/27-remove-direct.sh @@ -0,0 +1,171 @@ +#!/bin/bash + +# Scenario: Direct module removal with --remove-skill and --remove-mcp + +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +export PROJECT_DIR="${1:-$(cd "$SCRIPT_DIR/../.." && pwd)}" + +# Source helpers +# shellcheck source=../helpers.sh +source "$SCRIPT_DIR/../helpers.sh" + +# Setup test environment +setup_test_env +trap cleanup_test_env EXIT + +scenario "Remove skill with --remove-skill" + +# First do a minimal install then add a skill +run_install_expect ' + deselect_all_mcp + deselect_all_skills + decline_statusline + decline_agent_teams +' > /dev/null + +assert_file_exists "$INSTALLED_FILE" "installed.json exists" + +# Install a skill to remove later +HOME="$TEST_DIR" CLAUDE_DIR="$CLAUDE_DIR" MCP_CONFIG_FILE="$MCP_CONFIG_FILE" \ + SKIP_SKILL_DEPS=1 "$PROJECT_DIR/install.sh" --add-skill standards-kotlin > /dev/null 2>&1 + +assert_dir_exists "$CLAUDE_DIR/skills/standards-kotlin" "standards-kotlin installed" +assert_json_exists "$INSTALLED_FILE" '.skills[] | select(. == "standards-kotlin")' "skill tracked before removal" + +# Remove the skill +output=$(HOME="$TEST_DIR" CLAUDE_DIR="$CLAUDE_DIR" MCP_CONFIG_FILE="$MCP_CONFIG_FILE" \ + "$PROJECT_DIR/install.sh" --remove-skill standards-kotlin 2>&1) + +# Verify skill directory is gone +if [[ -d "$CLAUDE_DIR/skills/standards-kotlin" ]]; then + fail "skill directory should be removed" +else + pass "skill directory removed" +fi + +# Verify tracking is gone +if jq -e '.skills[] | select(. == "standards-kotlin")' "$INSTALLED_FILE" > /dev/null 2>&1; then + fail "skill should be removed from installed.json" +else + pass "skill removed from installed.json" +fi + +# Verify output messages +if echo "$output" | grep -q "Removing Skill"; then + pass "--remove-skill shows progress" +else + fail "--remove-skill should show progress" +fi + +if echo "$output" | grep -q "standards-kotlin removed"; then + pass "--remove-skill confirms removal" +else + fail "--remove-skill should confirm removal" +fi + +if echo "$output" | grep -q "CLAUDE.md updated"; then + pass "--remove-skill rebuilds CLAUDE.md" +else + fail "--remove-skill should rebuild CLAUDE.md" +fi + +# Verify removed skill is absent from CLAUDE.md content +if grep -q "standards-kotlin" "$CLAUDE_DIR/CLAUDE.md"; then + fail "removed skill should not appear in CLAUDE.md" +else + pass "removed skill absent from CLAUDE.md" +fi + +scenario "Remove MCP with --remove-mcp" + +# Install an MCP to remove later +HOME="$TEST_DIR" CLAUDE_DIR="$CLAUDE_DIR" MCP_CONFIG_FILE="$MCP_CONFIG_FILE" \ + SKIP_SKILL_DEPS=1 "$PROJECT_DIR/install.sh" --add-mcp pdf-reader > /dev/null 2>&1 + +assert_json_exists "$MCP_CONFIG_FILE" '.mcpServers["pdf-reader"]' "pdf-reader configured before removal" +assert_json_exists "$INSTALLED_FILE" '.mcp[] | select(. == "pdf-reader")' "MCP tracked before removal" + +# Remove the MCP +output=$(HOME="$TEST_DIR" CLAUDE_DIR="$CLAUDE_DIR" MCP_CONFIG_FILE="$MCP_CONFIG_FILE" \ + "$PROJECT_DIR/install.sh" --remove-mcp pdf-reader 2>&1) + +# Verify MCP is gone from config +if jq -e '.mcpServers["pdf-reader"]' "$MCP_CONFIG_FILE" > /dev/null 2>&1; then + fail "MCP should be removed from .claude.json" +else + pass "MCP removed from .claude.json" +fi + +# Verify tracking is gone +if jq -e '.mcp[] | select(. == "pdf-reader")' "$INSTALLED_FILE" > /dev/null 2>&1; then + fail "MCP should be removed from installed.json" +else + pass "MCP removed from installed.json" +fi + +# Verify output messages +if echo "$output" | grep -q "Removing MCP"; then + pass "--remove-mcp shows progress" +else + fail "--remove-mcp should show progress" +fi + +if echo "$output" | grep -q "pdf-reader removed"; then + pass "--remove-mcp confirms removal" +else + fail "--remove-mcp should confirm removal" +fi + +# Verify removed MCP is absent from CLAUDE.md content +if grep -q "pdf-reader" "$CLAUDE_DIR/CLAUDE.md"; then + fail "removed MCP should not appear in CLAUDE.md" +else + pass "removed MCP absent from CLAUDE.md" +fi + +scenario "Remove non-existent skill" + +output=$(HOME="$TEST_DIR" CLAUDE_DIR="$CLAUDE_DIR" MCP_CONFIG_FILE="$MCP_CONFIG_FILE" \ + "$PROJECT_DIR/install.sh" --remove-skill nonexistent 2>&1) || true + +if echo "$output" | grep -q "not installed"; then + pass "--remove-skill reports non-installed skill" +else + fail "--remove-skill should report non-installed skill" +fi + +scenario "Remove non-existent MCP" + +output=$(HOME="$TEST_DIR" CLAUDE_DIR="$CLAUDE_DIR" MCP_CONFIG_FILE="$MCP_CONFIG_FILE" \ + "$PROJECT_DIR/install.sh" --remove-mcp nonexistent 2>&1) || true + +if echo "$output" | grep -q "not installed"; then + pass "--remove-mcp reports non-installed MCP" +else + fail "--remove-mcp should report non-installed MCP" +fi + +scenario "--remove-skill without argument shows error" + +output=$(HOME="$TEST_DIR" "$PROJECT_DIR/install.sh" --remove-skill 2>&1) || true + +if echo "$output" | grep -q "requires"; then + pass "--remove-skill without arg shows error" +else + fail "--remove-skill without arg should show error" +fi + +scenario "--remove-mcp without argument shows error" + +output=$(HOME="$TEST_DIR" "$PROJECT_DIR/install.sh" --remove-mcp 2>&1) || true + +if echo "$output" | grep -q "requires"; then + pass "--remove-mcp without arg shows error" +else + fail "--remove-mcp without arg should show error" +fi + +# Print summary +print_summary diff --git a/website/pages/commands/claude-code-setup.mdx b/website/pages/commands/claude-code-setup.mdx index fcc960c..465f5f2 100644 --- a/website/pages/commands/claude-code-setup.mdx +++ b/website/pages/commands/claude-code-setup.mdx @@ -1,6 +1,6 @@ # /claude-code-setup -Check status and update Claude Code Setup from within Claude. +Check status and manage your Claude Code Setup installation from within Claude. ## Usage @@ -8,67 +8,81 @@ Check status and update Claude Code Setup from within Claude. /claude-code-setup ``` -## What It Does +## How It Works -1. **Checks versions** - - Compares installed version with available version - - Shows what's new in the update +The command runs in three phases, completing any scenario in just **2 permission prompts**. -2. **Offers actions** - - Upgrade base modules - - Upgrade custom modules - - Install new modules (Skills, MCP servers, External plugins) - - Remove installed modules +### Phase 1: Discovery (1 Bash call) -## Example Output +Clones the repo and runs a discovery script that gathers all status info as JSON: -``` -Claude Code Setup Status: +- Installed vs available version +- New modules (skills, MCP servers, plugins) +- Installed modules +- Custom repo status +- Agent Teams status -Installed: v22 -Available: v23 +If the network is unavailable or the installation is missing, the command shows a message and stops gracefully. -Changes in v23: -- Decision Log feature -- Context quality improvements +### Phase 2: Present + Ask (0 Bash calls) -What would you like to do? -1) Upgrade to v23 -2) Check custom modules -3) Add new modules -4) Remove modules -5) Cancel -``` +Shows your installation status and asks what you want to do. All options are generated from the discovery data — no additional shell commands needed. -## Upgrade vs Fresh Install +### Phase 3: Execute (1 Bash call) -| Action | What happens | -|--------|--------------| -| Upgrade | Preserves User Instructions section | -| Fresh install | Overwrites everything | +Chains all selected actions into a single command. Install, remove, and upgrade operations run in sequence, followed by cleanup. -Your customizations in the "User Instructions" section of `~/.claude/CLAUDE.md` are preserved during upgrades. +## Example Output -## Custom Modules +### Status with upgrade available -If you have custom modules installed (via `/add-custom`), this command also checks for updates to those. +``` +claude-code-setup status: +- Base: v50 installed, v52 available +- Custom: v1 (up-to-date) +- Agent Teams: enabled + +Modules available to install: + Skills: standards-kotlin + MCP Servers: brave-search + External Plugins: document-skills + +Changes in v51-v52: +- v52: Add /skill-creator command skill +- v51: Add standards-kotlin coding standards +``` + +### No upgrade available ``` -Custom modules: -- company-standards: v2 → v3 available -- internal-mcp: up to date +claude-code-setup status: +- Base: v52 (up-to-date) +- Custom: (not configured) +- Agent Teams: enabled + +Modules available to install: + MCP Servers: brave-search + +Tip: Use /add-custom to add company modules. ``` -## Modules Requiring API Keys +## Available Actions + +| Action | When shown | +|--------|-----------| +| Upgrade base | New version available | +| Upgrade custom | Custom repo has updates | +| Install new skills/MCP | Uninstalled modules exist | +| Install plugins | Uninstalled plugins exist | +| Remove modules | Installed modules exist | +| Enable Agent Teams | Not yet configured | -When installing MCP servers that require an API key, the interactive prompt cannot work within Claude Code (stdin is consumed by menu interactions). +You can select multiple actions at once — they all execute in a single chained command. + +## Modules Requiring API Keys -In this case, Claude will: -1. **Insert the config directly** into `~/.claude.json` with a placeholder -2. Tell you where to get the API key -3. Instruct you to replace the placeholder +When installing MCP servers that require an API key (like `brave-search`), Claude inserts the config directly with a placeholder instead of using the interactive installer prompt. -**Example output:** ``` MCP "brave-search" configured with placeholder. @@ -82,30 +96,30 @@ To get your API key: Then restart Claude Code. ``` -You only need to replace one value - no copy/pasting of JSON blocks required. - ## External Plugins -External plugins (like `code-review-ai`) are installed directly via the Claude CLI: +External plugins (like `code-review-ai`) are installed via the Claude CLI: -1. Claude adds the marketplace if needed (`claude plugin marketplace add`) -2. Claude installs the plugin (`claude plugin install`) +1. Claude adds the marketplace if needed +2. Claude installs the plugin 3. You restart Claude Code to activate -**Example output:** -``` -Installing external plugin code-review-ai... - Adding marketplace claude-code-workflows... - ✓ Marketplace claude-code-workflows added - Installing code-review-ai... - ✓ code-review-ai installed +## After Changes + +After any install, upgrade, or removal: -Restart Claude Code to activate the plugin. ``` +Completed: +- Upgraded base: v50 -> v52 +- Installed skill: standards-kotlin +- Enabled Agent Teams -This is fully automatic - you just select which plugins to install. +IMPORTANT: Restart Claude Code now. + After restart, run /catchup to reload context. +``` ## Related +- [Module Management](/features/module-management) - Terminal-based management with install.sh - [/add-custom](/commands/add-custom) - Install custom modules - [Installation](/getting-started/installation) - Initial setup diff --git a/website/pages/features/module-management.mdx b/website/pages/features/module-management.mdx index 70ecda4..e7f1fe9 100644 --- a/website/pages/features/module-management.mdx +++ b/website/pages/features/module-management.mdx @@ -4,7 +4,9 @@ Add, update, remove, and list installed modules (skills, MCP servers, plugins). ## From Within Claude -Use `/claude-code-setup` to manage modules interactively during a session. +Use `/claude-code-setup` to manage modules interactively during a session. It discovers your installation status and lets you install, remove, or upgrade — all in 2 permission prompts. + +See [/claude-code-setup](/commands/claude-code-setup) for details. ## From Terminal @@ -40,6 +42,15 @@ Shows module selector for: - Skills not yet installed - External plugins (via Claude CLI) +### --add-skill, --add-mcp + +Install a specific module by name. + +```bash +./install.sh --add-skill standards-kotlin +./install.sh --add-mcp brave-search +``` + ### --update Update all installed modules to latest versions. @@ -48,7 +59,7 @@ Update all installed modules to latest versions. ./install.sh --update ``` -Equivalent to running `/claude-code-setup` from within Claude. +Preserves your User Instructions section in `~/.claude/CLAUDE.md`. ### --remove @@ -70,6 +81,24 @@ Select modules to remove, then confirm. Removal: - Updates `installed.json` tracking - Regenerates CLAUDE.md tables +### --remove-skill, --remove-mcp + +Remove a specific module by name, non-interactively. + +```bash +./install.sh --remove-skill standards-kotlin +./install.sh --remove-mcp brave-search +``` + +Also works with custom modules using the `custom:` prefix: + +```bash +./install.sh --remove-skill custom:my-skill +./install.sh --remove-mcp custom:my-mcp +``` + +These flags are used by `/claude-code-setup` to chain removals into a single command. They're also useful for scripting. + ### --list Show what's currently installed. @@ -135,43 +164,10 @@ After initial install, update from within Claude: /claude-code-setup ``` -This preserves your User Instructions section. - -## Reducing Permission Prompts - -When running `/claude-code-setup`, Claude Code's sandbox prompts for permission on each command (`git clone`, `curl`, `jq`, etc.). - -### Recommended: Run from Terminal - -Run updates from terminal instead of within Claude - no permission prompts: +Or from terminal: ```bash -./install.sh --update +cd claude-code-setup && git pull && ./install.sh --update ``` -This is the simplest solution and works reliably. - -### Alternative: Allow Rules (Limited) - -import { Callout } from 'nextra/components' - -You can add permission rules to `~/.claude/settings.json`, but due to a [known limitation](https://github.com/anthropics/claude-code/issues/5503) in Claude Code, rules only work for simple commands: - -```json -{ - "permissions": { - "allow": [ - "Bash(curl -fsSL:*)", - "Bash(jq -r:*)", - "Bash(rm -rf /tmp/claude-setup-:*)", - "Read(~/.claude/**)" - ] - } -} -``` - - -**Limitation:** Rules with special characters (`$`, `[[`, `=`) at the command start don't work reliably. Commands like `temp_dir=$(...)` or `if [[ ...` will still prompt for permission. - - -For a prompt-free experience, use `./install.sh --update` from terminal instead. +Both preserve your User Instructions section.