11#!/usr/bin/env python3
2- """SessionStart 钩子:检测项目语言,把规则注入 AI 上下文。
2+ """SessionStart 宿主适配器:读取事件与本机插件配置,输出项目记忆。"""
3+ from __future__ import annotations
34
4- 行为:
5- 1. detect_lang 找到项目语言
6- 2. 检查是否已有 linter 配置(.pre-commit-config.yaml、checkstyle.xml、.clippy.toml、eslint.config.js、ruff.toml)
7- 3. 输出一段 AGENTS.md 风格的提示,让 AI 知道自己在一个被 codeguard 管理的项目里
8- """
95import contextlib
106import json
117import os
128import subprocess
139import sys
1410from pathlib import Path
1511
16- PLUGIN_ROOT = Path (__file__ ).resolve ().parents [1 ] # hooks/ 的上级 = 插件根(不依赖宿主环境变量)
12+ PLUGIN_ROOT = Path (__file__ ).resolve ().parents [1 ]
1713sys .path .insert (0 , str (PLUGIN_ROOT / "scripts" ))
1814
19- from detect_lang import ( # ensure_user_path/load_user_config 实际定义:scripts/paths.py、scripts/user_config.py
15+ from codeguard import startup_application
16+ from codeguard .reporting import plugin_version
17+ from detect_lang import ( # 兼容原有 Python 导入接口
2018 LANG_COMMANDS ,
2119 REGISTRY ,
2220 detect_languages ,
2523 load_user_config ,
2624)
2725
26+ __all__ = [
27+ "LANG_COMMANDS" ,
28+ "REGISTRY" ,
29+ "detect_languages" ,
30+ "detect_linter_config" ,
31+ "ensure_user_path" ,
32+ "find_project_root" ,
33+ "load_user_config" ,
34+ "main" ,
35+ "notify" ,
36+ "version_backlog_note" ,
37+ ]
38+
39+ detect_linter_config = startup_application .detect_linter_config
40+ version_backlog_note = startup_application .version_backlog_note
41+
2842
2943def notify (title : str , message : str ) -> None :
30- """macOS 系统通知(非 darwin 静默) """
44+ """macOS 系统通知;其他宿主静默。 """
3145 if sys .platform != "darwin" :
3246 return
33- safe_t = title .replace ('"' , "'" )
34- safe_m = message .replace ('"' , "'" )[:200 ]
47+ safe_title = title .replace ('"' , "'" )
48+ safe_message = message .replace ('"' , "'" )[:200 ]
3549 with contextlib .suppress (OSError ):
3650 subprocess .Popen (
3751 ["osascript" , "-e" ,
38- f'display notification "{ safe_m } " with title "{ safe_t } "' ],
52+ f'display notification "{ safe_message } " with title "{ safe_title } "' ],
3953 stdout = subprocess .DEVNULL , stderr = subprocess .DEVNULL ,
4054 )
4155
4256
43- def detect_linter_config (project_root : Path ) -> dict :
44- """返回 {language: [存在的 linter 配置文件]},由 languages.json 驱动。
45-
46- 每条 languages.json 条目的 `linter_config_files` 字段声明该语言的 linter
47- 配置文件路径(glob 模式允许,与 requiresConfig 同语义)。新增语言只需
48- 在注册表里填字段,env_check 自动盘点——不再有硬编码常量的同步陷阱。
49- """
50- found = {}
51- for lang_id , lang_def in REGISTRY .items ():
52- files = lang_def .get ("linter_config_files" ) or []
53- if not files :
54- continue
55- present = [f for f in files if (project_root / f ).exists ()]
56- if present :
57- found [lang_id ] = present
58- return found
59-
60-
61- def _semver_tuple (v : str ) -> tuple [int , ...]:
62- import re as _re
63- m = _re .match (r"(\d+(?:\.\d+)*)" , v .strip ())
64- if not m :
65- return (0 ,)
66- return tuple (int (x ) for x in m .group (1 ).split ("." ))
67-
68-
69- def version_backlog_note (current : str , cache_root : Path ) -> str | None :
70- """纯函数:本地插件 cache 存在比 current 更新的版本 → 提示文案;否则 None。
71-
72- 检测面 = `~/.zcode/cli/plugins/cache/<vendor>/codeguard/<version>` 的**版本目录
73- 集合**(离线可得的唯一权威)。抓两类漂移:同 vendor 升级后旧目录未清、跨
74- vendor 版本参差导致实际生效副本落后于本机已有副本。抓不到"本机最新 vs
75- 远端仓库最新"(需网络,钩子不联网)——文案里明示提醒核对远端。
76- """
77- newer : list [str ] = []
78- installed : list [str ] = []
79- if not cache_root .is_dir ():
80- return None
57+ def _enabled_plugin_orgs () -> tuple [str , ...]:
58+ """ZCode 宿主注册表只在适配器读取,其他宿主缺文件时无副作用。"""
8159 try :
82- for vendor_dir in sorted (cache_root .glob ("*/codeguard/*" )):
83- if not vendor_dir .is_dir ():
84- continue
85- v = vendor_dir .name
86- installed .append (v )
87- if _semver_tuple (v ) > _semver_tuple (current ):
88- newer .append (v )
89- except OSError :
90- return None
91- if not newer :
92- return None
93- newest = max (newer , key = _semver_tuple )
94- return (
95- f"- ⚠️ **版本积压**:当前生效 codeguard `v{ current } `,本机 cache 已有更新版本 "
96- f"`v{ newest } `(共 { len (installed )} 份: { ', ' .join (sorted (installed ))} )——"
97- "钩子行为以**生效副本**为准,修复可能已存在却没跑到;请更新插件并清理旧 "
98- "cache(`~/.zcode/cli/plugins/cache/*/codeguard/`),同时核对远端是否还有"
99- "更新版本(本检测离线,看不到仓库最新版)。"
100- )
60+ config = json .loads ((Path .home () / ".zcode" / "cli" / "config.json" ).read_text (encoding = "utf-8" ))
61+ orgs = {
62+ key .split ("@" , 1 )[1 ]
63+ for key in (config .get ("plugins" , {}) or {}).get ("enabledPlugins" , {})
64+ if key .startswith ("codeguard@" )
65+ }
66+ return tuple (sorted (orgs ))
67+ except (OSError , ValueError , AttributeError ):
68+ return ()
10169
10270
10371def main (payload : dict | None = None ) -> int :
@@ -108,100 +76,19 @@ def main(payload: dict | None = None) -> int:
10876
10977
11078def _main (payload : dict | None = None ) -> int :
111- ensure_user_path (from_login_shell = True ) # GUI 宿主 PATH 不含用户级工具目录,先补齐再盘点
112- project_root = find_project_root (os .getcwd ())
113- if project_root is None :
114- project_root = Path (os .getcwd ())
115-
116- languages = detect_languages (project_root )
117- if not languages :
118- return 0 # 未识别到代码项目,安静退出
119-
120- linter_cfg = detect_linter_config (project_root )
121- cfg = load_user_config ()
122- enabled = cfg .get ("enabled_languages" , [])
123- if enabled and enabled != ["auto" ]:
124- languages = [lang for lang in languages if lang in enabled ]
125-
126- # linter 盘点:直接复用门禁的探活(比裸 which 准——能发现 npx 包未装、运行时损坏)
127- from codeguard .toolchain import ToolchainProbe
128- probe = ToolchainProbe (project_root )
129- ready , missing = [], []
130- for lang in languages :
131- cmd_def = LANG_COMMANDS .get (lang )
132- if not cmd_def :
133- continue
134- ok , reason = probe .probe (cmd_def )
135- if ok :
136- ready .append (lang )
137- else :
138- hint = cmd_def .get ("install_hint" ) or "见 docs/LANGUAGES.md"
139- missing .append ((lang , reason , hint ))
140-
141- # 输出注入到 AI 上下文的内容(追加模式)
142- lines = []
143- lines .append ("# codeguard 项目记忆" )
144- lines .append ("" )
145- lines .append (f"- 项目根: `{ project_root } `" )
146- lines .append (f"- 检测到语言: { ', ' .join (languages )} " )
147-
148- if missing :
149- lines .append ("" )
150- lines .append ("**⚠️ 以下语言的检查未生效,这些语言的文件【不会】被检查(提交门禁也会跳过它们):**" )
151- for lang , reason , hint in missing :
152- lines .append (f" - { lang } ({ reason } )→ 修复: `{ hint } `" )
153- lines .append (" 请主动提示用户安装;安装后新写的文件才会被检查。" )
154- notify (
155- "codeguard 部分检查未生效" ,
156- f"{ len (missing )} 个语言未生效: " + ", " .join (lang for lang , _ , _ in missing ) +
157- ";详情见对话" ,
158- )
159- if ready :
160- lines .append (f"- 检查已生效的语言: { ', ' .join (ready )} " )
161-
162- if linter_cfg :
163- lines .append (f"- 已有的 linter 配置: { linter_cfg } " )
164- else :
165- lines .append ("- 未配置任何 linter(建议运行 /init 接入 codeguard)" )
166-
167- # 双副本检测:partme-ai 与 full-stack-plugins 同时启用时每个钩子事件
168- # 跑两遍(历史实测双份触发);提示用户停用其一,而不是默默重复。
169- try :
170- import json as _json
171- _reg = _json .loads ((Path .home () / ".zcode" / "cli" / "config.json" ).read_text (encoding = "utf-8" ))
172- _orgs = sorted ({
173- key .split ("@" , 1 )[1 ]
174- for key in (_reg .get ("plugins" , {}) or {}).get ("enabledPlugins" , {})
175- if key .startswith ("codeguard@" )
176- })
177- except (OSError , ValueError , AttributeError ):
178- _orgs = []
179- if len (_orgs ) > 1 :
180- lines .append (
181- f"- ⚠️ 检测到 codeguard **双副本同时启用**({ ', ' .join (_orgs )} ):"
182- "每个钩子事件会执行两遍,报告与统计可能翻倍或分裂——建议只保留一个来源"
183- )
184-
185- # 版本积压:本机 cache 里有比当前生效副本更新的版本(实测 8 份副本停留在
186- # 0.5.x 而源码已 0.8.x——"发布了但没跑到"的盲区,SessionStart 即可见)
187- try :
188- _mf = json .loads ((PLUGIN_ROOT / ".zcode-plugin" / "plugin.json" ).read_text (encoding = "utf-8" ))
189- _current = str (_mf .get ("version" ) or "" )
190- except (OSError , ValueError , AttributeError ):
191- _current = ""
192- if _current :
193- _note = version_backlog_note (
194- _current , Path .home () / ".zcode" / "cli" / "plugins" / "cache"
195- )
196- if _note :
197- lines .append (_note )
198-
199- lines .append ("- AI 写完代码会被 PostToolUse 钩子自动 lint,告警会出现在这里,按告警里的「怎么修」处理" )
200- lines .append ("- 用户要求「提交/push」时,UserPromptSubmit 钩子会再次确认所有 linter 通过,未通过会拦截提交" )
201- lines .append ("" )
202- lines .append ("**重要**:写代码前先阅读对应语言的规范(见 skills/codeguard-{language}/SKILL.md)" )
203-
204- print ("\n " .join (lines ))
79+ ensure_user_path (from_login_shell = True )
80+ project_root = find_project_root (os .getcwd ()) or Path (os .getcwd ())
81+ current = plugin_version ()
82+ report = startup_application .build_startup_report (
83+ project_root ,
84+ plugin_version = "" if current == "unknown" else current ,
85+ cache_root = Path .home () / ".zcode" / "cli" / "plugins" / "cache" ,
86+ enabled_plugin_orgs = _enabled_plugin_orgs (),
87+ )
88+ if report .notification :
89+ notify (* report .notification )
90+ if report .text :
91+ print (report .text )
20592 return 0
20693
20794
@@ -218,6 +105,6 @@ def _main(payload: dict | None = None) -> int:
218105 sys .exit (main (payload ))
219106 except SystemExit :
220107 raise
221- except Exception as exc : # noqa: BLE001 — 内部错误 fail-open:traceback 绝不进 AI 上下文/阻断工作流
108+ except Exception as exc : # noqa: BLE001 — 内部错误 fail-open,不阻断宿主工作流
222109 print (f"[codeguard] 内部错误已忽略(fail-open): { exc !r} " , file = sys .stderr )
223110 sys .exit (0 )
0 commit comments