teamai-cli 0.16.3 → 0.16.5
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 +159 -156
- package/README.zh-CN.md +326 -0
- package/dist/index.js +84 -94
- package/package.json +1 -1
- package/README.en.md +0 -323
package/README.zh-CN.md
ADDED
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
# TeamAI — The team harness for AI agents
|
|
2
|
+
|
|
3
|
+
> [English](README.md) | [简体中文](README.zh-CN.md)
|
|
4
|
+
|
|
5
|
+
[](https://github.com/Tencent/teamai-cli/actions/workflows/ci.yml)
|
|
6
|
+
[](https://www.npmjs.com/package/teamai-cli)
|
|
7
|
+
[](https://www.npmjs.com/package/teamai-cli)
|
|
8
|
+
[](LICENSE)
|
|
9
|
+
|
|
10
|
+
让每个 AI 编程助手都按同一套标准工作。
|
|
11
|
+
|
|
12
|
+
通过 Git 统一管理 skills、rules、docs,驾驭 20+ 种 AI 工具——一个人也能用,团队用更强。
|
|
13
|
+
|
|
14
|
+
**支持:** Claude Code、Codex、Cursor、CodeBuddy IDE,以及 Gemini CLI、Windsurf、Trae、Aider、Amp、OpenClaw 等 20+ 种 AI 编程工具(skills 同步)。
|
|
15
|
+
|
|
16
|
+
> 📖 **完整使用指南**:[docs/usage-guide.md](docs/usage-guide.md) — 涵盖从团队创建到日常使用的全流程。
|
|
17
|
+
|
|
18
|
+
> 📚 **Provider 说明**:[docs/providers.md](docs/providers.md) — GitHub / TGit 差异与认证配置。
|
|
19
|
+
|
|
20
|
+
如有问题或建议,欢迎提交 PR 或 Issue,一起共建这个项目。
|
|
21
|
+
|
|
22
|
+
## 安装
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
npm install -g teamai-cli
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
<details>
|
|
29
|
+
<summary>腾讯内部用户:通过 tnpm 安装 <code>@tencent/teamai-cli</code></summary>
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
npm install -g @tencent/teamai-cli --registry=http://r.tnpm.oa.com
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
两个包的代码内容一致,`@tencent/teamai-cli` 只是公网 `teamai-cli` 的内网镜像。
|
|
36
|
+
</details>
|
|
37
|
+
|
|
38
|
+
## 快速开始
|
|
39
|
+
|
|
40
|
+
### 团队成员
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
# 用户级初始化(默认,资源安装到 ~/)
|
|
44
|
+
teamai init --repo yourteam/yourproject
|
|
45
|
+
|
|
46
|
+
# 项目级初始化(资源安装到项目目录下)
|
|
47
|
+
cd /path/to/my-project
|
|
48
|
+
teamai init --repo yourteam/yourproject --scope project
|
|
49
|
+
|
|
50
|
+
# 非交互模式(适合 CI/CD 或 AI agent 自动化)
|
|
51
|
+
teamai init --repo yourteam/yourproject --scope user --role hai_dev --force
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### 管理员
|
|
55
|
+
|
|
56
|
+
先在 git 托管平台上创建好团队共享经验的仓库(默认 GitHub;TGit 也支持),并把所有团队成员加入到该仓库的 write 权限。
|
|
57
|
+
|
|
58
|
+
- **GitHub**:用 `gh repo create yourorg/yourproject --private` 创建,或在 UI 上建。然后用 Settings → Collaborators 把成员加进来,并把 master/main 设置为默认分支。
|
|
59
|
+
- **TGit(腾讯工蜂)**:在 [git.woa.com](https://git.woa.com/) 上创建,通过 user group 批量添加 master 权限。
|
|
60
|
+
|
|
61
|
+
CLI 会根据用户传入的 repo URL 自动选择 provider:
|
|
62
|
+
|
|
63
|
+
- `yourorg/yourrepo` 或 `https://github.com/yourorg/yourrepo` → GitHub
|
|
64
|
+
- `https://git.woa.com/yourteam/yourrepo` → TGit
|
|
65
|
+
|
|
66
|
+
## 命令
|
|
67
|
+
|
|
68
|
+
| 命令 | 说明 |
|
|
69
|
+
|------|------|
|
|
70
|
+
| `teamai init [--scope <user\|project>] [--role <id>] [--force]` | 初始化(自动安装 gf CLI、OAuth 登录、关联仓库、注册成员、配置 reviewers、注入 hooks) |
|
|
71
|
+
| `teamai push [--all] [--role <id>]` | 推送本地新资源到独立分支并创建 Merge Request;新 skill 交互式选择目标命名空间,可用 `--role` 覆盖 |
|
|
72
|
+
| `teamai pull [--silent]` | 拉取团队资源并注入到本地 AI 工具(支持双 scope 依次拉取) |
|
|
73
|
+
| `teamai status` | 查看本地 vs 团队仓库差异 |
|
|
74
|
+
| `teamai list [type] [--source repo\|local\|all] [--agent <id>]` | 列出资源(skills\|rules\|docs\|env\|wiki);`--source local` 或 `all` 时会扫描已安装 AI agent 下的 skills 目录,并标注每个 skill 的来源 (`[team]` / `[builtin]` / `[source:<name>]` / `[local-only]`) |
|
|
75
|
+
| `teamai skill [list\|show <name>]` | 默认列出全部 skill;`show <name>` 输出指定 skill 的来源、贡献者、已安装的 agent 列表与描述摘要 |
|
|
76
|
+
| `teamai members` | 列出已注册的团队成员 |
|
|
77
|
+
| `teamai remove <type> <name>` | 从团队仓库和本地删除资源并创建 MR(skills\|rules\|wiki) |
|
|
78
|
+
| `teamai roles` | 管理团队角色(`init`/`list`/`set`/`add`/`remove`/`update`) |
|
|
79
|
+
| `teamai source` | 管理跨团队 skill 订阅源(`add`/`remove`/`list`/`browse`) |
|
|
80
|
+
| `teamai contribute --file <path> [--scope <user\|project>]` | 将 AI 生成的经验文档推送到团队仓库 |
|
|
81
|
+
| `teamai recall <query>` | 搜索团队知识库,自动合并 user + project 双 scope 结果 |
|
|
82
|
+
| `teamai digest` | 生成团队 AI 使用周报(skill 排行、新增/更新 skill、session 摘要) |
|
|
83
|
+
| `teamai hooks` | 管理 AI 工具 hooks(list / inject / remove) |
|
|
84
|
+
| `teamai uninstall [--force]` | 卸载 teamai:移除 hooks、rules、skills、env、docs、~/.teamai/ |
|
|
85
|
+
| `teamai doctor` | 诊断配置问题 |
|
|
86
|
+
|
|
87
|
+
全局选项:
|
|
88
|
+
- `--dry-run` — 预览模式,不做实际变更
|
|
89
|
+
- `--verbose, -v` — 详细输出
|
|
90
|
+
|
|
91
|
+
## 工作原理
|
|
92
|
+
|
|
93
|
+
```
|
|
94
|
+
成员 A 成员 B
|
|
95
|
+
创建 skill / 写规则 同上
|
|
96
|
+
│ │
|
|
97
|
+
▼ ▼
|
|
98
|
+
teamai push teamai push
|
|
99
|
+
│ │
|
|
100
|
+
▼ ▼
|
|
101
|
+
创建分支 + MR 创建分支 + MR
|
|
102
|
+
│ │
|
|
103
|
+
└──────► 团队git仓库 ◄──────────────┘
|
|
104
|
+
│ ▲
|
|
105
|
+
│ │ reviewer 审批合并 MR
|
|
106
|
+
▼
|
|
107
|
+
SessionStart hook → teamai pull
|
|
108
|
+
自动拉取到所有成员本地
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
- `teamai push` 会创建独立分支(`teamai/push/<user>/<timestamp>`),推送后自动创建 Merge Request 并指派 reviewers
|
|
112
|
+
- `teamai init` 初始化时可配置默认 reviewers(记录在 `teamai.yaml` 的 `reviewers` 字段)
|
|
113
|
+
- `teamai init` 会自动注入与各工具格式对齐的 hooks(含 `SessionStart`、`Stop`、`PostToolUse`、`UserPromptSubmit` 等),会话中会执行 `teamai pull`、`teamai update`、追踪与仪表盘等(支持 Claude Code、Codex、Claude Code Internal、Codex Internal、Cursor、CodeBuddy IDE、OpenClaw、WorkBuddy)
|
|
114
|
+
- Skills 同步到 `~/.claude/skills/`、`~/.codex/skills/`、`~/.codex-internal/skills/`、`~/.claude-internal/skills/`、`~/.cursor/skills/`、`~/.codebuddy/skills/`
|
|
115
|
+
- Rules 同步到各工具的 rules 目录,并通过标记注释合并到 `CLAUDE.md`(支持 claude、claude-internal、codebuddy)
|
|
116
|
+
- Knowledge 同步到 `~/.teamai/docs/`
|
|
117
|
+
- Learnings 同步到 `~/.teamai/learnings/`,并基于该目录构建 recall 索引(全团队共享,不按角色拆分)
|
|
118
|
+
- Culture 同步团队文化文件(`culture.md`),编译 frontmatter 和 body 后注入到各 AI 工具的 `CLAUDE.md`
|
|
119
|
+
|
|
120
|
+
## 角色化 Skills
|
|
121
|
+
|
|
122
|
+
当团队资源仓库启用角色化目录后,Skills 按角色 namespace 组织,CLI 在 `teamai init` 时要求选择 `primaryRole` 和可选的 `additionalRoles`,并写入本地 `config.yaml`。
|
|
123
|
+
|
|
124
|
+
远端仓库目录约定:
|
|
125
|
+
|
|
126
|
+
```text
|
|
127
|
+
manifest/roles.yaml # 角色定义
|
|
128
|
+
skills/<namespace>/<skill>/ # 按 namespace 组织的 skills
|
|
129
|
+
rules/ # 全局,不做角色拆分
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
- `teamai pull` 读取 `manifest/roles.yaml`,只同步 `primaryRole + additionalRoles` 对应 namespace 中的 skills(同时保留 tag 过滤的并集)。
|
|
133
|
+
- Skills 从 `skills/<namespace>/<skill-name>/` 拍平安装到本地 `<tool>/skills/<skill-name>/`,用户无感知 namespace 结构。
|
|
134
|
+
- 如果激活 namespace 中出现同名 skill,`pull` 会直接失败,避免隐式覆盖。
|
|
135
|
+
- 不在激活 namespace 中、也不在 tag 过滤结果中的 skills 会被自动清理。
|
|
136
|
+
- `rules/`、`docs/`、`learnings/` 仍然保持原有逻辑,不做角色拆分(learnings 全团队共享)。
|
|
137
|
+
|
|
138
|
+
配置示例:
|
|
139
|
+
|
|
140
|
+
```yaml
|
|
141
|
+
primaryRole: hai
|
|
142
|
+
additionalRoles:
|
|
143
|
+
- pm
|
|
144
|
+
resourceProfileVersion: 1
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
这会同步 `skills/common/`、`skills/hai/`、`skills/pm/` 三个 namespace 中的所有 skills。
|
|
148
|
+
|
|
149
|
+
## 角色化推送
|
|
150
|
+
|
|
151
|
+
角色化仓库下,推送新 skill 时 CLI 会自动检测可用的命名空间并提供交互式选择:
|
|
152
|
+
|
|
153
|
+
```bash
|
|
154
|
+
# 交互式选择命名空间(推荐)
|
|
155
|
+
teamai push
|
|
156
|
+
# 输出:
|
|
157
|
+
# Which namespace should new skills be pushed to?
|
|
158
|
+
# 1. common
|
|
159
|
+
# 2. hai
|
|
160
|
+
# 3. pm
|
|
161
|
+
# Choose namespace [1-3] (default: 1 = common):
|
|
162
|
+
|
|
163
|
+
# 显式指定目标 namespace
|
|
164
|
+
teamai push --role pm
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
- 有 `primaryRole` 时,从 `manifest/roles.yaml` 展开可用 namespace 列表
|
|
168
|
+
- 无 `primaryRole` 时,自动扫描团队仓库目录结构中的 namespace
|
|
169
|
+
- 单一命名空间时自动选中,无需交互
|
|
170
|
+
- `--role <id>` 可临时覆盖目标 namespace
|
|
171
|
+
- 修改已有 skill 时自动保持原 namespace,无需重新选择
|
|
172
|
+
|
|
173
|
+
推送时 CLI 会自动检查 `SKILL.md` 的 YAML frontmatter(`name`/`description`),缺失则自动补全,无需手动维护。
|
|
174
|
+
|
|
175
|
+
## 团队文化(Culture)
|
|
176
|
+
|
|
177
|
+
在团队仓库根目录创建 `culture.md`,用 YAML frontmatter 定义公司和团队信息,body 部分写团队文化指引:
|
|
178
|
+
|
|
179
|
+
```markdown
|
|
180
|
+
---
|
|
181
|
+
company:
|
|
182
|
+
name: Acme Corp
|
|
183
|
+
mission: Build great things
|
|
184
|
+
values:
|
|
185
|
+
- Innovation
|
|
186
|
+
- Integrity
|
|
187
|
+
team:
|
|
188
|
+
name: Platform
|
|
189
|
+
mission: Enable developers
|
|
190
|
+
goals:
|
|
191
|
+
- Ship v2.0
|
|
192
|
+
- Improve test coverage
|
|
193
|
+
---
|
|
194
|
+
|
|
195
|
+
## 编码准则
|
|
196
|
+
|
|
197
|
+
- 所有 PR 必须有至少一个 reviewer 审批
|
|
198
|
+
- 禁止直接 push master
|
|
199
|
+
- 测试覆盖率不低于 80%
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
`teamai pull` 时会自动将 culture.md 编译为结构化内容,注入到各 AI 工具的 `CLAUDE.md` 中(`<!-- [teamai:culture:start] -->` / `<!-- [teamai:culture:end] -->` 标记之间)。AI 编码助手在每次会话中都能感知团队文化。
|
|
203
|
+
|
|
204
|
+
## 跨团队 Skill 订阅
|
|
205
|
+
|
|
206
|
+
通过 `teamai source` 订阅其他团队的公共 skill 仓库,pull 时自动同步订阅源的 skills:
|
|
207
|
+
|
|
208
|
+
```bash
|
|
209
|
+
# 添加订阅源
|
|
210
|
+
teamai source add https://git.woa.com/other-team/teamai-public.git --name other-team
|
|
211
|
+
|
|
212
|
+
# 查看已订阅的源
|
|
213
|
+
teamai source list
|
|
214
|
+
|
|
215
|
+
# 浏览订阅源的 skills
|
|
216
|
+
teamai source browse other-team
|
|
217
|
+
|
|
218
|
+
# 移除订阅(同时清理其 skills)
|
|
219
|
+
teamai source remove other-team
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
订阅源的 skills 在 `teamai pull` 时自动同步到本地,与团队自有 skills 共存。
|
|
223
|
+
|
|
224
|
+
## Scope(作用域)
|
|
225
|
+
|
|
226
|
+
TeamAI 支持两种 scope,可以共存:
|
|
227
|
+
|
|
228
|
+
| 维度 | User Scope(默认) | Project Scope |
|
|
229
|
+
|------|-------------------|---------------|
|
|
230
|
+
| **资源安装位置** | `~/` 下(如 `~/.claude/skills/`) | 项目目录下(如 `<project>/.claude/skills/`) |
|
|
231
|
+
| **配置文件** | `~/.teamai/config.yaml` | `<project>/.teamai/config.yaml` |
|
|
232
|
+
| **适用场景** | 通用团队规范、跨项目技能 | 项目特定的技能和规则 |
|
|
233
|
+
| **初始化** | `teamai init --repo <group>/<repo>` | `cd <project> && teamai init --repo <group>/<repo> --scope project` |
|
|
234
|
+
|
|
235
|
+
**双 scope 协同:**
|
|
236
|
+
- `teamai pull` 会依次拉取 user + project 两个 scope 的资源,互不冲突
|
|
237
|
+
- `teamai contribute --scope user/project` 可显式选择推送到哪个仓库
|
|
238
|
+
- `teamai recall` 自动合并两个 scope 的知识库,统一搜索排序,结果标注来源 `[user]`/`[project]`
|
|
239
|
+
- 远端 `teamai.yaml` 的 `scope` 字段锁定仓库类型,成员 init 时必须匹配
|
|
240
|
+
|
|
241
|
+
## 经验自动分享
|
|
242
|
+
|
|
243
|
+
当一次 AI coding session 结束时,系统会通过 Stop hook 智能评估 session 价值并提示分享:
|
|
244
|
+
|
|
245
|
+
```
|
|
246
|
+
AI coding session (持续工作中...)
|
|
247
|
+
│
|
|
248
|
+
▼ PostToolUse hook 持续追踪工具调用和 skill 使用
|
|
249
|
+
│
|
|
250
|
+
▼ 会话结束(Stop hook 触发)
|
|
251
|
+
│
|
|
252
|
+
├─ 智能评分:工具调用数量 + 工具多样性 + skill 使用 + 错误重试 + session 时长
|
|
253
|
+
│ (从 dashboard events.jsonl 提取,一次性评估,满分 100)
|
|
254
|
+
│
|
|
255
|
+
├─ 分数 < 35 → 不打扰(工具调用少或缺乏多样性,没有总结价值)
|
|
256
|
+
│
|
|
257
|
+
▼ 分数 ≥ 35
|
|
258
|
+
│
|
|
259
|
+
AI 提示:"本次 session 内容丰富,建议运行 /teamai-share-learnings 分享经验"
|
|
260
|
+
│
|
|
261
|
+
▼ 用户同意
|
|
262
|
+
│
|
|
263
|
+
/teamai-share-learnings (AI sub-agent)
|
|
264
|
+
├─ AI 总结本次 session 的经验
|
|
265
|
+
├─ 生成 Markdown 文档
|
|
266
|
+
└─ teamai contribute --file <path> → 直接 push 到团队仓库 learnings/
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
- `/teamai-share-learnings` 是 CLI 内置 skill,随 `teamai pull/init` 自动部署到本地
|
|
270
|
+
- 每个 session 最多提示一次(去重),用户可以忽略
|
|
271
|
+
- 文档直接 push 到 `learnings/` 目录,团队成员下次 pull 时可见
|
|
272
|
+
|
|
273
|
+
## 团队知识回忆
|
|
274
|
+
|
|
275
|
+
`teamai recall` 实现知识飞轮的"读出路径"——AI 可以自动搜索团队积累的经验文档:
|
|
276
|
+
|
|
277
|
+
```
|
|
278
|
+
contribute(写入) → pull(同步+索引) → recall(搜索) → upvote(投票) → 排序优化
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
```bash
|
|
282
|
+
$ teamai recall "fuse 端口"
|
|
283
|
+
[1/2] MR 审查发现 FUSE 端口冲突 Bug ★1 [user]
|
|
284
|
+
Author: jeffyxu | Score: 18.5 | Tags: troubleshooting, fuse, k8s
|
|
285
|
+
|
|
286
|
+
[2/2] FUSE 部署配置最佳实践 [project]
|
|
287
|
+
Author: alice | Score: 12.0 | Tags: fuse, deploy
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
- **双 scope 合并搜索**:自动合并 user 和 project scope 的知识库,结果标注来源
|
|
291
|
+
- Hybrid 中英文搜索(Intl.Segmenter + CJK bigrams)
|
|
292
|
+
- 搜索自动投票,好文档自然浮到顶部
|
|
293
|
+
- 投票按 scope 分别写入各自的 repo,归属正确
|
|
294
|
+
|
|
295
|
+
## 更新
|
|
296
|
+
|
|
297
|
+
```bash
|
|
298
|
+
teamai update # 自动检测并升级到最新版
|
|
299
|
+
npm update -g teamai-cli # 或手动触发 npm 升级
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
`teamai update` 会根据当前安装的包名自动选择 registry:
|
|
303
|
+
|
|
304
|
+
- `teamai-cli` → 公网 npm (`https://registry.npmjs.org`)
|
|
305
|
+
- `@tencent/teamai-cli` → 内网 tnpm (`http://r.tnpm.oa.com`)
|
|
306
|
+
|
|
307
|
+
如需手动覆盖 registry,可以设置环境变量 `TEAMAI_NPM_REGISTRY=<url>`。
|
|
308
|
+
|
|
309
|
+
### 自动更新控制
|
|
310
|
+
|
|
311
|
+
自动更新通过 Stop hook 在会话结束时执行,可在两个层级控制:
|
|
312
|
+
|
|
313
|
+
| 配置层级 | 文件 | 字段 | 可选值 |
|
|
314
|
+
|---------|------|------|-------|
|
|
315
|
+
| 团队默认 | `teamai.yaml` | `autoUpdate` | `true`(默认)/ `false` |
|
|
316
|
+
| 用户覆盖 | `~/.teamai/config.yaml` | `updatePolicy` | `auto` / `prompt` / `skip` |
|
|
317
|
+
|
|
318
|
+
用户级 `updatePolicy` 始终优先于团队级 `autoUpdate`。
|
|
319
|
+
|
|
320
|
+
## 许可证
|
|
321
|
+
|
|
322
|
+
[MIT](LICENSE)
|
|
323
|
+
|
|
324
|
+
## 贡献
|
|
325
|
+
|
|
326
|
+
欢迎 PR!请先阅读 [CONTRIBUTING.md](.github/CONTRIBUTING.md)。
|
package/dist/index.js
CHANGED
|
@@ -508,7 +508,8 @@ var init_fs = __esm({
|
|
|
508
508
|
"__pycache__",
|
|
509
509
|
".pyc",
|
|
510
510
|
".DS_Store",
|
|
511
|
-
"node_modules"
|
|
511
|
+
"node_modules",
|
|
512
|
+
".git"
|
|
512
513
|
]);
|
|
513
514
|
}
|
|
514
515
|
});
|
|
@@ -4306,77 +4307,60 @@ var init_wiki = __esm({
|
|
|
4306
4307
|
return relativePath.replace(/\.md$/, "");
|
|
4307
4308
|
}
|
|
4308
4309
|
/**
|
|
4309
|
-
*
|
|
4310
|
+
* Get the shared wiki directory for the current scope.
|
|
4311
|
+
* - User scope → ~/.teamai/wiki/
|
|
4312
|
+
* - Project scope → <projectRoot>/.teamai/wiki/
|
|
4313
|
+
*/
|
|
4314
|
+
static getSharedWikiDir(localConfig) {
|
|
4315
|
+
const teamaiHome = getTeamaiHome(localConfig.scope, localConfig.projectRoot);
|
|
4316
|
+
return path18.join(teamaiHome, "wiki");
|
|
4317
|
+
}
|
|
4318
|
+
/**
|
|
4319
|
+
* Scan the shared wiki directory for pages that are new or modified
|
|
4310
4320
|
* compared to the team repo.
|
|
4311
4321
|
*/
|
|
4312
4322
|
async scanLocalForPush(teamConfig, localConfig) {
|
|
4313
4323
|
const teamWikiDir = path18.join(localConfig.repo.localPath, "wiki");
|
|
4314
4324
|
const teamPages = /* @__PURE__ */ new Map();
|
|
4315
4325
|
if (await pathExists(teamWikiDir)) {
|
|
4316
|
-
const
|
|
4317
|
-
for (const file of
|
|
4326
|
+
const files2 = await listFilesRecursive(teamWikiDir);
|
|
4327
|
+
for (const file of files2) {
|
|
4318
4328
|
if (_WikiHandler.isWikiPage(file)) {
|
|
4319
4329
|
const name = _WikiHandler.pathToName(file);
|
|
4320
4330
|
teamPages.set(name, path18.join(teamWikiDir, file));
|
|
4321
4331
|
}
|
|
4322
4332
|
}
|
|
4323
4333
|
}
|
|
4334
|
+
const sharedWikiDir = _WikiHandler.getSharedWikiDir(localConfig);
|
|
4335
|
+
if (!await pathExists(sharedWikiDir)) return [];
|
|
4324
4336
|
const tombstones = await this.readTombstones(localConfig);
|
|
4325
|
-
const candidates = /* @__PURE__ */ new Map();
|
|
4326
|
-
for (const [__, toolPath] of Object.entries(teamConfig.toolPaths)) {
|
|
4327
|
-
if (!toolPath.wiki) continue;
|
|
4328
|
-
const wikiDir = path18.join(resolveBaseDir(localConfig), toolPath.wiki);
|
|
4329
|
-
if (!await pathExists(wikiDir)) continue;
|
|
4330
|
-
const files = await listFilesRecursive(wikiDir);
|
|
4331
|
-
for (const file of files) {
|
|
4332
|
-
if (!_WikiHandler.isWikiPage(file)) continue;
|
|
4333
|
-
const name = _WikiHandler.pathToName(file);
|
|
4334
|
-
if (tombstones.has(name)) continue;
|
|
4335
|
-
const localFilePath = path18.join(wikiDir, file);
|
|
4336
|
-
if (teamPages.has(name)) {
|
|
4337
|
-
const teamFilePath = teamPages.get(name);
|
|
4338
|
-
const equal = await fileContentEqual(localFilePath, teamFilePath);
|
|
4339
|
-
if (equal) continue;
|
|
4340
|
-
const mtime = await getFileMtime(localFilePath);
|
|
4341
|
-
const existing = candidates.get(name);
|
|
4342
|
-
if (!existing || mtime > existing.mtime) {
|
|
4343
|
-
candidates.set(name, {
|
|
4344
|
-
sourcePath: localFilePath,
|
|
4345
|
-
mtime,
|
|
4346
|
-
status: "modified"
|
|
4347
|
-
});
|
|
4348
|
-
}
|
|
4349
|
-
} else {
|
|
4350
|
-
const existing = candidates.get(name);
|
|
4351
|
-
if (!existing) {
|
|
4352
|
-
const mtime = await getFileMtime(localFilePath);
|
|
4353
|
-
candidates.set(name, {
|
|
4354
|
-
sourcePath: localFilePath,
|
|
4355
|
-
mtime,
|
|
4356
|
-
status: "new"
|
|
4357
|
-
});
|
|
4358
|
-
} else if (existing.status === "new") {
|
|
4359
|
-
const mtime = await getFileMtime(localFilePath);
|
|
4360
|
-
if (mtime > existing.mtime) {
|
|
4361
|
-
candidates.set(name, {
|
|
4362
|
-
sourcePath: localFilePath,
|
|
4363
|
-
mtime,
|
|
4364
|
-
status: "new"
|
|
4365
|
-
});
|
|
4366
|
-
}
|
|
4367
|
-
}
|
|
4368
|
-
}
|
|
4369
|
-
}
|
|
4370
|
-
}
|
|
4371
4337
|
const items = [];
|
|
4372
|
-
|
|
4373
|
-
|
|
4374
|
-
|
|
4375
|
-
|
|
4376
|
-
|
|
4377
|
-
|
|
4378
|
-
|
|
4379
|
-
|
|
4338
|
+
const files = await listFilesRecursive(sharedWikiDir);
|
|
4339
|
+
for (const file of files) {
|
|
4340
|
+
if (!_WikiHandler.isWikiPage(file)) continue;
|
|
4341
|
+
const name = _WikiHandler.pathToName(file);
|
|
4342
|
+
if (tombstones.has(name)) continue;
|
|
4343
|
+
const localFilePath = path18.join(sharedWikiDir, file);
|
|
4344
|
+
if (teamPages.has(name)) {
|
|
4345
|
+
const teamFilePath = teamPages.get(name);
|
|
4346
|
+
const equal = await fileContentEqual(localFilePath, teamFilePath);
|
|
4347
|
+
if (equal) continue;
|
|
4348
|
+
items.push({
|
|
4349
|
+
name,
|
|
4350
|
+
type: "wiki",
|
|
4351
|
+
sourcePath: localFilePath,
|
|
4352
|
+
relativePath: `wiki/${name}.md`,
|
|
4353
|
+
status: "modified"
|
|
4354
|
+
});
|
|
4355
|
+
} else {
|
|
4356
|
+
items.push({
|
|
4357
|
+
name,
|
|
4358
|
+
type: "wiki",
|
|
4359
|
+
sourcePath: localFilePath,
|
|
4360
|
+
relativePath: `wiki/${name}.md`,
|
|
4361
|
+
status: "new"
|
|
4362
|
+
});
|
|
4363
|
+
}
|
|
4380
4364
|
}
|
|
4381
4365
|
return items;
|
|
4382
4366
|
}
|
|
@@ -4404,57 +4388,47 @@ var init_wiki = __esm({
|
|
|
4404
4388
|
log.debug(`Copied wiki page ${item.name} \u2192 team repo`);
|
|
4405
4389
|
}
|
|
4406
4390
|
/**
|
|
4407
|
-
* Pull a wiki page from team repo to
|
|
4391
|
+
* Pull a wiki page from team repo to the shared wiki directory.
|
|
4408
4392
|
*/
|
|
4409
|
-
async pullItem(item,
|
|
4410
|
-
const
|
|
4411
|
-
|
|
4412
|
-
|
|
4413
|
-
|
|
4414
|
-
|
|
4415
|
-
|
|
4416
|
-
|
|
4417
|
-
|
|
4418
|
-
|
|
4419
|
-
|
|
4420
|
-
await copyFile(item.sourcePath, dest);
|
|
4421
|
-
log.debug(`Synced wiki page ${item.name} \u2192 ${tool}`);
|
|
4422
|
-
} catch (e) {
|
|
4423
|
-
log.warn(
|
|
4424
|
-
`Failed to sync wiki page ${item.name} to ${tool}: ${e.message}`
|
|
4425
|
-
);
|
|
4426
|
-
}
|
|
4393
|
+
async pullItem(item, _teamConfig, localConfig) {
|
|
4394
|
+
const sharedWikiDir = _WikiHandler.getSharedWikiDir(localConfig);
|
|
4395
|
+
const dest = path18.join(sharedWikiDir, `${item.name}.md`);
|
|
4396
|
+
await ensureDir(path18.dirname(dest));
|
|
4397
|
+
try {
|
|
4398
|
+
await copyFile(item.sourcePath, dest);
|
|
4399
|
+
log.debug(`Synced wiki page ${item.name} \u2192 shared wiki`);
|
|
4400
|
+
} catch (e) {
|
|
4401
|
+
log.warn(
|
|
4402
|
+
`Failed to sync wiki page ${item.name}: ${e.message}`
|
|
4403
|
+
);
|
|
4427
4404
|
}
|
|
4428
4405
|
}
|
|
4429
4406
|
/**
|
|
4430
|
-
* Remove a wiki page from the team repo and
|
|
4407
|
+
* Remove a wiki page from the team repo and the shared wiki directory.
|
|
4431
4408
|
*/
|
|
4432
|
-
async removeItem(name,
|
|
4409
|
+
async removeItem(name, _teamConfig, localConfig) {
|
|
4433
4410
|
const removed = [];
|
|
4434
|
-
const
|
|
4435
|
-
const fileName = `${name}.md`;
|
|
4436
|
-
const teamFile = path18.join(localConfig.repo.localPath, "wiki", fileName);
|
|
4411
|
+
const teamFile = path18.join(localConfig.repo.localPath, "wiki", `${name}.md`);
|
|
4437
4412
|
if (await pathExists(teamFile)) {
|
|
4438
4413
|
await remove(teamFile);
|
|
4439
4414
|
removed.push(teamFile);
|
|
4440
4415
|
}
|
|
4441
4416
|
await this.addTombstone(name, localConfig);
|
|
4442
|
-
|
|
4443
|
-
|
|
4444
|
-
|
|
4445
|
-
|
|
4446
|
-
|
|
4447
|
-
|
|
4448
|
-
log.debug(`Removed wiki page ${name} from ${tool}`);
|
|
4449
|
-
}
|
|
4417
|
+
const sharedWikiDir = _WikiHandler.getSharedWikiDir(localConfig);
|
|
4418
|
+
const sharedFile = path18.join(sharedWikiDir, `${name}.md`);
|
|
4419
|
+
if (await pathExists(sharedFile)) {
|
|
4420
|
+
await remove(sharedFile);
|
|
4421
|
+
removed.push(sharedFile);
|
|
4422
|
+
log.debug(`Removed wiki page ${name} from shared wiki`);
|
|
4450
4423
|
}
|
|
4451
4424
|
return removed;
|
|
4452
4425
|
}
|
|
4453
4426
|
/**
|
|
4454
|
-
* Rebuild _metadata.json from wiki pages
|
|
4427
|
+
* Rebuild _metadata.json from wiki pages in the shared wiki directory.
|
|
4455
4428
|
* Called after pull to reconstruct local metadata.
|
|
4456
4429
|
*/
|
|
4457
|
-
static async rebuildMetadata(
|
|
4430
|
+
static async rebuildMetadata(localConfig) {
|
|
4431
|
+
const wikiDir = _WikiHandler.getSharedWikiDir(localConfig);
|
|
4458
4432
|
if (!await pathExists(wikiDir)) return;
|
|
4459
4433
|
const files = await listFilesRecursive(wikiDir);
|
|
4460
4434
|
const pages = {};
|
|
@@ -6144,8 +6118,7 @@ async function pullForScope(localConfig, options) {
|
|
|
6144
6118
|
if (!options.dryRun) {
|
|
6145
6119
|
const tombstoneTypes = [
|
|
6146
6120
|
{ type: "rules", ext: ".md", toolPathField: "rules" },
|
|
6147
|
-
{ type: "skills", toolPathField: "skills" }
|
|
6148
|
-
{ type: "wiki", ext: ".md", toolPathField: "wiki" }
|
|
6121
|
+
{ type: "skills", toolPathField: "skills" }
|
|
6149
6122
|
];
|
|
6150
6123
|
const baseDir = resolveBaseDir(localConfig);
|
|
6151
6124
|
for (const { type, ext, toolPathField } of tombstoneTypes) {
|
|
@@ -6165,6 +6138,23 @@ async function pullForScope(localConfig, options) {
|
|
|
6165
6138
|
}
|
|
6166
6139
|
}
|
|
6167
6140
|
}
|
|
6141
|
+
try {
|
|
6142
|
+
const wikiHandler = getHandler("wiki");
|
|
6143
|
+
const wikiTombstones = await wikiHandler.readTombstones(localConfig);
|
|
6144
|
+
if (wikiTombstones.size > 0) {
|
|
6145
|
+
const teamaiHome = getTeamaiHome(localConfig.scope, localConfig.projectRoot);
|
|
6146
|
+
const wikiDir = path26.join(teamaiHome, "wiki");
|
|
6147
|
+
for (const name of wikiTombstones) {
|
|
6148
|
+
const wikiPath = path26.join(wikiDir, `${name}.md`);
|
|
6149
|
+
if (await pathExists(wikiPath)) {
|
|
6150
|
+
await remove(wikiPath);
|
|
6151
|
+
log.debug(`[${scopeLabel}] Cleaned up tombstoned wiki ${name} from shared wiki`);
|
|
6152
|
+
}
|
|
6153
|
+
}
|
|
6154
|
+
}
|
|
6155
|
+
} catch (e) {
|
|
6156
|
+
log.debug(`[${scopeLabel}] Wiki tombstone cleanup skipped: ${e.message}`);
|
|
6157
|
+
}
|
|
6168
6158
|
if (roleContext) {
|
|
6169
6159
|
await cleanupInactiveNamespaceSkills(
|
|
6170
6160
|
freshConfig,
|