@jspg-ai/coding-bb 0.0.1

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 (183) hide show
  1. package/README.md +41 -0
  2. package/cbb/dev-standards/rules/cbb-ai-behavior.md +104 -0
  3. package/cbb/dev-standards/rules/cbb-coding-rule.md +50 -0
  4. package/cbb/dev-standards/rules/cbb-priority.md +56 -0
  5. package/cbb/dev-standards/skills/architecture-specs/SKILL.md +129 -0
  6. package/cbb/dev-standards/skills/coding-specs/SKILL.md +376 -0
  7. package/cbb/dev-standards/skills/coding-specs/references/concurrency.md +53 -0
  8. package/cbb/dev-standards/skills/coding-specs/references/config-center.md +16 -0
  9. package/cbb/dev-standards/skills/coding-specs/references/distributed.md +42 -0
  10. package/cbb/dev-standards/skills/coding-specs/references/es-coding.md +60 -0
  11. package/cbb/dev-standards/skills/coding-specs/references/scheduled-task.md +25 -0
  12. package/cbb/dev-standards/skills/coding-specs/references/security.md +11 -0
  13. package/cbb/dev-standards/skills/coding-specs/references/unit-testing.md +43 -0
  14. package/cbb/dev-standards/skills/es-design-specs/SKILL.md +104 -0
  15. package/cbb/dev-standards/skills/mysql-design-specs/SKILL.md +89 -0
  16. package/cbb/lib/install/claude-code.js +20 -0
  17. package/cbb/lib/install/cleanup.js +64 -0
  18. package/cbb/lib/install/init.js +1875 -0
  19. package/cbb/lib/install/qoder.js +20 -0
  20. package/cbb/lib/install/workspaces.js +232 -0
  21. package/cbb/lib/openspec/index.js +554 -0
  22. package/cbb/lib/superpowers/index.js +265 -0
  23. package/cbb/lib/utils/check-update.js +147 -0
  24. package/cbb/lib/utils/checkbox.js +383 -0
  25. package/cbb/lib/utils/gitignore.js +69 -0
  26. package/cbb/lib/utils/output.js +64 -0
  27. package/cbb/lib/utils/settings.js +119 -0
  28. package/cbb/lib/utils/version.js +135 -0
  29. package/cbb/lib/wiki/api-client.js +358 -0
  30. package/cbb/lib/wiki/cli.js +427 -0
  31. package/cbb/lib/wiki/convert.js +464 -0
  32. package/cbb/lib/wiki/index.js +218 -0
  33. package/cbb/lib/wiki/mermaid-guard.js +103 -0
  34. package/cbb/lib/wiki/split.js +131 -0
  35. package/cbb/tools/cbb-decompile-jar/SKILL.md +220 -0
  36. package/cbb/tools/cbb-decompile-jar/scripts/decompile.py +863 -0
  37. package/cbb/tools/cbb-design-to-wiki/SKILL.md +180 -0
  38. package/cbb/tools/cbb-mvn-guide/SKILL.md +281 -0
  39. package/cbb/tools/cbb-mvn-guide/scripts/mvn_jdk_manager.py +378 -0
  40. package/cbb/tools/cbb-mvn-guide/scripts/mvn_pom_jdk_reader.py +197 -0
  41. package/cbb/tools/cbb-plantuml-authoring/SKILL.md +448 -0
  42. package/cbb/tools/cbb-plantuml-authoring/references/drawing-templates.md +479 -0
  43. package/cbb/tools/cbb-wiki-ops/SKILL.md +134 -0
  44. package/cbb/tools/cbb-wiki-ops/scripts/check-auth.js +44 -0
  45. package/cbb/worktrees/_shared/scripts/find-target-worktree.js +96 -0
  46. package/cbb/worktrees/_shared/scripts/find-workspace-root.js +67 -0
  47. package/cbb/worktrees/_shared/scripts/push-core.js +136 -0
  48. package/cbb/worktrees/_shared/scripts/silence-popup.js +23 -0
  49. package/cbb/worktrees/commands/close.md +64 -0
  50. package/cbb/worktrees/commands/extend.md +65 -0
  51. package/cbb/worktrees/commands/init.md +55 -0
  52. package/cbb/worktrees/commands/push.md +42 -0
  53. package/cbb/worktrees/skills/openspec-close-worktree/SKILL.md +449 -0
  54. package/cbb/worktrees/skills/openspec-close-worktree/scripts/check-env.js +76 -0
  55. package/cbb/worktrees/skills/openspec-close-worktree/scripts/check-unarchived.js +48 -0
  56. package/cbb/worktrees/skills/openspec-close-worktree/scripts/delete-branches.js +136 -0
  57. package/cbb/worktrees/skills/openspec-close-worktree/scripts/discover-apps.js +76 -0
  58. package/cbb/worktrees/skills/openspec-close-worktree/scripts/find-target-worktree.js +96 -0
  59. package/cbb/worktrees/skills/openspec-close-worktree/scripts/find-workspace-root.js +67 -0
  60. package/cbb/worktrees/skills/openspec-close-worktree/scripts/remove-worktrees.js +124 -0
  61. package/cbb/worktrees/skills/openspec-close-worktree/scripts/safety-check.js +174 -0
  62. package/cbb/worktrees/skills/openspec-close-worktree/scripts/silence-popup.js +23 -0
  63. package/cbb/worktrees/skills/openspec-extend-worktree/SKILL.md +390 -0
  64. package/cbb/worktrees/skills/openspec-extend-worktree/scripts/check-env.js +95 -0
  65. package/cbb/worktrees/skills/openspec-extend-worktree/scripts/check-repos.js +98 -0
  66. package/cbb/worktrees/skills/openspec-extend-worktree/scripts/create-branches-and-worktrees.js +135 -0
  67. package/cbb/worktrees/skills/openspec-extend-worktree/scripts/find-target-worktree.js +96 -0
  68. package/cbb/worktrees/skills/openspec-extend-worktree/scripts/find-workspace-root.js +67 -0
  69. package/cbb/worktrees/skills/openspec-extend-worktree/scripts/install-ai.js +150 -0
  70. package/cbb/worktrees/skills/openspec-extend-worktree/scripts/list-available-apps.js +88 -0
  71. package/cbb/worktrees/skills/openspec-extend-worktree/scripts/push-core.js +136 -0
  72. package/cbb/worktrees/skills/openspec-extend-worktree/scripts/silence-popup.js +23 -0
  73. package/cbb/worktrees/skills/openspec-extend-worktree/scripts/sync-repos.js +93 -0
  74. package/cbb/worktrees/skills/openspec-init-worktree/SKILL.md +539 -0
  75. package/cbb/worktrees/skills/openspec-init-worktree/scripts/auto-open.js +136 -0
  76. package/cbb/worktrees/skills/openspec-init-worktree/scripts/check-env-deep.js +87 -0
  77. package/cbb/worktrees/skills/openspec-init-worktree/scripts/check-env.js +90 -0
  78. package/cbb/worktrees/skills/openspec-init-worktree/scripts/check-repos.js +171 -0
  79. package/cbb/worktrees/skills/openspec-init-worktree/scripts/create-branches.js +103 -0
  80. package/cbb/worktrees/skills/openspec-init-worktree/scripts/create-worktrees.js +154 -0
  81. package/cbb/worktrees/skills/openspec-init-worktree/scripts/find-workspace-root.js +67 -0
  82. package/cbb/worktrees/skills/openspec-init-worktree/scripts/generate-app-options.js +93 -0
  83. package/cbb/worktrees/skills/openspec-init-worktree/scripts/install-ai.js +177 -0
  84. package/cbb/worktrees/skills/openspec-init-worktree/scripts/parse-config.js +67 -0
  85. package/cbb/worktrees/skills/openspec-init-worktree/scripts/push-branches.js +101 -0
  86. package/cbb/worktrees/skills/openspec-init-worktree/scripts/push-core.js +136 -0
  87. package/cbb/worktrees/skills/openspec-init-worktree/scripts/silence-popup.js +23 -0
  88. package/cbb/worktrees/skills/openspec-init-worktree/scripts/sync-repos.js +95 -0
  89. package/cbb/worktrees/skills/openspec-init-worktree/scripts/update-gitignore.js +134 -0
  90. package/cbb/worktrees/skills/openspec-push-worktrees/SKILL.md +307 -0
  91. package/cbb/worktrees/skills/openspec-push-worktrees/scripts/commit-worktrees.js +269 -0
  92. package/cbb/worktrees/skills/openspec-push-worktrees/scripts/find-target-worktree.js +96 -0
  93. package/cbb/worktrees/skills/openspec-push-worktrees/scripts/find-workspace-root.js +67 -0
  94. package/cbb/worktrees/skills/openspec-push-worktrees/scripts/push-core.js +136 -0
  95. package/cbb/worktrees/skills/openspec-push-worktrees/scripts/push-worktrees.js +159 -0
  96. package/cbb/worktrees/skills/openspec-push-worktrees/scripts/silence-popup.js +23 -0
  97. package/config/cbb.yaml +6 -0
  98. package/config/config.sample.json +16 -0
  99. package/config/openspec/config.yaml +28 -0
  100. package/config/openspec/schemas/spec-driven/schema.yaml +247 -0
  101. package/config/openspec/schemas/spec-driven/templates/design.md +475 -0
  102. package/config/openspec/schemas/spec-driven/templates/proposal.md +58 -0
  103. package/config/openspec/schemas/spec-driven/templates/spec.md +8 -0
  104. package/config/openspec/schemas/spec-driven/templates/tasks.md +9 -0
  105. package/config/workspaces.json +15 -0
  106. package/openspec/.version +6 -0
  107. package/openspec/commands/apply.md +182 -0
  108. package/openspec/commands/archive.md +223 -0
  109. package/openspec/commands/bulk-archive.md +334 -0
  110. package/openspec/commands/continue.md +112 -0
  111. package/openspec/commands/explore.md +206 -0
  112. package/openspec/commands/ff.md +111 -0
  113. package/openspec/commands/new.md +70 -0
  114. package/openspec/commands/onboard.md +555 -0
  115. package/openspec/commands/propose.md +157 -0
  116. package/openspec/commands/sync.md +256 -0
  117. package/openspec/commands/update.md +85 -0
  118. package/openspec/commands/verify.md +169 -0
  119. package/openspec/skills/openspec-apply-change/SKILL.md +187 -0
  120. package/openspec/skills/openspec-archive-change/SKILL.md +181 -0
  121. package/openspec/skills/openspec-bulk-archive-change/SKILL.md +338 -0
  122. package/openspec/skills/openspec-continue-change/SKILL.md +117 -0
  123. package/openspec/skills/openspec-explore/SKILL.md +342 -0
  124. package/openspec/skills/openspec-ff-change/SKILL.md +116 -0
  125. package/openspec/skills/openspec-new-change/SKILL.md +76 -0
  126. package/openspec/skills/openspec-onboard/SKILL.md +560 -0
  127. package/openspec/skills/openspec-propose/SKILL.md +162 -0
  128. package/openspec/skills/openspec-sync-specs/SKILL.md +261 -0
  129. package/openspec/skills/openspec-update-change/SKILL.md +90 -0
  130. package/openspec/skills/openspec-verify-change/SKILL.md +174 -0
  131. package/package.json +46 -0
  132. package/superpowers/.version +6 -0
  133. package/superpowers/skills/brainstorming/SKILL.md +250 -0
  134. package/superpowers/skills/brainstorming/scripts/frame-template.html +213 -0
  135. package/superpowers/skills/brainstorming/scripts/helper.js +167 -0
  136. package/superpowers/skills/brainstorming/scripts/server.cjs +723 -0
  137. package/superpowers/skills/brainstorming/scripts/start-server.sh +209 -0
  138. package/superpowers/skills/brainstorming/scripts/stop-server.sh +120 -0
  139. package/superpowers/skills/brainstorming/spec-document-reviewer-prompt.md +49 -0
  140. package/superpowers/skills/brainstorming/visual-companion.md +299 -0
  141. package/superpowers/skills/dispatching-parallel-agents/SKILL.md +167 -0
  142. package/superpowers/skills/executing-plans/SKILL.md +64 -0
  143. package/superpowers/skills/finishing-a-development-branch/SKILL.md +225 -0
  144. package/superpowers/skills/receiving-code-review/SKILL.md +205 -0
  145. package/superpowers/skills/requesting-code-review/SKILL.md +95 -0
  146. package/superpowers/skills/requesting-code-review/code-reviewer.md +181 -0
  147. package/superpowers/skills/subagent-driven-development/SKILL.md +568 -0
  148. package/superpowers/skills/subagent-driven-development/implementer-prompt.md +154 -0
  149. package/superpowers/skills/subagent-driven-development/re-review-prompt.md +115 -0
  150. package/superpowers/skills/subagent-driven-development/scripts/review-package +46 -0
  151. package/superpowers/skills/subagent-driven-development/scripts/sdd-workspace +40 -0
  152. package/superpowers/skills/subagent-driven-development/scripts/task-brief +41 -0
  153. package/superpowers/skills/subagent-driven-development/task-reviewer-prompt.md +207 -0
  154. package/superpowers/skills/systematic-debugging/CREATION-LOG.md +119 -0
  155. package/superpowers/skills/systematic-debugging/SKILL.md +283 -0
  156. package/superpowers/skills/systematic-debugging/condition-based-waiting-example.ts +158 -0
  157. package/superpowers/skills/systematic-debugging/condition-based-waiting.md +115 -0
  158. package/superpowers/skills/systematic-debugging/defense-in-depth.md +122 -0
  159. package/superpowers/skills/systematic-debugging/find-polluter.sh +72 -0
  160. package/superpowers/skills/systematic-debugging/root-cause-tracing.md +169 -0
  161. package/superpowers/skills/systematic-debugging/test-academic.md +14 -0
  162. package/superpowers/skills/systematic-debugging/test-pressure-1.md +58 -0
  163. package/superpowers/skills/systematic-debugging/test-pressure-2.md +68 -0
  164. package/superpowers/skills/systematic-debugging/test-pressure-3.md +69 -0
  165. package/superpowers/skills/test-driven-development/SKILL.md +320 -0
  166. package/superpowers/skills/test-driven-development/writing-good-tests.md +198 -0
  167. package/superpowers/skills/using-git-worktrees/SKILL.md +167 -0
  168. package/superpowers/skills/using-superpowers/SKILL.md +63 -0
  169. package/superpowers/skills/using-superpowers/references/antigravity-tools.md +23 -0
  170. package/superpowers/skills/using-superpowers/references/codex-tools.md +108 -0
  171. package/superpowers/skills/using-superpowers/references/gemini-tools.md +63 -0
  172. package/superpowers/skills/using-superpowers/references/hermes-tools.md +56 -0
  173. package/superpowers/skills/using-superpowers/references/pi-tools.md +16 -0
  174. package/superpowers/skills/verification-before-completion/SKILL.md +120 -0
  175. package/superpowers/skills/writing-plans/SKILL.md +171 -0
  176. package/superpowers/skills/writing-plans/plan-document-reviewer-prompt.md +49 -0
  177. package/superpowers/skills/writing-skills/SKILL.md +679 -0
  178. package/superpowers/skills/writing-skills/anthropic-best-practices.md +1150 -0
  179. package/superpowers/skills/writing-skills/examples/CLAUDE_MD_TESTING.md +189 -0
  180. package/superpowers/skills/writing-skills/graphviz-conventions.dot +172 -0
  181. package/superpowers/skills/writing-skills/persuasion-principles.md +187 -0
  182. package/superpowers/skills/writing-skills/render-graphs.js +169 -0
  183. package/superpowers/skills/writing-skills/testing-skills-with-subagents.md +384 -0
@@ -0,0 +1,554 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * OpenSpec 工作流管理模块
5
+ *
6
+ * 负责从 GitHub (Fission-AI/OpenSpec) 拉取官方 workflow 模板,
7
+ * 渲染并部署到本项目的 openspec/{commands,skills} 目录。
8
+ * 提供 `cbb update --upstream-openspec` 选项。
9
+ *
10
+ * 升级范围严格限定:只有 WORKFLOWS 白名单中的 commands/*.md 和
11
+ * skills/<workflow>/SKILL.md 会被覆盖写入。config/openspec/(schemas + config.yaml)、
12
+ * cbb/worktrees/ 等项目自有扩展位不会被触碰 —— 由 assertSafeDestination
13
+ * 兜底拦截,任何写入命中受保护前缀都会抛错退出。
14
+ */
15
+
16
+ const fs = require('fs');
17
+ const path = require('path');
18
+ const https = require('https');
19
+ const os = require('os');
20
+ const zlib = require('zlib');
21
+
22
+ const SHARED_STANDARDS_DIR = path.join(__dirname, '..', '..', '..');
23
+
24
+ // 部署目标根目录
25
+ const OPENSPEC_DIR = path.join(SHARED_STANDARDS_DIR, 'openspec');
26
+ const OPENSPEC_COMMANDS_DIR = path.join(OPENSPEC_DIR, 'commands');
27
+ const OPENSPEC_SKILLS_DIR = path.join(OPENSPEC_DIR, 'skills');
28
+
29
+ // 版本文件(commit SHA)
30
+ const VERSION_FILE = path.join(OPENSPEC_DIR, '.version');
31
+
32
+ // GitHub 配置
33
+ const GITHUB_REPO = 'Fission-AI/OpenSpec';
34
+ const GITHUB_BRANCH = 'main';
35
+ // 使用 codeload (不走 API,无速率限制) 与 git ls-remote (拉 SHA,无速率限制)
36
+ const GITHUB_TARBALL_URL = `https://codeload.github.com/${GITHUB_REPO}/tar.gz/refs/heads/${GITHUB_BRANCH}`;
37
+ const GITHUB_GIT_URL = `https://github.com/${GITHUB_REPO}.git`;
38
+
39
+ // 升级白名单:每个工作流的源文件名 → 命令文件名 + skill 目录名
40
+ // 部署 strategy:
41
+ // - cmd 不为空 → 写入 commands/<cmd>.md
42
+ // - skill 不为空 → 写入 skills/<skill>/SKILL.md
43
+ const WORKFLOWS = [
44
+ { name: 'propose', cmd: 'propose', skill: 'openspec-propose' },
45
+ { name: 'apply-change', cmd: 'apply', skill: 'openspec-apply-change' },
46
+ { name: 'archive-change', cmd: 'archive', skill: 'openspec-archive-change' },
47
+ { name: 'verify-change', cmd: 'verify', skill: 'openspec-verify-change' },
48
+ { name: 'ff-change', cmd: 'ff', skill: 'openspec-ff-change' },
49
+ { name: 'continue-change', cmd: 'continue', skill: 'openspec-continue-change' },
50
+ { name: 'new-change', cmd: 'new', skill: 'openspec-new-change' },
51
+ { name: 'explore', cmd: 'explore', skill: 'openspec-explore' },
52
+ { name: 'update-change', cmd: 'update', skill: 'openspec-update-change' },
53
+ { name: 'sync-specs', cmd: 'sync', skill: 'openspec-sync-specs' },
54
+ { name: 'bulk-archive-change',cmd: 'bulk-archive', skill: 'openspec-bulk-archive-change' },
55
+ { name: 'onboard', cmd: 'onboard', skill: 'openspec-onboard' },
56
+ ];
57
+
58
+ // ── 受保护路径(项目自有扩展位,绝对禁止覆盖写入)─────────────
59
+ //
60
+ // 与 AGENTS.md「原生内容保护与扩展位」一致:
61
+ // - config/openspec/schemas/ 项目自托管 schema 与模板(spec-driven)
62
+ // - cbb/worktrees/ 项目自有扩展(worktree 命令/技能/_shared)
63
+ // - config/openspec/config.yaml 项目自托管
64
+ //
65
+ // 任何写入命中下列任一前缀,assertSafeDestination 立即抛错,
66
+ // 避免后续重构意外把 schema.yaml / config.yaml / worktrees/ 冲掉。
67
+ const PROTECTED_PATH_PREFIXES = [
68
+ path.join(SHARED_STANDARDS_DIR, 'config', 'openspec', 'schemas'),
69
+ path.join(SHARED_STANDARDS_DIR, 'cbb', 'worktrees'),
70
+ path.join(SHARED_STANDARDS_DIR, 'config', 'openspec', 'config.yaml'),
71
+ ];
72
+
73
+ function assertSafeDestination(destPath, op) {
74
+ const abs = path.resolve(destPath);
75
+ for (const prefix of PROTECTED_PATH_PREFIXES) {
76
+ if (abs === prefix || abs.startsWith(prefix + path.sep)) {
77
+ throw new Error(
78
+ `[cbb update --upstream-openspec] ${op} 试图写入受保护路径: ${abs}\n` +
79
+ `命中前缀: ${prefix}\n` +
80
+ `该路径属于项目自有扩展位,绝对禁止被 cbb update --upstream-openspec 覆盖。\n` +
81
+ `详见 AGENTS.md「原生内容保护与扩展位」一节。`
82
+ );
83
+ }
84
+ }
85
+ }
86
+
87
+ // 与 lib/install/init.js 共用输出辅助
88
+ const { log } = require('../utils/output');
89
+
90
+ // 嵌入的 STORE_SELECTION_GUIDANCE (来自 Fission-AI/OpenSpec store-selection.ts)
91
+ // 用于替换官方 TS 模板中的 ${STORE_SELECTION_GUIDANCE} 占位符
92
+ const STORE_SELECTION_GUIDANCE_TEXT =
93
+ '**Store selection:** If the user names a store (a store is a standalone OpenSpec repo registered on this machine) or the work lives in one, run `openspec store list --json` to discover registered store ids, then pass `--store <id>` on the commands that read or write specs and changes (`new change`, `status`, `instructions`, `list`, `show`, `validate`, `archive`, `doctor`, `context`). Other commands do not take the flag. Hints printed by commands already carry the flag; keep it on follow-ups. Without a store, commands act on the nearest local `openspec/` root.';
94
+
95
+ // ── HTTP 工具 ──────────────────────────────────────────────────
96
+
97
+ function httpsGet(url, headers = {}, redirectCount = 0) {
98
+ if (redirectCount > 5) throw new Error('Too many redirects');
99
+
100
+ return new Promise((resolve, reject) => {
101
+ const defaultHeaders = { 'User-Agent': 'cbb-cli' };
102
+ https.get(url, { headers: { ...defaultHeaders, ...headers } }, (res) => {
103
+ if ([301, 302, 307, 308].includes(res.statusCode) && res.headers.location) {
104
+ res.resume();
105
+ return httpsGet(res.headers.location, headers, redirectCount + 1)
106
+ .then(resolve, reject);
107
+ }
108
+
109
+ const chunks = [];
110
+ res.on('data', chunk => chunks.push(chunk));
111
+ res.on('end', () => {
112
+ resolve({
113
+ statusCode: res.statusCode,
114
+ headers: res.headers,
115
+ body: Buffer.concat(chunks),
116
+ });
117
+ });
118
+ res.on('error', reject);
119
+ }).on('error', reject);
120
+ });
121
+ }
122
+
123
+ async function downloadToFile(url, destPath) {
124
+ const res = await httpsGet(url);
125
+ if (res.statusCode !== 200) {
126
+ throw new Error(`HTTP ${res.statusCode}: ${url}`);
127
+ }
128
+ fs.mkdirSync(path.dirname(destPath), { recursive: true });
129
+ fs.writeFileSync(destPath, res.body);
130
+ return res;
131
+ }
132
+
133
+ /**
134
+ * HEAD 请求拿 ETag,用于无 API 限速的版本比对。
135
+ * codeload 对同一 ref 的 tarball 返回稳定 ETag;上游变更即变化。
136
+ */
137
+ async function fetchEtag(url) {
138
+ return new Promise((resolve, reject) => {
139
+ const req = https.request(url, { method: 'HEAD', headers: { 'User-Agent': 'cbb-cli' } }, (res) => {
140
+ const etag = (res.headers.etag || '').replace(/^W\//, '').replace(/^"|"$/g, '');
141
+ res.resume();
142
+ res.on('end', () => resolve({ statusCode: res.statusCode, etag }));
143
+ });
144
+ req.on('error', reject);
145
+ req.end();
146
+ });
147
+ }
148
+
149
+ // ── tar.gz 解压 ──────────────────────────────────────────────────
150
+
151
+ /**
152
+ * 跨平台 .tar.gz 解压,用 zlib + 手写 tar parser,避开 OS shell tar。
153
+ * 支持路径嵌套目录创建,跳过权限/所有者字段。
154
+ */
155
+ function extractTarGz(tarGzPath, destDir) {
156
+ const compressed = fs.readFileSync(tarGzPath);
157
+ const data = zlib.gunzipSync(compressed);
158
+
159
+ let off = 0;
160
+ while (off + 512 <= data.length) {
161
+ const header = data.subarray(off, off + 512);
162
+ off += 512;
163
+
164
+ // 空块 = 连续两个 512-byte 零 = tar 终止
165
+ if (header.every(b => b === 0)) break;
166
+
167
+ // 文件名在 header[0..100] (NUL 终止)
168
+ let nameEnd = 0;
169
+ while (nameEnd < 100 && header[nameEnd] !== 0) nameEnd++;
170
+ const name = header.subarray(0, nameEnd).toString('utf-8');
171
+
172
+ // 文件大小在 header[124..136] (octal ASCII)
173
+ const sizeStr = header.subarray(124, 136).toString('utf-8').trim();
174
+ const size = parseInt(sizeStr, 8) || 0;
175
+
176
+ const target = path.join(destDir, name);
177
+ if (name.endsWith('/')) {
178
+ fs.mkdirSync(target, { recursive: true });
179
+ } else {
180
+ fs.mkdirSync(path.dirname(target), { recursive: true });
181
+ fs.writeFileSync(target, data.subarray(off, off + size));
182
+ }
183
+
184
+ // 数据按 512 字节对齐
185
+ off += Math.ceil(size / 512) * 512;
186
+ }
187
+ }
188
+
189
+ // ── 版本管理 ──────────────────────────────────────────────────
190
+
191
+ function getLocalVersion() {
192
+ if (!fs.existsSync(VERSION_FILE)) return null;
193
+ try {
194
+ const content = fs.readFileSync(VERSION_FILE, 'utf-8').trim();
195
+ if (content.startsWith('{')) {
196
+ const data = JSON.parse(content);
197
+ return data.sha || null;
198
+ }
199
+ return content || null;
200
+ } catch (_) {
201
+ return null;
202
+ }
203
+ }
204
+
205
+ function writeVersion(sha) {
206
+ const data = {
207
+ sha,
208
+ branch: GITHUB_BRANCH,
209
+ repo: GITHUB_REPO,
210
+ updatedAt: new Date().toISOString(),
211
+ };
212
+ fs.writeFileSync(VERSION_FILE, JSON.stringify(data, null, 2) + '\n');
213
+ }
214
+
215
+ // ── 渲染:从 *.ts 文件中提取 template literal 并替换占位符 ──
216
+
217
+ /**
218
+ * 反转义 TS 模板字符串中典型的转义:
219
+ * \` → ` (反引号)
220
+ * \$ → $ (美元符,常用于 \${\w} 形式防止被当作插值)
221
+ * \\ → \ (反斜杠本身)
222
+ */
223
+ function unescapeTsTemplate(s) {
224
+ return s
225
+ .replace(/\\`/g, '`')
226
+ .replace(/\\\$/g, '$')
227
+ .replace(/\\\\/g, '\\');
228
+ }
229
+
230
+ /**
231
+ * 将模板字符串中的 ${STORE_SELECTION_GUIDANCE} 替换为实际内容。
232
+ * 上游 store-selection.ts 是常量,我们直接内嵌字符串,避免运行时求值。
233
+ */
234
+ function substituteStoreGuidance(s) {
235
+ return s.split('${STORE_SELECTION_GUIDANCE}').join(STORE_SELECTION_GUIDANCE_TEXT);
236
+ }
237
+
238
+ /**
239
+ * 从 TS 源码中提取 SkillTemplate 函数体内的字段,并渲染成 SKILL.md。
240
+ * @param {string} tsContent - 整个 .ts 文件内容
241
+ * @param {string} functionName - 例如 'getProposeSkillTemplate' / 'getApplyChangeSkillTemplate'
242
+ * @returns {string|null} 渲染出的 SKILL.md 完整内容,若提取失败返回 null
243
+ */
244
+ function renderSkillFromTs(tsContent, functionName) {
245
+ // 匹配 functionName(...): SkillTemplate { return { ... }; }
246
+ const fnRegex = new RegExp(
247
+ `function\\s+${functionName}\\s*\\(\\)\\s*:\\s*SkillTemplate\\s*\\{[\\s\\S]*?return\\s*\\{([\\s\\S]*?)\\}\\s*;\\s*\\}`,
248
+ 'm'
249
+ );
250
+ const fnMatch = tsContent.match(fnRegex);
251
+ if (!fnMatch) return null;
252
+ const fields = fnMatch[1];
253
+
254
+ // 提取 name (单引号字符串)
255
+ const nameMatch = fields.match(/name:\s*'([^']+)'/);
256
+ const name = nameMatch ? nameMatch[1] : '';
257
+
258
+ // 提取 description (单引号字符串)
259
+ const descMatch = fields.match(/description:\s*'([\s\S]*?)',\s*\n\s*instructions/);
260
+ const description = descMatch ? descMatch[1].trim() : '';
261
+
262
+ // 提取 instructions (backtick 模板字符串,以 `\n license:` 为锚点)
263
+ let instMatch = fields.match(/instructions:\s*`([\s\S]*?)`,\s*\n\s*license/);
264
+ if (!instMatch) {
265
+ // 兜底: instructions 可能是函数调用,如 instructions: getOnboardInstructions(),
266
+ const fnCallMatch = fields.match(/instructions:\s*(\w+)\(\)/);
267
+ if (fnCallMatch) {
268
+ const helperFnName = fnCallMatch[1];
269
+ const helperFnRegex = new RegExp(
270
+ `function\\s+${helperFnName}\\s*\\([^)]*\\)\\s*:\\s*string\\s*\\{[\\s\\S]*?return\\s*\`([\\s\\S]*?)\`\\s*;\\s*\\}`,
271
+ 'm'
272
+ );
273
+ const helperMatch = tsContent.match(helperFnRegex);
274
+ if (helperMatch) {
275
+ instMatch = [null, helperMatch[1]];
276
+ }
277
+ }
278
+ }
279
+ if (!instMatch) return null;
280
+ let instructions = unescapeTsTemplate(substituteStoreGuidance(instMatch[1]));
281
+ // 去掉开头/结尾的多余换行
282
+ instructions = instructions.replace(/^\n+/, '').replace(/\n+$/, '') + '\n';
283
+
284
+ // 提取 license / compatibility / metadata
285
+ const licenseMatch = fields.match(/license:\s*'([^']+)'/);
286
+ const license = licenseMatch ? licenseMatch[1] : 'MIT';
287
+ const compatMatch = fields.match(/compatibility:\s*'([^']+)'/);
288
+ const compatibility = compatMatch ? compatMatch[1] : 'Requires openspec CLI.';
289
+ const metaMatch = fields.match(/metadata:\s*\{\s*author:\s*'([^']+)',\s*version:\s*'([^']+)'/);
290
+ const metaAuthor = metaMatch ? metaMatch[1] : 'openspec';
291
+ const metaVersion = metaMatch ? metaMatch[2] : '1.0';
292
+
293
+ // 构造 frontmatter (YAML)
294
+ const fm = [
295
+ '---',
296
+ `name: ${name}`,
297
+ `description: ${description}`,
298
+ `license: ${license}`,
299
+ `compatibility: ${compatibility}`,
300
+ 'metadata:',
301
+ ` author: ${metaAuthor}`,
302
+ ` version: "${metaVersion}"`,
303
+ ` generatedBy: "1.6.0"`,
304
+ '---',
305
+ '',
306
+ ].join('\n');
307
+
308
+ return fm + instructions;
309
+ }
310
+
311
+ /**
312
+ * 从 TS 源码中提取 CommandTemplate 函数体内的 content,并渲染成 .md。
313
+ * @param {string} tsContent
314
+ * @param {string} functionName - 例如 'getOpsxProposeCommandTemplate'
315
+ * @returns {string|null}
316
+ */
317
+ function renderCommandFromTs(tsContent, functionName) {
318
+ const fnRegex = new RegExp(
319
+ `function\\s+${functionName}\\s*\\(\\)\\s*:\\s*CommandTemplate\\s*\\{[\\s\\S]*?return\\s*\\{([\\s\\S]*?)\\}\\s*;\\s*\\}`,
320
+ 'm'
321
+ );
322
+ const fnMatch = tsContent.match(fnRegex);
323
+ if (!fnMatch) return null;
324
+ const fields = fnMatch[1];
325
+
326
+ // 提取 description (单引号字符串)
327
+ const descMatch = fields.match(/description:\s*'([\s\S]*?)',\s*\n\s*category/);
328
+ const description = descMatch ? descMatch[1].trim() : '';
329
+
330
+ // 提取 category / tags
331
+ const catMatch = fields.match(/category:\s*'([^']+)'/);
332
+ const category = catMatch ? catMatch[1] : 'Workflow';
333
+ const tagsMatch = fields.match(/tags:\s*\[([^\]]+)\]/);
334
+ let tags = '[]';
335
+ if (tagsMatch) {
336
+ const tagList = tagsMatch[1]
337
+ .split(',')
338
+ .map(t => t.trim().replace(/^['"]|['"]$/g, ''))
339
+ .filter(Boolean);
340
+ tags = '[' + tagList.join(', ') + ']';
341
+ }
342
+
343
+ // 提取 content (backtick 模板字符串;content 是 CommandTemplate 的最末字段,
344
+ // 所以以字符串结尾 `$` 作为锚点即可,不带 /m 标志)
345
+ let contentMatch = fields.match(/content:\s*`([\s\S]*?)`\s*$/);
346
+ if (!contentMatch) {
347
+ // 兜底: content 可能是函数调用,如 content: getOnboardInstructions(),
348
+ const fnCallMatch = fields.match(/content:\s*(\w+)\(\)/);
349
+ if (fnCallMatch) {
350
+ const helperFnName = fnCallMatch[1];
351
+ const helperFnRegex = new RegExp(
352
+ `function\\s+${helperFnName}\\s*\\([^)]*\\)\\s*:\\s*string\\s*\\{[\\s\\S]*?return\\s*\`([\\s\\S]*?)\`\\s*;\\s*\\}`,
353
+ 'm'
354
+ );
355
+ const helperMatch = tsContent.match(helperFnRegex);
356
+ if (helperMatch) {
357
+ contentMatch = [null, helperMatch[1]];
358
+ }
359
+ }
360
+ }
361
+ if (!contentMatch) return null;
362
+ let content = unescapeTsTemplate(substituteStoreGuidance(contentMatch[1]));
363
+ content = content.replace(/^\n+/, '').replace(/\n+$/, '') + '\n';
364
+
365
+ // 派生 name: 'OPSX: <Title>' 来自 description 的若干词
366
+ // 实际从 CommandTemplate.name 字段读取
367
+ const nameMatch = fields.match(/name:\s*'([^']+)'/);
368
+ const name = nameMatch ? nameMatch[1] : `OPSX: ${description.split(/[.,:!?]/)[0].slice(0, 24)}`;
369
+
370
+ const fm = [
371
+ '---',
372
+ `name: "${name}"`,
373
+ `description: ${description}`,
374
+ `category: ${category}`,
375
+ `tags: ${tags}`,
376
+ '---',
377
+ '',
378
+ ].join('\n');
379
+
380
+ return fm + content;
381
+ }
382
+
383
+ /**
384
+ * 把 workflow 名映射到它对应的 TS 函数名。
385
+ * 例如 'propose' → getOpsxProposeCommandTemplate / getOpsxProposeSkillTemplate
386
+ */
387
+ function functionNamesForWorkflow(name, kind /* 'cmd' | 'skill' */) {
388
+ // 所有官方 cmd 函数都以 getOpsx 开头,skill 以 get 开头,Workflow 名驼峰式接续
389
+ // 我们手动列出(因为没法从文件名可靠推导)
390
+ const map = {
391
+ 'propose': { cmd: 'getOpsxProposeCommandTemplate', skill: 'getOpsxProposeSkillTemplate' },
392
+ 'apply-change': { cmd: 'getOpsxApplyCommandTemplate', skill: 'getApplyChangeSkillTemplate' },
393
+ 'archive-change': { cmd: 'getOpsxArchiveCommandTemplate', skill: 'getArchiveChangeSkillTemplate' },
394
+ 'verify-change': { cmd: 'getOpsxVerifyCommandTemplate', skill: 'getVerifyChangeSkillTemplate' },
395
+ 'ff-change': { cmd: 'getOpsxFfCommandTemplate', skill: 'getFfChangeSkillTemplate' },
396
+ 'continue-change': { cmd: 'getOpsxContinueCommandTemplate', skill: 'getContinueChangeSkillTemplate' },
397
+ 'new-change': { cmd: 'getOpsxNewCommandTemplate', skill: 'getNewChangeSkillTemplate' },
398
+ 'explore': { cmd: 'getOpsxExploreCommandTemplate', skill: 'getExploreSkillTemplate' },
399
+ 'update-change': { cmd: 'getOpsxUpdateCommandTemplate', skill: 'getUpdateChangeSkillTemplate' },
400
+ 'sync-specs': { cmd: 'getOpsxSyncCommandTemplate', skill: 'getSyncSpecsSkillTemplate' },
401
+ 'bulk-archive-change':{ cmd: 'getOpsxBulkArchiveCommandTemplate', skill: 'getBulkArchiveChangeSkillTemplate' },
402
+ 'onboard': { cmd: 'getOpsxOnboardCommandTemplate', skill: 'getOnboardSkillTemplate' },
403
+ };
404
+ return map[name] ? map[name][kind] : null;
405
+ }
406
+
407
+ // ── 下载与部署 ──────────────────────────────────────────────────
408
+
409
+ async function fetchLatestOpenSpec() {
410
+ log(`正在获取 OpenSpec 版本元数据...`, 'info');
411
+ // 1) 先 HEAD 拿 ETag,避免每次都下载完整 tarball
412
+ const head = await fetchEtag(GITHUB_TARBALL_URL);
413
+ if (head.statusCode !== 200 || !head.etag) {
414
+ throw new Error(`无法获取版本信息: HTTP ${head.statusCode}`);
415
+ }
416
+ const versionToken = head.etag;
417
+ const localToken = getLocalVersion();
418
+
419
+ if (versionToken === localToken) {
420
+ // 即使版本号一致,也要校验全部文件是否已部署(白名单可能新增了条目)
421
+ const { valid } = validateDeployed();
422
+ if (valid) {
423
+ log(`已是最新版本 (${versionToken.slice(0, 12)})`, 'success');
424
+ return { version: versionToken, cmdCount: 0, skillCount: 0, skipped: true };
425
+ }
426
+ log(`版本一致但文件不完整,强制重新部署...`, 'info');
427
+ }
428
+
429
+ log(`检测到上游更新,正在下载 OpenSpec tarball...`, 'info');
430
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cbb-os-'));
431
+ const tarballPath = path.join(tmpDir, 'openspec.tar.gz');
432
+
433
+ try {
434
+ await downloadToFile(GITHUB_TARBALL_URL, tarballPath);
435
+ log('正在解压...', 'info');
436
+ // 跨平台解压:用 zlib + 自写 tar parser。无需 shell tar,Windows 也能跑。
437
+ extractTarGz(tarballPath, tmpDir);
438
+
439
+ // 通过「含 src/core/templates/workflows」特征定位真仓库根。
440
+ // codeload 的 tarball 顶层可能是 pax_global_header / 杂项,但
441
+ // 真正的源码根目录固定有一个 src/ 子树。这种探测比依赖目录名约定稳。
442
+ let extractedDir = null;
443
+ for (const entry of fs.readdirSync(tmpDir)) {
444
+ const cand = path.join(tmpDir, entry);
445
+ let isDir = false;
446
+ try { isDir = fs.statSync(cand).isDirectory(); } catch (_) { continue; }
447
+ if (!isDir) continue;
448
+ const probe = path.join(cand, 'src', 'core', 'templates', 'workflows');
449
+ if (fs.existsSync(probe)) {
450
+ extractedDir = cand;
451
+ break;
452
+ }
453
+ }
454
+ if (!extractedDir) {
455
+ throw new Error('解压后未在 tarball 中定位到含 src/core/templates/workflows 的 OpenSpec 根目录');
456
+ }
457
+
458
+ const workflowsDir = path.join(extractedDir, 'src', 'core', 'templates', 'workflows');
459
+ if (!fs.existsSync(workflowsDir)) throw new Error('未找到 workflows 目录');
460
+
461
+ let cmdCount = 0;
462
+ let skillCount = 0;
463
+
464
+ for (const wf of WORKFLOWS) {
465
+ const tsFile = path.join(workflowsDir, `${wf.name}.ts`);
466
+ if (!fs.existsSync(tsFile)) {
467
+ log(`跳过 ${wf.name}: 官方源无对应文件`, 'warn');
468
+ continue;
469
+ }
470
+ const tsContent = fs.readFileSync(tsFile, 'utf-8');
471
+
472
+ // 命令
473
+ if (wf.cmd) {
474
+ const fnName = functionNamesForWorkflow(wf.name, 'cmd');
475
+ const rendered = fnName ? renderCommandFromTs(tsContent, fnName) : null;
476
+ if (!rendered) {
477
+ log(`${wf.name}.ts: 未提取到 CommandTemplate,跳过`, 'warn');
478
+ } else {
479
+ const dest = path.join(OPENSPEC_COMMANDS_DIR, `${wf.cmd}.md`);
480
+ assertSafeDestination(dest, `写入命令 ${wf.name}`);
481
+ fs.mkdirSync(path.dirname(dest), { recursive: true });
482
+ fs.writeFileSync(dest, rendered);
483
+ cmdCount++;
484
+ }
485
+ }
486
+
487
+ // 技能
488
+ if (wf.skill) {
489
+ const fnName = functionNamesForWorkflow(wf.name, 'skill');
490
+ const rendered = fnName ? renderSkillFromTs(tsContent, fnName) : null;
491
+ if (!rendered) {
492
+ log(`${wf.name}.ts: 未提取到 SkillTemplate,跳过`, 'warn');
493
+ } else {
494
+ const dest = path.join(OPENSPEC_SKILLS_DIR, wf.skill, 'SKILL.md');
495
+ assertSafeDestination(dest, `写入技能 ${wf.name}`);
496
+ fs.mkdirSync(path.dirname(dest), { recursive: true });
497
+ fs.writeFileSync(dest, rendered);
498
+ skillCount++;
499
+ }
500
+ }
501
+ }
502
+
503
+ writeVersion(versionToken);
504
+ log(
505
+ `已更新 OpenSpec 工作流: ${localToken ? localToken.slice(0, 12) : '(无)'} → ${versionToken.slice(0, 12)} ` +
506
+ `(${cmdCount} 个命令 + ${skillCount} 个技能)`,
507
+ 'success'
508
+ );
509
+
510
+ return { version: versionToken, cmdCount, skillCount, skipped: false };
511
+ } catch (err) {
512
+ try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch (_) { /* ignore */ }
513
+ throw err;
514
+ }
515
+ }
516
+
517
+ // ── 完整性校验 ──────────────────────────────────────────────────
518
+
519
+ /**
520
+ * 校验 Safe-Overwrite 区的所有文件均已部署。
521
+ * @returns {{valid: boolean, missing: string[]}}
522
+ */
523
+ function validateDeployed() {
524
+ const missing = [];
525
+ for (const wf of WORKFLOWS) {
526
+ if (wf.cmd) {
527
+ const p = path.join(OPENSPEC_COMMANDS_DIR, `${wf.cmd}.md`);
528
+ if (!fs.existsSync(p)) missing.push(`commands/${wf.cmd}.md`);
529
+ }
530
+ if (wf.skill) {
531
+ const p = path.join(OPENSPEC_SKILLS_DIR, wf.skill, 'SKILL.md');
532
+ if (!fs.existsSync(p)) missing.push(`skills/${wf.skill}/SKILL.md`);
533
+ }
534
+ }
535
+ return { valid: missing.length === 0, missing };
536
+ }
537
+
538
+ // ── 导出 ────────────────────────────────────────────────────────
539
+
540
+ module.exports = {
541
+ OPENSPEC_DIR,
542
+ OPENSPEC_COMMANDS_DIR,
543
+ OPENSPEC_SKILLS_DIR,
544
+ VERSION_FILE,
545
+ WORKFLOWS,
546
+ GITHUB_REPO,
547
+ GITHUB_BRANCH,
548
+
549
+ PROTECTED_PATH_PREFIXES,
550
+ assertSafeDestination,
551
+ fetchLatestOpenSpec,
552
+ getLocalVersion,
553
+ validateDeployed,
554
+ };