@jspg-ai/coding-bb 0.0.3-beta.8 → 0.0.4

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.
Files changed (108) hide show
  1. package/cbb/dev-standards/rules/cbb-ai-behavior.md +0 -15
  2. package/cbb/dev-standards/rules/cbb-workspace-discipline.md +38 -0
  3. package/cbb/lib/external/index.js +228 -0
  4. package/cbb/lib/install/agents-nav.js +92 -0
  5. package/cbb/lib/install/claude-code.js +8 -1
  6. package/cbb/lib/install/command-skill.js +105 -0
  7. package/cbb/lib/install/init.js +399 -129
  8. package/cbb/lib/install/minimax.js +60 -0
  9. package/cbb/lib/install/rule-rewrite.js +1 -1
  10. package/cbb/lib/install/setup-decision.js +92 -0
  11. package/cbb/lib/install/tool-selection.js +23 -0
  12. package/cbb/lib/install/trae.js +18 -8
  13. package/cbb/lib/install/workbuddy.js +36 -0
  14. package/cbb/lib/install/workspaces.js +15 -61
  15. package/cbb/lib/utils/check-update.js +13 -5
  16. package/cbb/lib/utils/githook.js +230 -0
  17. package/cbb/lib/utils/gitignore.js +17 -4
  18. package/cbb/lib/utils/plugin-hook.js +92 -0
  19. package/cbb/worktrees/_shared/scripts/check-local-state.js +114 -0
  20. package/cbb/worktrees/_shared/scripts/check-unarchived.js +77 -0
  21. package/cbb/worktrees/_shared/scripts/delete-branches.js +147 -0
  22. package/cbb/worktrees/_shared/scripts/deprecate-branches.js +119 -0
  23. package/cbb/worktrees/_shared/scripts/find-target-worktree.js +148 -96
  24. package/cbb/worktrees/_shared/scripts/find-workspace-root.js +8 -8
  25. package/cbb/worktrees/_shared/scripts/install-git-hook.js +70 -0
  26. package/cbb/worktrees/_shared/scripts/junction-ai-config.js +192 -0
  27. package/cbb/worktrees/_shared/scripts/merge-branches.js +469 -0
  28. package/cbb/worktrees/_shared/scripts/push-core.js +1 -1
  29. package/cbb/worktrees/_shared/scripts/silence-popup.js +1 -1
  30. package/cbb/worktrees/_shared/scripts/start-local.js +180 -0
  31. package/cbb/worktrees/skills/cbb-local-mode/SKILL.md +194 -0
  32. package/cbb/worktrees/skills/cbb-local-mode/scripts/check-local-state.js +114 -0
  33. package/cbb/worktrees/skills/cbb-local-mode/scripts/delete-branches.js +147 -0
  34. package/cbb/worktrees/skills/cbb-local-mode/scripts/deprecate-branches.js +119 -0
  35. package/cbb/worktrees/skills/cbb-local-mode/scripts/install-git-hook.js +70 -0
  36. package/cbb/worktrees/skills/cbb-local-mode/scripts/merge-branches.js +469 -0
  37. package/cbb/worktrees/skills/cbb-local-mode/scripts/silence-popup.js +23 -0
  38. package/cbb/worktrees/skills/cbb-local-mode/scripts/start-local.js +180 -0
  39. package/cbb/worktrees/skills/cbb-worktree-close/SKILL.md +133 -81
  40. package/cbb/worktrees/skills/cbb-worktree-close/scripts/check-env.js +3 -3
  41. package/cbb/worktrees/skills/cbb-worktree-close/scripts/check-unarchived.js +77 -48
  42. package/cbb/worktrees/skills/cbb-worktree-close/scripts/delete-branches.js +147 -136
  43. package/cbb/worktrees/skills/cbb-worktree-close/scripts/deprecate-branches.js +119 -0
  44. package/cbb/worktrees/skills/cbb-worktree-close/scripts/discover-apps.js +14 -5
  45. package/cbb/worktrees/skills/cbb-worktree-close/scripts/find-target-worktree.js +148 -96
  46. package/cbb/worktrees/skills/cbb-worktree-close/scripts/find-workspace-root.js +8 -8
  47. package/cbb/worktrees/skills/cbb-worktree-close/scripts/junction-ai-config.js +192 -0
  48. package/cbb/worktrees/skills/cbb-worktree-close/scripts/merge-branches.js +469 -0
  49. package/cbb/worktrees/skills/cbb-worktree-close/scripts/remove-worktrees.js +44 -9
  50. package/cbb/worktrees/skills/cbb-worktree-close/scripts/safety-check.js +15 -4
  51. package/cbb/worktrees/skills/cbb-worktree-close/scripts/silence-popup.js +1 -1
  52. package/cbb/worktrees/skills/cbb-worktree-init/SKILL.md +136 -123
  53. package/cbb/worktrees/skills/cbb-worktree-init/scripts/check-env.js +1 -1
  54. package/cbb/worktrees/skills/cbb-worktree-init/scripts/check-repos.js +2 -2
  55. package/cbb/worktrees/skills/cbb-worktree-init/scripts/create-branches.js +33 -8
  56. package/cbb/worktrees/skills/cbb-worktree-init/scripts/create-worktrees.js +60 -46
  57. package/cbb/worktrees/skills/cbb-worktree-init/scripts/install-git-hook.js +70 -0
  58. package/cbb/worktrees/skills/cbb-worktree-init/scripts/junction-ai-config.js +192 -0
  59. package/cbb/worktrees/skills/cbb-worktree-init/scripts/parse-config.js +7 -7
  60. package/cbb/worktrees/skills/cbb-worktree-init/scripts/push-branches.js +3 -3
  61. package/cbb/worktrees/skills/cbb-worktree-init/scripts/push-core.js +1 -1
  62. package/cbb/worktrees/skills/cbb-worktree-init/scripts/silence-popup.js +1 -1
  63. package/cbb/worktrees/skills/cbb-worktree-init/scripts/sync-repos.js +11 -7
  64. package/cbb/worktrees/skills/cbb-worktree-init/scripts/update-gitignore.js +37 -9
  65. package/cbb/worktrees/skills/cbb-worktree-push/SKILL.md +212 -56
  66. package/cbb/worktrees/skills/cbb-worktree-push/scripts/check-unarchived.js +77 -0
  67. package/cbb/worktrees/skills/cbb-worktree-push/scripts/commit-worktrees.js +70 -12
  68. package/cbb/worktrees/skills/cbb-worktree-push/scripts/find-target-worktree.js +148 -96
  69. package/cbb/worktrees/skills/cbb-worktree-push/scripts/find-workspace-root.js +8 -8
  70. package/cbb/worktrees/skills/cbb-worktree-push/scripts/make-mr-urls.js +211 -0
  71. package/cbb/worktrees/skills/cbb-worktree-push/scripts/merge-branches.js +469 -0
  72. package/cbb/worktrees/skills/cbb-worktree-push/scripts/push-core.js +1 -1
  73. package/cbb/worktrees/skills/cbb-worktree-push/scripts/push-worktrees.js +67 -20
  74. package/cbb/worktrees/skills/cbb-worktree-push/scripts/silence-popup.js +1 -1
  75. package/config/{workspace-config.sample.json → CBB-WORKSPACE.sample.json} +1 -1
  76. package/config/openspec/schemas/spec-driven/schema.yaml +20 -0
  77. package/config/upstream-mirrors.json +5 -0
  78. package/external/frontend-design/.version +6 -0
  79. package/external/frontend-design/LICENSE.txt +177 -0
  80. package/external/frontend-design/SKILL.md +71 -0
  81. package/openspec/.version +2 -2
  82. package/openspec/commands/archive.md +15 -8
  83. package/openspec/commands/bulk-archive.md +13 -6
  84. package/openspec/commands/continue.md +0 -1
  85. package/openspec/commands/explore.md +1 -1
  86. package/openspec/commands/ff.md +1 -1
  87. package/openspec/commands/onboard.md +9 -4
  88. package/openspec/commands/update.md +11 -5
  89. package/openspec/commands/verify.md +57 -26
  90. package/openspec/skills/openspec-apply-change/SKILL.md +1 -1
  91. package/openspec/skills/openspec-archive-change/SKILL.md +16 -9
  92. package/openspec/skills/openspec-bulk-archive-change/SKILL.md +14 -7
  93. package/openspec/skills/openspec-continue-change/SKILL.md +1 -2
  94. package/openspec/skills/openspec-explore/SKILL.md +2 -2
  95. package/openspec/skills/openspec-ff-change/SKILL.md +2 -2
  96. package/openspec/skills/openspec-new-change/SKILL.md +1 -1
  97. package/openspec/skills/openspec-onboard/SKILL.md +10 -5
  98. package/openspec/skills/openspec-propose/SKILL.md +1 -1
  99. package/openspec/skills/openspec-sync-specs/SKILL.md +1 -1
  100. package/openspec/skills/openspec-update-change/SKILL.md +12 -6
  101. package/openspec/skills/openspec-verify-change/SKILL.md +59 -27
  102. package/package.json +3 -2
  103. package/superpowers/.version +3 -3
  104. package/superpowers/skills/writing-plans/SKILL.md +32 -20
  105. package/cbb/lib/install/codebuddy.js +0 -30
  106. package/cbb/worktrees/skills/cbb-worktree-init/scripts/find-workspace-root.js +0 -67
  107. package/config/workspace-agents.sample.md +0 -41
  108. package/superpowers/skills/writing-plans/plan-document-reviewer-prompt.md +0 -49
@@ -99,21 +99,6 @@ trigger: always_on
99
99
 
100
100
  **检验标准:** 用户从未因"AI 直接改了代码没打招呼"而感到意外。
101
101
 
102
- ## 8. 业务空间目录纪律
103
-
104
- **仅当工作目录是业务空间根(存在 `workspace-config.json`)时生效:写操作命令必须落在需求工作树内。**
105
-
106
- 业务空间根(主分支)只负责组织与调度,不做需求开发。接到需求先建工作树(`cbb-worktree-init` skill,或斜杠命令 `/cbb-worktree-init`),之后:
107
-
108
- - 一切产生写操作的命令(git commit / openspec / 构建 / 测试 / 文件修改)必须以 `.worktrees/worktree-<需求名>/`(或其应用子目录)为工作目录:显式 `cd` 或绝对路径
109
- - 禁止在空间根执行写操作:不在主分支提交代码、不在空间根创建 openspec 变更产物、不改 `.codespace/` 基准代码
110
- - 只读命令(git status / log、查看文件)在空间根执行无妨
111
- - 会话开始时确认本次需求对应的工作树路径;存在多个未完成工作树时,先与用户确认目标,不要猜
112
-
113
- **会话与工作目录是两回事:** AI 会话窗口始终停在**空间根**(worktree 内不装 AI 配置,无工作流命令),`/opsx:*` 等命令仍在空间根会话发起;只是**产生写操作的工作目录**指向需求工作树(显式 `cd` 或绝对路径)。不要把会话切到 worktree 内,也不要因「在 worktree 内开发」就在空间根直接动手。
114
-
115
- **检验标准:** 空间根主分支的 `git status` 永远干净(除工具维护的配置文件外),openspec 变更产物只出现在需求工作树内。
116
-
117
102
  ---
118
103
 
119
104
  **这些准则生效的标志:** diff 中不必要的改动减少、不会因过度设计而重写、澄清问题在实现前而非出错后提出。
@@ -0,0 +1,38 @@
1
+ ---
2
+ trigger: always_on
3
+ ---
4
+
5
+ # 业务空间开发纪律(双模式,用户选择制)
6
+
7
+ **仅当处于业务空间体系(空间根、`.worktrees/` 工作树、及其中各应用检出)时生效。空间支持两种开发模式,能力对称(都能修改 workspace 内容与应用代码、都强制分支隔离、交付均有 MR 与直合并两条通道,ADR 0007/0008),由用户显式选择,agent 只给推荐不代决。**
8
+
9
+ **模式一 · 本地模式(单线程快改,当前会话就地干):**
10
+
11
+ - 就地切分支:根 + 各涉及应用 `.codespace/<app>/` 同名 `checkout -b`(基线 = `origin/<main>`,经 `cbb-local-mode` 技能执行)
12
+ - 当前会话原地开发,相对路径原样生效;**一次只承载一个需求**(独占本地检出;期间并行新需求走 worktree 模式,互不干扰)
13
+ - 收尾(收尾合并门禁,ADR 0007):`merge-branches.js --check` 判定已合并;未合并须三选一(提 MR / 直合并 `--allow-checkout` / 确认废弃)后方可收尾;各参与仓库 `checkout main && pull && 删分支`
14
+
15
+ **模式二 · worktree 模式(并行 / 隔离):**
16
+
17
+ - `cbb-worktree-init` 建隔离 worktree(AI 配置内容子目录已 junction 就位),**开发会话开进 `.worktrees/worktree-<需求名>/`**,可多需求并行;app 检出收拢在 `<wt>/.codespace/<app>/`——**镜像空间根布局**(`.codespace/<app>/...` 相对路径两模式通用,ADR 0003)
18
+ - 根会话保留作控制面(worktree 生灭、`cbb update` 等空间级操作);**会话形态按工具定,能自动则 agent 亲自做**:OpenCode 桌面端 = agent 自动把会话切进/切出 worktree(`session_move`,上下文保留、不列入用户待办,空间根路径在案供回切);无会话搬移能力的桌面端 = 用户手动切根;CLI/TUI = 新开会话进 worktree
19
+
20
+ **基准与防线(两模式共同,ADR 0002):**
21
+
22
+ - **基准 = `origin/main`(唯一真相源);`.codespace/<app>/` 的检出 = 视图**——本地模式可暂态被需求分支占用,收尾恢复
23
+ - **严禁直接在任何 main 检出上提交载荷**:分支感知 pre-commit hook 机械拦截(workspace 仓库仅放行控制面白名单:`CBB-WORKSPACE.json` / `.gitignore` / `AGENTS.md` / `openspec/config.yaml` / `openspec/schemas/**`;应用仓库 main 全拦;**merge 提交(`MERGE_HEAD` 存在)放行**,见 ADR 0007)——**被拦是防线生效,不是故障**;`--no-verify` 绕过仅限用户显式要求
24
+ - 需求分支推送唯一出口 = `cbb-worktree-push`(init 创建流程零远端操作,lazy push);推送后可选提 MR 或**直接合并**(`merge-branches.js`,主干 / 测试分支,ADR 0008);收尾合并门禁同源(ADR 0007)
25
+ - **收尾合并门禁(ADR 0007)**:两模式收尾均须每仓库"已合并"(`safety-check` / `merge-branches.js --check` 判定);未合并三选一——提 MR / 直合并 / 确认废弃(远程重命名 `<分支>-deprecated`);交付完成自动删本地+远程分支
26
+ - **模式误判零代价**:本地模式中途要并行/隔离 → 重跑 `cbb-worktree-init`(分支已存在自动复用,提交随身带走)
27
+
28
+ **五时刻对齐(agent 必须遵守):**
29
+
30
+ 1. **开工前——状态自检**(`cbb-local-mode` 的 `check-local-state.js`):干净态零交互直接开工;**遗留态(别的需求未收尾,任一仓库检出在非主干分支)必须让用户四选一**(继续 / 先收尾 / 升级 worktree / 丢弃),不得自行决定
31
+ 2. **开工时——布局确认**:向用户展示参与仓库与分支名,确认后执行
32
+ 3. **进行中——插单归属**:用户插入非本需求的改动,先确认归属(并入 / 收尾后处理 / 转 worktree),不得静默混入
33
+ 4. **打包前——主干对齐**(任何构建打包动作前):对产物所涉及的仓库(按代码来源判定:哪个仓库的代码被编进产物就同步哪个,与产物存放位置无关;多仓聚合产物则全部同步)先对齐再打包——① `git fetch <远端>`;② 在当前需求分支执行 `git merge <远端>/<主干>`(远端通常为 `origin`,主干名以远端 HEAD 为准,常见 main / master;在 main 检出上打包时改为 `git pull`;工作区未提交修改阻碍合并时,先提交或暂存再对齐);③ 冲突未解决时**禁止打包**,停下报告用户;fetch/merge 失败 → 报告并等待用户指示;④ 对齐完成后(由此产生的 merge 提交属正常)再执行打包命令(`mvn package` / `npm run build` / 前端 bundle / 生成部署包等凡产出构建产物者);产物目录须已被 `.gitignore` 忽略(未忽略先补 ignore——该修改属普通变更,随需求分支提交)。产物基线 = 主干最新,避免基于过时分支打包导致的集成返工;**不 rebase、不 force push**,merge 提交随下次 push 一并推送
34
+ 5. **打包后——产物收集与留存**:打包完成后把各仓库产物**复制**到 `<空间根>/.build/<需求名>/<应用名>/`(原件保持原位;需求名 = 分支名;workspace 仓库产物用 `workspace` 子目录;目标目录已存在时先向用户确认清理或覆盖;`.build/` 由 cbb 写入空间根 `.gitignore`)——产物按需求聚合留存,worktree 关闭后不丢失;打包时顺带报告 `.build/` 积压现状(需求目录数与总体积),建议清理已交付或已废弃需求的产物目录,经用户逐项确认后执行、**永不自动删除**
35
+
36
+ **读基准代码约定:** 本地模式进行中,工作区是开发态而非基准——要读"main 现状"先确认检出分支,或直接 `git show main:<path>` / `git diff main`;不要默认工作区即基准。
37
+
38
+ **检验标准:** 任何 main 检出上无载荷提交(hook 兜底,git log 可机械审计;merge 提交除外);本地模式收尾后全部仓库回主干且 clean;worktree 模式的**变更(代码改动)**只出现在需求工作树内(打包产物按第 5 条收集到空间根 `.build/`,不在此限);需求分支只经 push 技能产生;**未合并且非废弃的分支无法通过收尾(门禁兜底)**。
@@ -0,0 +1,228 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * External 小型外部依赖管理模块
5
+ *
6
+ * 管理顶层 external/ 目录下的零散第三方依赖(当前为 frontend-design skill,
7
+ * 来源 anthropics/skills)。与 superpowers / openspec 的整仓 pin 不同,
8
+ * 这里是「一依赖一目录、pin 文件放依赖自己的子目录内」的轻量形态:
9
+ *
10
+ * external/<dep-name>/
11
+ * ├── .version # pin:{ repo, ref(commit sha), path(tarball 内子路径), updatedAt }
12
+ * └── ... # 上游 path 子目录的内容(如 SKILL.md + LICENSE.txt)
13
+ *
14
+ * 新增外部依赖 = 在 external/ 下新建子目录并写入 .version,无需改本模块。
15
+ *
16
+ * 网络:GitHub 直连可能被墙 / 被本机 DNS 劫持,走 config/upstream-mirrors.json
17
+ * 的 'external' 镜像前缀列表(按顺序尝试,首个成功即用;空字符串 = 直连)。
18
+ *
19
+ * 升级方式:cbbm update --upstream-external [name](按 .version 的 ref 幂等重拉,省略 name = 全部)
20
+ * 版本检查:cbbm check-upstream(对比 pinned sha 与上游 main 该 path 的最新提交)
21
+ */
22
+
23
+ const fs = require('fs');
24
+ const path = require('path');
25
+ const os = require('os');
26
+
27
+ const SHARED_STANDARDS_DIR = path.join(__dirname, '..', '..', '..');
28
+
29
+ // External 依赖根目录
30
+ const EXTERNAL_DIR = path.join(SHARED_STANDARDS_DIR, 'external');
31
+ const VERSION_FILE_NAME = '.version';
32
+ // skill 型依赖的完整性标记(上游 path 内容含 SKILL.md 时以其校验完整性)
33
+ const SKILL_MARKER = 'SKILL.md';
34
+
35
+ const { log } = require('../utils/output');
36
+ const { extractTarGz } = require('../utils/tar');
37
+ const { loadMirrors, fetchWithMirrors } = require('../utils/upstream');
38
+
39
+ // ── 依赖发现与 pin 读取 ──────────────────────────────────────
40
+
41
+ /**
42
+ * 读取依赖目录内的 .version pin。字段不全 / JSON 损坏 / 文件缺失返回 null。
43
+ * @param {string} dir
44
+ * @returns {{repo: string, ref: string, path: string, updatedAt?: string}|null}
45
+ */
46
+ function readPin(dir) {
47
+ const file = path.join(dir, VERSION_FILE_NAME);
48
+ if (!fs.existsSync(file)) return null;
49
+ try {
50
+ const data = JSON.parse(fs.readFileSync(file, 'utf-8'));
51
+ if (!data || typeof data.repo !== 'string' || !data.repo.includes('/') ||
52
+ typeof data.ref !== 'string' || !data.ref ||
53
+ typeof data.path !== 'string' || !data.path) {
54
+ return null;
55
+ }
56
+ return data;
57
+ } catch (_) {
58
+ return null;
59
+ }
60
+ }
61
+
62
+ /**
63
+ * 扫描 external/ 下各依赖子目录,返回全部依赖(pin 无效的目录跳过并告警)。
64
+ * @returns {Array<{name: string, dir: string, pin: object}>}
65
+ */
66
+ function listDeps() {
67
+ if (!fs.existsSync(EXTERNAL_DIR)) return [];
68
+ const deps = [];
69
+ for (const entry of fs.readdirSync(EXTERNAL_DIR)) {
70
+ const dir = path.join(EXTERNAL_DIR, entry);
71
+ if (!fs.statSync(dir).isDirectory()) continue;
72
+ const pin = readPin(dir);
73
+ if (!pin) {
74
+ log(`external/${entry}: 缺少有效 .version pin({ repo, ref, path }),跳过`, 'warn');
75
+ continue;
76
+ }
77
+ deps.push({ name: entry, dir, pin });
78
+ }
79
+ return deps;
80
+ }
81
+
82
+ /**
83
+ * 依赖内容完整性:skill 型(上游含 SKILL.md)要求本地 SKILL.md 存在;
84
+ * 其余类型要求除 .version 外至少有一个条目。
85
+ * @param {string} dir
86
+ * @returns {boolean}
87
+ */
88
+ function validateDepDir(dir) {
89
+ if (!fs.existsSync(dir)) return false;
90
+ if (fs.existsSync(path.join(dir, SKILL_MARKER))) return true;
91
+ return fs.readdirSync(dir).some(f => f !== VERSION_FILE_NAME);
92
+ }
93
+
94
+ // ── 上游拉取 ──────────────────────────────────────────────────
95
+
96
+ /** tarball 下载地址(按镜像前缀拼接;ref = commit sha,上游无 skill 粒度 tag) */
97
+ function tarballUrl(mirror, pin) {
98
+ return `${mirror}https://api.github.com/repos/${pin.repo}/tarball/${pin.ref}`;
99
+ }
100
+
101
+ /**
102
+ * 按 .version 的 ref 下载 tarball,解压出 pin.path 子目录,内容覆盖到 external/<name>/。
103
+ * 幂等:pin 一致且内容完整 → 跳过重拉。.version 由本模块改写,不被上游内容覆盖。
104
+ * @param {string[]} [names] - 指定依赖名;省略 = 全部
105
+ * @returns {Promise<Array<{name: string, ref: string, skipped: boolean, error?: string}>>}
106
+ */
107
+ async function fetchDeps(names) {
108
+ const all = listDeps();
109
+ let deps = all;
110
+ if (names && names.length > 0) {
111
+ const known = new Set(all.map(d => d.name));
112
+ const unknown = names.filter(n => !known.has(n));
113
+ if (unknown.length > 0) {
114
+ throw new Error(`未知的 external 依赖: ${unknown.join(', ')}(现有: ${[...known].join(', ') || '无'})`);
115
+ }
116
+ deps = all.filter(d => names.includes(d.name));
117
+ }
118
+
119
+ const results = [];
120
+ for (const dep of deps) {
121
+ try {
122
+ results.push(await fetchDep(dep));
123
+ } catch (err) {
124
+ results.push({ name: dep.name, ref: dep.pin.ref, skipped: false, error: err.message });
125
+ }
126
+ }
127
+ return results;
128
+ }
129
+
130
+ /**
131
+ * 拉取单个依赖。
132
+ * @param {{name: string, dir: string, pin: object}} dep
133
+ * @returns {Promise<{name: string, ref: string, skipped: boolean}>}
134
+ */
135
+ async function fetchDep(dep) {
136
+ const { name, dir, pin } = dep;
137
+
138
+ // pin 一致且内容完整 → 幂等跳过
139
+ if (validateDepDir(dir)) {
140
+ log(`external/${name} 已按 pin ${pin.ref.slice(0, 12)} 就位,跳过重拉`, 'success');
141
+ return { name, ref: pin.ref, skipped: true };
142
+ }
143
+ log(`external/${name} 内容不完整,按 pin ${pin.ref.slice(0, 12)} 拉取...`, 'info');
144
+
145
+ const mirrors = loadMirrors('external');
146
+ log(`正在获取 ${pin.repo}@${pin.ref.slice(0, 12)}(${mirrors.map(m => m || '直连').join(' → ')})...`, 'info');
147
+ const { buffer, mirror } = await fetchWithMirrors(mirrors, m => tarballUrl(m, pin));
148
+ log(`已从 ${mirror || '(直连)'} 下载 ${(buffer.length / 1024).toFixed(0)} KB`, 'info');
149
+
150
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cbb-ext-'));
151
+ try {
152
+ extractTarGz(buffer, tmpDir);
153
+
154
+ // GitHub tarball 顶层目录名格式:<owner>-<repo>-<short-sha>
155
+ const topPrefix = `${pin.repo.replace('/', '-')}-`;
156
+ const tops = fs.readdirSync(tmpDir).filter(f => {
157
+ const p = path.join(tmpDir, f);
158
+ return fs.statSync(p).isDirectory() && f.startsWith(topPrefix);
159
+ });
160
+ if (tops.length === 0) throw new Error(`解压后未找到 ${topPrefix}* 目录`);
161
+
162
+ // pin.path 是 tarball 内相对仓库根的子路径(如 skills/frontend-design)
163
+ const source = path.join(tmpDir, tops[0], pin.path);
164
+ if (!fs.existsSync(source)) {
165
+ throw new Error(`上游 ${pin.repo}@${pin.ref.slice(0, 12)} 中不存在路径 ${pin.path}(ref 或 path 配置有误)`);
166
+ }
167
+
168
+ // 覆盖内容到 external/<name>/(cpSync 合并,.version 保留)
169
+ fs.cpSync(source, dir, { recursive: true });
170
+ if (!validateDepDir(dir)) throw new Error('拉取后完整性校验未通过');
171
+
172
+ // 刷新 pin 时间戳(ref 不变)
173
+ fs.writeFileSync(
174
+ path.join(dir, VERSION_FILE_NAME),
175
+ JSON.stringify({ ...pin, updatedAt: new Date().toISOString() }, null, 2) + '\n'
176
+ );
177
+
178
+ log(`external/${name} 已更新: ${pin.repo}@${pin.ref.slice(0, 12)}`, 'success');
179
+ return { name, ref: pin.ref, skipped: false };
180
+ } finally {
181
+ try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch (_) { /* ignore */ }
182
+ }
183
+ }
184
+
185
+ /**
186
+ * 检查全部依赖上游是否有新提交(只读,不改动任何文件)。
187
+ * 对比 pinned sha 与上游 main 分支 pin.path 的最新提交 sha
188
+ * (commits?path= 只统计该子路径的变更,仓库其他目录的提交不告警)。
189
+ * 单依赖失败不阻塞其他依赖(返回 {name, error} 条目)。
190
+ * @returns {Promise<Array<{name: string, current: string, latest: string, outdated: boolean}|{name: string, error: string}>>}
191
+ */
192
+ async function checkUpstream() {
193
+ const deps = listDeps();
194
+ const results = await Promise.allSettled(deps.map(async dep => {
195
+ const mirrors = loadMirrors('external');
196
+ const { buffer } = await fetchWithMirrors(mirrors, m =>
197
+ `${m}https://api.github.com/repos/${dep.pin.repo}/commits?path=${encodeURIComponent(dep.pin.path)}&sha=main&per_page=1`
198
+ );
199
+ const data = JSON.parse(buffer.toString('utf-8'));
200
+ const latest = Array.isArray(data) && data[0] ? data[0].sha : null;
201
+ return {
202
+ name: dep.name,
203
+ current: dep.pin.ref,
204
+ latest,
205
+ outdated: Boolean(latest && latest !== dep.pin.ref),
206
+ };
207
+ }));
208
+ return results.map((r, i) => r.status === 'fulfilled'
209
+ ? r.value
210
+ : { name: deps[i].name, error: r.reason.message });
211
+ }
212
+
213
+ // ── 导出 ──────────────────────────────────────────────────────
214
+
215
+ module.exports = {
216
+ // 常量
217
+ EXTERNAL_DIR,
218
+ VERSION_FILE_NAME,
219
+ SKILL_MARKER,
220
+
221
+ // 核心函数
222
+ listDeps,
223
+ readPin,
224
+ validateDepDir,
225
+ fetchDeps,
226
+ checkUpstream,
227
+ tarballUrl,
228
+ };
@@ -0,0 +1,92 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * AGENTS.md 托管导航块(agentsNav)。
5
+ *
6
+ * MiniMax Code 不读取 .minimax/rules(源码无该逻辑),其项目级规则注入通道
7
+ * 只有工作区根 AGENTS.md(local-runtime/src/project/instructions.ts,32KB 预算)。
8
+ * 安装时在 AGENTS.md 注入一句托管导航,引导 AI 先读取规则;块形如:
9
+ *
10
+ * <!-- cbb:agents-nav:minimax begin -->
11
+ * ⚠️ 规则优先:……
12
+ * <!-- cbb:agents-nav:minimax end -->
13
+ *
14
+ * 约束:
15
+ * - 幂等:块内容随安装更新,块外用户内容永不改动;
16
+ * - 标记带适配器名,多适配器共存不互撞;
17
+ * - 卸载只摘除本包块;摘除后文件为空(本包创建的情形)则删除文件。
18
+ *
19
+ * AGENTS.md 本身不进清单(cleanup.js 的 PROTECTED_FROM_CLEANUP 同样保护它),
20
+ * 托管块的注入与摘除由 init() 安装流程 / uninstall() 按适配器显式处理。
21
+ */
22
+
23
+ const fs = require('fs');
24
+ const path = require('path');
25
+
26
+ /** 转义正则元字符(适配器名入正则上下文) */
27
+ function escapeRe(value) {
28
+ return String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
29
+ }
30
+
31
+ function markersFor(adapterName) {
32
+ const safe = escapeRe(adapterName);
33
+ return {
34
+ begin: `<!-- cbb:agents-nav:${adapterName} begin -->`,
35
+ end: `<!-- cbb:agents-nav:${adapterName} end -->`,
36
+ // [\s\S]*? 非贪婪:块内可含任意内容(含 \r),遇最近的 end 标记即止
37
+ blockRe: new RegExp(`<!-- cbb:agents-nav:${safe} begin -->[\\s\\S]*?<!-- cbb:agents-nav:${safe} end -->`, 'm'),
38
+ };
39
+ }
40
+
41
+ /**
42
+ * 注入(或更新)导航块。
43
+ * @param {string} filePath 目标 AGENTS.md 绝对路径
44
+ * @param {string} adapterName 适配器名(标记命名空间)
45
+ * @param {string} text 导航句
46
+ * @returns {'created'|'updated'|'unchanged'} created = 文件原不存在或为空白
47
+ */
48
+ function inject(filePath, adapterName, text) {
49
+ const { begin, end, blockRe } = markersFor(adapterName);
50
+ const existed = fs.existsSync(filePath);
51
+ const content = existed ? fs.readFileSync(filePath, 'utf-8') : '';
52
+ // 行尾与既有文件保持一致(Windows 生成的 AGENTS.md 多为 CRLF)
53
+ const eol = content.includes('\r\n') ? '\r\n' : '\n';
54
+ const block = `${begin}${eol}${text}${eol}${end}`;
55
+
56
+ if (blockRe.test(content)) {
57
+ const next = content.replace(blockRe, block);
58
+ if (next === content) return 'unchanged';
59
+ fs.writeFileSync(filePath, next);
60
+ return 'updated';
61
+ }
62
+
63
+ const wasBlank = content.trim() === '';
64
+ const base = content.replace(/\n*$/, ''); // 收敛尾部空行,块另起段落
65
+ const next = wasBlank ? `${block}${eol}` : `${base}${eol}${eol}${block}${eol}`;
66
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
67
+ fs.writeFileSync(filePath, next);
68
+ return wasBlank && !existed ? 'created' : wasBlank ? 'updated' : 'created';
69
+ }
70
+
71
+ /**
72
+ * 精确摘除导航块(块外内容不动)。
73
+ * @returns {boolean} 是否有改动(块不存在 → false)
74
+ */
75
+ function remove(filePath, adapterName) {
76
+ if (!fs.existsSync(filePath)) return false;
77
+ const { blockRe } = markersFor(adapterName);
78
+ const content = fs.readFileSync(filePath, 'utf-8');
79
+ if (!blockRe.test(content)) return false;
80
+
81
+ // 只删块本身;随后收敛尾部空白(块移除后的残留空行)
82
+ const next = content.replace(blockRe, '').replace(/\s+$/, '');
83
+ if (next.trim() === '') {
84
+ // 文件只剩本包内容(由本包创建的情形)→ 连文件一起删除
85
+ fs.unlinkSync(filePath);
86
+ return true;
87
+ }
88
+ fs.writeFileSync(filePath, next + '\n');
89
+ return true;
90
+ }
91
+
92
+ module.exports = { inject, remove };
@@ -2,6 +2,11 @@
2
2
  * Claude Code 适配器
3
3
  *
4
4
  * 将编码规范分发到 .claude/rules/ 目录
5
+ *
6
+ * 退市(selectable: false):团队当前不使用 Claude Code,不在安装面板提供。
7
+ * 适配器代码与全部能力保留(含用户级 settings hook);旧记录中的遗留内容
8
+ * 不在勾选范围时随清单差集与用户级差集清理移除(ADR 0006)。
9
+ * 恢复入口:删掉 selectable 即可重新出现在面板(决策:不留参数开关)。
5
10
  */
6
11
 
7
12
  const path = require('path');
@@ -13,8 +18,10 @@ module.exports = {
13
18
  rulesDir: '.claude/rules',
14
19
  skillsDir: '.claude/skills',
15
20
  commandsDir: '.claude/commands/opsx',
16
- // 用户级 settings,写入全局生效,不污染项目目录
21
+ // 用户级 settings,写入全局生效,不污染项目目录
17
22
  settingsFile: path.join(os.homedir(), '.claude', 'settings.json'),
18
23
  settingsFileIsUserLevel: true,
19
24
  hookEvents: ['UserPromptSubmit'],
25
+ // 退市:不在安装面板提供(代码保留;旧记录遗留随清单/用户级差集清理,ADR 0006)
26
+ selectable: false,
20
27
  };
@@ -0,0 +1,105 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * openspec 命令 → skill 形态转换(commandsAsSkills 适配器用,当前仅 MiniMax Code)。
5
+ *
6
+ * MiniMax Code 无项目级 commands 目录,skill 即其命令形态:每个 openspec 命令
7
+ * 落为 <skillsDir>/<prefix><name>/SKILL.md,目录名 = frontmatter name(Agent
8
+ * Skills 约定),/opsx-propose 斜杠照常可用。
9
+ *
10
+ * 同时承接命令 frontmatter 消毒(sanitizeCommandFrontmatter):opencode 新版
11
+ * 校验命令配置(description 必须为 string | undefined),上游个别命令
12
+ * (openspec/commands/update.md)的 description 为空值(YAML 解析为 null),
13
+ * 会拒绝加载整个项目配置。此处对部署副本回填正文首句作为描述(超过 120 字
14
+ * 截断),上游源文件不动(CLI 接管区)。
15
+ */
16
+
17
+ const path = require('path');
18
+
19
+ /**
20
+ * 命令文件 → skill 目录名(文件名基准,保证目录名稳定)。
21
+ * @param {string} fileName 如 propose.md
22
+ * @param {string} prefix 如 opsx-
23
+ * @returns {string} 如 opsx-propose
24
+ */
25
+ function skillNameFor(fileName, prefix) {
26
+ return (prefix || '') + path.basename(fileName, '.md');
27
+ }
28
+
29
+ /** 取 frontmatter(首个 --- 到闭合 --- 之间);无 frontmatter 返回 -1 */
30
+ function findFrontmatterEnd(lines) {
31
+ if ((lines[0] || '').trim() !== '---') return -1;
32
+ for (let i = 1; i < lines.length; i++) {
33
+ if ((lines[i] || '').trim() === '---') return i;
34
+ }
35
+ return -1;
36
+ }
37
+
38
+ /**
39
+ * 命令 frontmatter 消毒(见文件头注释)。无 frontmatter 或无需改动时原样返回。
40
+ */
41
+ function sanitizeCommandFrontmatter(content) {
42
+ const lines = content.split('\n');
43
+ if ((lines[0] || '').trim() !== '---') return content;
44
+ const end = findFrontmatterEnd(lines);
45
+ if (end === -1) return content;
46
+ let sentence = '';
47
+ for (let i = end + 1; i < lines.length; i++) {
48
+ const t = (lines[i] || '').trim();
49
+ if (t) { sentence = t; break; }
50
+ }
51
+ let changed = false;
52
+ for (let i = 1; i < end; i++) {
53
+ if (/^description:\s*(null|~)?\s*\r?$/.test(lines[i])) {
54
+ if (sentence) {
55
+ if (sentence.length > 120) sentence = sentence.slice(0, 117) + '...';
56
+ lines[i] = 'description: ' + JSON.stringify(sentence) + (lines[i].endsWith('\r') ? '\r' : '');
57
+ } else {
58
+ lines[i] = null; // 无正文可回填则移除(undefined 同样合法)
59
+ }
60
+ changed = true;
61
+ }
62
+ }
63
+ return changed ? lines.filter(l => l !== null).join('\n') : content;
64
+ }
65
+
66
+ /**
67
+ * 把命令内容转换为 skill 的 SKILL.md 内容:
68
+ * 1. frontmatter description 消毒(null → 回填正文首句);
69
+ * 2. frontmatter name 强制为 skillName —— 目录名 = name 是 Agent Skills 约定,
70
+ * 也规避上游标题与文件名不一致(如 ff.md 的 "OPSX: Fast Forward")。
71
+ *
72
+ * 上游 frontmatter 的其余字段(category / tags 等)原样保留。
73
+ *
74
+ * @param {string} content 命令原文(调用方已应用 contentRewrites)
75
+ * @param {string} skillName 目标 skill 名(= 目录名)
76
+ * @returns {string} SKILL.md 内容
77
+ */
78
+ function convertCommandToSkill(content, skillName) {
79
+ const sanitized = sanitizeCommandFrontmatter(content);
80
+ const lines = sanitized.split('\n');
81
+
82
+ // 无 frontmatter:补建(name 必填;description 回填正文首句)
83
+ if ((lines[0] || '').trim() !== '---') {
84
+ const sentence = (lines.find(l => l.trim()) || '').trim().slice(0, 120);
85
+ const fm = ['---', `name: "${skillName}"`];
86
+ if (sentence) fm.push(`description: ${JSON.stringify(sentence)}`);
87
+ fm.push('---', '');
88
+ return fm.concat(lines).join('\n');
89
+ }
90
+
91
+ const end = findFrontmatterEnd(lines);
92
+ if (end === -1) return sanitized; // 开了没闭合的异常 frontmatter:不动
93
+
94
+ let nameLine = -1;
95
+ for (let i = 1; i < end; i++) {
96
+ if (/^name:/.test(lines[i])) { nameLine = i; break; }
97
+ }
98
+ const nameValue = `name: "${skillName}"`;
99
+ if (nameLine === -1) lines.splice(1, 0, nameValue); // 缺 name:紧跟开标记插入
100
+ else lines[nameLine] = nameValue + (lines[nameLine].endsWith('\r') ? '\r' : '');
101
+
102
+ return lines.join('\n');
103
+ }
104
+
105
+ module.exports = { skillNameFor, sanitizeCommandFrontmatter, convertCommandToSkill };