From 8eb4c58d2d4b9b01585d6fee555d4b8b6114bf33 Mon Sep 17 00:00:00 2001 From: richblack Date: Sun, 13 Sep 2026 14:55:26 +0800 Subject: [PATCH] =?UTF-8?q?isep-doctor=EF=BC=9A=E8=AC=9B=E5=87=BA=E9=80=99?= =?UTF-8?q?=E5=8F=B0=E7=9A=84=20ISEP=20plugin=20=E6=9C=89=E6=B2=92?= =?UTF-8?q?=E6=9C=89=E7=9C=9F=E7=9A=84=E8=BC=89=E5=85=A5=EF=BC=88inkstone/?= =?UTF-8?q?ISEP#150=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 根因不是「Desktop 不跑 hook」:Desktop 1.52386.0/CC 2.1.266 內實測專案 PreToolUse hook 照擋。真正斷的是 ~/.claude/settings.json extraKnownMarketplaces.inkstone.source 多了 path,CC 把它當 git 抓取欄位, marketplace 被忽略 ⇒ isep@inkstone「Marketplace inkstone not found」, 59 支閘在 CLI 與 Desktop 一起失效,而且沒有任何閘講得出來。 本支從 plugin 外面查(claude plugin list --json + settings 對 known_marketplaces), 點名多出的欄位並印出一行修法。迴歸測試 scripts/test-isep-doctor.sh 5/5。 假設:修 ~/.claude/settings.json 是 leo 的個人設定,由 leo 親手執行,本 commit 不動它。 Co-Authored-By: Claude Opus 5 --- scripts/isep-doctor | 102 ++++++++++++++++++++++++++++++++++++ scripts/test-isep-doctor.sh | 43 +++++++++++++++ 2 files changed, 145 insertions(+) create mode 100755 scripts/isep-doctor create mode 100755 scripts/test-isep-doctor.sh diff --git a/scripts/isep-doctor b/scripts/isep-doctor new file mode 100755 index 0000000..091993a --- /dev/null +++ b/scripts/isep-doctor @@ -0,0 +1,102 @@ +#!/usr/bin/env python3 +"""isep-doctor — 這台機器的 Claude Code 到底有沒有把 ISEP 載進來(inkstone/ISEP#150) + +為什麼要有這支: + plugin 載入失敗時,ISEP 的 59 支閘**一支都不會跑**,而且**沒有任何閘能講出來**—— + 講話的 isep-presence-beacon 本身就在那個沒載入的 plugin 裡。 + 2026-09-05 起 `~/.claude/settings.json` 的 `extraKnownMarketplaces.inkstone.source` + 多了一個 `path`(本機 checkout 路徑),Claude Code 把它當成 git 來源的抓取欄位 + (debug log:「its network source differs from the one declared for it in settings + (… path …)」),整個 marketplace 被忽略 ⇒ `isep@inkstone` 報 "Marketplace inkstone not found"。 + CLI 與 Desktop 同病,當時被誤判成「Desktop 不跑 hook」。 + +判準(狀態,不是措辭): + ① `claude plugin list --json` 裡 isep@inkstone 帶 errors ⇒ ❌ + ② settings 對 inkstone 的宣告,比 known_marketplaces.json 記的多了抓取欄位 ⇒ 指出是哪一個、給修法 + 都沒有 ⇒ ✅,離開碼 0 + +離開碼:0 通|1 沒載入(附原因與一行修法)|2 查不了(找不到 claude 可執行檔等) + +測試用環境變數:ISEP_DOCTOR_PLUGIN_LIST_JSON(直接給 plugin list 的輸出檔)、 +ISEP_DOCTOR_CLAUDE_HOME(代替 ~/.claude)。迴歸測試:scripts/test-isep-doctor.sh +""" +import glob +import json +import os +import shutil +import subprocess +import sys + +PLUGIN_ID = "isep@inkstone" +MARKET = "inkstone" +FETCH_FIELDS = ("path", "ref", "headers", "sparsePaths") + +home = os.environ.get("ISEP_DOCTOR_CLAUDE_HOME") or os.path.expanduser("~/.claude") + + +def find_claude(): + c = shutil.which("claude") + if c: + return c + cands = sorted(glob.glob(os.path.expanduser( + "~/Library/Application Support/Claude/claude-code/*/claude.app/Contents/MacOS/claude"))) + return cands[-1] if cands else None + + +def plugin_list(): + f = os.environ.get("ISEP_DOCTOR_PLUGIN_LIST_JSON") + if f: + return json.load(open(f)) + exe = find_claude() + if not exe: + return None + out = subprocess.run([exe, "plugin", "list", "--json"], capture_output=True, text=True, timeout=60) + return json.loads(out.stdout) + + +def load(p): + try: + return json.load(open(p)) + except Exception: + return {} + + +try: + plugins = plugin_list() +except Exception as e: # noqa: BLE001 + print(f"◐ 查不了:claude plugin list 失敗({e})") + sys.exit(2) +if plugins is None: + print("◐ 查不了:找不到 claude 可執行檔") + sys.exit(2) + +entry = next((p for p in plugins if p.get("id") == PLUGIN_ID), None) +declared = (load(os.path.join(home, "settings.json")).get("extraKnownMarketplaces") or {}).get(MARKET, {}).get("source") +known = (load(os.path.join(home, "plugins", "known_marketplaces.json")).get(MARKET) or {}).get("source") + +extra = [] +if isinstance(declared, dict): + extra = [k for k in FETCH_FIELDS if k in declared and (not isinstance(known, dict) or declared.get(k) != known.get(k))] + +if entry is None: + print(f"❌ {PLUGIN_ID} 沒有安裝(這台的閘全部不存在)") + print(" 修法:claude plugin marketplace add https://git.uncle6.me/inkstone/ISEP.git && claude plugin install isep@inkstone") + sys.exit(1) + +errors = entry.get("errors") or [] +if not errors and not extra: + print(f"✅ {PLUGIN_ID} {entry.get('version')} 已載入(enabled={entry.get('enabled')})") + sys.exit(0) + +print(f"❌ {PLUGIN_ID} {entry.get('version')} 沒有載入——這台 Claude Code(CLI 與 Desktop 都一樣)一支 ISEP 閘都不會跑") +for e in errors: + print(f" Claude Code 回報:{e}") +if extra: + print(f" 原因:~/.claude/settings.json 的 extraKnownMarketplaces.{MARKET}.source 多了 {', '.join(extra)}," + "Claude Code 把它當 git 抓取欄位,跟已安裝的 marketplace 對不上,整個 marketplace 被忽略") + fields = ", ".join(repr(k) for k in extra) + print(" 修法(一行,改完重開 Claude Desktop/CLI session):") + print(f" python3 -c \"import json,os;p=os.path.expanduser('~/.claude/settings.json');d=json.load(open(p));" + f"s=d['extraKnownMarketplaces']['{MARKET}']['source'];[s.pop(k,None) for k in ({fields},)];" + "json.dump(d,open(p,'w'),ensure_ascii=False,indent=2)\"") +sys.exit(1) diff --git a/scripts/test-isep-doctor.sh b/scripts/test-isep-doctor.sh new file mode 100755 index 0000000..b5a79ce --- /dev/null +++ b/scripts/test-isep-doctor.sh @@ -0,0 +1,43 @@ +#!/usr/bin/env bash +# 迴歸測試:scripts/isep-doctor(inkstone/ISEP#150) +set -u +HERE="$(cd "$(dirname "$0")" && pwd)" +T=$(mktemp -d); trap 'rm -rf "$T"' EXIT +pass=0; fail=0 +check() { # name expected_exit grep_pattern + local out code; out=$(ISEP_DOCTOR_CLAUDE_HOME="$T/home" ISEP_DOCTOR_PLUGIN_LIST_JSON="$T/list.json" python3 "$HERE/isep-doctor" 2>&1); code=$? + if [ "$code" = "$2" ] && printf '%s' "$out" | grep -q -- "$3"; then pass=$((pass+1)); echo "ok $1" + else fail=$((fail+1)); echo "FAIL $1 (exit=$code)"; printf '%s\n' "$out" | sed 's/^/ /'; fi +} +mkdir -p "$T/home/plugins" +echo '{"inkstone":{"source":{"source":"git","url":"https://git.uncle6.me/inkstone/ISEP.git"}}}' > "$T/home/plugins/known_marketplaces.json" + +# 1. 09-13 實況:settings 多了 path、plugin list 報 Marketplace not found → 1,點名 path +echo '{"extraKnownMarketplaces":{"inkstone":{"source":{"source":"git","url":"https://git.uncle6.me/inkstone/ISEP.git","path":"/Users/x/ISEP"}}}}' > "$T/home/settings.json" +echo '[{"id":"isep@inkstone","version":"0.30.1","enabled":true,"errors":["Marketplace inkstone not found"]}]' > "$T/list.json" +check "path 造成載入失敗" 1 "多了 path" + +# 2. 修好後:沒有 path、沒有 errors → 0 +echo '{"extraKnownMarketplaces":{"inkstone":{"source":{"source":"git","url":"https://git.uncle6.me/inkstone/ISEP.git"}}}}' > "$T/home/settings.json" +echo '[{"id":"isep@inkstone","version":"0.30.1","enabled":true}]' > "$T/list.json" +check "正常載入" 0 "已載入" + +# 3. 別的原因失敗(沒有多餘欄位)→ 1,照實轉述 Claude Code 的錯 +echo '[{"id":"isep@inkstone","version":"0.30.1","enabled":true,"errors":["Plugin directory missing"]}]' > "$T/list.json" +check "其他錯誤照實轉述" 1 "Plugin directory missing" + +# 4. 根本沒裝 → 1 +echo '[]' > "$T/list.json" +check "沒安裝" 1 "沒有安裝" + +# 5. 修法那一行真的修得好(在假 HOME 上執行印出來的指令) +echo '{"extraKnownMarketplaces":{"inkstone":{"source":{"source":"git","url":"https://git.uncle6.me/inkstone/ISEP.git","path":"/Users/x/ISEP"}}},"model":"keep"}' > "$T/home/settings.json" +echo '[{"id":"isep@inkstone","version":"0.30.1","enabled":true,"errors":["Marketplace inkstone not found"]}]' > "$T/list.json" +fixline=$(ISEP_DOCTOR_CLAUDE_HOME="$T/home" ISEP_DOCTOR_PLUGIN_LIST_JSON="$T/list.json" python3 "$HERE/isep-doctor" | grep '^ python3 ' | sed 's/^ //') +mkdir -p "$T/fake/.claude" && cp "$T/home/settings.json" "$T/fake/.claude/settings.json" +HOME="$T/fake" bash -c "$fixline" # 原封不動執行印給 leo 的那一行 +if python3 -c "import json,sys;d=json.load(open('$T/fake/.claude/settings.json'));s=d['extraKnownMarketplaces']['inkstone']['source'];sys.exit(0 if 'path' not in s and d['model']=='keep' and s['url'].endswith('ISEP.git') else 1)"; then + pass=$((pass+1)); echo "ok 修法一行拿掉 path、其他設定不動" +else fail=$((fail+1)); echo "FAIL 修法一行"; cat "$T/home/settings.json"; fi + +echo "pass=$pass fail=$fail"; [ "$fail" = 0 ]