From 27b4f5ee2139ac6312682303746f17f02632f347 Mon Sep 17 00:00:00 2001 From: Rii Salcedo Date: Fri, 3 Apr 2026 04:22:44 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=F0=9F=90=9B=20fix=20dev=20install=20to?= =?UTF-8?q?=20use=20correct=20marketplace=20name=20and=20registration?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Use riicodespretty (from marketplace.json name field) instead of invented 'local' as marketplace name. Register in known_marketplaces.json with directory source so Claude Code can resolve headroom@riicodespretty. Also revert mcpServers.headroom from ~/.claude.json on uninstall. --- .claude-plugin/marketplace.json | 5 +- dev-install.sh | 63 ++++++++++++++++++++--- dev-uninstall.sh | 88 +++++++++++++++++++++++++++------ hooks/hooks.json | 13 +---- scripts/manager.py | 2 +- 5 files changed, 132 insertions(+), 39 deletions(-) diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 5275c6a..bcdbb28 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -6,10 +6,7 @@ "plugins": [ { "name": "headroom", - "source": { - "source": "github", - "repo": "riicodespretty/headroom-claude-plugin" - } + "source": "./" } ] } diff --git a/dev-install.sh b/dev-install.sh index 1f3cd5a..99bcbaa 100755 --- a/dev-install.sh +++ b/dev-install.sh @@ -2,7 +2,12 @@ set -euo pipefail REPO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -CACHE_DIR="$HOME/.claude/plugins/cache/local/headroom/1.0.0" +MARKETPLACE_NAME="riicodespretty" +PLUGIN_NAME="headroom" +PLUGIN_VERSION="1.0.0" +CACHE_DIR="$HOME/.claude/plugins/cache/$MARKETPLACE_NAME/$PLUGIN_NAME/$PLUGIN_VERSION" +MARKETPLACE_DIR="$HOME/.claude/plugins/marketplaces/$MARKETPLACE_NAME" +KNOWN_MARKETPLACES="$HOME/.claude/plugins/known_marketplaces.json" echo "Installing headroom plugin (dev mode)..." @@ -15,7 +20,7 @@ if [ ! -x "$HOME/.venv/bin/headroom" ]; then fi echo " ✓ headroom binary found at ~/.venv/bin/headroom" -# 1. Create symlink into plugin cache +# 1. Symlink repo into plugin cache mkdir -p "$(dirname "$CACHE_DIR")" if [ -L "$CACHE_DIR" ]; then rm "$CACHE_DIR" @@ -26,10 +31,55 @@ fi ln -s "$REPO_DIR" "$CACHE_DIR" echo " ✓ Symlinked $REPO_DIR → $CACHE_DIR" -# 2. Register in enabledPlugins using Python for safe JSON editing -python3 - <<'PYEOF' +# 2. Symlink repo as the marketplace directory so Claude Code can resolve it +mkdir -p "$(dirname "$MARKETPLACE_DIR")" +if [ -L "$MARKETPLACE_DIR" ]; then + rm "$MARKETPLACE_DIR" +elif [ -e "$MARKETPLACE_DIR" ]; then + echo " ERROR: $MARKETPLACE_DIR exists and is not a symlink — remove it manually first." >&2 + exit 1 +fi +ln -s "$REPO_DIR" "$MARKETPLACE_DIR" +echo " ✓ Symlinked $REPO_DIR → $MARKETPLACE_DIR" + +# 3. Register in known_marketplaces.json so Claude Code recognises the marketplace +python3 - "$REPO_DIR" "$MARKETPLACE_NAME" "$MARKETPLACE_DIR" <<'PYEOF' import json, os, sys from pathlib import Path +from datetime import datetime, timezone + +repo_dir, marketplace_name, marketplace_dir = sys.argv[1], sys.argv[2], sys.argv[3] + +known_path = Path.home() / ".claude" / "plugins" / "known_marketplaces.json" +known_path.parent.mkdir(parents=True, exist_ok=True) + +data = {} +if known_path.exists(): + try: + data = json.loads(known_path.read_text()) + except (json.JSONDecodeError, OSError): + pass + +data[marketplace_name] = { + "source": {"source": "directory", "path": repo_dir}, + "installLocation": marketplace_dir, + "lastUpdated": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%S.000Z"), + "autoUpdate": False, +} + +tmp = known_path.with_suffix(".json.tmp") +tmp.write_text(json.dumps(data, indent=2)) +os.replace(tmp, known_path) +print(f" ✓ Registered {marketplace_name} in known_marketplaces.json") +PYEOF + +# 4. Enable the plugin and clear any stale ANTHROPIC_BASE_URL in settings.json +python3 - "$MARKETPLACE_NAME" "$PLUGIN_NAME" <<'PYEOF' +import json, os, sys +from pathlib import Path + +marketplace_name, plugin_name = sys.argv[1], sys.argv[2] +plugin_key = f"{plugin_name}@{marketplace_name}" settings_path = Path.home() / ".claude" / "settings.json" if not settings_path.exists(): @@ -37,13 +87,12 @@ if not settings_path.exists(): sys.exit(1) settings = json.loads(settings_path.read_text()) -settings.setdefault("enabledPlugins", {}) -settings["enabledPlugins"]["headroom@local"] = True +settings.setdefault("enabledPlugins", {})[plugin_key] = True tmp = settings_path.with_suffix(".json.tmp") tmp.write_text(json.dumps(settings, indent=2)) os.replace(tmp, settings_path) -print(" ✓ Registered headroom@local in enabledPlugins") +print(f" ✓ Enabled {plugin_key} in enabledPlugins") PYEOF echo "" diff --git a/dev-uninstall.sh b/dev-uninstall.sh index 6aab688..1a5d019 100755 --- a/dev-uninstall.sh +++ b/dev-uninstall.sh @@ -1,42 +1,74 @@ #!/usr/bin/env bash set -euo pipefail -CACHE_DIR="$HOME/.claude/plugins/cache/local/headroom/1.0.0" +MARKETPLACE_NAME="riicodespretty" +PLUGIN_NAME="headroom" +PLUGIN_VERSION="1.0.0" +CACHE_DIR="$HOME/.claude/plugins/cache/$MARKETPLACE_NAME/$PLUGIN_NAME/$PLUGIN_VERSION" +MARKETPLACE_DIR="$HOME/.claude/plugins/marketplaces/$MARKETPLACE_NAME" echo "Uninstalling headroom plugin (dev mode)..." # 1. Remove symlink from plugin cache if [ -L "$CACHE_DIR" ]; then rm "$CACHE_DIR" - echo " ✓ Removed symlink $CACHE_DIR" + echo " ✓ Removed cache symlink $CACHE_DIR" elif [ -e "$CACHE_DIR" ]; then echo " ⚠ $CACHE_DIR exists but is not a symlink — skipping (manual removal needed)" else - echo " ✓ Symlink already absent" + echo " ✓ Cache symlink already absent" fi -# 2. Remove from enabledPlugins and clear ANTHROPIC_BASE_URL using Python -python3 - <<'PYEOF' +# 2. Remove marketplace symlink +if [ -L "$MARKETPLACE_DIR" ]; then + rm "$MARKETPLACE_DIR" + echo " ✓ Removed marketplace symlink $MARKETPLACE_DIR" +elif [ -e "$MARKETPLACE_DIR" ]; then + echo " ⚠ $MARKETPLACE_DIR exists but is not a symlink — skipping (manual removal needed)" +else + echo " ✓ Marketplace symlink already absent" +fi + +# 3. Remove from known_marketplaces.json, enabledPlugins, and clear ANTHROPIC_BASE_URL +python3 - "$MARKETPLACE_NAME" "$PLUGIN_NAME" <<'PYEOF' import json, os, sys from pathlib import Path +marketplace_name, plugin_name = sys.argv[1], sys.argv[2] +plugin_key = f"{plugin_name}@{marketplace_name}" + +# known_marketplaces.json +known_path = Path.home() / ".claude" / "plugins" / "known_marketplaces.json" +if known_path.exists(): + try: + data = json.loads(known_path.read_text()) + if marketplace_name in data: + del data[marketplace_name] + tmp = known_path.with_suffix(".json.tmp") + tmp.write_text(json.dumps(data, indent=2)) + os.replace(tmp, known_path) + print(f" ✓ Removed {marketplace_name} from known_marketplaces.json") + else: + print(f" ✓ {marketplace_name} not in known_marketplaces.json — nothing to remove") + except (json.JSONDecodeError, OSError) as e: + print(f" ⚠ Could not update known_marketplaces.json: {e}", file=sys.stderr) + +# settings.json — enabledPlugins and ANTHROPIC_BASE_URL settings_path = Path.home() / ".claude" / "settings.json" if not settings_path.exists(): - print(f" ⚠ {settings_path} not found — nothing to update", file=sys.stderr) + print(f" ⚠ {settings_path} not found — skipping", file=sys.stderr) sys.exit(0) settings = json.loads(settings_path.read_text()) -# Remove from enabledPlugins plugins = settings.get("enabledPlugins", {}) -if "headroom@local" in plugins: - del plugins["headroom@local"] +if plugin_key in plugins: + del plugins[plugin_key] settings["enabledPlugins"] = plugins - print(" ✓ Removed headroom@local from enabledPlugins") + print(f" ✓ Removed {plugin_key} from enabledPlugins") else: - print(" ✓ headroom@local was not registered — nothing to remove") + print(f" ✓ {plugin_key} was not registered — nothing to remove") -# Clear ANTHROPIC_BASE_URL from env env = settings.get("env", {}) if "ANTHROPIC_BASE_URL" in env: del env["ANTHROPIC_BASE_URL"] @@ -45,14 +77,40 @@ if "ANTHROPIC_BASE_URL" in env: else: print(" ✓ ANTHROPIC_BASE_URL was not set — nothing to clear") -# Write back atomically tmp = settings_path.with_suffix(".json.tmp") tmp.write_text(json.dumps(settings, indent=2)) os.replace(tmp, settings_path) PYEOF +# 4. Remove mcpServers.headroom from ~/.claude.json (reverting the command patch) +python3 - <<'PYEOF' +import json, os, sys +from pathlib import Path + +claude_json = Path.home() / ".claude.json" +if not claude_json.exists(): + print(" ✓ ~/.claude.json not found — nothing to revert") + sys.exit(0) + +try: + data = json.loads(claude_json.read_text()) +except (json.JSONDecodeError, OSError) as e: + print(f" ⚠ Could not read ~/.claude.json: {e} — skipping", file=sys.stderr) + sys.exit(0) + +mcp_servers = data.get("mcpServers", {}) +if "headroom" in mcp_servers: + del mcp_servers["headroom"] + data["mcpServers"] = mcp_servers + tmp = claude_json.with_suffix(".json.tmp") + tmp.write_text(json.dumps(data, indent=2)) + os.replace(tmp, claude_json) + print(" ✓ Removed mcpServers.headroom from ~/.claude.json") +else: + print(" ✓ mcpServers.headroom not present in ~/.claude.json — nothing to remove") +PYEOF -# 3. Kill running proxy (if any) before removing state directory +# 5. Kill running proxy (if any) before removing state directory PORT_FILE="$HOME/.headroom/proxy.port" if [ -f "$PORT_FILE" ]; then PROXY_PORT="$(cat "$PORT_FILE" 2>/dev/null || true)" @@ -69,7 +127,7 @@ else echo " ✓ No running proxy (port file absent)" fi -# 4. Clean up runtime state directory +# 6. Clean up runtime state directory HEADROOM_DIR="$HOME/.headroom" if [ -d "$HEADROOM_DIR" ]; then rm -rf "$HEADROOM_DIR" diff --git a/hooks/hooks.json b/hooks/hooks.json index a221076..302bb42 100644 --- a/hooks/hooks.json +++ b/hooks/hooks.json @@ -13,24 +13,13 @@ ] } ], - "Stop": [ - { - "hooks": [ - { - "type": "command", - "command": "python3 ${CLAUDE_PLUGIN_ROOT}/scripts/manager.py stop $PPID", - "timeout": 10 - } - ] - } - ], "SessionEnd": [ { "hooks": [ { "type": "command", "command": "python3 ${CLAUDE_PLUGIN_ROOT}/scripts/manager.py stop $PPID", - "timeout": 10 + "timeout": 30 } ] } diff --git a/scripts/manager.py b/scripts/manager.py index a680539..a7581a9 100644 --- a/scripts/manager.py +++ b/scripts/manager.py @@ -62,7 +62,7 @@ def check_proxy_health(port: int) -> bool: """Return True if the proxy at the given port reports healthy.""" try: url = f"http://127.0.0.1:{port}/health" - with urllib.request.urlopen(url, timeout=2) as resp: + with urllib.request.urlopen(url, timeout=10) as resp: data = json.loads(resp.read()) return data.get("status") == "healthy" except Exception: