@rowansenne/workflow-run 0.17.0 → 0.19.0

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/CHANGELOG.md CHANGED
@@ -4,6 +4,17 @@
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.19.0] - 2026-10-18
8
+
9
+ ### 变更(破坏性)
10
+ - **去掉「同时安装到 Codex 用户级 `~/.codex/skills`」的询问**:只装到项目里。之前选过的,`~/.codex/skills` 下本工具的 skill 需要自己删除。
11
+ - 目录里没有安装清单时,和新版不同的文件仍然备份后覆盖,但不再逐个报「被手动修改过」,改为一行汇总(哪个目录、备份了几个文件)。
12
+
13
+ ### 新增
14
+ - **支持腾讯 WorkBuddy**(`--envs workbuddy`,交互菜单第 5 项):skills 按 skill 软链接到 `.codebuddy/skills/`(WorkBuddy 读项目的 `.codebuddy/`,与 CodeBuddy Code 一致;`--copy` 时独立拷贝),DocNexus 的 skills 也链接过去,段落写在 `AGENTS.md`。`doctor`、`uninstall`、数据库配置查找、画像查找都认这个目录。
15
+
16
+ ## [0.18.0] - 2026-10-18
17
+
7
18
  ### 变更(破坏性,不兼容旧版本)
8
19
  - **包改名为 `@rowansenne/workflow-run`**,命令改为 `workflow-run`(`npx @rowansenne/workflow-run`);项目里的目录 `skills-init/` 改为 `workflow-run/`,安装清单 `.skills-init.json` 改为 `.workflow-run.json`,`CLAUDE.md` / `AGENTS.md` 段落标记改为 `workflow-run:docnexus`,环境变量 `SKILLS_INIT_ADMIN` / `SKILLS_INIT_NO_SYMLINK` 改为 `WORKFLOW_RUN_ADMIN` / `WORKFLOW_RUN_NO_SYMLINK`,导入 DocNexus 的模板标签改为 `workflow-run-template`。
9
20
  - 不自动迁移:项目里还有 `skills-init/` 或 `.skills-init.json` 时,任何命令都会列出改名命令后停下;旧的 `CLAUDE.md` / `AGENTS.md` 段落和含 `skills-init/` 的忽略规则只提示删除。
@@ -240,7 +251,9 @@
240
251
  - 安装器改为单一 Node 实现,`init-skills.sh` / `init-skills.ps1` 只是薄封装。
241
252
  - `code-review` 改名为 `project-review`,升级时自动清理旧名。
242
253
 
243
- [Unreleased]: https://github.com/guilianerde/skills-init/compare/v0.17.0...HEAD
254
+ [Unreleased]: https://github.com/guilianerde/skills-init/compare/v0.19.0...HEAD
255
+ [0.19.0]: https://github.com/guilianerde/skills-init/compare/v0.18.0...v0.19.0
256
+ [0.18.0]: https://github.com/guilianerde/skills-init/compare/v0.17.0...v0.18.0
244
257
  [0.17.0]: https://github.com/guilianerde/skills-init/compare/v0.16.0...v0.17.0
245
258
  [0.16.0]: https://github.com/guilianerde/skills-init/compare/v0.15.0...v0.16.0
246
259
  [0.15.0]: https://github.com/guilianerde/skills-init/compare/v0.14.0...v0.15.0
package/README.md CHANGED
@@ -8,7 +8,7 @@
8
8
 
9
9
  技能目录,团队共享(可提交 git)。
10
10
 
11
- 支持环境:**Codex CLI / Claude Code / 豆包办公(Doubao)/ 千问(Qwen Code)**。
11
+ 支持环境:**Codex CLI / Claude Code / 豆包办公(Doubao)/ 千问(Qwen Code)/ 腾讯 WorkBuddy**。
12
12
 
13
13
  > 本工具在项目里的产出(需求产物、DocNexus 工作区、项目模板、临时文件)都放在项目根的
14
14
  > `workflow-run/`
@@ -64,18 +64,15 @@ npx @rowansenne/workflow-run docs-repo git@git.example.com:team/team-docs.git
64
64
  | claude-code | `.claude/skills/<skill>` → 软链接到 `.agents/skills/<skill>` | Claude Code 自动发现,无需额外配置 |
65
65
  | doubao | `.agents/skills/`(与 codex 共用) | 豆包办公自动发现(新会话生效) |
66
66
  | qwen | `.qwen/skills/<skill>` → 软链接到 `.agents/skills/<skill>` | Qwen Code 自动发现;`/skills` 面板可查看与切换 |
67
+ | workbuddy | `.codebuddy/skills/<skill>` → 软链接到 `.agents/skills/<skill>` | WorkBuddy 打开项目目录后读取(项目级优先于用户级 `~/.workbuddy/skills/`) |
67
68
 
68
- **单一来源 + 软链接**:skills 只在 `.agents/skills/` 装一份完整文件,`.claude/skills`、`.qwen/skills` 下按 skill 建相对软链接(`../../.agents/skills/<skill>`),改模板、填配置都只需改一处。因此即使只选了 claude 或 qwen,`.agents/skills/` 也会生成。
69
+ **单一来源 + 软链接**:skills 只在 `.agents/skills/` 装一份完整文件,`.claude/skills`、`.qwen/skills`、`.codebuddy/skills` 下按 skill 建相对软链接(`../../.agents/skills/<skill>`),改模板、填配置都只需改一处。因此即使只选了 claude、qwen 或 workbuddy,`.agents/skills/` 也会生成。
69
70
 
70
71
  * 按 skill 建链接而不是链接整个目录,这样 `.claude/skills/` 里还能放其他 skill(例如 DocNexus 的链接)。
71
72
  * 之前用 `--copy`(或建不了链接时)装的独立拷贝,再不带 `--copy` 安装时会换成链接:拷贝里填过的用户配置(`config.json`、`schema.md`、`wiki-config.json`、`profile.json`)先并入 `.agents/skills/`,原目录移到 `workflow-run/tmp/backup/<时间>/<环境>/`;两份都填过且内容不同时不会自动合并,会保留那个目录并提示手动处理。
72
73
  * Windows:安装与升级(`init` / `upgrade`)**必须在管理员终端执行**(建目录软链接需要该权限);在普通终端运行会直接停下、不做任何改动,并提示改用「以管理员身份运行」的终端。`doctor`、`seed-templates`、`uninstall` 不要求管理员。少数文件系统仍建不了软链接时,该环境自动退回独立拷贝。在 Windows 上 clone 含软链接的仓库需要 `git config core.symlinks true`。
73
74
  * 不想用软链接:加 `--copy`,每个环境各装一份完整文件。
74
75
 
75
- > Codex 也可选择额外安装到用户级
76
- > `~/.codex/skills/`
77
- > (所有项目可用),脚本会询问。
78
-
79
76
  每个 skill 目录结构(四环境一致):
80
77
 
81
78
 
@@ -181,7 +178,7 @@ npx @rowansenne/workflow-run docs-repo git@git.example.com:team/team-docs.git
181
178
 
182
179
  | 参数 | 说明 |
183
180
  | ----------------------- | --------------------------------------------------------------------- |
184
- | `--envs` | 环境列表,逗号分隔:`codex,claude,doubao,qwen`;`all` = 全部 |
181
+ | `--envs` | 环境列表,逗号分隔:`codex,claude,doubao,qwen,workbuddy`;`all` = 全部 |
185
182
  | `--skip-db` | 跳过数据库连接配置(database-query 不生成 config.json) |
186
183
  | `--with-docnexus` | 可选:接入 [DocNexus](https://www.npmjs.com/package/@rowansenne/docnexus) 项目文档库(需 Node >= 22.13),见下方「DocNexus 文档库(可选)」 |
187
184
  | `--with-codegraph` | 可选:接入 [CodeGraph](https://github.com/colbymchenry/codegraph) 代码图谱,见下方「CodeGraph 代码图谱(可选)」 |
@@ -210,12 +207,13 @@ npx @rowansenne/workflow-run docs-repo git@git.example.com:team/team-docs.git
210
207
  | --- | --- | --- |
211
208
  | claude | `.claude/skills/` | `CLAUDE.md` |
212
209
  | codex / doubao | `.agents/skills/`(两者共用) | `AGENTS.md` |
210
+ | workbuddy | `.codebuddy/skills/` | `AGENTS.md`(WorkBuddy 读项目的 `AGENTS.md`,与 `CODEBUDDY.md` 并存) |
213
211
  | qwen | `.qwen/skills/` | 不写 `QWEN.md`;需要自动召回时可在 `QWEN.md` 引用 `AGENTS.md` 的 DocNexus 段落,或在 `.qwen/settings.json` 的 `context.fileName` 加入 `AGENTS.md` |
214
212
 
215
213
  说明:
216
214
 
217
215
  * DocNexus 的 skills 不在本仓库中,始终以安装的 npm 包为准;重跑脚本会按包版本重新同步与链接(`docnexus init` 可重复执行,不影响已有记忆)。
218
- * `.qwen/skills/` 下已存在的同名**真实目录**不会被替换,脚本会提示跳过。
216
+ * `.qwen/skills/`、`.codebuddy/skills/` 下已存在的同名**真实目录**不会被替换,脚本会提示跳过。
219
217
  * npm 安装或 `docnexus init` 失败时只跳过 DocNexus,本仓库的 skills 照常安装,脚本最后以退出码 1 结束;修好后重跑即可。
220
218
  * 安装后 `dev`、`project-review` 会先按 `docnexus-recall` 查项目约定;`dev`、`project-review` 交付时**每次都问**要不要沉淀(写入后把库内路径记回交付说明 / 审查报告的「DocNexus」元信息行),`create-tech-md-doc`、`create-release-md-doc` 结束时有值得记的内容才**提议**沉淀;**任何写入 DocNexus 的操作都必须经你授权**。
221
219
  * 离线或测试时可用环境变量指定安装来源,例如 `DOCNEXUS_PKG=/path/to/rowansenne-docnexus-0.13.0.tgz npx @rowansenne/workflow-run --with-docnexus`(PowerShell:`$env:DOCNEXUS_PKG=...`)。
@@ -224,7 +222,7 @@ npx @rowansenne/workflow-run docs-repo git@git.example.com:team/team-docs.git
224
222
  ### DocNexus 的位置
225
223
 
226
224
  * DocNexus 装在 `workflow-run/docnexus/`,以 `workflow-run/` 为项目根运行(`docnexus init --agent none`:它自己不往 `workflow-run/` 里链接 skills、写 `CLAUDE.md`)。
227
- * 本项目里由 workflow-run 接上:把 `workflow-run/docnexus/skills/*` 相对链接到 `.claude/skills` / `.agents/skills` / `.qwen/skills`,在 `CLAUDE.md` / `AGENTS.md` 写一段说明(概念索引 `@workflow-run/docnexus/CONCEPTS.md`;docnexus 系列 skill 的命令改用 `node <skills>/workflow/scripts/docnexus.mjs` 运行,不用先 `cd`),用自己的标记,不碰手写内容。升级时刷新,卸载时撤掉,`workflow-run/docnexus/` 本身不动。
225
+ * 本项目里由 workflow-run 接上:把 `workflow-run/docnexus/skills/*` 相对链接到 `.claude/skills` / `.agents/skills` / `.qwen/skills` / `.codebuddy/skills`,在 `CLAUDE.md` / `AGENTS.md` 写一段说明(概念索引 `@workflow-run/docnexus/CONCEPTS.md`;docnexus 系列 skill 的命令改用 `node <skills>/workflow/scripts/docnexus.mjs` 运行,不用先 `cd`),用自己的标记,不碰手写内容。升级时刷新,卸载时撤掉,`workflow-run/docnexus/` 本身不动。
228
226
  * 各 skill 的脚本已按位置处理:`resolve.mjs status` 给出 `docnexus.root` 与 `run`(`node <skills>/workflow/scripts/docnexus.mjs`),`knowledge.mjs` 在那里运行并给出从项目根读正文的路径。
229
227
  * `workflow-run/` 是独立文档仓库时,知识库随文档仓库提交,各服务共用;沉淀后的改动和产物一样由你确认后提交(`requirement.mjs pending` 单独列出)。DocNexus 的依赖和检索库(`node_modules/`、`store/`)不进仓库,各自 `--with-docnexus` 装一次。
230
228
 
@@ -267,7 +265,7 @@ dev、project-review、技术方案、上线方案都在对应环节调用它:
267
265
  2. 所选环境里有 claude / codex 时,执行 `codegraph install --target=<claude,codex> --location=local --yes`,把 MCP 配置写进本项目:
268
266
  - claude:`.mcp.json`、`.claude/settings.json`(自动放行 `codegraph_explore`)、`.claude/CLAUDE.md`;
269
267
  - codex:`.codex/config.toml`、`AGENTS.md`;Codex 只在受信任的项目里加载项目级配置。
270
- - doubao、qwen 不在 CodeGraph 支持的 agent 里,在这两个环境用 code-graph skill 和 `codegraph` CLI。
268
+ - doubao、qwen、workbuddy 不在 CodeGraph 支持的 agent 里,在这些环境用 code-graph skill 和 `codegraph` CLI。
271
269
  3. 执行 `codegraph init <项目根> --yes` 建索引,生成 `.codegraph/`(本地 SQLite),并把它加入 `.gitignore`。之后 agent 使用期间自动增量同步;`cg.mjs` 发现索引落后于工作区时也会先 `codegraph sync`。
272
270
 
273
271
  * 能力探测:`resolve.mjs status` 的 `codegraph`(`ok` = 有 `.codegraph/` 且 `codegraph` 命令可用)。`/workflow status` 和 `doctor` 都会显示。
@@ -312,7 +310,8 @@ mv .agents/skills/.skills-init.json .agents/skills/.workflow-run.json
312
310
  | 情况 | 处理 |
313
311
  | --- | --- |
314
312
  | 当前内容与清单记录一致(没改过) | 直接更新 |
315
- | 与清单记录不同(手改过),或没有清单无从判断 | 原文件先备份到 `workflow-run/tmp/upgrade-backup/<时间>/<相对项目根的路径>`,再更新为新版;结尾列出这些文件和备份目录 |
313
+ | 与清单记录不同(手改过) | 原文件先备份到 `workflow-run/tmp/upgrade-backup/<时间>/<相对项目根的路径>`,再更新为新版;结尾列出这些文件和备份目录 |
314
+ | 目录里没有清单,无从判断 | 和新版不同的文件同样备份后更新;结尾只用一行说明哪个目录没有清单、备份了几个文件,不当作手改过 |
316
315
  | 清单里有、新版里已没有 | 没改过直接删除;改过的备份后删除 |
317
316
 
318
317
  * 这样 `SKILL.md`、`skill.json`、脚本始终是同一个版本,不会出现「新 SKILL.md 配旧 skill.json」的混搭。
@@ -325,12 +324,11 @@ mv .agents/skills/.skills-init.json .agents/skills/.workflow-run.json
325
324
  npx @rowansenne/workflow-run uninstall
326
325
  ```
327
326
 
328
- 按清单移除本项目安装的 skill 文件、`.claude/skills` 与 `.qwen/skills` 下指向 `.agents/skills` 的软链接,以及清单本身:
327
+ 按清单移除本项目安装的 skill 文件、`.claude/skills`、`.qwen/skills`、`.codebuddy/skills` 下指向 `.agents/skills` 的软链接,以及清单本身:
329
328
 
330
329
  * 用户配置(`config.json`、`schema.md`、`wiki-config.json`、`profile.json`)和手改过的模板文件先移到 `workflow-run/tmp/uninstall-backup/<时间戳>/<安装目录>/<skill>/`,结尾列出;
331
330
  * 不在清单里的文件、你自己的 skill 和链接都不动;skill 目录里还有你自己加的文件时保留该目录并提示;目录清空后才删除;
332
331
  * `.gitignore` 里追加的规则和 DocNexus 不处理;没有清单时什么也不做。
333
- * 用户级 `~/.codex/skills` 不在卸载范围内。
334
332
 
335
333
  ### skill.json
336
334
 
@@ -659,4 +657,6 @@ node scripts/eval-triggers.mjs --only dev # 只跑期望为 dev 的用例
659
657
 
660
658
  * **Claude Code** 项目级为 `.claude/skills/`,自动发现。
661
659
 
662
- * **Qwen Code** 项目级为 `.qwen/skills/`,自动发现。
660
+ * **Qwen Code** 项目级为 `.qwen/skills/`,自动发现。
661
+
662
+ * **腾讯 WorkBuddy** 读项目工作目录下的 `.codebuddy/`(与 CodeBuddy Code 一致),项目级技能在 `.codebuddy/skills/`,同名时优先于用户级;它也读 `AGENTS.md`。
@@ -1,6 +1,6 @@
1
1
  // CodeGraph(https://github.com/colbymchenry/codegraph):预建的代码知识图谱(符号、调用边、影响面),100% 本地。
2
2
  // --with-codegraph 时:没有 CLI 就 npm 全局安装 → 给 claude / codex 写项目级 MCP 配置 → 建本项目索引(.codegraph/)。
3
- // doubao / qwen 不在 CodeGraph 支持的 agent 里,skill 在这些环境直接调用 codegraph CLI。
3
+ // doubao / qwen / workbuddy 不在 CodeGraph 支持的 agent 里,skill 在这些环境直接调用 codegraph CLI。
4
4
 
5
5
  import fs from 'node:fs';
6
6
  import path from 'node:path';
@@ -19,7 +19,7 @@ export function codegraphVersion(cwd) {
19
19
 
20
20
  export const codegraphIndexed = (root) => fs.existsSync(path.join(root, '.codegraph'));
21
21
 
22
- // 我们的环境 → codegraph install 的 target id;doubao / qwen 没有对应 target
22
+ // 我们的环境 → codegraph install 的 target id;doubao / qwen / workbuddy 没有对应 target
23
23
  const TARGETS = { claude: 'claude', codex: 'codex' };
24
24
 
25
25
  const run = (cmd, args, cwd) => spawnSync(cmd, args, { cwd, stdio: 'inherit', shell: IS_WIN, env: ENV }).status === 0;
package/bin/lib/core.mjs CHANGED
@@ -11,8 +11,10 @@ export const TPL_DIR = path.resolve(path.dirname(fileURLToPath(import.meta.url))
11
11
  export const PKG = JSON.parse(fs.readFileSync(path.join(TPL_DIR, 'package.json'), 'utf8'));
12
12
  export const IS_WIN = process.platform === 'win32';
13
13
 
14
- export const ENV_NAMES = ['codex', 'claude', 'doubao', 'qwen'];
14
+ export const ENV_NAMES = ['codex', 'claude', 'doubao', 'qwen', 'workbuddy'];
15
15
  export const CANON_DIR = path.join('.agents', 'skills'); // 单一来源目录
16
+ // 建软链接(或 --copy 各装一份)的环境 → 项目级 skills 目录。WorkBuddy 读项目的 .codebuddy/(与 CodeBuddy Code 一致)
17
+ export const LINK_ENVS = { claude: path.join('.claude', 'skills'), qwen: path.join('.qwen', 'skills'), workbuddy: path.join('.codebuddy', 'skills') };
16
18
  // skill 列表、用户配置、文档模板都来自各 skill 目录的 skill.json(见 bin/skills.mjs)
17
19
  export const SKILL_LIST = loadSkills(TPL_DIR);
18
20
  export const META = deriveMeta(SKILL_LIST);
@@ -113,7 +115,7 @@ export function timestamp() {
113
115
  return `${d.getFullYear()}${z(d.getMonth() + 1)}${z(d.getDate())}-${z(d.getHours())}${z(d.getMinutes())}${z(d.getSeconds())}`;
114
116
  }
115
117
 
116
- export const INSTALL_BASES = [CANON_DIR, path.join('.claude', 'skills'), path.join('.qwen', 'skills')];
118
+ export const INSTALL_BASES = [CANON_DIR, ...Object.values(LINK_ENVS)];
117
119
  // 找到本项目的安装清单(单一来源优先;--copy 安装时各目录各有一份)
118
120
  export function findManifests(root) {
119
121
  return INSTALL_BASES.map((rel) => ({ rel, base: path.join(root, rel), m: readManifest(path.join(root, rel)) })).filter((x) => x.m);
package/bin/lib/db.mjs CHANGED
@@ -15,7 +15,7 @@ export function defaultConn(cfg) {
15
15
  }
16
16
  export function findExistingDbConfig(root) {
17
17
  for (const rel of ['.codex/database-query/config.json', '.agents/skills/database-query/config.json',
18
- '.claude/skills/database-query/config.json', '.qwen/skills/database-query/config.json']) {
18
+ '.claude/skills/database-query/config.json', '.qwen/skills/database-query/config.json', '.codebuddy/skills/database-query/config.json']) {
19
19
  const f = path.join(root, rel);
20
20
  const cfg = readJson(f);
21
21
  const d = defaultConn(cfg);
@@ -46,8 +46,11 @@ export function linkDocNexusSkills(root, dirs) {
46
46
  // 用自己的标记,不碰 DocNexus 自己的段落
47
47
  const CTX_START = '<!-- workflow-run:docnexus:start -->';
48
48
  const CTX_END = '<!-- workflow-run:docnexus:end -->';
49
- function contextBlock(file) {
50
- const run = `${file === 'CLAUDE.md' ? '.claude/skills' : '.agents/skills'}/workflow/scripts/docnexus.mjs`;
49
+ // AGENTS.md 由 codex / doubao / workbuddy 共用:命令指向装了 workflow 的那个目录(--copy 只装 workbuddy 时没有 .agents/skills)
50
+ function contextBlock(root, file) {
51
+ const has = (dir) => fs.existsSync(path.join(root, dir, 'workflow', 'scripts', 'docnexus.mjs'));
52
+ const dir = file === 'CLAUDE.md' ? '.claude/skills' : ['.agents/skills', '.codebuddy/skills'].find(has) ?? '.agents/skills';
53
+ const run = `${dir}/workflow/scripts/docnexus.mjs`;
51
54
  const index = file === 'CLAUDE.md' ? `@${HOME}/docnexus/CONCEPTS.md` : `\`${HOME}/docnexus/CONCEPTS.md\`(每个任务开始时读)`;
52
55
  return [
53
56
  CTX_START,
@@ -79,7 +82,7 @@ export function writeDocNexusContext(root, files) {
79
82
  for (const file of files) {
80
83
  const f = path.join(root, file);
81
84
  const cur = fs.existsSync(f) ? fs.readFileSync(f, 'utf8') : '';
82
- const next = insertBlock(cur, CTX_START, CTX_END, contextBlock(file));
85
+ const next = insertBlock(cur, CTX_START, CTX_END, contextBlock(root, file));
83
86
  if (next !== cur) { fs.writeFileSync(f, next); ok(`${file} 已写入 DocNexus 段落(工作区在 ${HOME}/docnexus/)`); }
84
87
  }
85
88
  }
@@ -91,6 +94,8 @@ function agentTargets(sel) {
91
94
  if (sel.includes('claude')) { dirs.add(path.join('.claude', 'skills')); files.add('CLAUDE.md'); }
92
95
  if (sel.includes('codex') || sel.includes('doubao')) { dirs.add(CANON_DIR); files.add('AGENTS.md'); }
93
96
  if (sel.includes('qwen')) dirs.add(path.join('.qwen', 'skills'));
97
+ // WorkBuddy 读项目的 AGENTS.md(与 CODEBUDDY.md 并存)
98
+ if (sel.includes('workbuddy')) { dirs.add(path.join('.codebuddy', 'skills')); files.add('AGENTS.md'); }
94
99
  return { dirs: [...dirs], files: [...files] };
95
100
  }
96
101
 
@@ -3,7 +3,7 @@
3
3
  import fs from 'node:fs';
4
4
  import path from 'node:path';
5
5
  import { spawnSync } from 'node:child_process';
6
- import { PKG, CANON_DIR, HOME, DOCNEXUS_PKG, docnexusUpgradeCmd, SKILLS, step, ok, warn, fail, toPosix, fileHash, readManifest, lstat, findManifests, cmpVersion } from './core.mjs';
6
+ import { PKG, CANON_DIR, INSTALL_BASES, LINK_ENVS, HOME, DOCNEXUS_PKG, docnexusUpgradeCmd, SKILLS, step, ok, warn, fail, toPosix, fileHash, readManifest, lstat, findManifests, cmpVersion } from './core.mjs';
7
7
  import { reportTemplateStatus, docnexusSupportsCategory, docnexusSupportsRecallCategory } from './docnexus.mjs';
8
8
  import { codegraphVersion } from './codegraph.mjs';
9
9
 
@@ -59,19 +59,20 @@ export function doctor(root, offline) {
59
59
  const canon = found.find((x) => x.rel === CANON_DIR);
60
60
  if (canon && m.mode !== 'copy') {
61
61
  step('软链接');
62
- for (const env of ['claude', 'qwen']) {
62
+ for (const [env, rel] of Object.entries(LINK_ENVS)) {
63
63
  if (!(m.envs ?? []).includes(env)) continue;
64
- const dir = path.join(root, `.${env}`, 'skills');
64
+ const dir = path.join(root, rel);
65
+ const d = toPosix(rel);
65
66
  let good = 0;
66
67
  for (const skill of Object.keys(canon.m.skills)) {
67
68
  const link = path.join(dir, skill);
68
69
  const st = lstat(link);
69
- if (!st) bad(`.${env}/skills/${skill} 不存在:运行 workflow-run upgrade 重建`);
70
- else if (!st.isSymbolicLink()) { if (!readManifest(dir)) meh(`.${env}/skills/${skill} 是普通目录而非软链接(可能是建链接失败后的独立拷贝)`); else good++; }
71
- else if (!fs.existsSync(link)) bad(`.${env}/skills/${skill} 是断开的软链接`);
70
+ if (!st) bad(`${d}/${skill} 不存在:运行 workflow-run upgrade 重建`);
71
+ else if (!st.isSymbolicLink()) { if (!readManifest(dir)) meh(`${d}/${skill} 是普通目录而非软链接(可能是建链接失败后的独立拷贝)`); else good++; }
72
+ else if (!fs.existsSync(link)) bad(`${d}/${skill} 是断开的软链接`);
72
73
  else good++;
73
74
  }
74
- if (good === Object.keys(canon.m.skills).length) ok(`.${env}/skills 全部指向 ${toPosix(CANON_DIR)}`);
75
+ if (good === Object.keys(canon.m.skills).length) ok(`${d} 全部指向 ${toPosix(CANON_DIR)}`);
75
76
  }
76
77
  }
77
78
 
@@ -89,7 +90,7 @@ export function doctor(root, offline) {
89
90
  if (st.wiki.ok) ok('wiki 已配置'); else console.log(' -- wiki 未配置(可选;也可能通过 wiki MCP 提供)');
90
91
  if (st.docnexus.ok) {
91
92
  // DocNexus 以 workflow-run/ 为项目根:本项目要有 docnexus skill 的链接和 CLAUDE.md / AGENTS.md 段落,agent 才知道去哪里找
92
- const linked = [CANON_DIR, path.join('.claude', 'skills'), path.join('.qwen', 'skills')].some((b) => lstat(path.join(root, b, 'docnexus-recall'))?.isSymbolicLink());
93
+ const linked = INSTALL_BASES.some((b) => lstat(path.join(root, b, 'docnexus-recall'))?.isSymbolicLink());
93
94
  const ctx = ['CLAUDE.md', 'AGENTS.md'].some((f) => { try { return fs.readFileSync(path.join(root, f), 'utf8').includes('workflow-run:docnexus:start'); } catch { return false; } });
94
95
  if (linked && ctx) ok(`DocNexus 在 ${HOME}/docnexus/,本项目已链接它的 skills 并写入段落`);
95
96
  else meh(`DocNexus 在 ${HOME}/docnexus/,但本项目${linked ? '' : '缺少 docnexus skill 链接'}${!linked && !ctx ? '、' : ''}${ctx ? '' : '缺少 CLAUDE.md / AGENTS.md 里的 DocNexus 段落'}:运行 workflow-run upgrade`);
@@ -7,8 +7,9 @@ import { writeDbConfig } from './db.mjs';
7
7
  import { layout, ensureIgnore, homeIsRepo, IGNORE_LINE, TMP_DIR } from '../../workflow/scripts/requirement.mjs';
8
8
  import { detectEnv } from '../../workflow/scripts/resolve.mjs';
9
9
 
10
- // 结尾汇总:被手改过、已用新版覆盖(原文件已备份)的 / 模板已删除、手改过而备份后删除的文件
11
- export const report = { overwritten: [], removed: [], backupDir: null };
10
+ // 结尾汇总:被手改过、已用新版覆盖(原文件已备份)的 / 模板已删除、手改过而备份后删除的文件 /
11
+ // 没有安装清单、内容与新版不同而备份后覆盖的文件(无从判断是否手改)
12
+ export const report = { overwritten: [], removed: [], unlisted: [], unlistedDirs: [], backupDir: null };
12
13
 
13
14
  // skills 目录里的文件一律以安装包为准:手改过的先备份到 <项目根>/workflow-run/tmp/upgrade-backup/<时间>/(同一次运行共用一个目录),
14
15
  // 再覆盖或删除。项目自己的定制应放在不会被升级覆盖的地方:workflow/profile.json、workflow-run/templates/、DocNexus、CLAUDE.md / AGENTS.md
@@ -17,8 +18,7 @@ function backup(root, file) {
17
18
  stamp ??= timestamp();
18
19
  const dir = path.join(root, TMP_DIR, 'upgrade-backup', stamp);
19
20
  const rel = path.relative(root, file);
20
- // 装在项目外(如 ~/.codex/skills)的文件按绝对路径放在 outside-project/ 下
21
- const target = rel.startsWith('..') || path.isAbsolute(rel) ? path.join(dir, 'outside-project', file.replace(/^[A-Za-z]:/, '').replace(/^[\\/]+/, '')) : path.join(dir, rel);
21
+ const target = path.join(dir, rel);
22
22
  fs.mkdirSync(path.dirname(target), { recursive: true });
23
23
  fs.copyFileSync(file, target);
24
24
  report.backupDir = toPosix(path.relative(root, dir));
@@ -34,12 +34,13 @@ export function pruneFile(file, recorded, stop, label, root) {
34
34
  }
35
35
 
36
36
  // 模板文件:总是写成新版。和上次安装记录的哈希不同(被手改过)时先备份原文件。
37
- // 没有清单时无从判断是否手改,内容不同就备份后覆盖
37
+ // 没有清单时无从判断是否手改,内容不同就备份后覆盖(单独汇总,不报「手动修改过」)
38
38
  export function installTemplate(dst, content, recorded, hasManifest, ctx, label) {
39
39
  if (!fs.existsSync(dst)) { writeFile(dst, content); return; }
40
40
  const cur = fileHash(dst);
41
41
  if (cur === sha256(content)) return;
42
- if (!hasManifest || cur !== recorded) { backup(ctx.root, dst); report.overwritten.push(label); }
42
+ if (!hasManifest) { backup(ctx.root, dst); report.unlisted.push(label); }
43
+ else if (cur !== recorded) { backup(ctx.root, dst); report.overwritten.push(label); }
43
44
  writeFile(dst, content);
44
45
  }
45
46
 
@@ -59,6 +60,7 @@ export function pruneSkill(base, skill, prevSkill, entry, root) {
59
60
  export async function installSkills(base, db, ctx) {
60
61
  const selected = ctx.skills ?? SKILLS;
61
62
  const prev = readManifest(base);
63
+ const unlistedBefore = report.unlisted.length;
62
64
  const manifest = { installer: PKG.name, version: PKG.version, installedAt: new Date().toISOString(), envs: ctx.envs, mode: ctx.mode, skills: {}, templates: prev?.templates ?? {} };
63
65
  for (const skill of selected) {
64
66
  const srcDir = path.join(TPL_DIR, skill);
@@ -109,6 +111,7 @@ export async function installSkills(base, db, ctx) {
109
111
  // 清单里有、本版本已不再提供的 skill:按同样规则清理
110
112
  else pruneSkill(base, skill, prevSkill, null, ctx.root);
111
113
  }
114
+ if (report.unlisted.length > unlistedBefore) report.unlistedDirs.push(toPosix(path.relative(ctx.root, base)));
112
115
  writeFile(path.join(base, MANIFEST), JSON.stringify(manifest, null, 2) + '\n');
113
116
  }
114
117
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  import fs from 'node:fs';
4
4
  import path from 'node:path';
5
- import { IS_WIN, CANON_DIR, MANIFEST, step, ok, warn, toPosix, fileHash, readManifest, removeEmptyDirs, lstat, timestamp, removeLink, docnexusRoot } from './core.mjs';
5
+ import { IS_WIN, CANON_DIR, INSTALL_BASES, MANIFEST, step, ok, warn, toPosix, fileHash, readManifest, removeEmptyDirs, lstat, timestamp, removeLink, docnexusRoot } from './core.mjs';
6
6
  import { removeDocNexusContext } from './docnexus.mjs';
7
7
 
8
8
  // 软链接是否指向 absTarget(按解析后的绝对路径比较,兼容 Windows 上 readlink 返回的 \\?\ 前缀)
@@ -17,14 +17,14 @@ export function linkPointsTo(link, absTarget) {
17
17
  export function rmdirIfEmpty(dir) { try { fs.rmdirSync(dir); return true; } catch { return false; } }
18
18
 
19
19
  export function uninstall(root) {
20
- const bases = [CANON_DIR, path.join('.claude', 'skills'), path.join('.qwen', 'skills')];
20
+ const bases = INSTALL_BASES;
21
21
  const found = bases.map((rel) => [rel, readManifest(path.join(root, rel))]).filter(([, m]) => m);
22
22
  if (!found.length) { warn(`未找到安装清单(${bases.map((b) => toPosix(path.join(b, MANIFEST))).join(' / ')}),没有可卸载的内容`); return 0; }
23
23
  const backupRoot = path.join(root, 'workflow-run', 'tmp', 'uninstall-backup', timestamp());
24
24
  const backedUp = [];
25
25
  const leftovers = [];
26
26
 
27
- // 1. .claude / .qwen 下指向单一来源、且在清单里的 skill 链接;用户自己的链接和目录不动
27
+ // 1. .claude / .qwen / .codebuddy 下指向单一来源、且在清单里的 skill 链接;用户自己的链接和目录不动
28
28
  const canon = found.find(([rel]) => rel === CANON_DIR)?.[1];
29
29
  for (const rel of bases.slice(1)) {
30
30
  const dir = path.join(root, rel);
@@ -81,7 +81,7 @@ export function uninstall(root) {
81
81
  fs.rmSync(path.join(base, MANIFEST), { force: true });
82
82
  }
83
83
 
84
- // 3. 清空后的环境目录一并移除(.claude/.qwen 里还有别的东西时保留)
84
+ // 3. 清空后的环境目录一并移除(.claude / .qwen / .codebuddy 里还有别的东西时保留)
85
85
  for (const rel of bases) if (rmdirIfEmpty(path.join(root, rel))) rmdirIfEmpty(path.dirname(path.join(root, rel)));
86
86
 
87
87
  step('卸载完成');
@@ -5,7 +5,7 @@
5
5
  // npx @rowansenne/workflow-run [命令] [选项] # 在项目根执行(唯一的安装方式)
6
6
  //
7
7
  // 单一来源:skills 只完整安装到 .agents/skills(codex / doubao 直接使用);
8
- // .claude/skills、.qwen/skills 下按 skill 建相对软链接指向它;--copy 退回各环境独立拷贝。
8
+ // .claude/skills、.qwen/skills、.codebuddy/skills(WorkBuddy)下按 skill 建相对软链接指向它;--copy 退回各环境独立拷贝。
9
9
  // 重跑安全:SKILL.md / references / 模板会更新;用户已填写的 wiki-config.json、schema.md、
10
10
  // workflow/profile.json 保留不覆盖;database-query/config.json 仅在本次提供了数据库配置时写入。
11
11
  // 安装清单:每个安装目录写 .workflow-run.json,记录模板文件的 sha256。重跑时据此判断模板文件是否被手改过
@@ -15,10 +15,9 @@
15
15
  // install(安装 / 软链接 / 改名清理 / .gitignore)、uninstall、docnexus(接入与模板记录)、doctor(upgrade / doctor)。
16
16
 
17
17
  import fs from 'node:fs';
18
- import os from 'node:os';
19
18
  import path from 'node:path';
20
19
  import { seedTemplates, updateTemplates } from './seed-templates.mjs';
21
- import { TPL_DIR, PKG, ENV_NAMES, CANON_DIR, HOME, SKILLS, resolveSkillSelection, step, ok, warn, CliError, fail, isAdminTerminal } from './lib/core.mjs';
20
+ import { TPL_DIR, PKG, ENV_NAMES, CANON_DIR, LINK_ENVS, HOME, SKILLS, resolveSkillSelection, step, ok, warn, CliError, fail, isAdminTerminal } from './lib/core.mjs';
22
21
  import { ask, closePrompt } from './lib/prompt.mjs';
23
22
  import { findExistingDbConfig, getDb } from './lib/db.mjs';
24
23
  import { report, installSkills, linkSkills, updateGitignore, setupHome, recordEnv } from './lib/install.mjs';
@@ -46,7 +45,7 @@ const HELP = `用法: workflow-run [命令] [选项]
46
45
  不带地址时显示当前状态
47
46
 
48
47
  init 选项:
49
- --envs <列表> 环境,逗号分隔:codex,claude,doubao,qwen;all = 全部(不传则交互选择)
48
+ --envs <列表> 环境,逗号分隔:codex,claude,doubao,qwen,workbuddy;all = 全部(不传则交互选择)
50
49
  --skip-db 跳过数据库连接配置
51
50
  --yes 非交互:使用已有数据库配置;都没有且未 --skip-db 时报错退出
52
51
  --with-docnexus 同时接入 DocNexus 文档库(装在 workflow-run/docnexus/,需 Node >= 22.13 与 npm),并导入文档模板
@@ -126,9 +125,10 @@ async function resolveEnvs(o) {
126
125
  console.log(' 2) claude -> .claude/skills (Claude Code 项目级)');
127
126
  console.log(' 3) doubao -> .agents/skills (豆包办公项目级)');
128
127
  console.log(' 4) qwen -> .qwen/skills (Qwen Code 项目级)');
128
+ console.log(' 5) workbuddy -> .codebuddy/skills(腾讯 WorkBuddy 项目级)');
129
129
  const choice = await ask('输入编号(逗号分隔,如 1,3;直接回车 = 全部): ');
130
130
  if (!choice) return [...ENV_NAMES];
131
- const sel = choice.split(/[,,\s]+/).filter((p) => /^[1-4]$/.test(p)).map((p) => ENV_NAMES[Number(p) - 1]);
131
+ const sel = choice.split(/[,,\s]+/).filter((p) => /^[1-5]$/.test(p)).map((p) => ENV_NAMES[Number(p) - 1]);
132
132
  if (!sel.length) { warn('未识别到有效编号,按全部安装'); return [...ENV_NAMES]; }
133
133
  return [...new Set(sel)].sort();
134
134
  }
@@ -207,16 +207,10 @@ async function main() {
207
207
  const links = [];
208
208
  if (o.copy) {
209
209
  if (sel.includes('codex') || sel.includes('doubao')) targets.push(['codex/doubao', CANON_DIR]);
210
- if (sel.includes('claude')) targets.push(['claude', path.join('.claude', 'skills')]);
211
- if (sel.includes('qwen')) targets.push(['qwen', path.join('.qwen', 'skills')]);
210
+ for (const [env, dir] of Object.entries(LINK_ENVS)) if (sel.includes(env)) targets.push([env, dir]);
212
211
  } else {
213
212
  targets.push(['单一来源', CANON_DIR]);
214
- if (sel.includes('claude')) links.push(['claude', path.join('.claude', 'skills')]);
215
- if (sel.includes('qwen')) links.push(['qwen', path.join('.qwen', 'skills')]);
216
- }
217
- if (sel.includes('codex') && !o.yes) {
218
- const a = (await ask('是否同时安装到 Codex 用户级 ~/.codex/skills(所有项目可用)?[y/N]: ')).toLowerCase();
219
- if (a === 'y') targets.push(['codex(user)', path.join(os.homedir(), '.codex', 'skills')]);
213
+ for (const [env, dir] of Object.entries(LINK_ENVS)) if (sel.includes(env)) links.push([env, dir]);
220
214
  }
221
215
 
222
216
  const ctx = { root: o.root, yes: o.yes, envs: sel, mode: o.copy ? 'copy' : 'symlink', skills };
@@ -260,6 +254,7 @@ async function main() {
260
254
  console.log(' claude : Claude Code 自动发现 .claude/skills,无需额外配置');
261
255
  console.log(' doubao : 豆包办公自动发现 .agents/skills(新会话生效)');
262
256
  console.log(' qwen : Qwen Code 自动发现 .qwen/skills;/skills 面板可查看与切换');
257
+ console.log(' workbuddy : WorkBuddy 打开本项目目录后读取 .codebuddy/skills(项目级优先于用户级);DocNexus 段落写在 AGENTS.md');
263
258
  if (db) ok('database-query 连接已生成并写入;凭据已加入 .gitignore,请勿提交。');
264
259
  else if (fs.existsSync(path.join(o.root, CANON_DIR, 'database-query', 'config.json'))) ok('database-query/config.json 已存在,保留不动。');
265
260
  else warn('注意:本次未写入数据库连接。可重跑补配,或手动放置 database-query/config.json(已有的保留不动)。');
@@ -270,11 +265,16 @@ async function main() {
270
265
  if (docnexusConnected(o.root)) reportTemplateStatus(o.root);
271
266
  if (docnexus === 'failed') warn('注意:DocNexus 接入失败(见上方输出),其他 skills 不受影响;修复后可重跑(加 --with-docnexus)。');
272
267
  if (codegraph === 'failed') warn('注意:CodeGraph 接入失败(见上方输出),其他 skills 不受影响;skills 会退回 grep / 读文件;修复后可重跑(加 --with-codegraph)。');
268
+ const keep = '(skills 目录里的文件以安装包为准;项目定制请放到 workflow/profile.json、workflow-run/templates/、DocNexus 或 CLAUDE.md / AGENTS.md)';
273
269
  if (report.overwritten.length || report.removed.length) {
274
- warn(`以下文件被手动修改过,已按新版${report.removed.length ? '覆盖或删除' : '覆盖'},原文件备份在 ${report.backupDir}/(skills 目录里的文件以安装包为准;项目定制请放到 workflow/profile.json、workflow-run/templates/、DocNexus 或 CLAUDE.md / AGENTS.md):`);
270
+ warn(`以下文件被手动修改过,已按新版${report.removed.length ? '覆盖或删除' : '覆盖'},原文件备份在 ${report.backupDir}/${keep}:`);
275
271
  for (const f of report.overwritten) warn(` ${f}`);
276
272
  for (const f of report.removed) warn(` ${f}(新版已不提供,已删除)`);
277
273
  }
274
+ // 目录里没有安装清单(不是本工具装的,或清单丢了):无从判断是否手改,内容和新版不同就备份后覆盖,汇总成一行
275
+ if (report.unlisted.length) {
276
+ ok(`${report.unlistedDirs.join('、')} 里没有安装清单,${report.unlisted.length} 个和新版不同的文件已备份到 ${report.backupDir}/ 后覆盖(无从判断是否手改过)`);
277
+ }
278
278
  console.log('先运行 /workflow profile 填写项目画像;说明见 workflow-run README。');
279
279
  // skills 已装好,但显式要求的 DocNexus 未接入:返回非 0 便于自动化感知
280
280
  return docnexus === 'failed' || codegraph === 'failed' || seedFailed ? 1 : 0;
package/package.json CHANGED
@@ -1,12 +1,13 @@
1
1
  {
2
2
  "name": "@rowansenne/workflow-run",
3
- "version": "0.17.0",
3
+ "version": "0.19.0",
4
4
  "description": "项目级 Agent Skills 一键初始化:workflow / dev / project-review / code-graph / database-query / md-to-wiki / 技术与上线方案生成",
5
5
  "keywords": [
6
6
  "agent-skills",
7
7
  "claude-code",
8
8
  "codex",
9
9
  "qwen-code",
10
+ "workbuddy",
10
11
  "skills",
11
12
  "workflow",
12
13
  "code-review"
@@ -18,7 +18,7 @@
18
18
  ## 读代码:CodeGraph 原生工具
19
19
 
20
20
  - **有 MCP 工具时**(Claude Code / Codex 经 `--with-codegraph` 写了配置):用 `codegraph_explore`,一次调用返回相关符号的源码(带行号)、调用路径和影响面摘要。问题里点名类名 / 方法名 / 文件,结果更准。
21
- - **没有 MCP 工具时**(doubao、qwen、子 agent,或 MCP 没加载):在项目根用同名 CLI,输出相同:
21
+ - **没有 MCP 工具时**(doubao、qwen、workbuddy、子 agent,或 MCP 没加载):在项目根用同名 CLI,输出相同:
22
22
 
23
23
  | 要做的事 | 命令 |
24
24
  |---|---|
@@ -8,7 +8,7 @@ import { spawnSync } from 'node:child_process';
8
8
 
9
9
  const read = (f) => { try { return fs.readFileSync(f, 'utf8'); } catch { return ''; } };
10
10
  const readJson = (f) => { try { return JSON.parse(fs.readFileSync(f, 'utf8')); } catch { return null; } };
11
- const SKIP_DIRS = new Set(['node_modules', '.git', 'target', 'build', 'dist', 'out', '.gradle', '.idea', 'vendor', 'tmp', '.agents', '.claude', '.qwen', 'docnexus', 'workflow-run']);
11
+ const SKIP_DIRS = new Set(['node_modules', '.git', 'target', 'build', 'dist', 'out', '.gradle', '.idea', 'vendor', 'tmp', '.agents', '.claude', '.qwen', '.codebuddy', 'docnexus', 'workflow-run']);
12
12
 
13
13
  function git(root, args) {
14
14
  const r = spawnSync('git', args, { cwd: root, encoding: 'utf8' });
@@ -38,7 +38,7 @@ export const homeIsRepo = (root) => fs.existsSync(path.join(root, HOME, '.git'))
38
38
  export const FILES = { plan: 'plan', delivery: 'delivery', review: 'review', tech: 'tech-doc', release: 'release-doc' };
39
39
  const LABELS = { plan: '开发方案', delivery: '交付说明', review: '审查报告', tech: '技术方案', release: '上线方案' };
40
40
  // 安装后的 skills 目录(安装器从安装包调用本脚本时,到项目里找画像)
41
- const INSTALL_BASES = ['.agents/skills', '.claude/skills', '.qwen/skills'];
41
+ const INSTALL_BASES = ['.agents/skills', '.claude/skills', '.qwen/skills', '.codebuddy/skills'];
42
42
 
43
43
  const git = (cwd, args) => spawnSync('git', args, { cwd, encoding: 'utf8' });
44
44
  const readJson = (f) => { try { return JSON.parse(fs.readFileSync(f, 'utf8')); } catch { return null; } };