agent-syncer 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -12,9 +12,13 @@
12
12
  .claude/skills .trae/skills .codex/skills
13
13
  ```
14
14
 
15
+ > 本文讲的是**分发这一头**:项目里的 `.agents/` 怎么摆、怎么链接给各工具。
16
+ > **存内容那一头**——内容仓库(`skills/` `rules/` `bundles/` 那些)怎么组织、
17
+ > 各类文件的格式约定、从零建一个的完整步骤——见 **[`CONTENT-REPO.md`](CONTENT-REPO.md)**。
18
+
15
19
  ## 为什么不用 Claude Code 的插件机制
16
20
 
17
- 试过,有硬伤(详见 `../HANDOFF.md`):
21
+ 试过,有硬伤:
18
22
 
19
23
  - 插件级 hook 的 `additionalContext` 有上游 bug(issue #16538),注入不了上下文
20
24
  - 插件没有 `rules` 组件
@@ -25,84 +29,440 @@
25
29
 
26
30
  ## 快速开始
27
31
 
28
- > ⚠️ **当前是 alpha 版,发布在 `next` tag 下。**
29
- > 必须带 `@next`,写成 `npx agent-syncer` 会取不到(npm 默认只认 `latest`)。
30
- > 等发稳定版后去掉 `@next` 即可。
32
+ > ⚠️ **当前是 alpha 版,正式发布走 `next` tag。**
33
+ > 首个版本会被 npm 自动挂到 `latest` 上,所以现在 `npx agent-syncer` 也能取到;
34
+ > 之后的预发布版本只进 `next`,要跟预发布就显式写 `npx agent-syncer@next`。
31
35
 
32
36
  ```bash
33
- # 1. 只读检查环境
37
+ # 第一次用:一路问下来,生成 agents.json
38
+ npx agent-syncer@next init
39
+
40
+ # 一条命令:装模板 + 建链接
41
+ npx agent-syncer@next sync --from=<内容仓库路径> --bundle=java-backend
42
+
43
+ # 只读检查环境
34
44
  npx agent-syncer@next doctor
35
45
 
36
- # 2. 看当前状态
46
+ # 看当前状态
37
47
  npx agent-syncer@next status
38
48
 
39
- # 3. 预演一遍(不写盘)
40
- npx agent-syncer@next link --dry-run
49
+ # 仓库里有哪些东西可以挑
50
+ npx agent-syncer@next list
51
+
52
+ # 预演一遍(不写盘)
53
+ npx agent-syncer@next sync --bundle=frontend --dry-run
54
+ ```
55
+
56
+ **把选择写进项目,之后就不用再带参数。** 项目根放一份 `agents.json`:
41
57
 
42
- # 4. 真正建好
43
- npx agent-syncer@next link
58
+ ```json
59
+ {
60
+ "content": "https://git.example.com/team/dept-content.git",
61
+ "ref": "main",
62
+ "bundle": "java-backend",
63
+ "links": ["claude"]
64
+ }
44
65
  ```
45
66
 
46
- > **这一步之前,项目里得先有 `.agents/`。** `link` 只建链接、不拉内容;
47
- > `.agents/` 不存在时会直接报错退出。内容从远端仓库拉取是第二阶段 `sync` 的事。
67
+ 之后 `npx agent-syncer@next sync` 一条命令就够了——拉取、装内容、建链接、维护 `.gitignore`。
48
68
 
49
- **第一次跑 `link` 时会让你勾选。** 项目根没有 `agents.json` 的情况下,它不会自作主张,
50
- 而是把每一条链接目标列出来让你选:
69
+ `links` 里写**工具名**。省略的话 `link` 会问你要用哪些,或者告诉你该怎么写(见下文)。
51
70
 
71
+ **`content` 既可以是 git 地址,也可以是本地路径**,自动判别:
72
+
73
+ ```json
74
+ { "content": "C:/path/to/dept-content" }
52
75
  ```
53
- 没有找到 agents.json,请勾选要建立链接的目录:
54
- ❯ ◉ Claude Code · .claude/skills
55
- ◉ Claude Code · .claude/rules
56
- ◯ Claude Code · .claude/agents (.agents/agents/ 暂无内容)
57
- Trae · .trae/skills
58
- ◉ Codex CLI · .codex/skills
59
- ↑↓ 移动 · 空格 勾选 · a 全选 · 回车 确认 · Ctrl-C 取消
76
+
77
+ 指向本地已克隆的目录有三个好处:不联网、快、改完内容立刻能试——
78
+ 在维护内容仓库时比走远端方便得多。
79
+
80
+ `ref` 可以是分支名、标签名或提交 SHA,省略则取默认分支。
81
+ 用标签可以把项目钉在某个版本上:
82
+
83
+ ```json
84
+ { "content": "https://git.example.com/team/dept-content.git", "ref": "v2.0.0" }
60
85
  ```
61
86
 
62
- 勾选结果会写进 `agents.json`,下次不再问:
87
+ > git 地址是**每次 sync 都重新克隆到临时目录、用完即删**的,不做本地缓存。
88
+ > 多花的是一次浅克隆的时间(内容仓库通常很小,实测不到 1 秒),
89
+ > 换来的是没有「缓存过期」这类说不清的状态。
90
+
91
+ ### 第一次用:`init`
92
+
93
+ 不想手写 `agents.json` 就跑它,依次问四件事:
94
+
95
+ ```bash
96
+ npx agent-syncer@next init
97
+ ```
98
+
99
+ ```
100
+ 第 1 步 · 内容仓库 本地路径 或 git 地址
101
+ 第 2 步 · 版本(ref) 分支 / 标签(本地路径跳过这一步,见下)
102
+ 第 3 步 · 模板(bundle) 可以多选;一个都不选也行
103
+ 第 4 步 · 编码工具 写进 "links"
104
+ 第 5 步 · 确认 把整份配置打印出来,确认过才落盘,然后问要不要立刻 sync
105
+ ```
106
+
107
+ **第 2 步只在内容仓库是 git 地址时出现。本地路径直接跳过**——`sync` 读的是那个目录的
108
+ **工作树**(这正是本地路径「改完内容立刻能试」的来由),`ref` 一个字都用不上。给你选一个
109
+ 不生效的版本,比不让你选更糟。要钉版本就把 `content` 写成 git 地址,`file:///…` 也算。
110
+ 手写的配置里「本地路径 + `ref`」同时出现时,`sync` 和 `list` 都会明确告警。
111
+
112
+ **选项的顺序是 `main`、`master` → 标签 → 其它分支。** 光标默认停在「不指定」,
113
+ **不替你猜 main**——仓库同时有 main 和 master 时猜错方向,会把你悄悄钉在一个过时的分支上。
114
+ 已经配过的项目再跑 `init`,`--ref` 或文件里原有的值会预选。
115
+
116
+ > 「不指定」和「选 `main`」行为很接近——都是每次 sync 去取最新,真正的区别在**标签**:
117
+ > 标签是钉死的版本,仓库往后走了它也不跟。
118
+
119
+ 内容仓库连不上(不是 git 仓库、离线、要凭据),**这一步也是跳过而不是报错**——
120
+ 取不到列表不该拦着你先把一份能用的配置写下来。
121
+
122
+ **没有 `agents.json` 时直接跑 `sync`,在终端里也会先带你走一遍 `init`**,
123
+ `--from` / `--ref` / `--bundle` 传进来的值会当作对应问题的预填值。
124
+
125
+ **非交互环境(CI、管道、`postinstall`)和 `--yes` 都不会弹提示**,还是照旧报错并告诉你
126
+ 配置怎么写——在那些地方弹提示会永久挂住。`--dry-run` 同理:`init` 的全部产出就是那份
127
+ 配置文件,一边说「预演、不写盘」一边把配置写下去是自相矛盾。
128
+
129
+ 不确定有哪些模板?不带 `--bundle` 跑一次,它会列出来:
130
+
131
+ ```
132
+ 可用模板:
133
+ common 通用 全体研发适用的基座:编码规范、提交信息、评审清单与安全红线…
134
+ frontend 前端 前端应用模板:在通用基座之上追加前端开发约定。
135
+ java-backend Java 后端 后端服务模板:在通用基座之上追加接口约定、建服务命令,以及配套的 MCP server。
136
+ ```
137
+
138
+ ### 有哪些内容可以挑:`list`
139
+
140
+ 想用 `include` 逐条挑(「自定义」用法),得先知道 id 叫什么。`list` 就是干这个的:
141
+
142
+ ```bash
143
+ npx agent-syncer@next list # 六类条目 + 模板
144
+ npx agent-syncer@next list --kind=skill # 只看一类(单复数都行)
145
+ npx agent-syncer@next list --bundle=java-backend # 这个模板最终会装哪些条目
146
+ ```
147
+
148
+ ```
149
+ 条目
150
+ ✅ skill 2 个
151
+ api-conventions
152
+ code-style
153
+ ✅ rule 1 个
154
+ java-style
155
+ ➖ command 没有
156
+ ➖ agent 没有
157
+ ➖ hook 没有
158
+ ➖ mcp 没有
159
+
160
+ 模板
161
+ java-backend Java 后端 后端通用
162
+ ```
163
+
164
+ 类型名用的是**单数**——那正是 `include` 里要写的那个词(`"skill:api-conventions"`)。
165
+
166
+ `--bundle` 那一栏会把项目级 `include` / `exclude` 一起算进去,所以它给出的答案
167
+ 和真跑一次 `sync` 的结果一致,不会骗人。`list` 读的是**内容仓库**而不是项目里的
168
+ `.agents/`,因为要回答的问题正是「仓库里有什么可以挑」;内容是 git 地址时会先浅克隆。
169
+
170
+ ### 挑内容:模板 + 项目级增删
171
+
172
+ **模板可以要多个,取并集**(`--bundle=a,b` 或写成数组):
173
+
174
+ ```json
175
+ { "bundle": ["common", "frontend"] }
176
+ ```
177
+
178
+ **也可以一个模板都不用,直接逐条挑**——这就是"自定义":
179
+
180
+ ```json
181
+ { "include": ["skill:code-style", "rule:*"] }
182
+ ```
183
+
184
+ **在模板之上还能增删**,`exclude` 最后过一遍:
63
185
 
64
186
  ```json
65
187
  {
66
- "links": [
67
- "claude/skills",
68
- "claude/rules",
69
- "trae/skills",
70
- "codex/skills"
71
- ]
188
+ "bundle": ["java-backend"],
189
+ "include": ["mcp:dept-wiki"],
190
+ "exclude": ["hook:dept-hooks"]
72
191
  }
73
192
  ```
74
193
 
75
- 想写成"这个工具的类型全要"也行,两种写法等价:
194
+ 流水线是三段,每一步都在上一步的结果上做增删:
195
+
196
+ ```
197
+ 模板并集 → + 项目 include → − 项目 exclude
198
+ ```
199
+
200
+ 所以 **`exclude` 能砍掉上面任何一步加进来的东西**,包括 `include` 刚加的。
201
+ `include` / `exclude` 的写法与模板文件里完全一致:`skill:code-style` 单条、`skill:*` 整类。
202
+
203
+ **组合模板只有一种写法:`bundle` 字段。** 模板文件里也能写它,含义与这里一字不差——
204
+ `bundles/java-backend.json` 里一句 `"bundle": ["common"]`,拿到的就是"通用"那部分。
205
+ 字段在哪一层出现都叫 `bundle`,没有第二套语法可记。
206
+
207
+ > 早期版本允许在 `include` 里写 `"@common"` 引用模板,**现在会直接报错**并给出新写法。
208
+ > 不静默兼容是有意的:一处改了一处没改,最可能的结果是"引用看起来还在、其实早就不生效"。
209
+ > 代价是 **`exclude` 从此排不掉一整个模板**(以前 `exclude: ["@common"]` 可以),
210
+ > 只能逐条列、或者把模板拆小——见 [`CONTENT-REPO.md`](CONTENT-REPO.md) 的「已知限制」。
211
+
212
+ **多个模板之间互不干扰**:各自解析完再取并集,A 模板的 `exclude` 砍不到 B 模板的内容。
213
+ 否则 `bundle: ["a", "b"]` 的结果会取决于组合顺序和谁的 `exclude` 更宽,没法推理。
214
+ **模板里再套模板也一样**——`"bundle": ["a", "b"]` 写进模板文件后用同一条规则。
215
+
216
+ **`exclude` 引用一个当前不存在的条目,只告警不报错。** 它是叠在模板之上的最后一层,
217
+ 模板内容一变动就把所有人的配置搞崩,代价太大。但宽容仅限"条目不存在"——
218
+ 语法写错、类型写错、引用不存在的模板,一律照旧报错。
219
+ (模板文件里的 `exclude` 更严格:排一个不存在的条目是**错误**。模板应当自洽。)
220
+
221
+ **sync 是幂等的**——内容没变就报「已是最新」,不会重复写。
222
+
223
+ **切换模板时不会自动删东西。** 上个模板留下的、本模板不再需要的条目只会被列出来:
224
+
225
+ ```
226
+ ⚠️ 有 2 项是以前装的、现在不需要了——不会自动删除
227
+ · skill:old-convention
228
+ · script:legacy.sh
229
+ 加 --prune 可以删掉(.agents/ 本身在版本库里,删错了能找回)。
230
+ ```
231
+
232
+ 加 `--prune` 才真的删。**你自己从头写的不会被碰**——它不在记录里。但**装过的条目
233
+ 不会被自动认出来是不是你改过**,想保住就得自己声明:
234
+
235
+ ```json
236
+ { "protect": ["skill:old-convention"] }
237
+ ```
238
+
239
+ 见下文「锁定:`protect`」和「装过什么」。
240
+
241
+ ---
242
+
243
+ **内容已经在 `.agents/` 里了、只想建链接**,用 `link`。
244
+
245
+ **第一次跑 `link` 时会让你勾选。** 项目根没有 `agents.json`、或者配置里没写 `links` 时,
246
+ 它不会自作主张,而是列出可选工具让你选:
247
+
248
+ ```
249
+ 没有找到 agents.json,请勾选这个项目使用哪些编码工具:
250
+ ❯ ◯ Claude Code · .claude/{skills, rules, commands, agents}
251
+ ◯ Trae · .trae/{skills, rules, commands} (会新建 .trae/)
252
+ ◯ Codex CLI · .codex/{skills} (.agents/ 下暂无该工具可用的内容,会新建 .codex/)
253
+ ↑↓ 移动 · 空格 勾选 · a 全选 · 回车 确认 · Ctrl-C 取消
254
+ ```
255
+
256
+ 勾选结果会写进 `agents.json`,下次不再问:
76
257
 
77
258
  ```json
78
259
  {
79
- "tools": ["claude", "trae", "codex"]
260
+ "links": ["claude", "codex"]
80
261
  }
81
262
  ```
82
263
 
83
- **非交互环境(CI、管道、`postinstall`)不会弹提示**——`link` 会退回到按已有目录推断,
84
- 并明确打印推断了什么。脚本里想跳过询问,用 `--yes`。
264
+ **`links` 里写的是工具名,不是目录。** 选用一个工具就是它的全部类型一起适配——
265
+ 没有"只要 `.claude/rules` 不要 `.claude/skills`"这种真实场景,所以配置里不提供
266
+ 目录级的开关。拆到目录那一层给不出有用的选择,只会让配置多一层需要解释的东西。
267
+
268
+ > 早期版本允许写 `"claude/skills"` 这种目录形式,**现在会直接报错**并告诉你改成 `"claude"`。
269
+ > 不静默兼容是有意的:用户写下的是一个比实际行为**更窄**的东西,
270
+ > 默默按整个工具处理,他事后会以为自己只启用了 skills。
271
+
272
+ **非交互环境(CI、管道、`postinstall`)不会弹提示**,而 `link` 也**不会替你猜**——
273
+ 它会报错,并把可选项和写法一起列出来:
274
+
275
+ ```
276
+ ❌ 不知道要给哪些工具建链接
277
+ agents.json 里没有 links。
278
+ 当前不是交互终端,没法问你。
279
+
280
+ 两种做法:
281
+ · 在 agents.json 里写一行,之后不用再管(推荐)
282
+ "links": ["claude"]
283
+ · 或者在终端里直接跑 agent-syncer link,会列出来让你勾选
284
+
285
+ 可用工具(links 里写名字,要多个就写成数组):
286
+ ◯ claude Claude Code .claude/{skills, rules, commands, agents}
287
+ ◯ trae Trae .trae/{skills, rules, commands}
288
+ ◯ codex Codex CLI .codex/{skills}
289
+ 例如: "links": ["claude", "trae"]
290
+ ```
291
+
292
+ `--yes` 的含义因此收窄成**「不弹提示」**,它不负责猜——脚本里要先写好 `links`。
293
+
294
+ **已经声明了的时候,也会顺带提示还有哪些没启用**:
295
+
296
+ ```
297
+ ✅ 工具:claude(Claude Code)
298
+ 还支持 trae(Trae)、codex(Codex CLI)——想一起适配就加进 agents.json 的 "links"
299
+ ```
300
+
301
+ ### 为什么不再按已有目录推断
302
+
303
+ 早期版本会看项目里有哪些工具目录(`.claude` / `.trae` / `.codex`)来推断。
304
+ 去掉了,因为**猜错的代价不对称**:
305
+
306
+ - 「这个项目用 Trae」是个**持久事实**。从「目录恰好存在」推出来的东西,
307
+ 用户既没同意过、也看不见——只是碰巧有个 `.trae/` 目录,不代表想让它接管
308
+ - 猜错的方向还很糟:给一个其实不用的工具**建了链接并改了 `.gitignore`**,
309
+ 而这一切只留一句「按已有目录推断」的提示
310
+
311
+ 现在读配置和决定用哪些工具是分开的两件事:`loadConfig` 不做推断,`tools` 留空并标出
312
+ `toolsDeclared: false`,由 `link` 去问或者列选项。`config.js` 里留了注释说明别加回来。
313
+
314
+ ### 换工具之后:`--prune`
315
+
316
+ `link` **只加不减**。把某个工具从 `links` 里去掉,已经建好的链接不会自己消失,
317
+ 而 `.gitignore` 托管段却已经不忽略它们了——git 会顺着这些链接把 `.agents/`
318
+ 的内容**再提交一份**,托管段存在的唯一目的就此被绕过。
319
+
320
+ 所以 `link` 和 `status` 都会把这类「不再使用的链接」列出来:
321
+
322
+ ```
323
+ ⚠️ 发现 2 个不再使用的链接——不会自动删除
324
+ · .trae/rules → .agents/rules
325
+ · .trae/skills → .agents/skills
326
+ ```
327
+
328
+ **默认只报告,不删**——分不清这是刚去掉的残留,还是你自己建的链接。
329
+ 确认不需要后加 `--prune`:
330
+
331
+ ```bash
332
+ npx agent-syncer link --prune
333
+ ```
334
+
335
+ `--prune` 只删链接,实体目录一个都不动(和 `--force` 一样,删除前会先确认那是链接)。
336
+ 删空之后剩下的空目录会顺手收掉——`.trae/` 空着留着不影响版本库(git 不跟踪空目录),
337
+ 但 `doctor` 见到它还在就会报「目录存在,但未在 `links` 里声明」,白白让人以为配置写漏了。
338
+ **一个非空的目录都收不掉**(`rmdir` 只对空目录成功,这层保险在内核里,不靠判断)。
339
+
340
+ 同样的道理,**合并产物也会留下残留**:某个工具从 `links` 里去掉之后,本工具以前写进它
341
+ 配置文件的条目没人去清,而那个文件是提交进版本库的。`status` 会单列一节,
342
+ `sync --prune` 一起摘掉——只摘记录里说本工具写过的那些。
343
+
344
+ ### 单独建一条链接:`--src` / `--dst`
345
+
346
+ `link` 默认读 `agents.json`,把 `.agents/` 下的内容按工具分发过去。给上 `--src`
347
+ 和 `--dst` 则是另一种用法——**在这儿建一条链接**,跟项目配置全无关系:
348
+
349
+ ```bash
350
+ npx agent-syncer link --src=../shared/prompts --dst=.claude/skills
351
+ ```
352
+
353
+ ```
354
+ agent-syncer link --src/--dst
355
+ 工作目录:/home/me/proj
356
+
357
+ ../shared/prompts → .claude/skills
358
+
359
+ ✅ .claude/skills 已创建
360
+
361
+ 这条链接不在托管范围内:status 看不到它,--prune 也不会清理它。
362
+ 注意:这个落点在某个工具的托管目录里——之后跑批量 link 会认为它「指向别处」
363
+ 而拒绝接管。那是它在保护这条链接,别照着 --force 的提示把它覆盖掉。
364
+ ```
365
+
366
+ 相对路径按**当前目录**解析(`--cwd` 可改),绝对路径原样用。源必须已经存在且是目录。
367
+ 安全检查和不带参数时**完全一样**:拒绝覆盖实体目录或文件、只删链接、指向别处的链接要
368
+ `--force` 才接管、创建后校验。`--dry-run` 同样有效。
369
+
370
+ **两种模式会互相看见,但不会互相接管。** 落点落在某个工具的托管目录里(上面那个例子
371
+ 就是)时,之后跑批量 `link` 会把它当成「指向别处」而拒绝——那是它在保护这条链接,
372
+ 不要顺手加 `--force` 覆盖掉,那会把这条特意建的链接换成指向 `.agents/` 的。
373
+ 反过来,`link` 的提示里也会说清 `--force` 是**换成**而不是「修好」。
374
+
375
+ **它刻意不在托管范围内。** 不写 `.gitignore` 托管段、不进 `.agent-sync.json`、
376
+ `status` 看不见它、`--prune` 也不碰它。理由是托管那一整套的前提是「内容都从
377
+ `.agents/` 来」——`findStaleLinks` 只认指向本项目 `.agents/<kind>/` 的链接,记录文件
378
+ 也假定自己**与机器无关**(它是要提交进版本库的),而 junction 记的是绝对路径,
379
+ 天生是本机一次性的。硬塞一条任意路径进去,等于让每个子系统都记住一个例外。
380
+
381
+ 想让它被管起来,就把内容放进 `.agents/` 走默认模式。
382
+
383
+ ⚠️ **落点在 git 仓库里时,自己把落点加进 `.gitignore`。** junction 记录的是绝对路径,
384
+ 提交上去在别人机器上全是指错的;更糟的是 git 会**穿透链接**,把源的内容在仓库里
385
+ 再提交一份。
85
386
 
86
387
  ## 命令
87
388
 
88
389
  | 命令 | 作用 | 写盘 |
89
390
  | --- | --- | --- |
90
- | `link` | 建好目录链接并维护 `.gitignore`;没有 `agents.json` 时先让你勾选 | 是(支持 `--dry-run`) |
91
- | `status` | 每个链接是否健康、`.agents/` 下各有多少内容 | |
92
- | `doctor` | 运行环境、链接能力实测、`.gitignore`、各工具是否存在 | |
391
+ | `init` | 交互式生成 `agents.json`:问内容仓库、版本、模板、要适配的工具 | 是(**确认过才写**) |
392
+ | `sync` | 从内容仓库挑一个模板装进 `.agents/`,然后建链接 | 是(支持 `--dry-run`) |
393
+ | `link` | 只建目录链接并维护 `.gitignore`;没写 `links` 时会问你或告诉你怎么写。加 `--src`/`--dst` 则是单独建一条链接,不读配置 | 是(支持 `--dry-run`) |
394
+ | `list` | 内容仓库里有哪些条目和模板 | 否 |
395
+ | `status` | 每个链接是否健康、`.agents/` 下各有多少内容、装过什么、hooks/mcp 合并没有、有没有「不再使用的链接 / 合并产物」 | 否 |
396
+ | `doctor` | 运行环境、链接能力实测、内容、**记录文件**、项目配置、`.gitignore`、`.gitattributes`、合并产物、MCP 批没批准、各工具是否存在 | 否 |
397
+
398
+ 选项:`--from=<路径>` `--bundle=<名字>` `--kind=<类型>` `--ref=<版本>` `--src=<目录>` `--dst=<路径>` `--dry-run` `--force` `--prune` `--yes` `--no-save` `--cwd=<路径>` `--help`
93
399
 
94
- 选项:`--dry-run` `--force` `--yes` `--no-save` `--cwd=<路径>` `--help`
400
+ 选项一律写成 `--名字=值`。**空格分隔不支持**(`--src ../shared/prompts`)——本工具会
401
+ 当场提醒你写成等号,而不是把它当成「没给这个选项」。
95
402
 
96
403
  ## 目录约定
97
404
 
98
405
  ```
99
406
  .agents/
100
- ├── skills/<名字>/SKILL.md # 技能(目录形态,可含 references/ scripts/ 等)
101
- ├── rules/<名字>.md # 规则
102
- ├── commands/<名字>.md # 斜杠命令
103
- └── agents/<名字>.md # 子代理
407
+ ├── skills/<名字>/SKILL.md # 技能(目录形态,可含 references/ scripts/ 等)→ 链接给各工具
408
+ ├── rules/<名字>.md # 规则 → 链接给各工具
409
+ ├── commands/<名字>.md # 斜杠命令 → 链接给各工具
410
+ ├── agents/<名字>.md # 子代理 → 链接给各工具
411
+ ├── hooks/<名字>.json # hook 片段(顶层键就是事件名) → 合并进 .claude/settings.json
412
+ ├── mcp/<名字>.json # 单个 MCP server 的定义 → 合并进 .mcp.json / .trae/mcp.json
413
+ ├── scripts/ # 被 hooks / mcp 按路径引用(选了这两类才同步)
414
+ └── .agent-sync.json # 本工具写的:记着哪些内容是它装的(要提交,见下文)
104
415
  ```
105
416
 
417
+ 前四类靠**链接**分发——内容只存一份,各工具目录指过来。
418
+
419
+ 后三类不是链接:`hooks/` 和 `mcp/` 要**合并**进各工具自己的配置文件
420
+ (那些文件里还有用户自己的东西,不能整份覆盖),`scripts/` 则是被 hook 命令按路径引用的真实文件。
421
+ **这三类都必须提交到版本库**——`.gitignore` 托管段会为它们开白名单,漏了的话别人克隆下来只剩空壳。
422
+
423
+ > `scripts/` **不参与条目挑选**,但要分清「不挑拣」和「无条件」:本轮选了 hook 或 mcp
424
+ > 就整个搬过去(脚本按路径被引用,挑漏一个是运行时静默失败),**一个都没选就不同步**——
425
+ > 那时候没有任何东西会引用它们,搬过去只是往项目里塞用不上的文件。
426
+
427
+ ### 合并:怎么做到「只动自己那份」
428
+
429
+ `.mcp.json` 和 `.claude/settings.json` 里混着用户自己的 server 和 hook。所以本工具
430
+ 记下**自己往哪个文件里写了什么**(`.agents/.agent-sync.json` 的 `merged` 字段),
431
+ 而且判定是**被验证的**,不是被断言的:
432
+
433
+ > 记录说某条是本工具写的**还不够**,还要**现场确实还是当初写的那份**才动。
434
+
435
+ 对不上的时候(你改过它、或者它本来就是你写的同名条目),本工具**保留你的、报一声,
436
+ 绝不覆盖回去**。代价是记录文件会大一些;换来的是不会静默吃掉你的东西。
437
+
438
+ **合并产物要提交进版本库。** 它们是团队共享的配置,密钥一律写成 `${环境变量}` 占位符。
439
+ `doctor` 会检查它们有没有被 `.gitignore` 挡在外面(`.gitignore` 对**已跟踪**的文件无效,
440
+ 所以这个检查是先看有没有被跟踪、再看有没有被忽略)。
441
+
442
+ > ⚠️ **MCP 写进去了不等于生效。** Claude Code 要你**逐条批准**才会连,而批准记录
443
+ > 存在你本机的 `~/.claude.json` 里,**不随仓库共享**——队友要各自批一次。
444
+ > 就算把 `enableAllProjectMcpServers: true` 提交进 `.claude/settings.json`,
445
+ > 在未信任的目录里也会被忽略。`doctor` 会去读那一格,告诉你还差几条。
446
+
447
+ > **项目根变量写哪个都认。** 内容里引用项目内脚本时,`${CLAUDE_PROJECT_DIR}` 和
448
+ > `${workspaceFolder}` 都行,合并时按目标工具翻译成它自己的写法。别的 `${VAR}`
449
+ > 原样保留,由工具自己在运行时展开。
450
+
451
+ 各工具的合并目标:
452
+
453
+ | 内容 | Claude Code | Trae | Codex |
454
+ | --- | --- | --- | --- |
455
+ | hooks | `.claude/settings.json` 的 `hooks` 段 | —— | —— |
456
+ | mcp | `.mcp.json`(项目根) | `.trae/mcp.json` ⚠️ 未实证 | —— 不支持,见下 |
457
+
458
+ > **Trae 的 MCP 路径未经实证**(本机没装 Trae,路径和变量名都来自网络资料)。
459
+ > `doctor` 会把它标成「未实证」而不是「正常」。
460
+ >
461
+ > **Codex 的 MCP 暂时不做**:它的配置是 TOML,项目级 `.codex/config.toml` 又只在
462
+ > **受信任项目**里加载,而 `codex mcp add` 只能写用户级(上游 issue #23487 还开着)。
463
+ > 声明了 Codex 而仓库里有 MCP 内容时,`status` / `doctor` 会**明确告诉你装了不生效**,
464
+ > 而不是静默跳过。
465
+
106
466
  内容只在上面保存一份,各工具的实际位置由内置映射表决定:
107
467
 
108
468
  | 内容 | Claude Code | Trae | Codex |
@@ -129,22 +489,154 @@ npx agent-syncer@next link
129
489
  文件链接则必须管理员或开发者模式。为了所有人开箱即用,本工具干脆不支持文件链接——
130
490
  单文件的配置(如 `settings.json`)以后走「读-改-写合并」,不走链接。
131
491
 
132
- **绝不删除实体目录。** 路径上已经有用户自己的目录或文件时,`link` 一律拒绝并报错,
492
+ **绝不删除有内容的目录。** 路径上已经有用户自己的目录或文件时,`link` 一律拒绝并报错,
133
493
  退出码非零。指向别处的链接需要显式 `--force` 才接管,且 `--force` 也只删链接、不删实体目录。
134
494
 
495
+ **路径上有链接就不往里写。** `sync` 落每一份内容之前,会看路径**自己**和它的**每一层
496
+ 上层目录**:哪一段是链接就拒绝,并报出是哪一段。原先只看最后那一段,于是
497
+ `.agents/skills` 自己是链接(比如你把它指向了别处的共享目录)时,`.agents/skills/alpha`
498
+ 并不是链接,守卫整条放过——内容顺着链接写进了那个目录,退出码还是 0。而那里的内容
499
+ 很可能**还有别的项目在管**(每个项目的记录各管各的),这边 `--prune` 一删,那边就凭空少东西。
500
+ 上溯只到**项目根**为止:项目根自己的祖先是不是链接(macOS 上 `/tmp` 就是)不归本工具管。
501
+
502
+ `--prune` 会收掉被删空的目录,但那和上面这条不矛盾:`fs.rmdirSync` **只对空目录成功**,
503
+ 非空一律报错,所以「有内容的目录删不掉」不是靠判断维持的,是内核挡在那儿——
504
+ 判断逻辑写错了也删不掉。另外只认**实体**目录,`lstat` 一看是链接就放手:
505
+ Windows 上 `RemoveDirectoryW` 对 junction 的语义是「摘掉重解析点」而非「删目标内容」,
506
+ 传一个指向别处的 junction 进去就会把别人的链接摘了,而链接本来就归 `removeLink` 管。
507
+
135
508
  **`.gitignore` 托管段是必需的。** 链接记录的是**绝对路径**,提交上去在别人机器上全是错的;
136
509
  更危险的是 git 会**穿透链接**,把 `.agents/` 的内容原样再提交一份,仓库里出现两套内容。
137
510
  `link` 会自动写入这段(`.claude/settings.json`、`.claude/hooks/` **不在**忽略之列,那是要进版本库的真实配置)。
138
511
 
139
- **没有状态文件。** 状态全部从文件系统推出来。少存一份状态,就少一次不同步的机会。
512
+ 段里**只列真正建了链接的目标**,两条判断缺一不可:内容目录不存在的不列(link 会跳过它),
513
+ 路径上已经是实体目录的也不列(link 拒绝覆盖它,那是用户自己的东西,忽略它会导致人家写的
514
+ 东西进不了版本库)。配置写的是工具名,一个工具最多带出四个目标,所以这两条判断是必需的
515
+ ——不然会给一堆根本没建链接的路径平白加上忽略规则。
516
+
517
+ 段是**整段重建**的:你在段里手写的行会在下一次 `link` 时无声消失。所以 `status` / `doctor`
518
+ 会把它们报出来(「托管段里有 N 行不是本工具生成的」),而不是报一句「完整」了事。
519
+
520
+ **只存一份状态,而且只存推不出来的那部分。** 链接、内容、`ref` 全从文件系统推,
521
+ 不额外记。唯一的例外是 `.agents/.agent-sync.json`,见下节——因为它回答的问题
522
+ (「这份内容是本工具装的吗」)**光看文件系统推不出来**。
523
+
524
+ ## 「装过什么」:唯一的状态文件
525
+
526
+ `.agents/.agent-sync.json` 记着**哪些内容是 sync 装进来的**,以及**本工具往哪些
527
+ 工具配置文件里写了什么**。
528
+
529
+ ```json
530
+ {
531
+ "schemaVersion": 2,
532
+ "installed": ["rule:commit-message", "skill:api-conventions"],
533
+ "scripts": ["session-context.sh"],
534
+ "merged": {
535
+ "claude": {
536
+ "mcp": { "dept-wiki": { "type": "http", "url": "https://mcp.example.com/api" } }
537
+ }
538
+ }
539
+ }
540
+ ```
541
+
542
+ `installed` / `scripts` **只有名字**。`merged` 是唯一的例外——它**存的是值**,
543
+ 因为要回答的问题不一样:
544
+
545
+ - `installed` 回答「`.agents/mcp/foo.json` 这个**文件**是不是本工具放的」
546
+ - `merged` 回答「`.mcp.json` 里那个 **条目**是不是本工具写的」
547
+
548
+ **两者会分叉。** 用户完全可能在自己的 `.mcp.json` 里写一个和部门条目同名的 server;
549
+ 本工具报了冲突没敢覆盖,但 `.agents/mcp/foo.json` 确实装上了,于是 `installed` 照样
550
+ 会记上 `mcp:foo`——**下一轮就把它当成自己人覆盖掉**。所以合并的归属只能来自 `merged`,
551
+ 而且判定是**被验证的**:记录说是我写的还不够,还要**现场确实还是我写的那份**才动。
552
+
553
+ 代价是这份要提交的文件会大一些(多了合并进去的那几条内容)。换来的是不会静默吃掉
554
+ 用户的东西,以及「这条为什么还在」永远解释得清。
555
+
556
+ **为什么非要有它。** 看起来有条捷径:`.agents/` 里有、内容仓库里没有 ⇒ 是用户自己写的,别动。
557
+ 但它恰好在最要紧的情况下失效——**内容仓库某一次把某个技能整个删掉了**,真源里已经没有它,
558
+ 这个启发式会把项目里那份误判成「用户内容」而永久保留。`scripts/` 更糟:删掉一个脚本
559
+ 连提示都没有,因为孤儿检查本来就不看它。
560
+
561
+ **它要提交进版本库。** 它描述的是 `.agents/` 的内容,跟机器无关;不提交的话队友克隆下来,
562
+ sync 就认不出哪些是本工具装的,清理功能等于失效。`.gitignore` 托管段里因此给它开了一行
563
+ 白名单(`!.agents/.agent-sync.json`——上面那条 `.agents/*` 本来会把它忽略掉)。
564
+
565
+ **里面没有内容仓库地址。** 它每个项目都要提交一份,内部坐标不该跟着走。
566
+
567
+ `.agents/` 那半边**没有内容指纹**:指纹按原始字节算,`core.autocrlf` 的检出会让同一份
568
+ 内容算出不同值,于是这份被提交的文件会在 LF / CRLF 两种值之间来回翻——而它本来还会因为
569
+ 「内容更新了」而频繁变动,等于每次内容改动都在每个项目的仓库里多出一份 diff。
570
+ 所以那一半的代价是:**认不出「你改过的」,得你自己说**(见下一节)。
571
+
572
+ `merged` 那半边不适用这条:它比的是**解析后的结构**,换行符影响不到。比的时候还**不看
573
+ 键序**——工具自己重排一次键序就触发一次写入的话,那个文件会一直抖。
574
+
575
+ 记录坏了、或者 `schemaVersion` 对不上,一律当作「没有记录」:**什么都不会删**,
576
+ 下次 sync 重新写一份——**写之前先把坏的那份备份成 `.agent-sync.json.bak`**(只备一次,
577
+ 不把最初那份越冲越远)。这份文件里记的归属没有第二处能推导出来,直接盖掉是不可逆的。
578
+
579
+ ⚠️ 「当作没有记录」对**删除**是保守的,对**归属**却是破坏性的:`merged` 一丢,本工具就
580
+ 认不出 `.mcp.json` 里哪条是自己写的了,下一轮会把它报成「与记录之外的 server 重名」,
581
+ 劝你「删掉那一份,或写进 protect」——而那份本来就是它自己写的。所以 `doctor` 会专门查
582
+ 这份文件,`status` 也会报。顺带一提,带 BOM 的记录(记事本、PowerShell 的 `>` 都会写)
583
+ 照常读得出来,不会走到这条路。
584
+
585
+ > 首次升级到带记录功能的版本时,`.agents/` 里已有的内容**不会被认领**——sync 分不清
586
+ > 它们是谁的。那一次它会照老规矩把「不在本模板里」的条目全列出来,之后写下的记录
587
+ > 只包含本次装的那些。所以升级后第一次 sync 的输出值得看一眼。
588
+
589
+ ## 锁定:`protect`
590
+
591
+ `--prune` 删什么,只看两条:**记录里说它装过**,且**没被 `protect` 锁住**。
592
+ sync 认不出「你手工改过的」——所以想保住某个装过的条目,得自己写进 `agents.json`:
593
+
594
+ ```json
595
+ {
596
+ "bundle": ["java-backend"],
597
+ "protect": ["skill:api-conventions", "script:tools/legacy.sh"]
598
+ }
599
+ ```
600
+
601
+ **锁 = 本工具整个不碰它**:sync 不覆盖、`--prune` 不删。
602
+
603
+ > 只挡删除是不够的。sync 仍会把它当普通选中项**覆盖**掉,用户以为锁上了、
604
+ > 内容却被冲了,比不锁更糟。所以锁定是「不覆盖也不删」这一个语义。
605
+
606
+ 写法是 `"类型:名字"`,脚本写 `"script:相对路径"`——和 `sync --prune` 输出里用的是同一套词。
607
+ **不支持 `*` 通配**:`skill:*` 的实际含义是「这个项目别用 sync」,说成通配只会让人以为
608
+ 只是顺手省几个字。写错了会**直接报错**——这是你用来保护自己劳动成果的声明,静默忽略比
609
+ 报错危险得多。
610
+
611
+ 对**合并**也一样:`"protect": ["mcp:dept-wiki"]` 会让这条既不写进 `.mcp.json`、
612
+ 也不从里面摘掉。合并那边其实还有一道自己的保险——**你改过的条目本来就不会被覆盖**
613
+ (记录说它是本工具写的,但现场对不上,就保留你的并报一声)。`protect` 是给
614
+ 「我知道它是部门装的,但这一份我要自己管」这种情况用的。
615
+
616
+ 没锁的残留会被 `--prune` 直接删。不过:
617
+
618
+ - 不带 `--prune` 时 `sync` 只报告、不删,会先把要删的列出来给你看
619
+ - `.agents/` 本身在版本库里,真删错了能找回来
140
620
 
141
621
  ## 已知限制
142
622
 
143
- - **Trae 的路径约定未经实证**——本机没装 Trae,映射表沿用既有项目里在用的配置
144
- - **Codex 没有 rules / commands 目录概念**,它只有 `AGENTS.md` 和 `config.toml`。
145
- 这两类内容将来要合并进 `AGENTS.md`,目前直接跳过
623
+ - **Codex MCP 还不能用**——它的配置是 TOML,项目级 `.codex/config.toml` 又只在受信任
624
+ 项目里加载,而 `codex mcp add` 只能写用户级。声明了 Codex 而仓库里有 MCP 内容时会被明确告知
625
+ - **换了 Claude Code 的配置格式就失效**——`.mcp.json` / `.claude/settings.json` 的形态是
626
+ 照着 2.1.267 实测出来的,上游改了格式得跟着改
627
+ - **Codex 的 rules / commands 还不能用**——Codex 没有规则目录(只有 `AGENTS.md`),
628
+ 目录形式的它只认 skills。这两类目前直接跳过
629
+ - **Trae 的路径约定未经实证**——本机没装 Trae,映射表沿用既有项目里在用的配置。
630
+ 它的 MCP 路径(`.trae/mcp.json`)和变量名(`${workspaceFolder}`)同样没验证过,
631
+ `doctor` 会把这两项标成「未实证」而不是「正常」
632
+ - **项目仓库最好有 `.gitattributes`**。`sync` 判断「内容变没变」是按字节比的,所以
633
+ 检出成 CRLF 的那份会被当成「变了」——多报一次「更新」并把文件重写回 LF,
634
+ 第二遍就恢复正常。会不会顺带弄脏 git 取决于项目自己的 `core.autocrlf`
635
+ (`true` 时不会,`false` 时行尾改动作会被当成真改动)。根治办法是在项目根放一份
636
+ `.gitattributes`:`* text=auto`。内容仓库那边有同样的要求。
637
+ `doctor` 会检查这一条并给出该写的内容,但**绝不代写**——`* text=auto` 改的是整个
638
+ 仓库所有文件的检出行为,是仓库级的决定,得由人来做
146
639
  - **Windows 的 junction 不支持 UNC 网络路径**。工作目录在网络盘上时会明确报错
147
- - 目前只做**链接**。从远端仓库拉取内容、合并配置文件属于第二阶段
148
640
 
149
641
  ## 开发
150
642
 
@@ -152,4 +644,5 @@ npx agent-syncer@next link
152
644
  npm test # node --test,零依赖
153
645
  ```
154
646
 
155
- 代码约定与 `iqcs_web/scripts/` 保持一致:ESM、`node:` 前缀、手写参数解析、中文 + emoji 输出。
647
+ 代码约定:ESM、`node:` 前缀、手写参数解析(不引第三方依赖)、中文 + emoji 输出。
648
+ `lib/` 与 `bin/` 里没有一行第三方 import。