@jspg-ai/coding-bb 0.0.1 → 0.0.2-beta.23

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 (145) hide show
  1. package/cbb/bin/cbb.js +6 -0
  2. package/cbb/bin/cbbm.js +6 -0
  3. package/cbb/dev-standards/rules/cbb-ai-behavior.md +103 -103
  4. package/cbb/dev-standards/rules/cbb-priority.md +55 -55
  5. package/cbb/lib/install/claude-code.js +2 -0
  6. package/cbb/lib/install/init.js +496 -290
  7. package/cbb/lib/install/opencode.js +31 -0
  8. package/cbb/lib/install/qoder.js +2 -0
  9. package/cbb/lib/install/workspaces.js +53 -13
  10. package/cbb/lib/openspec/index.js +3 -3
  11. package/cbb/lib/superpowers/index.js +1 -1
  12. package/cbb/lib/utils/check-update.js +16 -14
  13. package/cbb/lib/utils/gitignore.js +2 -1
  14. package/cbb/lib/utils/output.js +92 -4
  15. package/cbb/lib/utils/version.js +2 -31
  16. package/cbb/lib/wiki/cli.js +25 -25
  17. package/cbb/lib/wiki/index.js +16 -16
  18. package/cbb/tools/cbb-design-to-wiki/SKILL.md +14 -14
  19. package/cbb/tools/cbb-wiki-ops/SKILL.md +22 -22
  20. package/cbb/worktrees/_shared/scripts/find-target-worktree.js +2 -2
  21. package/cbb/worktrees/_shared/scripts/find-workspace-root.js +7 -7
  22. package/cbb/worktrees/_shared/scripts/push-core.js +18 -0
  23. package/cbb/worktrees/commands/{close.md → worktree-close.md} +8 -8
  24. package/cbb/worktrees/commands/{init.md → worktree-init.md} +9 -9
  25. package/cbb/worktrees/commands/{push.md → worktree-push.md} +3 -3
  26. package/cbb/worktrees/skills/{openspec-close-worktree → cbb-worktree-close}/SKILL.md +20 -20
  27. package/cbb/worktrees/skills/{openspec-close-worktree → cbb-worktree-close}/scripts/check-env.js +4 -4
  28. package/cbb/worktrees/skills/{openspec-close-worktree → cbb-worktree-close}/scripts/check-unarchived.js +1 -1
  29. package/cbb/worktrees/skills/{openspec-close-worktree → cbb-worktree-close}/scripts/delete-branches.js +3 -3
  30. package/cbb/worktrees/skills/{openspec-close-worktree → cbb-worktree-close}/scripts/discover-apps.js +6 -6
  31. package/cbb/worktrees/skills/{openspec-extend-worktree → cbb-worktree-close}/scripts/find-target-worktree.js +2 -2
  32. package/cbb/worktrees/skills/{openspec-push-worktrees → cbb-worktree-close}/scripts/find-workspace-root.js +7 -7
  33. package/cbb/worktrees/skills/{openspec-close-worktree → cbb-worktree-close}/scripts/remove-worktrees.js +4 -4
  34. package/cbb/worktrees/skills/{openspec-close-worktree → cbb-worktree-close}/scripts/safety-check.js +4 -4
  35. package/cbb/worktrees/skills/{openspec-init-worktree → cbb-worktree-init}/SKILL.md +81 -107
  36. package/cbb/worktrees/skills/{openspec-init-worktree → cbb-worktree-init}/scripts/auto-open.js +1 -1
  37. package/cbb/worktrees/skills/{openspec-init-worktree → cbb-worktree-init}/scripts/check-env-deep.js +4 -4
  38. package/cbb/worktrees/skills/{openspec-init-worktree → cbb-worktree-init}/scripts/check-env.js +6 -6
  39. package/cbb/worktrees/skills/{openspec-init-worktree → cbb-worktree-init}/scripts/check-repos.js +4 -4
  40. package/cbb/worktrees/skills/{openspec-init-worktree → cbb-worktree-init}/scripts/create-branches.js +4 -4
  41. package/cbb/worktrees/skills/{openspec-init-worktree → cbb-worktree-init}/scripts/create-worktrees.js +8 -7
  42. package/cbb/worktrees/skills/{openspec-close-worktree → cbb-worktree-init}/scripts/find-workspace-root.js +7 -7
  43. package/cbb/worktrees/skills/{openspec-init-worktree → cbb-worktree-init}/scripts/install-ai.js +33 -33
  44. package/cbb/worktrees/skills/{openspec-init-worktree → cbb-worktree-init}/scripts/parse-config.js +7 -7
  45. package/cbb/worktrees/skills/{openspec-init-worktree → cbb-worktree-init}/scripts/push-branches.js +9 -7
  46. package/cbb/worktrees/skills/{openspec-extend-worktree → cbb-worktree-init}/scripts/push-core.js +18 -0
  47. package/cbb/worktrees/skills/{openspec-init-worktree → cbb-worktree-init}/scripts/sync-repos.js +10 -12
  48. package/cbb/worktrees/skills/{openspec-init-worktree → cbb-worktree-init}/scripts/update-gitignore.js +19 -6
  49. package/cbb/worktrees/skills/{openspec-push-worktrees → cbb-worktree-push}/SKILL.md +16 -16
  50. package/cbb/worktrees/skills/{openspec-push-worktrees → cbb-worktree-push}/scripts/commit-worktrees.js +2 -2
  51. package/cbb/worktrees/skills/{openspec-push-worktrees → cbb-worktree-push}/scripts/find-target-worktree.js +2 -2
  52. package/cbb/worktrees/skills/{openspec-extend-worktree → cbb-worktree-push}/scripts/find-workspace-root.js +7 -7
  53. package/cbb/worktrees/skills/{openspec-push-worktrees → cbb-worktree-push}/scripts/push-core.js +18 -0
  54. package/cbb/worktrees/skills/{openspec-push-worktrees → cbb-worktree-push}/scripts/push-worktrees.js +7 -5
  55. package/config/workspace-config.sample.json +18 -0
  56. package/config/workspaces.json +11 -6
  57. package/openspec/.version +6 -6
  58. package/openspec/commands/apply.md +7 -7
  59. package/openspec/commands/archive.md +7 -7
  60. package/openspec/commands/bulk-archive.md +7 -7
  61. package/openspec/commands/continue.md +7 -7
  62. package/openspec/commands/explore.md +7 -7
  63. package/openspec/commands/ff.md +7 -7
  64. package/openspec/commands/new.md +7 -7
  65. package/openspec/commands/onboard.md +7 -7
  66. package/openspec/commands/propose.md +7 -7
  67. package/openspec/commands/sync.md +7 -7
  68. package/openspec/commands/update.md +7 -7
  69. package/openspec/commands/verify.md +7 -7
  70. package/openspec/skills/openspec-apply-change/SKILL.md +187 -187
  71. package/openspec/skills/openspec-archive-change/SKILL.md +181 -181
  72. package/openspec/skills/openspec-bulk-archive-change/SKILL.md +338 -338
  73. package/openspec/skills/openspec-continue-change/SKILL.md +117 -117
  74. package/openspec/skills/openspec-explore/SKILL.md +342 -342
  75. package/openspec/skills/openspec-ff-change/SKILL.md +116 -116
  76. package/openspec/skills/openspec-new-change/SKILL.md +76 -76
  77. package/openspec/skills/openspec-onboard/SKILL.md +560 -560
  78. package/openspec/skills/openspec-propose/SKILL.md +162 -162
  79. package/openspec/skills/openspec-sync-specs/SKILL.md +261 -261
  80. package/openspec/skills/openspec-update-change/SKILL.md +90 -90
  81. package/openspec/skills/openspec-verify-change/SKILL.md +174 -174
  82. package/package.json +4 -3
  83. package/superpowers/.version +6 -6
  84. package/superpowers/skills/brainstorming/SKILL.md +250 -250
  85. package/superpowers/skills/brainstorming/scripts/helper.js +167 -167
  86. package/superpowers/skills/brainstorming/scripts/start-server.sh +209 -209
  87. package/superpowers/skills/brainstorming/scripts/stop-server.sh +120 -120
  88. package/superpowers/skills/brainstorming/spec-document-reviewer-prompt.md +49 -49
  89. package/superpowers/skills/brainstorming/visual-companion.md +299 -299
  90. package/superpowers/skills/dispatching-parallel-agents/SKILL.md +167 -167
  91. package/superpowers/skills/executing-plans/SKILL.md +64 -64
  92. package/superpowers/skills/finishing-a-development-branch/SKILL.md +225 -225
  93. package/superpowers/skills/receiving-code-review/SKILL.md +205 -205
  94. package/superpowers/skills/requesting-code-review/SKILL.md +95 -95
  95. package/superpowers/skills/requesting-code-review/code-reviewer.md +181 -181
  96. package/superpowers/skills/subagent-driven-development/SKILL.md +568 -568
  97. package/superpowers/skills/subagent-driven-development/implementer-prompt.md +154 -154
  98. package/superpowers/skills/subagent-driven-development/re-review-prompt.md +115 -115
  99. package/superpowers/skills/subagent-driven-development/task-reviewer-prompt.md +207 -207
  100. package/superpowers/skills/systematic-debugging/CREATION-LOG.md +119 -119
  101. package/superpowers/skills/systematic-debugging/SKILL.md +283 -283
  102. package/superpowers/skills/systematic-debugging/condition-based-waiting-example.ts +158 -158
  103. package/superpowers/skills/systematic-debugging/condition-based-waiting.md +115 -115
  104. package/superpowers/skills/systematic-debugging/defense-in-depth.md +122 -122
  105. package/superpowers/skills/systematic-debugging/find-polluter.sh +72 -72
  106. package/superpowers/skills/systematic-debugging/root-cause-tracing.md +169 -169
  107. package/superpowers/skills/systematic-debugging/test-academic.md +14 -14
  108. package/superpowers/skills/systematic-debugging/test-pressure-1.md +58 -58
  109. package/superpowers/skills/systematic-debugging/test-pressure-2.md +68 -68
  110. package/superpowers/skills/systematic-debugging/test-pressure-3.md +69 -69
  111. package/superpowers/skills/test-driven-development/SKILL.md +320 -320
  112. package/superpowers/skills/test-driven-development/writing-good-tests.md +198 -198
  113. package/superpowers/skills/using-git-worktrees/SKILL.md +167 -167
  114. package/superpowers/skills/using-superpowers/SKILL.md +63 -63
  115. package/superpowers/skills/using-superpowers/references/antigravity-tools.md +23 -23
  116. package/superpowers/skills/using-superpowers/references/codex-tools.md +108 -108
  117. package/superpowers/skills/using-superpowers/references/gemini-tools.md +63 -63
  118. package/superpowers/skills/using-superpowers/references/hermes-tools.md +56 -56
  119. package/superpowers/skills/using-superpowers/references/pi-tools.md +16 -16
  120. package/superpowers/skills/verification-before-completion/SKILL.md +120 -120
  121. package/superpowers/skills/writing-plans/SKILL.md +171 -171
  122. package/superpowers/skills/writing-plans/plan-document-reviewer-prompt.md +49 -49
  123. package/superpowers/skills/writing-skills/SKILL.md +679 -679
  124. package/superpowers/skills/writing-skills/anthropic-best-practices.md +1150 -1150
  125. package/superpowers/skills/writing-skills/examples/CLAUDE_MD_TESTING.md +189 -189
  126. package/superpowers/skills/writing-skills/persuasion-principles.md +187 -187
  127. package/superpowers/skills/writing-skills/render-graphs.js +169 -169
  128. package/superpowers/skills/writing-skills/testing-skills-with-subagents.md +384 -384
  129. package/cbb/worktrees/commands/extend.md +0 -65
  130. package/cbb/worktrees/skills/openspec-close-worktree/scripts/find-target-worktree.js +0 -96
  131. package/cbb/worktrees/skills/openspec-extend-worktree/SKILL.md +0 -390
  132. package/cbb/worktrees/skills/openspec-extend-worktree/scripts/check-env.js +0 -95
  133. package/cbb/worktrees/skills/openspec-extend-worktree/scripts/check-repos.js +0 -98
  134. package/cbb/worktrees/skills/openspec-extend-worktree/scripts/create-branches-and-worktrees.js +0 -135
  135. package/cbb/worktrees/skills/openspec-extend-worktree/scripts/install-ai.js +0 -150
  136. package/cbb/worktrees/skills/openspec-extend-worktree/scripts/list-available-apps.js +0 -88
  137. package/cbb/worktrees/skills/openspec-extend-worktree/scripts/sync-repos.js +0 -93
  138. package/cbb/worktrees/skills/openspec-init-worktree/scripts/find-workspace-root.js +0 -67
  139. package/cbb/worktrees/skills/openspec-init-worktree/scripts/generate-app-options.js +0 -93
  140. package/cbb/worktrees/skills/openspec-init-worktree/scripts/push-core.js +0 -136
  141. package/cbb/worktrees/skills/openspec-push-worktrees/scripts/silence-popup.js +0 -23
  142. package/config/config.sample.json +0 -16
  143. /package/cbb/worktrees/skills/{openspec-close-worktree → cbb-worktree-close}/scripts/silence-popup.js +0 -0
  144. /package/cbb/worktrees/skills/{openspec-extend-worktree → cbb-worktree-init}/scripts/silence-popup.js +0 -0
  145. /package/cbb/worktrees/skills/{openspec-init-worktree → cbb-worktree-push}/scripts/silence-popup.js +0 -0
@@ -0,0 +1,31 @@
1
+ /**
2
+ * opencode 适配器
3
+ *
4
+ * 将编码规范分发到 .opencode/ 目录(opencode 项目级配置域)。
5
+ *
6
+ * 与 Claude Code / Qoder 的映射差异:
7
+ * - rules:opencode 无原生 rules 目录,部署到 .opencode/rules/ 并由安装流程
8
+ * 将 glob 注册进项目根 opencode.json 的 instructions 字段(官方支持的加载方式)
9
+ * - skills:.opencode/skills/<name>/SKILL.md,结构与 Claude 同构
10
+ * - commands:命令名 = 文件名(嵌套目录无命名空间,且 init.md 等会撞内置 /init),
11
+ * 因此扁平化为 opsx-<name>.md(/opsx-propose);worktree 管理命令同理扁平化为
12
+ * cbb-<name>.md(/cbb-worktree-init),部署副本中的 /opsx: 与 /cbb: 引用同步改写
13
+ * - hooks:opencode 无 UserPromptSubmit 等价事件,版本检查 hook 不安装(settingsFile 留空即跳过)
14
+ */
15
+
16
+ module.exports = {
17
+ name: 'opencode',
18
+ displayName: 'opencode',
19
+ rulesDir: '.opencode/rules',
20
+ skillsDir: '.opencode/skills',
21
+ commandsDir: '.opencode/commands',
22
+ // 部署时给命令文件名加此前缀(规避内置命令名冲突)
23
+ commandPrefix: 'opsx-',
24
+ // worktree 管理命令(非 openspec 上游)落在同一扁平目录,用 cbb- 前缀区分
25
+ worktreeCommandsDir: '.opencode/commands',
26
+ worktreeCommandPrefix: 'cbb-',
27
+ // 部署副本中将该引用串改写为 opencode 的命令风格(配合 commandPrefix)
28
+ contentRewrites: [['/opsx:', '/opsx-'], ['/cbb:', '/cbb-']],
29
+ // rules 加载登记:注入项目根 opencode.json 的 instructions(glob,相对项目根)
30
+ instructionsPattern: '.opencode/rules/*.md',
31
+ };
@@ -13,6 +13,8 @@ module.exports = {
13
13
  rulesDir: '.qoder/rules',
14
14
  skillsDir: '.qoder/skills',
15
15
  commandsDir: '.qoder/commands/opsx',
16
+ // worktree 管理命令(非 openspec 上游):独立命名空间 /cbb:worktree-*
17
+ worktreeCommandsDir: '.qoder/commands/cbb',
16
18
  // 用户级 settings,写入全局生效,不污染项目目录
17
19
  settingsFile: path.join(os.homedir(), '.qoder', 'settings.json'),
18
20
  settingsFileIsUserLevel: true,
@@ -15,7 +15,7 @@ const path = require('path');
15
15
  const { execSync } = require('child_process');
16
16
 
17
17
  const DEFAULT_CONFIG = 'config/workspaces.json';
18
- const SAMPLE_CONFIG = 'config/config.sample.json';
18
+ const SAMPLE_CONFIG = 'config/workspace-config.sample.json';
19
19
 
20
20
  /**
21
21
  * 归一化 git remote URL:提取 "namespace/repo" 形式(去掉 .git 后缀)。
@@ -102,39 +102,40 @@ function loadWorkspaces(configPath = DEFAULT_CONFIG) {
102
102
  }
103
103
 
104
104
  /**
105
- * 校验业务工作空间 CONFIG.json 的格式是否合法。
105
+ * 校验业务工作空间 workspace-config.json 的格式是否合法。
106
106
  *
107
107
  * 校验项:
108
108
  * - 文件存在且为合法 JSON
109
109
  * - 含 apps 数组且非空
110
110
  * - 每个 app 对象必须含 name / repo / side 三个字段
111
111
  * - name、repo 为非空字符串;side 必须为 'front' 或 'back'
112
+ * - desc 为可选字段;一旦出现必须是非空字符串(应用职责描述)
112
113
  *
113
- * @param {string} configPath CONFIG.json 的绝对路径
114
+ * @param {string} configPath workspace-config.json 的绝对路径
114
115
  * @returns {{ valid: boolean, message?: string, config?: object }}
115
116
  */
116
117
  function validateWorkspaceConfig(configPath) {
117
118
  if (!fs.existsSync(configPath)) {
118
- return { valid: false, message: `CONFIG.json 不存在:${configPath}` };
119
+ return { valid: false, message: `workspace-config.json 不存在:${configPath}` };
119
120
  }
120
121
 
121
122
  let raw;
122
123
  try {
123
124
  raw = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
124
125
  } catch (e) {
125
- return { valid: false, message: `CONFIG.json 不是合法 JSON:${e.message}` };
126
+ return { valid: false, message: `workspace-config.json 不是合法 JSON:${e.message}` };
126
127
  }
127
128
 
128
129
  if (!raw || typeof raw !== 'object') {
129
- return { valid: false, message: 'CONFIG.json 内容应为 JSON 对象' };
130
+ return { valid: false, message: 'workspace-config.json 内容应为 JSON 对象' };
130
131
  }
131
132
 
132
133
  if (!Array.isArray(raw.apps)) {
133
- return { valid: false, message: 'CONFIG.json 缺少 apps 数组' };
134
+ return { valid: false, message: 'workspace-config.json 缺少 apps 数组' };
134
135
  }
135
136
 
136
137
  if (raw.apps.length === 0) {
137
- return { valid: false, message: 'CONFIG.json 中 apps 数组为空,请至少配置一个关联应用' };
138
+ return { valid: false, message: 'workspace-config.json 中 apps 数组为空,请至少配置一个关联应用' };
138
139
  }
139
140
 
140
141
  const VALID_SIDES = ['front', 'back'];
@@ -143,19 +144,22 @@ function validateWorkspaceConfig(configPath) {
143
144
  const label = `第 ${i + 1} 项`;
144
145
 
145
146
  if (!app || typeof app !== 'object') {
146
- return { valid: false, message: `CONFIG.json ${label}不是对象` };
147
+ return { valid: false, message: `workspace-config.json ${label}不是对象` };
147
148
  }
148
149
  if (typeof app.name !== 'string' || !app.name.trim()) {
149
- return { valid: false, message: `CONFIG.json ${label}缺少 name(应用名)` };
150
+ return { valid: false, message: `workspace-config.json ${label}缺少 name(应用名)` };
150
151
  }
151
152
  if (typeof app.repo !== 'string' || !app.repo.trim()) {
152
- return { valid: false, message: `CONFIG.json "${app.name}" 缺少 repo(Git 仓库地址)` };
153
+ return { valid: false, message: `workspace-config.json "${app.name}" 缺少 repo(Git 仓库地址)` };
153
154
  }
154
155
  if (typeof app.side !== 'string' || !app.side.trim()) {
155
- return { valid: false, message: `CONFIG.json "${app.name}" 缺少 side(仓库类型,可选值:front / back)` };
156
+ return { valid: false, message: `workspace-config.json "${app.name}" 缺少 side(仓库类型,可选值:front / back)` };
156
157
  }
157
158
  if (!VALID_SIDES.includes(app.side.trim())) {
158
- return { valid: false, message: `CONFIG.json "${app.name}" 的 side 字段值无效:"${app.side}"(可选值:front / back)` };
159
+ return { valid: false, message: `workspace-config.json "${app.name}" 的 side 字段值无效:"${app.side}"(可选值:front / back)` };
160
+ }
161
+ if (app.desc !== undefined && (typeof app.desc !== 'string' || !app.desc.trim())) {
162
+ return { valid: false, message: `workspace-config.json "${app.name}" 的 desc 字段应为非空字符串(该应用的职责描述),不需要可整条删除` };
159
163
  }
160
164
  }
161
165
 
@@ -219,6 +223,39 @@ function extractRepoBasename(url) {
219
223
  return parts.length > 0 ? parts[parts.length - 1] : '';
220
224
  }
221
225
 
226
+ /**
227
+ * 判断 repo 是否为样例占位地址(RFC 2606 保留域名)。
228
+ * 样例 workspace-config.json 的 repo 用 example.com 等占位,clone 必然失败;
229
+ * setup 据此逐条跳过空跑并给出明确编辑提示。
230
+ * 支持 SSH(git@host:path)与 URL(ssh://、https://、git://)两种形式的 host 提取。
231
+ *
232
+ * 注意:不含 `.localhost`——本地 Git 服务(`git@localhost:<ns>/<repo>.git`)是真实可用地址,
233
+ * 误判会导致正常应用的代码被跳过。
234
+ */
235
+ const SAMPLE_HOST_RE = /(?:^|\.)(?:example\.(?:com|org|net)|test|invalid)$/i;
236
+
237
+ function getRepoHost(repo) {
238
+ if (typeof repo !== 'string') return '';
239
+ let s = repo.trim().replace(/^[a-z]+:\/\//i, '');
240
+ const at = s.lastIndexOf('@');
241
+ if (at >= 0) s = s.slice(at + 1);
242
+ const m = s.match(/^[^:/]+/);
243
+ return m ? m[0].toLowerCase() : '';
244
+ }
245
+
246
+ function isSampleRepo(repo) {
247
+ const host = getRepoHost(repo);
248
+ return !!host && SAMPLE_HOST_RE.test(host);
249
+ }
250
+
251
+ /** 从 apps 数组中挑出占位条目,返回 [{ name, repo }](非数组输入返回 []) */
252
+ function findSampleApps(apps) {
253
+ if (!Array.isArray(apps)) return [];
254
+ return apps
255
+ .filter(a => a && typeof a === 'object' && isSampleRepo(a.repo))
256
+ .map(a => ({ name: a.name, repo: a.repo }));
257
+ }
258
+
222
259
  module.exports = {
223
260
  DEFAULT_CONFIG,
224
261
  SAMPLE_CONFIG,
@@ -229,4 +266,7 @@ module.exports = {
229
266
  readGitRemotes,
230
267
  detectRepo,
231
268
  extractRepoBasename,
269
+ getRepoHost,
270
+ isSampleRepo,
271
+ findSampleApps,
232
272
  };
@@ -5,7 +5,7 @@
5
5
  *
6
6
  * 负责从 GitHub (Fission-AI/OpenSpec) 拉取官方 workflow 模板,
7
7
  * 渲染并部署到本项目的 openspec/{commands,skills} 目录。
8
- * 提供 `cbb update --upstream-openspec` 选项。
8
+ * 提供 `cbbm update --upstream-openspec` 选项。
9
9
  *
10
10
  * 升级范围严格限定:只有 WORKFLOWS 白名单中的 commands/*.md 和
11
11
  * skills/<workflow>/SKILL.md 会被覆盖写入。config/openspec/(schemas + config.yaml)、
@@ -75,9 +75,9 @@ function assertSafeDestination(destPath, op) {
75
75
  for (const prefix of PROTECTED_PATH_PREFIXES) {
76
76
  if (abs === prefix || abs.startsWith(prefix + path.sep)) {
77
77
  throw new Error(
78
- `[cbb update --upstream-openspec] ${op} 试图写入受保护路径: ${abs}\n` +
78
+ `[cbbm update --upstream-openspec] ${op} 试图写入受保护路径: ${abs}\n` +
79
79
  `命中前缀: ${prefix}\n` +
80
- `该路径属于项目自有扩展位,绝对禁止被 cbb update --upstream-openspec 覆盖。\n` +
80
+ `该路径属于项目自有扩展位,绝对禁止被 cbbm update --upstream-openspec 覆盖。\n` +
81
81
  `详见 AGENTS.md「原生内容保护与扩展位」一节。`
82
82
  );
83
83
  }
@@ -4,7 +4,7 @@
4
4
  * Superpowers Skills 管理模块
5
5
  *
6
6
  * 负责从 GitHub (obra/superpowers) 下载 skills 并部署到本项目。
7
- * 提供 cbb update --upstream-superpowers 选项,以及 init 时的自动部署能力。
7
+ * 提供 cbbm update --upstream-superpowers 选项,以及 init 时的自动部署能力。
8
8
  */
9
9
 
10
10
  const fs = require('fs');
@@ -10,12 +10,16 @@
10
10
  */
11
11
 
12
12
  const fs = require('fs');
13
+ const os = require('os');
13
14
  const path = require('path');
14
15
 
15
16
  // 共享版本工具(readInstalledVersion / fetchLatest / compareVersions / 常量)
16
17
  const versionUtil = require('./version');
17
18
 
18
- const CACHE_FILE = path.join('.cbb', '.update-check-cache.json');
19
+ // 缓存提升到用户级:registry 最新版本号是全局属性(npm 全局包只有一个版本),
20
+ // 团队多 workspace / 多 worktree 沙箱共享一份,每日真实请求一次;
21
+ // 「本项目是否落后」仍按项目清单版本在本地判定。
22
+ const CACHE_FILE = path.join(os.homedir(), '.cbb', '.update-check-cache.json');
19
23
  const CACHE_TTL = 24 * 60 * 60 * 1000;
20
24
 
21
25
  function getProjectDir() {
@@ -48,20 +52,18 @@ function readInstalledVersion(projectDir, adapterName) {
48
52
  }
49
53
  }
50
54
 
51
- function readCache(projectDir) {
52
- const cachePath = path.join(projectDir, CACHE_FILE);
53
- if (!fs.existsSync(cachePath)) return null;
55
+ function readCache() {
56
+ if (!fs.existsSync(CACHE_FILE)) return null;
54
57
  try {
55
- return JSON.parse(fs.readFileSync(cachePath, 'utf-8'));
58
+ return JSON.parse(fs.readFileSync(CACHE_FILE, 'utf-8'));
56
59
  } catch (_) {
57
60
  return null;
58
61
  }
59
62
  }
60
63
 
61
- function writeCache(projectDir, data) {
62
- const cachePath = path.join(projectDir, CACHE_FILE);
63
- fs.mkdirSync(path.dirname(cachePath), { recursive: true });
64
- fs.writeFileSync(cachePath, JSON.stringify(data) + '\n');
64
+ function writeCache(data) {
65
+ fs.mkdirSync(path.dirname(CACHE_FILE), { recursive: true });
66
+ fs.writeFileSync(CACHE_FILE, JSON.stringify(data) + '\n');
65
67
  }
66
68
 
67
69
  /** 读取 hook 传入的 stdin(session_id);TTY 或异常返回空对象 */
@@ -104,7 +106,7 @@ async function main() {
104
106
  const isPrerelease = versionUtil.detectIsPrerelease(installed);
105
107
  const tag = isPrerelease ? 'beta' : 'latest';
106
108
  const now = Date.now();
107
- let cache = readCache(projectDir) || {};
109
+ let cache = readCache() || {};
108
110
 
109
111
  // 决定最新版本:命中每日缓存且 tag 一致则复用,否则请求 registry
110
112
  let latest = cache.latest;
@@ -116,7 +118,7 @@ async function main() {
116
118
  return; // 网络失败静默
117
119
  }
118
120
  cache = { ...cache, lastCheck: now, latest, tag };
119
- writeCache(projectDir, cache);
121
+ writeCache(cache);
120
122
  }
121
123
 
122
124
  if (!latest || versionUtil.compareVersions(latest, installed) <= 0) return; // 已是最新
@@ -125,8 +127,8 @@ async function main() {
125
127
 
126
128
  // Hook 调用(有 sessionId):以 additionalContext 注入,由 AI 助手提醒用户
127
129
  if (sessionId) {
128
- // 按适配器分别去重,同一会话同一适配器只提示一次
129
- const notifiedKey = adapterName || '_default';
130
+ // 按「项目+适配器」分别去重,同一会话同一项目同一适配器只提示一次
131
+ const notifiedKey = `${projectDir}::${adapterName || '_default'}`;
130
132
  if (!cache.notifiedSessions) cache.notifiedSessions = {};
131
133
  if (cache.notifiedSessions[notifiedKey] === sessionId) return; // 本会话本适配器已提示过
132
134
  process.stdout.write(Buffer.from(JSON.stringify({
@@ -136,7 +138,7 @@ async function main() {
136
138
  },
137
139
  }) + '\n', 'utf8'));
138
140
  cache.notifiedSessions[notifiedKey] = sessionId;
139
- writeCache(projectDir, cache);
141
+ writeCache(cache);
140
142
  return;
141
143
  }
142
144
 
@@ -17,7 +17,8 @@ const GITIGNORE_RULES = [
17
17
  { add: '.claude', accept: ['.claude', '.claude/'] },
18
18
  { add: '.qoder', accept: ['.qoder', '.qoder/'] },
19
19
  { add: '.cbb/', accept: ['.cbb/'] },
20
- { add: '.code/', accept: ['.code/', '.code'] },
20
+ { add: '.codespace/', accept: ['.codespace/', '.codespace'] },
21
+ { add: '.opencode', accept: ['.opencode', '.opencode/'] },
21
22
  ];
22
23
 
23
24
  /** 将 .gitignore 文本拆为行数组。 */
@@ -48,9 +48,94 @@ function section(icon, title) {
48
48
  console.log(c('gray', '─'.repeat(48)));
49
49
  }
50
50
 
51
- /** 以"label + value"两列对齐的方式输出一条字段记录 */
52
- function field(label, value, valueColor) {
53
- console.log(` ${c('gray', (label + ':').padEnd(8, ' '))} ${valueColor ? c(valueColor, value) : value}`);
51
+ /** 视觉宽度:CJK / 全角字符按 2 列计,其余按 1 列(终端对齐用) */
52
+ function visualWidth(text) {
53
+ let w = 0;
54
+ for (const ch of String(text)) {
55
+ const cp = ch.codePointAt(0);
56
+ const wide =
57
+ (cp >= 0x1100 && cp <= 0x115f) ||
58
+ cp === 0x2329 || cp === 0x232a ||
59
+ (cp >= 0x2e80 && cp <= 0xa4cf && cp !== 0x303f) ||
60
+ (cp >= 0xac00 && cp <= 0xd7a3) ||
61
+ (cp >= 0xf900 && cp <= 0xfaff) ||
62
+ (cp >= 0xfe30 && cp <= 0xfe6f) ||
63
+ (cp >= 0xff00 && cp <= 0xff60) ||
64
+ (cp >= 0xffe0 && cp <= 0xffe6);
65
+ w += wide ? 2 : 1;
66
+ }
67
+ return w;
68
+ }
69
+
70
+ /** 按视觉宽度右补空格(中文标签对齐用) */
71
+ function padVisual(text, width) {
72
+ return text + ' '.repeat(Math.max(0, width - visualWidth(text)));
73
+ }
74
+
75
+ // 实时进度行的 spinner 帧(TTY 下循环播放,表示进程仍活着)
76
+ const SPINNER_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
77
+
78
+ /**
79
+ * 长耗时步骤的单行实时进度:显示「正在做什么 + 已耗时」,避免看起来像卡死。
80
+ * TTY:spinner 原地刷新(\r + 清行);非 TTY:内容变化时换行输出(CI 日志友好)。
81
+ * 结束时必须调用 done() 清掉本行,再由调用方补一条最终 step()。
82
+ * @param {string} label 步骤标签(与 step 同宽,如「应用」)
83
+ * @param {{stream?:NodeJS.WriteStream,indent?:number,labelWidth?:number,intervalMs?:number}} [opts]
84
+ * @returns {{update(text:string):void, done():void}}
85
+ */
86
+ function progress(label, opts = {}) {
87
+ const { stream = process.stdout, indent = 2, labelWidth = 10, intervalMs = 80 } = opts;
88
+ const tty = stream.isTTY === true;
89
+ const startedAt = Date.now();
90
+ let frame = 0;
91
+ let text = '';
92
+ let timer = null;
93
+
94
+ const head = icon => ' '.repeat(indent) + c('cyan', icon) + ' ' + c('gray', padVisual(label, labelWidth));
95
+ const elapsed = () => Math.round((Date.now() - startedAt) / 1000);
96
+
97
+ const render = () => {
98
+ const sec = elapsed();
99
+ const value = sec > 0 ? `${text} ${c('gray', sec + 's')}` : text;
100
+ stream.write(`\r\x1b[2K${head(SPINNER_FRAMES[frame++ % SPINNER_FRAMES.length])} ${value}`);
101
+ };
102
+
103
+ return {
104
+ update(next) {
105
+ if (next === text) return;
106
+ text = next;
107
+ if (tty) {
108
+ render();
109
+ if (!timer) {
110
+ timer = setInterval(render, intervalMs);
111
+ if (timer.unref) timer.unref();
112
+ }
113
+ } else {
114
+ stream.write(`${head('·')} ${text}\n`);
115
+ }
116
+ },
117
+ done() {
118
+ if (timer) {
119
+ clearInterval(timer);
120
+ timer = null;
121
+ }
122
+ if (tty) stream.write('\r\x1b[2K');
123
+ text = '';
124
+ },
125
+ };
126
+ }
127
+
128
+ /**
129
+ * 缩进步进行:` ✓ 标签 值`
130
+ * @param {string} icon 图标(✓ / ! / ✗ / ⏳ / ▸ …)
131
+ * @param {string} label 标签(中文;按视觉宽度对齐到 labelWidth)
132
+ * @param {string} [value] 值文本
133
+ * @param {{iconColor?:string,labelColor?:string,valueColor?:string,indent?:number,labelWidth?:number}} [opts]
134
+ */
135
+ function step(icon, label, value = '', opts = {}) {
136
+ const { iconColor = 'green', labelColor = 'gray', valueColor = null, indent = 2, labelWidth = 10 } = opts;
137
+ const head = ' '.repeat(indent) + c(iconColor, icon) + ' ' + c(labelColor, padVisual(label, labelWidth));
138
+ console.log(value ? `${head} ${valueColor ? c(valueColor, value) : value}` : head);
54
139
  }
55
140
 
56
141
  module.exports = {
@@ -60,5 +145,8 @@ module.exports = {
60
145
  LOG_STYLES,
61
146
  log,
62
147
  section,
63
- field,
148
+ visualWidth,
149
+ padVisual,
150
+ progress,
151
+ step,
64
152
  };
@@ -12,7 +12,7 @@
12
12
 
13
13
  const fs = require('fs');
14
14
  const path = require('path');
15
- const http = require('http');
15
+ const https = require('https');
16
16
 
17
17
  const PKG_NAME = '@jspg-ai/coding-bb';
18
18
  const REGISTRY = 'https://registry.npmjs.org';
@@ -41,7 +41,7 @@ function readInstalledVersion(projectDir) {
41
41
  function fetchLatest(tag) {
42
42
  return new Promise((resolve, reject) => {
43
43
  const url = `${REGISTRY}/${encodeURIComponent(PKG_NAME).replace('%2F', '/')}`;
44
- const req = http.get(url, { timeout: REQUEST_TIMEOUT }, res => {
44
+ const req = https.get(url, { timeout: REQUEST_TIMEOUT }, res => {
45
45
  if (res.statusCode !== 200) { res.resume(); reject(new Error(`status ${res.statusCode}`)); return; }
46
46
  let body = '';
47
47
  res.setEncoding('utf-8');
@@ -94,34 +94,6 @@ function detectIsPrerelease(version) {
94
94
  return typeof version === 'string' && version.includes('-');
95
95
  }
96
96
 
97
- /**
98
- * 反查指定版本所属的 dist-tag:从 registry 获取全量 tag 映射,
99
- * 找到指向该版本的 tag 名称(如 zjc / beta / latest)。
100
- * - 不依赖版本号格式,兼容任意 prerelease 命名
101
- * - 网络失败 / 找不到 / 多个 tag 指向同一版本(选第一个)→ 返回 null,由调用方降级
102
- */
103
- function findTagForVersion(version) {
104
- return new Promise((resolve, reject) => {
105
- const url = `${REGISTRY}/${encodeURIComponent(PKG_NAME).replace('%2F', '/')}`;
106
- const req = http.get(url, { timeout: REQUEST_TIMEOUT }, res => {
107
- if (res.statusCode !== 200) { res.resume(); reject(new Error(`status ${res.statusCode}`)); return; }
108
- let body = '';
109
- res.setEncoding('utf-8');
110
- res.on('data', chunk => { body += chunk; });
111
- res.on('end', () => {
112
- try {
113
- const distTags = JSON.parse(body)['dist-tags'] || {};
114
- // 找到指向目标版本的 tag(取第一个,避免重复)
115
- const found = Object.keys(distTags).find(k => distTags[k] === version);
116
- resolve(found || null);
117
- } catch (err) { reject(err); }
118
- });
119
- });
120
- req.on('timeout', () => req.destroy(new Error('timeout')));
121
- req.on('error', reject);
122
- });
123
- }
124
-
125
97
  module.exports = {
126
98
  PKG_NAME,
127
99
  REGISTRY,
@@ -131,5 +103,4 @@ module.exports = {
131
103
  fetchLatest,
132
104
  compareVersions,
133
105
  detectIsPrerelease,
134
- findTagForVersion,
135
106
  };
@@ -1,20 +1,20 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  /**
4
- * cbb wiki 子命令 — Wiki 查询与管理。
4
+ * cbbm wiki 子命令 — Wiki 查询与管理。
5
5
  *
6
6
  * 用法:
7
- * cbb wiki get <pageId> [--expand=...] [--full]
8
- * cbb wiki children <pageId> [--limit=50] [--start=0]
9
- * cbb wiki search "<cql>" [--limit=20] [--excerpt=highlight] [--order=lastmodified desc]
10
- * cbb wiki spaces [--limit=100] [--type=global]
11
- * cbb wiki comments <pageId> [--limit=50]
12
- * cbb wiki user [--current] [--account-id=<id>] [--username=<name>]
13
- * cbb wiki versions <pageId> [--limit=20]
14
- * cbb wiki attachments <pageId> list [--limit=50]
15
- * cbb wiki attachments <pageId> upload --file=<path> [--comment="..."]
16
- * cbb wiki attachments <pageId> delete --attachment-id=<id>
17
- * cbb wiki comment <pageId> --body-file=<path> [--parent=<parentCommentId>]
7
+ * cbbm wiki get <pageId> [--expand=...] [--full]
8
+ * cbbm wiki children <pageId> [--limit=50] [--start=0]
9
+ * cbbm wiki search "<cql>" [--limit=20] [--excerpt=highlight] [--order=lastmodified desc]
10
+ * cbbm wiki spaces [--limit=100] [--type=global]
11
+ * cbbm wiki comments <pageId> [--limit=50]
12
+ * cbbm wiki user [--current] [--account-id=<id>] [--username=<name>]
13
+ * cbbm wiki versions <pageId> [--limit=20]
14
+ * cbbm wiki attachments <pageId> list [--limit=50]
15
+ * cbbm wiki attachments <pageId> upload --file=<path> [--comment="..."]
16
+ * cbbm wiki attachments <pageId> delete --attachment-id=<id>
17
+ * cbbm wiki comment <pageId> --body-file=<path> [--parent=<parentCommentId>]
18
18
  */
19
19
 
20
20
  const fs = require('fs');
@@ -79,7 +79,7 @@ function parseArgs(argv) {
79
79
  }
80
80
 
81
81
  function showHelp() {
82
- out(`cbb wiki — Wiki 操作(查询 + 发布 + 管理)
82
+ out(`cbbm wiki — Wiki 操作(查询 + 发布 + 管理)
83
83
 
84
84
  参数格式:
85
85
  本工具同时支持两种写法,请按习惯任选其一:
@@ -104,9 +104,9 @@ function showHelp() {
104
104
  comment <pageId> --body-file=<path> 创建评论
105
105
 
106
106
  示例:
107
- cbb wiki create --parent-id 123456 --title "方案" --file design.md
108
- cbb wiki get 123456 --full
109
- cbb wiki search 'type=page AND title~"freight"'
107
+ cbbm wiki create --parent-id 123456 --title "方案" --file design.md
108
+ cbbm wiki get 123456 --full
109
+ cbbm wiki search 'type=page AND title~"freight"'
110
110
 
111
111
  认证:环境变量 CBB_WIKI_EMAIL + CBB_WIKI_TOKEN,或 ~/.cbb/wiki-config.json
112
112
  令牌获取: https://wiki.amh-group.com/plugins/tokens/view.action`);
@@ -115,7 +115,7 @@ function showHelp() {
115
115
  // ── 子命令实现 ────────────────────────────────────────
116
116
 
117
117
  async function cmdGet({ pos, opts }) {
118
- if (!pos[0]) { err('用法: cbb wiki get <pageId> [--expand=...] [--full]'); process.exit(1); }
118
+ if (!pos[0]) { err('用法: cbbm wiki get <pageId> [--expand=...] [--full]'); process.exit(1); }
119
119
  const result = await api.getPage(pos[0], { includeBody: opts.full });
120
120
  if (!result.success) { err(`获取失败: ${result.error}`); process.exit(1); }
121
121
 
@@ -141,7 +141,7 @@ async function cmdGet({ pos, opts }) {
141
141
  }
142
142
 
143
143
  async function cmdChildren({ pos, opts }) {
144
- if (!pos[0]) { err('用法: cbb wiki children <pageId> [--limit=50] [--start=0]'); process.exit(1); }
144
+ if (!pos[0]) { err('用法: cbbm wiki children <pageId> [--limit=50] [--start=0]'); process.exit(1); }
145
145
  const result = await api.getChildren(pos[0], { limit: Number(opts.limit) || 50, start: Number(opts.start) || 0 });
146
146
  if (!result.success) { err(`获取失败: ${result.error}`); process.exit(1); }
147
147
 
@@ -162,7 +162,7 @@ async function cmdChildren({ pos, opts }) {
162
162
  }
163
163
 
164
164
  async function cmdSearch({ pos, opts }) {
165
- if (!pos[0]) { err('用法: cbb wiki search "<cql>" [--limit=20] [--excerpt=highlight] [--order=lastmodified desc]'); process.exit(1); }
165
+ if (!pos[0]) { err('用法: cbbm wiki search "<cql>" [--limit=20] [--excerpt=highlight] [--order=lastmodified desc]'); process.exit(1); }
166
166
  const result = await api.search(pos[0], {
167
167
  limit: Number(opts.limit) || 20,
168
168
  excerpt: opts.excerpt,
@@ -204,7 +204,7 @@ async function cmdSpaces({ opts }) {
204
204
  }
205
205
 
206
206
  async function cmdComments({ pos, opts }) {
207
- if (!pos[0]) { err('用法: cbb wiki comments <pageId> [--limit=50]'); process.exit(1); }
207
+ if (!pos[0]) { err('用法: cbbm wiki comments <pageId> [--limit=50]'); process.exit(1); }
208
208
  const result = await api.getComments(pos[0], { limit: Number(opts.limit) || 50 });
209
209
  if (!result.success) { err(`获取失败: ${result.error}`); process.exit(1); }
210
210
 
@@ -246,7 +246,7 @@ async function cmdUser({ opts }) {
246
246
  }
247
247
 
248
248
  async function cmdVersions({ pos, opts }) {
249
- if (!pos[0]) { err('用法: cbb wiki versions <pageId> [--limit=20]'); process.exit(1); }
249
+ if (!pos[0]) { err('用法: cbbm wiki versions <pageId> [--limit=20]'); process.exit(1); }
250
250
  const result = await api.getVersions(pos[0], { limit: Number(opts.limit) || 20 });
251
251
  if (!result.success) { err(`获取失败: ${result.error}`); process.exit(1); }
252
252
 
@@ -267,7 +267,7 @@ async function cmdVersions({ pos, opts }) {
267
267
  }
268
268
 
269
269
  async function cmdAttachments({ pos, opts }) {
270
- if (!pos[0] || !pos[1]) { err('用法: cbb wiki attachments <pageId> <list|upload|delete> [选项]'); process.exit(1); }
270
+ if (!pos[0] || !pos[1]) { err('用法: cbbm wiki attachments <pageId> <list|upload|delete> [选项]'); process.exit(1); }
271
271
  const pageId = pos[0];
272
272
  const action = pos[1];
273
273
 
@@ -320,7 +320,7 @@ async function cmdAttachments({ pos, opts }) {
320
320
 
321
321
  async function cmdComment({ pos, opts }) {
322
322
  if (!pos[0] || !opts['body-file']) {
323
- err('用法: cbb wiki comment <pageId> --body-file=<path> [--parent=<parentCommentId>]');
323
+ err('用法: cbbm wiki comment <pageId> --body-file=<path> [--parent=<parentCommentId>]');
324
324
  process.exit(1);
325
325
  }
326
326
  const body = fs.readFileSync(opts['body-file'], 'utf8');
@@ -347,7 +347,7 @@ function readAndConvert(filePath) {
347
347
 
348
348
  async function cmdCreate({ pos, opts }) {
349
349
  if (!opts['parent-id'] || !opts.title || !opts.file) {
350
- err('用法: cbb wiki create --parent-id <id> --title "..." --file <md>');
350
+ err('用法: cbbm wiki create --parent-id <id> --title "..." --file <md>');
351
351
  process.exit(1);
352
352
  }
353
353
  const xhtml = readAndConvert(opts.file);
@@ -369,7 +369,7 @@ async function cmdCreate({ pos, opts }) {
369
369
 
370
370
  async function cmdUpdate({ pos, opts }) {
371
371
  if (!opts['page-id'] || !opts.file) {
372
- err('用法: cbb wiki update --page-id <id> --file <md>');
372
+ err('用法: cbbm wiki update --page-id <id> --file <md>');
373
373
  process.exit(1);
374
374
  }
375
375
  const xhtml = readAndConvert(opts.file);