Files
ISEP/scripts/isep-doctor
T
Leo fbc56b9e59 isep-doctor 判準改成「每一個 Claude Code 自己回報有沒有載入」,不再看設定長相(inkstone/ISEP#150 → c6995)
同一台 Mac 有 CLI 與 Desktop 內建兩個 Claude Code,讀同一份 ~/.claude 卻不一定同結論:
settings 多了 path 時 Desktop 2.1.266 忽略 inkstone marketplace、CLI 2.1.220 照常載入(c6991)。
第一版看到 path 就對整台判 ,在 CLI 上是假陽性。

- 逐一找 CLI(PATH)與 Desktop(最新版本,數字排序),各跑 plugin list --json,依 errors 各判各的
- 設定比對只在某一個真的報錯時當解釋+修法,永遠不單獨判 
- 查不了 ⇒ ◐、離開碼 2,不講成 
- test-isep-doctor.sh 離線 11 條(假 claude 可執行檔);舊版跑這份 pass=1 fail=10
- test-isep-doctor-live.sh 實機 3 條:真的 CLI 2.1.220/Desktop 2.1.266,隔離 HOME 重現有/無 path,不動 ~/.claude
- 盤點表與 plugin.json 描述的腳本數 62→65(這棵樹上實數)

版本號待總管定。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-13 16:23:40 +08:00

153 lines
6.8 KiB
Python
Executable File
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""isep-doctor — 這台機器上每一個 Claude Code,到底有沒有把 ISEP 載進來(inkstone/ISEP#150
為什麼要有這支:
plugin 載入失敗時,ISEP 的閘**一支都不會跑**,而且**沒有任何閘能講出來**——
講話的 isep-presence-beacon 本身就在那個沒載入的 plugin 裡。
為什麼一台機器要分開問(09-13 總管複驗 c6991 推翻了第一版):
同一台 Mac 上有**不只一個** Claude CodeCLI(例:/opt/homebrew/bin/claude 2.1.220
與 Claude Desktop 內建的那一份(~/Library/Application Support/Claude/claude-code/<版本>/…,例:2.1.266)。
它們讀同一份 ~/.claude,**卻不一定得出同一個結論**:
settings.json 的 extraKnownMarketplaces.inkstone.source 多了 path 時——
2.1.266:整個 marketplace 被忽略 ⇒ isep@inkstone 報 "Marketplace inkstone not found"
2.1.220:照常載入
第一版只看「設定長得像會出事」就對整台機器判 ❌,在 2.1.220 上是假陽性。
判準(實際狀態,不是設定長相):
對每一個找得到的 Claude Code 各跑一次 `plugin list --json`,看它**自己**回報的 isep@inkstone
沒有這一筆 ⇒ ❌ 沒安裝
帶 errors ⇒ ❌ 沒載入(照實轉述;若 settings 比已安裝的 marketplace 多了抓取欄位,附上原因與修法)
沒有 errors ⇒ ✅(即使設定裡有 path——那台實際載得進來就是載得進來)
跑不動/看不懂 ⇒ ◐ 查不了(讀不到不等於沒載入,也不等於有)
設定比對**只當解釋**,永遠不單獨判 ❌。
離開碼:0 全部 ✅|1 至少一個 ❌|2 沒有 ❌、但至少一個查不了(或一個 Claude Code 都找不到)
測試用環境變數:
ISEP_DOCTOR_CLAUDE_BINS 用這份清單取代自動尋找;以 os.pathsep 分隔,每項 `名稱=可執行檔路徑`
ISEP_DOCTOR_CLAUDE_HOME 代替 ~/.claude
迴歸測試:scripts/test-isep-doctor.sh(離線)/scripts/test-isep-doctor-live.sh(真的 Claude Code、隔離 HOME
"""
import glob
import json
import os
import re
import shutil
import subprocess
import sys
PLUGIN_ID = "isep@inkstone"
MARKET = "inkstone"
FETCH_FIELDS = ("path", "ref", "headers", "sparsePaths")
DESKTOP_GLOB = "~/Library/Application Support/Claude/claude-code/*/claude.app/Contents/MacOS/claude"
home = os.environ.get("ISEP_DOCTOR_CLAUDE_HOME") or os.path.expanduser("~/.claude")
def _ver_key(path):
m = re.search(r"/claude-code/([0-9][0-9.]*)/", path)
return tuple(int(x) for x in m.group(1).split(".") if x) if m else ()
def find_claudes():
"""回傳 [(名稱, 路徑)]。Desktop 只取最新版本那一份(它啟動的就是那一份;舊資料夾可能殘留)。"""
env = os.environ.get("ISEP_DOCTOR_CLAUDE_BINS")
if env is not None:
out = []
for item in env.split(os.pathsep):
if "=" in item:
name, path = item.split("=", 1)
out.append((name, path))
return out
out = []
cli = shutil.which("claude")
if cli:
out.append(("CLI", cli))
desk = sorted(glob.glob(os.path.expanduser(DESKTOP_GLOB)), key=_ver_key) # 數字排序:2.1.99 < 2.1.266
if desk:
out.append(("Desktop", desk[-1]))
return out
def version_of(exe):
try:
r = subprocess.run([exe, "--version"], capture_output=True, text=True, timeout=30)
return (r.stdout.strip().split() or ["?"])[0]
except Exception: # noqa: BLE001
return "?"
def ask(exe):
"""回傳 (狀態, 內容)。狀態:'list' 內容=plugin list 陣列;'err' 內容=查不了的原因。"""
try:
r = subprocess.run([exe, "plugin", "list", "--json"], capture_output=True, text=True, timeout=90)
except Exception as e: # noqa: BLE001
return "err", f"執行失敗({e}"
try:
data = json.loads(r.stdout)
if not isinstance(data, list):
raise ValueError("不是陣列")
return "list", data
except Exception: # noqa: BLE001
tail = (r.stdout or r.stderr or "").strip().splitlines()[-1:] or ["(沒有輸出)"]
return "err", f"plugin list --json 看不懂(exit={r.returncode}{tail[0][:200]}"
def load(p):
try:
return json.load(open(p))
except Exception: # noqa: BLE001
return {}
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))]
claudes = find_claudes()
if not claudes:
print("◐ 查不了:這台找不到任何 Claude Code 可執行檔(CLI 與 Desktop 都沒有)")
sys.exit(2)
bad, unknown, extra_hit = [], [], False
for name, exe in claudes:
label = f"{name} {version_of(exe)}"
state, data = ask(exe)
if state == "err":
unknown.append(name)
print(f"◐ {label}:查不了——{data}(讀不到不等於沒載入)")
print(f" {exe}")
continue
entry = next((p for p in data if p.get("id") == PLUGIN_ID), None)
if entry is None:
bad.append(name)
print(f"❌ {label}{PLUGIN_ID} 沒有安裝——用這個 Claude Code 開的 session 一支 ISEP 閘都沒有")
print(" 修法:claude plugin marketplace add https://git.uncle6.me/inkstone/ISEP.git && claude plugin install isep@inkstone")
continue
errors = entry.get("errors") or []
if not errors:
print(f"✅ {label}{PLUGIN_ID} {entry.get('version')} 已載入(enabled={entry.get('enabled')}")
continue
bad.append(name)
print(f"❌ {label}{PLUGIN_ID} {entry.get('version')} 沒有載入——用這個 Claude Code 開的 session 一支 ISEP 閘都沒有")
for e in errors:
print(f" Claude Code 回報:{e}")
if extra:
extra_hit = True
print(f" 可能原因:~/.claude/settings.json 的 extraKnownMarketplaces.{MARKET}.source 多了 {', '.join(extra)}"
"這一版把它當 git 抓取欄位,跟已安裝的 marketplace 對不上,整個 marketplace 被忽略")
if extra_hit:
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)\"")
if bad:
sys.exit(1)
sys.exit(2 if unknown else 0)