cli.py 指令層

所有 subcommand 的入口——每個指令怎麼路由到對應模組
檔案:cli.py(4044 行)· __main__.py

大方向

graphify 的 CLI 分成兩層:__main__.pymain() / _run_cli() 是殼(處理 encoding、broken pipe、help、版本),真正的指令分派在 cli.pydispatch_command()。而 install/uninstall 家族在 0.8.x 被拆到 install.py(所以 __main__.py 有一大串 re-export)。


1. __main__.py — 入口殼

main() · _run_cli() · _silence_broken_pipe() __main__.pyconsole 入口:把「下游讀者提早關閉 pipe」當成成功而不是當機。

main() 的 docstring 講了一個很實際的問題(issue #1807):當 graphify query … | head 這種用法,下游 reader(head)提早關掉 stdout,Python 會在上層 flush 時拋 BrokenPipeError 並以 255 結束——CI 或 agent harness 會把一次成功的查詢誤判成失敗。

解法:

  • _run_cli() 包在 try 裡,先對 stdout/stderr 做 reconfigure(encoding="utf-8")(跨平台 UTF-8)。
  • 執行前檢查所有平台的 skill 版本戳記(跳過 install/uninstall/hook-check/hook-guard 這些靜默指令)。
  • -h/--help/-?` 印出完整的 subcommand 清單。
  • 捕獲 BrokenPipeError 或 Windows 的 OSError(EINVAL/EPIPE),呼叫 _silence_broken_pipe()——把 stdout 導到 devnull 後 sys.exit(0)
教學重點:這是「Unix pipe 習慣 × Python 行為」的相容層——工程師很少想到,但 | head 失敗會讓所有 wrapper 壞掉。
_run_cli() if len(sys.argv) < 2 or sys.argv[1] in ("-h","--help","-?"):
_run_cli() print("Usage: graphify <command>") # 全部 subcommand 清單
_run_cli() if dispatch_install_cli(cmd): return # install/uninstall 家族
_run_cli() dispatch_command(cmd) # 其餘指令
main() try: _run_cli(); sys.stdout.flush()
main() except BrokenPipeError: _silence_broken_pipe()
_check_skill_version(skill_dst) · _version_tuple() __main__.py警告「skill 與套件版本不一致」。

段落式說明:_check_skill_version 比對安裝的 skill 版本戳記與套件版本。特別處理兩種情形:skill 比套件(那不能叫使用者 graphify install,會 downgrade skill——要升級套件,issue #1568)、references/ sidecar 遺失(提示 repair)。_version_tuple 把版本字串解析成可比較的整數 tuple,非數字 segment 退化成 0。


2. cli.py — 指令分派

dispatch_command(cmd) str → None cli.py:714把第一個參數路由到對應的實作。

這是一個巨大的 if/elif 分派(4044 行的檔案,多半是它)。每個 subcommand 的處理模式大致是:

  1. 解析 sys.argv 的旗標(用 --flag value--flag=value 兩種形式)。
  2. graph.json_default_graph_path(),預設 graphify-out/graph.json)。
  3. 呼叫對應模組(graphify.serve / graphify.cluster / graphify.analyze…)或直接操作圖。
  4. 列印結果(文字或 --json)。

幾個值得注意的指令類別:

類別指令對應模組
建圖extract / update / cluster-only / labeldetect / extract / build / cluster / llm
查詢query / path / explain / affected / god-nodesserve(query 引擎)
進料add / clone / merge-graphsingest / clone
維運watch / check-update / hook / benchmarkwatch / hooks
輸出export callflow-html / treecallflow_html / tree_html
記憶save-result / reflectreflect
進階provider / global / merge-driver / diagnosellm / global_graph / build
_run_hook_guard(kind, strict) cli.py:491hook-guard / hook-check 的實作——「搜尋前先推一下圖」。

這是 always-on 機制的核心:當 Claude Code / Gemini 的 PreToolUse hook 觸發時,會執行 graphify hook-guard。它判斷目前的搜尋/讀檔是否該被「推一把」——如果圖存在且不是 strict 模式,回傳一個 additionalContext 提示(_SEARCH_NUDGE / _READ_NUDGE)引導助手先 graphify query;strict 模式(_hook_strict_enabled)則會擋住第一次原始讀檔並重導到圖。

為什麼搬進 Python subcommand?舊 hook 是內嵌 bash 的 python -c "…" 一行流,Windows cmd/PowerShell 解析不了(issue #522)。改成 graphify hook-guard subcommand 後,sh / cmd / PowerShell 都能跑。

_clone_repo(url, …) cli.py:650clone GitHub repo 並印出路徑(graphify clone)。

段落式說明:graphify clone <github-url> 的實作,把 repo clone 到 ~/.graphify/repos/<owner>/<repo>(或 --out),預設用 repo 的預設分支(--branch 可覆寫),最後印出路徑供 skill 的 Step 0 使用。

_StageTimer · _enforce_graph_size_cap_or_exit · _default_graph_path cli.py 各處CLI 的基礎設施。

段落式說明:

  • _StageTimer--timing flag 的實作,用 perf_counter 量每個 pipeline stage 的牆鐘時間。
  • _enforce_graph_size_cap_or_exit — 載入圖前檢查 512 MiB 上限(與 security 對應)。
  • _default_graph_path — 預設 graphify-out/graph.json 的解析(尊重 GRAPHIFY_OUT 環境變數)。
  • _stale_graph_sources / _prune_graph_json_sources — 檢查/剪除過期來源(更新時)。
看完這頁你應該能說出:CLI 的兩層結構、broken pipe 為何重要、hook-guard 跟 install 的關係、install/uninstall 為什麼被拆到 install.py。指令的行為詳解在 文件教學 與 README 的 full command reference

① 進階真實情境 Worked Example:把 graphify 塞進 CI 的「六個不可中斷的呼叫」

前面是「看懂 CLI 兩層結構」;這次是把 CLI 當 CI 元件:你的 CI pipeline 要在每次 PR 跑一連串 graphify 指令,且「任何一個失敗都要有明確訊息」。

  1. 建圖與檢查graphify extract . 建圖;接 graphify report 或檢查 built_at_commit 確認圖新鮮。
  2. 技能健康graphify --check-skill-version(或對應旗標)確認 skill 與套件版本一致——避免 CI 環境裝了舊 skill 配新套件。
  3. hook-guard 測試:在 CI 跑一次 graphify hook-guard 模擬 agent 搜尋前的行為——驗證 always-on 機制在 CI 環境(可能沒裝 GUI、Windows runner)也能跑。
  4. 基準回歸graphify benchmark graphify-out/graph.json 對比 README 數字,建立「準確度回歸門檻」。
  5. 資料完整性graphify diagnose(或 validate 路徑)檢查圖檔大小上限(512 MiB)與 schema。
  6. 輸出給下游graphify export --svg --graphml 產出 CI artifact,附在 PR 上供人工檢視。
為什麼選這條路徑:CLI 的價值在「可腳本化、可稽核、失敗即明確錯誤」——每個 subcommand 都是獨立程序,退出碼與 stderr 可被 CI 解讀;--json 輸出可被下游客器解析。更重要的是 broken pipe 處理:graphify query … | head 在 CI 裡會遇到,處理得當才不會把成功誤判成失敗。

② 深入原理擴充:broken pipe、hook-guard、與「CLI 也是產品」的設計哲學

cli 頁那些「小工具」其實藏著產品思維:

  • BrokenPipeError 被當成成功(issue #1807)graphify query | head 時下游提早關閉 stdout,Python 在 flush 時拋 BrokenPipeError 並以 255 結束——CI/agent harness 會誤判。解法是 _silence_broken_pipe():把 stdout 導到 devnull 後 sys.exit(0)。這是一個「懂 Unix 慣例」的相容層。
  • hook-guard 從 bash 一行流搬進 Python(issue #522):舊 hook 是內嵌 python -c "…",Windows cmd/PowerShell 解析不了。改成 graphify hook-guard subcommand 後,sh/cmd/PowerShell 都能跑——「跨平台」不是口號,是實作決策。
  • install/uninstall 被拆到 install.py:0.8.x 重構把「裝 skill」與「跑圖」分離,__main__.py 只剩 re-export。職責單一讓測試可以只 mock 一部分。
  • UTF-8 reconfigure_run_cli() 開場對 stdout/stderr reconfigure(encoding="utf-8")——跨平台中文輸出(含本站)不亂碼。
大家以為建圖正確、但其實有誤的案例:以為「graphify query 走過的路徑就是建圖時驗證過的路徑」。其實 query 是對 graph.json 的即時檢索——如果圖是用舊語料建的、或增量更新漏了某些檔,query 會「正確地」回傳正確圖裡的錯誤答案。最常見的是「改了檔案但忘了 graphify update」:graph.json 停在舊 commit,query 卻照常回應。CLI 不會提醒你圖過期——把「檢查 built_at_commit」寫進你的工作流程,而不是期待工具自動發現。

③ 診斷式疑難排解表

症狀可能原因解決方案
CI 把成功查詢判成失敗(exit 255)下游 pipe 關閉造成 BrokenPipeError 未處理確認版本含 _silence_broken_pipe(issue #1807);升級 graphify
Windows runner 上 hook 不執行舊版 hook 是 bash 一行流,cmd/PowerShell 解析不了改用 graphify hook-guard subcommand 形式(issue #522)
query 回答與最新程式碼不符圖過期:改了檔但沒 graphify update檢查 built_at_commit;養成修改後 update 的習慣(AGENTS.md 規則)
--json 輸出在 CI 解析失敗stderr 混入警告(skill 版本提示等)污染了 stdout確認警告走 stderr;CI 只吃 stdout;或先跑 --check-skill-version 淨化環境
skill 版本比套件新、install 被拒skill 更新領先 pip 套件(issue #1568)升級套件而非降級 skill;確認 references/ 側車存在

④ 進階挑戰題

  1. broken pipe 的解法是把「失敗」轉成 exit 0。請討論這個決策在「資料丟失」情境的風險:如果下游被 head -1 截斷,退出碼 0 會隱藏「輸出被截斷」的事實。你怎麼在「尊重 pipe 慣例」與「誠實報告」之間取捨?
  2. hook-guard 從 bash 搬進 Python 是為跨平台。請列舉另一個「藏在 CLI 實作裡、其實是為了跨平台/跨 harness 而存在」的設計,並說明它對應哪個 issue 或問題。
  3. 設計一個「圖新鮮度檢查」的 CLI 子命令:graphify fresh,輸入 repo 根,輸出「圖是否落後 HEAD」並給 exit code。列出它的判斷規則與邊界案例(未 commit 的變更算不算?)。