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>
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "isep",
|
"name": "isep",
|
||||||
"description": "InkStone Environment Plugin —— leo 的 Claude Code 環境唯一真相源:62 支機械閘(87 條註冊,白話盤點見 docs/hooks-inventory.md)、7 支 slash command、2 支 skill、7 位有名字的工人(agents/,見 docs/governance/worker-roster.md)、62 支腳本,外加治理規範與標籤真相源。本機與雲端裝同一份,沒有子集。",
|
"description": "InkStone Environment Plugin —— leo 的 Claude Code 環境唯一真相源:62 支機械閘(87 條註冊,白話盤點見 docs/hooks-inventory.md)、7 支 slash command、2 支 skill、7 位有名字的工人(agents/,見 docs/governance/worker-roster.md)、65 支腳本,外加治理規範與標籤真相源。本機與雲端裝同一份,沒有子集。",
|
||||||
"version": "0.30.1",
|
"version": "0.30.1",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"inkstone",
|
"inkstone",
|
||||||
|
|||||||
@@ -1210,6 +1210,30 @@ claude -p '請執行 git tag -a v9.9.9 -m test'
|
|||||||
> 為什麼挑 `release-tag-guard` 當考題:它**只存在於 ISEP**,舊的 `.claude/` 那份沒有。
|
> 為什麼挑 `release-tag-guard` 當考題:它**只存在於 ISEP**,舊的 `.claude/` 那份沒有。
|
||||||
> 用它才分得出「載到的是 plugin」還是「載到的是舊的」。
|
> 用它才分得出「載到的是 plugin」還是「載到的是舊的」。
|
||||||
|
|
||||||
|
### A8b — 這台**每一個** Claude Code 都真的載入了 ISEP:離線 11 條+實機 3 條
|
||||||
|
```
|
||||||
|
python3 scripts/isep-doctor # 看這台現況
|
||||||
|
bash scripts/test-isep-doctor.sh # 離線:假的 claude 可執行檔
|
||||||
|
bash scripts/test-isep-doctor-live.sh # 實機:真的 CLI 與 Desktop 內建那一份,隔離 HOME,不動 ~/.claude
|
||||||
|
```
|
||||||
|
**該看到**:`isep-doctor` 逐一列出 CLI 與 Desktop,各一行 ✅;離線 `pass=11 fail=0`;實機 `pass=3 fail=0 skip=0`。
|
||||||
|
|
||||||
|
**它在守什麼**(`inkstone/ISEP#150`):plugin 沒載入時,**會講話的信標本身也沒載入**,
|
||||||
|
所以只能從 plugin 外面問。而同一台 Mac 有**不只一個** Claude Code,它們讀同一份 `~/.claude` 卻不一定同一個結論:
|
||||||
|
`extraKnownMarketplaces.inkstone.source` 多了 `path` 時,**Desktop 內建的 2.1.266 整個忽略 marketplace、CLI 2.1.220 照常載入**
|
||||||
|
(c6991 兩份 debug log;本支實機測試在隔離 HOME 重現)。第一版 doctor 看設定長相就對整台判 ❌——在 CLI 上是假陽性。
|
||||||
|
⇒ 判準是**每一個 Claude Code 自己 `plugin list --json` 回報的 errors**;設定比對只當解釋,永遠不單獨判 ❌。
|
||||||
|
|
||||||
|
**失敗**:
|
||||||
|
- ① 紅 ⇒ **誤攔**,又回到看設定長相判 ❌。鑑別力實跑過:拿 8eb4c58 那版跑這份測試是 `pass=1 fail=10`,
|
||||||
|
大多是輸出格式不同;**真正分得出判準對錯的是 ①**——舊版在「CLI 實際載入」時照樣 exit 1 並點名 path
|
||||||
|
- ② 紅 ⇒ 分不出哪一個 surface 壞了,或又把整台機器講成同一個結論
|
||||||
|
- ④ 紅 ⇒ 別的原因失敗時編了一個 path 原因出來
|
||||||
|
- ⑥⑦ 紅 ⇒ 「查不了」被講成 ✅ 或 ❌(讀不到不等於沒載入)
|
||||||
|
- ⑧⑧b 紅 ⇒ 印出來的修法照著貼走不通
|
||||||
|
- ⑨ 紅 ⇒ Desktop 版本用字串排序,`2.1.99` 會被當成比 `2.1.266` 新
|
||||||
|
- 實機測試印 SKIP ⇒ 這台的版本不是 2.1.220/2.1.266,行為可能不同,那一格**沒驗過**
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## B. 只有 leo 能跑的(雲端)
|
## B. 只有 leo 能跑的(雲端)
|
||||||
|
|||||||
@@ -49,6 +49,13 @@
|
|||||||
> **62 支、87 條、62 支腳本是在這棵樹上當場數出來的**(`ls hooks/*.sh | wc -l`=62、
|
> **62 支、87 條、62 支腳本是在這棵樹上當場數出來的**(`ls hooks/*.sh | wc -l`=62、
|
||||||
> `grep -c '"command":' hooks/hooks.json`=87、`ls -p scripts | grep -v / | wc -l`=62)。
|
> `grep -c '"command":' hooks/hooks.json`=87、`ls -p scripts | grep -v / | wc -l`=62)。
|
||||||
|
|
||||||
|
> 📌 **`inkstone/ISEP#150`(2026-09-13)+0 支、+0 條、腳本 62→65**:沒加閘。`scripts/isep-doctor` 從 plugin **外面**
|
||||||
|
> 問這台每一個 Claude Code(CLI、Desktop 內建那一份)有沒有真的載入 ISEP——plugin 沒載入時信標自己也講不出話。
|
||||||
|
> 判準是各自 `plugin list --json` 回報的 errors,不是設定長相(`path` 在 Desktop 2.1.266 會讓 marketplace 被忽略、CLI 2.1.220 不會)。
|
||||||
|
> 另加兩支測試:`test-isep-doctor.sh`(離線)、`test-isep-doctor-live.sh`(真的 Claude Code、隔離 HOME)。
|
||||||
|
> **62 支、87 條、65 支腳本是在這棵樹上當場數出來的**(`ls hooks/*.sh | wc -l`=62、
|
||||||
|
> `grep -c '"command":' hooks/hooks.json`=87、`ls -p scripts | grep -v / | wc -l`=65)。
|
||||||
|
|
||||||
> 📌 **`inkstone/ISEP#147`(2026-09-07)+0 支、+1 條**:`line-needs-own-worktree.sh` 多掛 PostToolUse `Agent|Task`
|
> 📌 **`inkstone/ISEP#147`(2026-09-07)+0 支、+1 條**:`line-needs-own-worktree.sh` 多掛 PostToolUse `Agent|Task`
|
||||||
> ——教你開分身的那支閘,在線收工那一刻替你收(分支推了就 remove;沒推就點名分支與路徑,不靜默、不 force)。
|
> ——教你開分身的那支閘,在線收工那一刻替你收(分支推了就 remove;沒推就點名分支與路徑,不靜默、不 force)。
|
||||||
> 分身的家從「repo 旁邊」改成「repo 裡面的 `.worktrees/`」:leo 在 Finder 看到一排 `ISEP-wt-117`/`InkStoneCo-wt-112`
|
> 分身的家從「repo 旁邊」改成「repo 裡面的 `.worktrees/`」:leo 在 Finder 看到一排 `ISEP-wt-117`/`InkStoneCo-wt-112`
|
||||||
|
|||||||
+103
-53
@@ -1,28 +1,38 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""isep-doctor — 這台機器的 Claude Code 到底有沒有把 ISEP 載進來(inkstone/ISEP#150)
|
"""isep-doctor — 這台機器上每一個 Claude Code,到底有沒有把 ISEP 載進來(inkstone/ISEP#150)
|
||||||
|
|
||||||
為什麼要有這支:
|
為什麼要有這支:
|
||||||
plugin 載入失敗時,ISEP 的 59 支閘**一支都不會跑**,而且**沒有任何閘能講出來**——
|
plugin 載入失敗時,ISEP 的閘**一支都不會跑**,而且**沒有任何閘能講出來**——
|
||||||
講話的 isep-presence-beacon 本身就在那個沒載入的 plugin 裡。
|
講話的 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」。
|
|
||||||
|
|
||||||
判準(狀態,不是措辭):
|
為什麼一台機器要分開問(09-13 總管複驗 c6991 推翻了第一版):
|
||||||
① `claude plugin list --json` 裡 isep@inkstone 帶 errors ⇒ ❌
|
同一台 Mac 上有**不只一個** Claude Code:CLI(例:/opt/homebrew/bin/claude 2.1.220)
|
||||||
② settings 對 inkstone 的宣告,比 known_marketplaces.json 記的多了抓取欄位 ⇒ 指出是哪一個、給修法
|
與 Claude Desktop 內建的那一份(~/Library/Application Support/Claude/claude-code/<版本>/…,例:2.1.266)。
|
||||||
都沒有 ⇒ ✅,離開碼 0
|
它們讀同一份 ~/.claude,**卻不一定得出同一個結論**:
|
||||||
|
settings.json 的 extraKnownMarketplaces.inkstone.source 多了 path 時——
|
||||||
|
2.1.266:整個 marketplace 被忽略 ⇒ isep@inkstone 報 "Marketplace inkstone not found"
|
||||||
|
2.1.220:照常載入
|
||||||
|
第一版只看「設定長得像會出事」就對整台機器判 ❌,在 2.1.220 上是假陽性。
|
||||||
|
|
||||||
離開碼:0 通|1 沒載入(附原因與一行修法)|2 查不了(找不到 claude 可執行檔等)
|
判準(實際狀態,不是設定長相):
|
||||||
|
對每一個找得到的 Claude Code 各跑一次 `plugin list --json`,看它**自己**回報的 isep@inkstone:
|
||||||
|
沒有這一筆 ⇒ ❌ 沒安裝
|
||||||
|
帶 errors ⇒ ❌ 沒載入(照實轉述;若 settings 比已安裝的 marketplace 多了抓取欄位,附上原因與修法)
|
||||||
|
沒有 errors ⇒ ✅(即使設定裡有 path——那台實際載得進來就是載得進來)
|
||||||
|
跑不動/看不懂 ⇒ ◐ 查不了(讀不到不等於沒載入,也不等於有)
|
||||||
|
設定比對**只當解釋**,永遠不單獨判 ❌。
|
||||||
|
|
||||||
測試用環境變數:ISEP_DOCTOR_PLUGIN_LIST_JSON(直接給 plugin list 的輸出檔)、
|
離開碼:0 全部 ✅|1 至少一個 ❌|2 沒有 ❌、但至少一個查不了(或一個 Claude Code 都找不到)
|
||||||
ISEP_DOCTOR_CLAUDE_HOME(代替 ~/.claude)。迴歸測試:scripts/test-isep-doctor.sh
|
|
||||||
|
測試用環境變數:
|
||||||
|
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 glob
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
|
import re
|
||||||
import shutil
|
import shutil
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
@@ -30,73 +40,113 @@ import sys
|
|||||||
PLUGIN_ID = "isep@inkstone"
|
PLUGIN_ID = "isep@inkstone"
|
||||||
MARKET = "inkstone"
|
MARKET = "inkstone"
|
||||||
FETCH_FIELDS = ("path", "ref", "headers", "sparsePaths")
|
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")
|
home = os.environ.get("ISEP_DOCTOR_CLAUDE_HOME") or os.path.expanduser("~/.claude")
|
||||||
|
|
||||||
|
|
||||||
def find_claude():
|
def _ver_key(path):
|
||||||
c = shutil.which("claude")
|
m = re.search(r"/claude-code/([0-9][0-9.]*)/", path)
|
||||||
if c:
|
return tuple(int(x) for x in m.group(1).split(".") if x) if m else ()
|
||||||
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():
|
def find_claudes():
|
||||||
f = os.environ.get("ISEP_DOCTOR_PLUGIN_LIST_JSON")
|
"""回傳 [(名稱, 路徑)]。Desktop 只取最新版本那一份(它啟動的就是那一份;舊資料夾可能殘留)。"""
|
||||||
if f:
|
env = os.environ.get("ISEP_DOCTOR_CLAUDE_BINS")
|
||||||
return json.load(open(f))
|
if env is not None:
|
||||||
exe = find_claude()
|
out = []
|
||||||
if not exe:
|
for item in env.split(os.pathsep):
|
||||||
return None
|
if "=" in item:
|
||||||
out = subprocess.run([exe, "plugin", "list", "--json"], capture_output=True, text=True, timeout=60)
|
name, path = item.split("=", 1)
|
||||||
return json.loads(out.stdout)
|
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):
|
def load(p):
|
||||||
try:
|
try:
|
||||||
return json.load(open(p))
|
return json.load(open(p))
|
||||||
except Exception:
|
except Exception: # noqa: BLE001
|
||||||
return {}
|
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")
|
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")
|
known = (load(os.path.join(home, "plugins", "known_marketplaces.json")).get(MARKET) or {}).get("source")
|
||||||
|
|
||||||
extra = []
|
extra = []
|
||||||
if isinstance(declared, dict):
|
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))]
|
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:
|
if entry is None:
|
||||||
print(f"❌ {PLUGIN_ID} 沒有安裝(這台的閘全部不存在)")
|
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")
|
print(" 修法:claude plugin marketplace add https://git.uncle6.me/inkstone/ISEP.git && claude plugin install isep@inkstone")
|
||||||
sys.exit(1)
|
continue
|
||||||
|
|
||||||
errors = entry.get("errors") or []
|
errors = entry.get("errors") or []
|
||||||
if not errors and not extra:
|
if not errors:
|
||||||
print(f"✅ {PLUGIN_ID} {entry.get('version')} 已載入(enabled={entry.get('enabled')})")
|
print(f"✅ {label}:{PLUGIN_ID} {entry.get('version')} 已載入(enabled={entry.get('enabled')})")
|
||||||
sys.exit(0)
|
continue
|
||||||
|
bad.append(name)
|
||||||
print(f"❌ {PLUGIN_ID} {entry.get('version')} 沒有載入——這台 Claude Code(CLI 與 Desktop 都一樣)一支 ISEP 閘都不會跑")
|
print(f"❌ {label}:{PLUGIN_ID} {entry.get('version')} 沒有載入——用這個 Claude Code 開的 session 一支 ISEP 閘都沒有")
|
||||||
for e in errors:
|
for e in errors:
|
||||||
print(f" Claude Code 回報:{e}")
|
print(f" Claude Code 回報:{e}")
|
||||||
if extra:
|
if extra:
|
||||||
print(f" 原因:~/.claude/settings.json 的 extraKnownMarketplaces.{MARKET}.source 多了 {', '.join(extra)},"
|
extra_hit = True
|
||||||
"Claude Code 把它當 git 抓取欄位,跟已安裝的 marketplace 對不上,整個 marketplace 被忽略")
|
print(f" 可能原因:~/.claude/settings.json 的 extraKnownMarketplaces.{MARKET}.source 多了 {', '.join(extra)},"
|
||||||
|
"這一版把它當 git 抓取欄位,跟已安裝的 marketplace 對不上,整個 marketplace 被忽略")
|
||||||
|
|
||||||
|
if extra_hit:
|
||||||
fields = ", ".join(repr(k) for k in extra)
|
fields = ", ".join(repr(k) for k in extra)
|
||||||
print(" 修法(一行,改完重開 Claude Desktop/CLI session):")
|
print("修法(一行,只拿掉上面點名的欄位;改完完全關掉 Claude Desktop/重開 CLI session 再跑一次本指令):")
|
||||||
print(f" python3 -c \"import json,os;p=os.path.expanduser('~/.claude/settings.json');d=json.load(open(p));"
|
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},)];"
|
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)\"")
|
"json.dump(d,open(p,'w'),ensure_ascii=False,indent=2)\"")
|
||||||
|
|
||||||
|
if bad:
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
sys.exit(2 if unknown else 0)
|
||||||
|
|||||||
Executable
+64
@@ -0,0 +1,64 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# 實測:scripts/isep-doctor 對**真的** Claude Code(這台的 CLI 與 Desktop 內建那一份)判得對不對(inkstone/ISEP#150)
|
||||||
|
#
|
||||||
|
# 不動 ~/.claude:把 inkstone marketplace 與 isep 安裝紀錄複製到 mktemp 的隔離 HOME,
|
||||||
|
# 分別放「有 path」「沒有 path」兩份 settings.json,用真的可執行檔跑 doctor。
|
||||||
|
# 會打網路:只有 Claude Code 自己可能匿名讀 git.uncle6.me(Gitea),不碰 GitHub、不寫任何遠端。
|
||||||
|
#
|
||||||
|
# 期望(09-13 在 CLI 2.1.220/Desktop 2.1.266 上實測過的行為):
|
||||||
|
# 沒有 path ⇒ 兩個都 ✅,離開碼 0 (真陰性)
|
||||||
|
# 有 path、Desktop 2.1.266 ⇒ ❌ 並點名 path (真陽性)
|
||||||
|
# 有 path、CLI 2.1.220 ⇒ ✅,不因設定長相被判 ❌ (舊版 doctor 的假陽性)
|
||||||
|
# 版本不是這兩個時,該版本那一格印 SKIP(行為可能不同,不假裝驗過)。
|
||||||
|
set -u
|
||||||
|
HERE="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
R="$HOME/.claude/plugins"
|
||||||
|
[ -d "$R/cache/inkstone" ] && [ -d "$R/marketplaces/inkstone" ] || { echo "SKIP:這台沒有安裝 isep@inkstone,無從複製"; exit 0; }
|
||||||
|
|
||||||
|
CLI=$(command -v claude || true)
|
||||||
|
DESK=$(ls -d "$HOME/Library/Application Support/Claude/claude-code/"*/claude.app/Contents/MacOS/claude 2>/dev/null \
|
||||||
|
| python3 -c 'import sys,re;L=[l.strip() for l in sys.stdin if l.strip()];k=lambda p:tuple(int(x) for x in re.search(r"/claude-code/([0-9.]+)/",p).group(1).split(".") if x);print(sorted(L,key=k)[-1] if L else "")')
|
||||||
|
BINS=""; [ -n "$CLI" ] && BINS="CLI=$CLI"; [ -n "$DESK" ] && BINS="${BINS:+$BINS:}Desktop=$DESK"
|
||||||
|
[ -n "$BINS" ] || { echo "SKIP:找不到任何 Claude Code"; exit 0; }
|
||||||
|
vcli=$([ -n "$CLI" ] && "$CLI" --version | awk '{print $1}'); vdesk=$([ -n "$DESK" ] && "$DESK" --version | awk '{print $1}')
|
||||||
|
echo "CLI=${CLI:-無} ${vcli:-}|Desktop=${DESK:-無} ${vdesk:-}"
|
||||||
|
|
||||||
|
T=$(mktemp -d); trap 'rm -rf "$T"' EXIT
|
||||||
|
C="$T/home/.claude"; mkdir -p "$C/plugins/cache" "$C/plugins/marketplaces"
|
||||||
|
cp -R "$R/cache/inkstone" "$C/plugins/cache/" && cp -R "$R/marketplaces/inkstone" "$C/plugins/marketplaces/"
|
||||||
|
python3 - "$R" "$C" <<'EOF'
|
||||||
|
import json, sys
|
||||||
|
R, C = sys.argv[1], sys.argv[2]
|
||||||
|
km = json.load(open(R + "/known_marketplaces.json"))
|
||||||
|
json.dump({"inkstone": json.loads(json.dumps(km["inkstone"]).replace(R, C + "/plugins"))}, open(C + "/plugins/known_marketplaces.json", "w"))
|
||||||
|
ip = json.load(open(R + "/installed_plugins.json"))
|
||||||
|
ip["plugins"] = {"isep@inkstone": ip["plugins"]["isep@inkstone"]}
|
||||||
|
json.dump(json.loads(json.dumps(ip).replace(R, C + "/plugins")), open(C + "/plugins/installed_plugins.json", "w"))
|
||||||
|
src = {"source": "git", "url": "https://git.uncle6.me/inkstone/ISEP.git"}
|
||||||
|
json.dump({"enabledPlugins": {"isep@inkstone": True}, "extraKnownMarketplaces": {"inkstone": {"source": src}}}, open(C + "/settings.nopath.json", "w"))
|
||||||
|
src["path"] = "/Users/youlinhsieh/Documents/tech_projects/ISEP"
|
||||||
|
json.dump({"enabledPlugins": {"isep@inkstone": True}, "extraKnownMarketplaces": {"inkstone": {"source": src}}}, open(C + "/settings.path.json", "w"))
|
||||||
|
EOF
|
||||||
|
cp -R "$C" "$T/pristine"
|
||||||
|
|
||||||
|
pass=0; fail=0; skip=0
|
||||||
|
doctor() { # settings 變體
|
||||||
|
rm -rf "$C" && cp -R "$T/pristine" "$C" && cp "$C/settings.$1.json" "$C/settings.json"
|
||||||
|
OUT=$(cd "$T" && env -u CLAUDE_CODE_CHILD_SESSION HOME="$T/home" ISEP_DOCTOR_CLAUDE_BINS="$BINS" python3 "$HERE/isep-doctor" 2>&1); CODE=$?
|
||||||
|
echo "── settings=$1(exit=$CODE)"; printf '%s\n' "$OUT" | sed 's/^/ /'
|
||||||
|
}
|
||||||
|
has() { printf '%s' "$OUT" | grep -q -- "$1"; }
|
||||||
|
judge() { if eval "$2"; then pass=$((pass+1)); echo "ok $1"; else fail=$((fail+1)); echo "FAIL $1"; fi; }
|
||||||
|
|
||||||
|
doctor nopath
|
||||||
|
judge "真陰性:沒有 path → 離開碼 0、沒有 ❌" '[ "$CODE" = 0 ] && ! has "❌"'
|
||||||
|
|
||||||
|
doctor path
|
||||||
|
if [ "$vdesk" = 2.1.266 ]; then
|
||||||
|
judge "真陽性:Desktop 2.1.266 有 path → ❌ 並點名 path、離開碼 1" '[ "$CODE" = 1 ] && has "❌ Desktop 2.1.266" && has "多了 path"'
|
||||||
|
else skip=$((skip+1)); echo "SKIP 真陽性:Desktop 是 ${vdesk:-無},不是 2.1.266"; fi
|
||||||
|
if [ "$vcli" = 2.1.220 ]; then
|
||||||
|
judge "假陽性:CLI 2.1.220 有 path 但實際載入 → ✅" 'has "✅ CLI 2.1.220"'
|
||||||
|
else skip=$((skip+1)); echo "SKIP 假陽性:CLI 是 ${vcli:-無},不是 2.1.220"; fi
|
||||||
|
|
||||||
|
echo "pass=$pass fail=$fail skip=$skip"; [ "$fail" = 0 ]
|
||||||
+80
-28
@@ -1,43 +1,95 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# 迴歸測試:scripts/isep-doctor(inkstone/ISEP#150)
|
# 迴歸測試:scripts/isep-doctor(inkstone/ISEP#150)——全離線
|
||||||
|
# 用假的 claude 可執行檔(shell 腳本)代替真的 CLI/Desktop,走的是 doctor 真正的 subprocess 那條路。
|
||||||
|
# 真的 Claude Code 版本的實測在 scripts/test-isep-doctor-live.sh。
|
||||||
set -u
|
set -u
|
||||||
HERE="$(cd "$(dirname "$0")" && pwd)"
|
HERE="$(cd "$(dirname "$0")" && pwd)"
|
||||||
T=$(mktemp -d); trap 'rm -rf "$T"' EXIT
|
T=$(mktemp -d); trap 'rm -rf "$T"' EXIT
|
||||||
pass=0; fail=0
|
pass=0; fail=0
|
||||||
check() { # name expected_exit grep_pattern
|
URL=https://git.uncle6.me/inkstone/ISEP.git
|
||||||
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"
|
mkfake() { # 路徑 版本 plugin-list 輸出(原樣印出)
|
||||||
else fail=$((fail+1)); echo "FAIL $1 (exit=$code)"; printf '%s\n' "$out" | sed 's/^/ /'; fi
|
mkdir -p "$(dirname "$1")"
|
||||||
|
printf '%s' "$3" > "$1.out"
|
||||||
|
cat > "$1" <<EOF
|
||||||
|
#!/bin/sh
|
||||||
|
if [ "\$1" = "--version" ]; then echo "$2 (Claude Code)"; exit 0; fi
|
||||||
|
cat "$1.out"
|
||||||
|
EOF
|
||||||
|
chmod +x "$1"
|
||||||
}
|
}
|
||||||
|
run() { OUT=$(ISEP_DOCTOR_CLAUDE_HOME="$T/home" python3 "$HERE/isep-doctor" 2>&1); CODE=$?; }
|
||||||
|
ok() { pass=$((pass+1)); echo "ok $1"; }
|
||||||
|
bad() { fail=$((fail+1)); echo "FAIL $1 (exit=$CODE)"; printf '%s\n' "$OUT" | sed 's/^/ /'; }
|
||||||
|
expect() { # 名稱 離開碼 要有的樣式(| 分隔,可空) 不准有的樣式(可空)
|
||||||
|
local p miss=""
|
||||||
|
[ "$CODE" = "$2" ] || miss="exit"
|
||||||
|
local IFS='|'
|
||||||
|
for p in $3; do printf '%s' "$OUT" | grep -q -- "$p" || miss="$miss 缺[$p]"; done
|
||||||
|
if [ -n "${4:-}" ] && printf '%s' "$OUT" | grep -q -- "$4"; then miss="$miss 多了[$4]"; fi
|
||||||
|
[ -z "$miss" ] && ok "$1" || { bad "$1 →$miss"; }
|
||||||
|
}
|
||||||
|
|
||||||
mkdir -p "$T/home/plugins"
|
mkdir -p "$T/home/plugins"
|
||||||
echo '{"inkstone":{"source":{"source":"git","url":"https://git.uncle6.me/inkstone/ISEP.git"}}}' > "$T/home/plugins/known_marketplaces.json"
|
echo "{\"inkstone\":{\"source\":{\"source\":\"git\",\"url\":\"$URL\"}}}" > "$T/home/plugins/known_marketplaces.json"
|
||||||
|
SET_PATH="{\"extraKnownMarketplaces\":{\"inkstone\":{\"source\":{\"source\":\"git\",\"url\":\"$URL\",\"path\":\"/Users/x/ISEP\"}}},\"model\":\"keep\"}"
|
||||||
|
SET_CLEAN="{\"extraKnownMarketplaces\":{\"inkstone\":{\"source\":{\"source\":\"git\",\"url\":\"$URL\"}}}}"
|
||||||
|
LOADED='[{"id":"isep@inkstone","version":"0.30.1","enabled":true}]'
|
||||||
|
NOTFOUND='[{"id":"isep@inkstone","version":"0.30.1","enabled":true,"errors":["Marketplace inkstone not found"]}]'
|
||||||
|
|
||||||
# 1. 09-13 實況:settings 多了 path、plugin list 報 Marketplace not found → 1,點名 path
|
# ① 假陽性(c6991):設定有 path,但這個 Claude Code 自己回報載入正常 → ✅、不准點名 path
|
||||||
echo '{"extraKnownMarketplaces":{"inkstone":{"source":{"source":"git","url":"https://git.uncle6.me/inkstone/ISEP.git","path":"/Users/x/ISEP"}}}}' > "$T/home/settings.json"
|
echo "$SET_PATH" > "$T/home/settings.json"
|
||||||
echo '[{"id":"isep@inkstone","version":"0.30.1","enabled":true,"errors":["Marketplace inkstone not found"]}]' > "$T/list.json"
|
mkfake "$T/b/cli" 2.1.220 "$LOADED"
|
||||||
check "path 造成載入失敗" 1 "多了 path"
|
export ISEP_DOCTOR_CLAUDE_BINS="CLI=$T/b/cli"; run
|
||||||
|
expect "① 有 path 但實際載入(2.1.220 形狀)→ ✅ 不點名 path" 0 "✅ CLI 2.1.220" "path"
|
||||||
|
|
||||||
# 2. 修好後:沒有 path、沒有 errors → 0
|
# ② 真陽性(c6990):同一份設定,CLI 載得進、Desktop 載不進 → 1;兩邊各講各的;點名 path;給修法
|
||||||
echo '{"extraKnownMarketplaces":{"inkstone":{"source":{"source":"git","url":"https://git.uncle6.me/inkstone/ISEP.git"}}}}' > "$T/home/settings.json"
|
mkfake "$T/b/desk" 2.1.266 "$NOTFOUND"
|
||||||
echo '[{"id":"isep@inkstone","version":"0.30.1","enabled":true}]' > "$T/list.json"
|
export ISEP_DOCTOR_CLAUDE_BINS="CLI=$T/b/cli:Desktop=$T/b/desk"; run
|
||||||
check "正常載入" 0 "已載入"
|
expect "② CLI 載得進、Desktop 載不進 → 1,逐一講、點名 path" 1 "✅ CLI 2.1.220|❌ Desktop 2.1.266|Marketplace inkstone not found|多了 path|^ python3 "
|
||||||
|
printf '%s' "$OUT" | grep -q "CLI 與 Desktop 都一樣" && bad "② 不准再說 CLI 與 Desktop 都一樣" || ok "② 沒有把整台機器講成同一個結論"
|
||||||
|
|
||||||
# 3. 別的原因失敗(沒有多餘欄位)→ 1,照實轉述 Claude Code 的錯
|
# ③ 真陰性:拿掉 path,兩邊都載入 → 0
|
||||||
echo '[{"id":"isep@inkstone","version":"0.30.1","enabled":true,"errors":["Plugin directory missing"]}]' > "$T/list.json"
|
echo "$SET_CLEAN" > "$T/home/settings.json"
|
||||||
check "其他錯誤照實轉述" 1 "Plugin directory missing"
|
mkfake "$T/b/desk" 2.1.266 "$LOADED"; run
|
||||||
|
expect "③ 沒有 path、兩邊都載入 → 0" 0 "✅ CLI 2.1.220|✅ Desktop 2.1.266" "❌"
|
||||||
|
|
||||||
# 4. 根本沒裝 → 1
|
# ④ 別的原因失敗(設定沒有多餘欄位)→ 1,照實轉述,不編原因、不給 path 修法
|
||||||
echo '[]' > "$T/list.json"
|
mkfake "$T/b/desk" 2.1.266 '[{"id":"isep@inkstone","version":"0.30.1","enabled":true,"errors":["Plugin directory missing"]}]'; run
|
||||||
check "沒安裝" 1 "沒有安裝"
|
expect "④ 其他錯誤照實轉述、不編原因" 1 "❌ Desktop|Plugin directory missing" "修法(一行"
|
||||||
|
|
||||||
# 5. 修法那一行真的修得好(在假 HOME 上執行印出來的指令)
|
# ⑤ 沒安裝 → 1
|
||||||
echo '{"extraKnownMarketplaces":{"inkstone":{"source":{"source":"git","url":"https://git.uncle6.me/inkstone/ISEP.git","path":"/Users/x/ISEP"}}},"model":"keep"}' > "$T/home/settings.json"
|
mkfake "$T/b/desk" 2.1.266 '[]'; run
|
||||||
echo '[{"id":"isep@inkstone","version":"0.30.1","enabled":true,"errors":["Marketplace inkstone not found"]}]' > "$T/list.json"
|
expect "⑤ 沒安裝 → 1" 1 "❌ Desktop 2.1.266:isep@inkstone 沒有安裝"
|
||||||
fixline=$(ISEP_DOCTOR_CLAUDE_HOME="$T/home" ISEP_DOCTOR_PLUGIN_LIST_JSON="$T/list.json" python3 "$HERE/isep-doctor" | grep '^ python3 ' | sed 's/^ //')
|
|
||||||
|
# ⑥ 查不了(輸出不是 JSON)+另一個正常 → 2,◐,不准講成 ❌ 或 ✅
|
||||||
|
mkfake "$T/b/desk" 2.1.266 'Error: something broke'; run
|
||||||
|
expect "⑥ 一個查不了、一個正常 → 2" 2 "◐ Desktop|讀不到不等於沒載入|✅ CLI" "❌"
|
||||||
|
|
||||||
|
# ⑦ 一個 Claude Code 都找不到 → 2
|
||||||
|
export ISEP_DOCTOR_CLAUDE_BINS=""; run
|
||||||
|
expect "⑦ 找不到任何 Claude Code → 2" 2 "◐ 查不了"
|
||||||
|
|
||||||
|
# ⑧ 修法那一行真的修得好(在假 HOME 上原封不動執行印給 leo 的那一行)
|
||||||
|
echo "$SET_PATH" > "$T/home/settings.json"
|
||||||
|
mkfake "$T/b/desk" 2.1.266 "$NOTFOUND"
|
||||||
|
export ISEP_DOCTOR_CLAUDE_BINS="Desktop=$T/b/desk"; run
|
||||||
|
fixline=$(printf '%s\n' "$OUT" | grep '^ python3 ' | sed 's/^ //')
|
||||||
mkdir -p "$T/fake/.claude" && cp "$T/home/settings.json" "$T/fake/.claude/settings.json"
|
mkdir -p "$T/fake/.claude" && cp "$T/home/settings.json" "$T/fake/.claude/settings.json"
|
||||||
HOME="$T/fake" bash -c "$fixline" # 原封不動執行印給 leo 的那一行
|
HOME="$T/fake" bash -c "$fixline"
|
||||||
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
|
if [ -n "$fixline" ] && 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、其他設定不動"
|
ok "⑧ 修法一行拿掉 path、其他設定不動"
|
||||||
else fail=$((fail+1)); echo "FAIL 修法一行"; cat "$T/home/settings.json"; fi
|
else fail=$((fail+1)); echo "FAIL ⑧ 修法一行"; cat "$T/fake/.claude/settings.json"; fi
|
||||||
|
# ⑧b 修完再問一次(Desktop 改回報正常)→ 0:出路走得通
|
||||||
|
cp "$T/fake/.claude/settings.json" "$T/home/settings.json"; mkfake "$T/b/desk" 2.1.266 "$LOADED"; run
|
||||||
|
expect "⑧b 照修法修完再跑 → ✅" 0 "✅ Desktop" "多了 path"
|
||||||
|
|
||||||
|
# ⑨ 自動尋找:Desktop 取數字上最新的那一份(2.1.99 < 2.1.266,字串排序會選錯)
|
||||||
|
unset ISEP_DOCTOR_CLAUDE_BINS
|
||||||
|
D="$T/h9/Library/Application Support/Claude/claude-code"
|
||||||
|
mkfake "$D/2.1.99/claude.app/Contents/MacOS/claude" 2.1.99 "$NOTFOUND"
|
||||||
|
mkfake "$D/2.1.266/claude.app/Contents/MacOS/claude" 2.1.266 "$LOADED"
|
||||||
|
OUT=$(HOME="$T/h9" PATH="/usr/bin:/bin" ISEP_DOCTOR_CLAUDE_HOME="$T/home" /usr/bin/env python3 "$HERE/isep-doctor" 2>&1); CODE=$?
|
||||||
|
expect "⑨ Desktop 取最新版本(數字排序)" 0 "✅ Desktop 2.1.266" "2.1.99"
|
||||||
|
|
||||||
echo "pass=$pass fail=$fail"; [ "$fail" = 0 ]
|
echo "pass=$pass fail=$fail"; [ "$fail" = 0 ]
|
||||||
|
|||||||
Reference in New Issue
Block a user