@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,135 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * 版本管理工具:被 check-update.js(hook)和 init.js(cbb setup 升级确认)共用。
5
+ * - 读取已安装版本(manifest 中的 version 字段)
6
+ * - 查询npm registry 最新版本
7
+ * - 比较版本号(含预发布后缀)
8
+ *
9
+ * 所有 IO 出错一律走 Promise reject / 返回 null,由调用方决定如何降级处理,
10
+ * 本模块绝不抛出阻塞上层的异常。
11
+ */
12
+
13
+ const fs = require('fs');
14
+ const path = require('path');
15
+ const http = require('http');
16
+
17
+ const PKG_NAME = '@jspg-ai/coding-bb';
18
+ const REGISTRY = 'https://registry.npmjs.org';
19
+ const MANIFEST_FILE = path.join('.cbb', '.managed-by-cbb');
20
+ const REQUEST_TIMEOUT = 3000;
21
+
22
+ /**
23
+ * 读取项目内已安装的版本号(从 .cbb/.managed-by-cbb 清单)。
24
+ * 不存在或解析失败返回 null。
25
+ */
26
+ function readInstalledVersion(projectDir) {
27
+ const manifestPath = path.join(projectDir, MANIFEST_FILE);
28
+ if (!fs.existsSync(manifestPath)) return null;
29
+ try {
30
+ const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf-8'));
31
+ return manifest.version || null;
32
+ } catch (_) {
33
+ return null;
34
+ }
35
+ }
36
+
37
+ /**
38
+ * 从 registry 查询指定 tag(latest / beta)的最新版本号。
39
+ * 网络错误 / 非 200 / 缺 tag 均 reject,由调用方降级。
40
+ */
41
+ function fetchLatest(tag) {
42
+ return new Promise((resolve, reject) => {
43
+ const url = `${REGISTRY}/${encodeURIComponent(PKG_NAME).replace('%2F', '/')}`;
44
+ const req = http.get(url, { timeout: REQUEST_TIMEOUT }, res => {
45
+ if (res.statusCode !== 200) { res.resume(); reject(new Error(`status ${res.statusCode}`)); return; }
46
+ let body = '';
47
+ res.setEncoding('utf-8');
48
+ res.on('data', chunk => { body += chunk; });
49
+ res.on('end', () => {
50
+ try {
51
+ const ver = JSON.parse(body)['dist-tags'][tag];
52
+ if (!ver) { reject(new Error(`no ${tag} tag`)); return; }
53
+ resolve(ver);
54
+ } catch (err) { reject(err); }
55
+ });
56
+ });
57
+ req.on('timeout', () => req.destroy(new Error('timeout')));
58
+ req.on('error', reject);
59
+ });
60
+ }
61
+
62
+ /**
63
+ * 语义化版本比较:a > b 返回 1,a < b 返回 -1,相等返回 0。
64
+ * 正确处理预发布后缀(如 1.0.4-beta.7 > 1.0.4-beta.6,正式版 > 任意预发布版)。
65
+ */
66
+ function compareVersions(a, b) {
67
+ const parse = v => {
68
+ const [core, pre] = String(v).split('-');
69
+ return { nums: core.split('.').map(n => parseInt(n, 10) || 0), pre: pre ? pre.split('.') : null };
70
+ };
71
+ const pa = parse(a), pb = parse(b);
72
+ for (let i = 0; i < 3; i++) {
73
+ const d = (pa.nums[i] || 0) - (pb.nums[i] || 0);
74
+ if (d !== 0) return d > 0 ? 1 : -1;
75
+ }
76
+ if (!pa.pre && !pb.pre) return 0;
77
+ if (!pa.pre) return 1;
78
+ if (!pb.pre) return -1;
79
+ const len = Math.max(pa.pre.length, pb.pre.length);
80
+ for (let i = 0; i < len; i++) {
81
+ const x = pa.pre[i], y = pb.pre[i];
82
+ if (x === undefined) return -1;
83
+ if (y === undefined) return 1;
84
+ const nx = parseInt(x, 10), ny = parseInt(y, 10);
85
+ const bothNum = !isNaN(nx) && !isNaN(ny) && String(nx) === x && String(ny) === y;
86
+ if (bothNum) { if (nx !== ny) return nx > ny ? 1 : -1; }
87
+ else if (x !== y) return x > y ? 1 : -1;
88
+ }
89
+ return 0;
90
+ }
91
+
92
+ /** 判断是否为预发布版本(含 -beta / -rc 等后缀) */
93
+ function detectIsPrerelease(version) {
94
+ return typeof version === 'string' && version.includes('-');
95
+ }
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
+ module.exports = {
126
+ PKG_NAME,
127
+ REGISTRY,
128
+ MANIFEST_FILE,
129
+ REQUEST_TIMEOUT,
130
+ readInstalledVersion,
131
+ fetchLatest,
132
+ compareVersions,
133
+ detectIsPrerelease,
134
+ findTagForVersion,
135
+ };
@@ -0,0 +1,358 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Confluence REST API 客户端:通过 HTTP Basic Auth 调用 Confluence REST API,
5
+ * 完成页面的增删改查。Wiki 地址从 config/cbb.yaml 读取(项目级配置),
6
+ * 认证凭据来自环境变量 CBB_WIKI_EMAIL/CBB_WIKI_TOKEN 或 ~/.cbb/wiki-config.json。
7
+ * 所有函数返回 { success, data?, error? }。
8
+ */
9
+
10
+
11
+
12
+
13
+ const https = require('https');
14
+ const fs = require('fs');
15
+ const path = require('path');
16
+ const os = require('os');
17
+
18
+ const yaml = require('js-yaml');
19
+ const PROJECT_CONFIG_PATH = path.join(__dirname, '..', '..', '..', 'config', 'cbb.yaml');
20
+ function loadBaseUrl() {
21
+ try {
22
+ const cfg = yaml.load(fs.readFileSync(PROJECT_CONFIG_PATH, 'utf-8'));
23
+ if (cfg && cfg.wiki && cfg.wiki.baseUrl) return cfg.wiki.baseUrl;
24
+ } catch (_) { /* 配置缺失或解析失败,用默认 */ }
25
+ return 'wiki.amh-group.com';
26
+ }
27
+ const BASE_URL = loadBaseUrl();
28
+ const CONFIG_FILE = path.join(os.homedir(), '.cbb', 'wiki-config.json');
29
+ const REQUEST_TIMEOUT = 30000;
30
+ const MAX_ATTEMPTS = 3;
31
+
32
+ // ── 认证 ──────────────────────────────────────────────
33
+
34
+ function getAuthHeader() {
35
+ const email = process.env.CBB_WIKI_EMAIL;
36
+ const token = process.env.CBB_WIKI_TOKEN;
37
+ if (email && token) {
38
+ return 'Basic ' + Buffer.from(email + ':' + token).toString('base64');
39
+ }
40
+ if (fs.existsSync(CONFIG_FILE)) {
41
+ try {
42
+ const config = JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf-8'));
43
+ if (config.email && config.token) {
44
+ return 'Basic ' + Buffer.from(config.email + ':' + config.token).toString('base64');
45
+ }
46
+ } catch (_) { /* ignore */ }
47
+ }
48
+ return null;
49
+ }
50
+
51
+
52
+ function hasAuth() {
53
+ return getAuthHeader() !== null;
54
+ }
55
+
56
+ // ── HTTP 请求(参考 check-update.js 模式) ────────────
57
+
58
+
59
+ function request(method, apiPath, body, extraHeaders, rawBody) {
60
+ const auth = getAuthHeader();
61
+ if (!auth) {
62
+ return Promise.resolve({ success: false, error: '未配置 Wiki 认证。请设置环境变量 CBB_WIKI_EMAIL 和 CBB_WIKI_TOKEN,或创建 ~/.cbb/wiki-config.json 文件(格式:{"email":"...","token":"..."})' });
63
+ }
64
+
65
+ const bodyStr = rawBody !== null && rawBody !== undefined ? rawBody : (body ? JSON.stringify(body) : null);
66
+
67
+
68
+
69
+ return new Promise((resolve) => {
70
+ let attempt = 0;
71
+ let lastResult = null;
72
+
73
+ const tryOnce = () => {
74
+ attempt++;
75
+ const headers = {
76
+ 'Accept': 'application/json',
77
+ 'Authorization': auth,
78
+ 'X-Atlassian-Token': 'no-check',
79
+ ...extraHeaders,
80
+ };
81
+ if (bodyStr && !extraHeaders?.['Content-Type']) {
82
+ headers['Content-Type'] = 'application/json';
83
+ }
84
+ if (bodyStr) {
85
+ headers['Content-Length'] = Buffer.byteLength(bodyStr);
86
+ }
87
+
88
+ const options = {
89
+ hostname: BASE_URL,
90
+ port: 443,
91
+ path: apiPath,
92
+ method,
93
+ headers,
94
+ timeout: REQUEST_TIMEOUT,
95
+ };
96
+
97
+
98
+ const req = https.request(options, (res) => {
99
+ let data = '';
100
+ res.setEncoding('utf-8');
101
+ res.on('data', chunk => { data += chunk; });
102
+ res.on('end', () => {
103
+ try {
104
+ const parsed = data ? JSON.parse(data) : {};
105
+ if (res.statusCode >= 200 && res.statusCode < 300) {
106
+ resolve({ success: true, statusCode: res.statusCode, data: parsed });
107
+ } else {
108
+ resolve({ success: false, statusCode: res.statusCode, error: parsed.message || `HTTP ${res.statusCode}`, data: parsed });
109
+ }
110
+ } catch (e) {
111
+ resolve({ success: false, statusCode: res.statusCode, error: `响应解析失败: ${e.message}` });
112
+ }
113
+ });
114
+ });
115
+
116
+ req.on('timeout', () => {
117
+ req.destroy();
118
+ lastResult = { success: false, error: '请求超时' };
119
+ if (attempt < MAX_ATTEMPTS) retryAfter();
120
+ else resolve(lastResult);
121
+ });
122
+ req.on('error', (err) => {
123
+ lastResult = { success: false, error: err.message };
124
+ if (attempt < MAX_ATTEMPTS) retryAfter();
125
+ else resolve(lastResult);
126
+ });
127
+
128
+ if (bodyStr) req.write(bodyStr);
129
+ req.end();
130
+ };
131
+
132
+ const retryAfter = () => {
133
+
134
+ const delay = 1000 * attempt;
135
+ setTimeout(tryOnce, delay);
136
+ };
137
+
138
+ tryOnce();
139
+ });
140
+ }
141
+
142
+ // ── 公开 API ──────────────────────────────────────────
143
+
144
+
145
+ async function getPage(pageId, options = {}) {
146
+ const expand = options.includeBody ? 'version,space,body.storage' : 'version,space';
147
+ const result = await request('GET', `/rest/api/content/${pageId}?expand=${expand}`);
148
+ if (!result.success) return result;
149
+ return {
150
+ success: true,
151
+ data: {
152
+ id: result.data.id,
153
+ version: result.data.version?.number || 1,
154
+ title: result.data.title || '',
155
+ spaceKey: result.data.space?.key || '',
156
+ spaceName: result.data.space?.name || '',
157
+ bodyStorage: result.data.body?.storage?.value || '',
158
+ },
159
+ };
160
+ }
161
+
162
+
163
+ async function createPage(title, parentId, xhtmlContent) {
164
+ const parentResult = await request('GET', `/rest/api/content/${parentId}?expand=space`);
165
+ if (!parentResult.success) {
166
+ return { success: false, error: `无法获取父页面信息: ${parentResult.error}` };
167
+ }
168
+ const spaceKey = parentResult.data.space?.key;
169
+ if (!spaceKey) {
170
+ return { success: false, error: `父页面 ${parentId} 无 space 信息` };
171
+ }
172
+
173
+
174
+
175
+ const cql = `type=page AND ancestor=${parentId} AND title="${title.replace(/"/g, '\\"')}"`;
176
+ const dupResult = await request('GET', `/rest/api/content/search?cql=${encodeURIComponent(cql)}&limit=1`);
177
+ if (dupResult.success && Array.isArray(dupResult.data.results) && dupResult.data.results.length > 0) {
178
+ const existing = dupResult.data.results[0];
179
+ const existingId = existing.id;
180
+
181
+ const pageInfo = await getPage(existingId);
182
+ if (!pageInfo.success) {
183
+ return { success: false, error: `同名页面已存在但获取版本失败: ${pageInfo.error}` };
184
+ }
185
+ const updateResult = await updatePage(existingId, pageInfo.data.version, pageInfo.data.title, xhtmlContent);
186
+ if (updateResult.success) {
187
+ return { ...updateResult, mode: 'updated' };
188
+ }
189
+ return updateResult;
190
+ }
191
+
192
+ const payload = {
193
+ type: 'page',
194
+ title,
195
+ space: { key: spaceKey },
196
+ ancestors: [{ id: parentId }],
197
+ body: {
198
+ storage: {
199
+ value: xhtmlContent,
200
+ representation: 'storage',
201
+ },
202
+ },
203
+ };
204
+
205
+ const result = await request('POST', '/rest/api/content', payload);
206
+ if (result.success) {
207
+ return { ...result, mode: 'created' };
208
+ }
209
+ return result;
210
+ }
211
+
212
+
213
+ async function updatePage(pageId, currentVersion, title, xhtmlContent, minorEdit = false) {
214
+ const payload = {
215
+ id: pageId,
216
+ type: 'page',
217
+ title,
218
+ version: {
219
+ number: currentVersion + 1,
220
+ minorEdit,
221
+ },
222
+ body: {
223
+ storage: {
224
+ value: xhtmlContent,
225
+ representation: 'storage',
226
+ },
227
+ },
228
+ };
229
+
230
+ return request('PUT', `/rest/api/content/${pageId}`, payload);
231
+ }
232
+
233
+
234
+ async function getChildren(pageId, opts = {}) {
235
+ const limit = opts.limit || 50;
236
+ const start = opts.start || 0;
237
+ return request('GET', `/rest/api/content/${pageId}/child/page?limit=${limit}&start=${start}`);
238
+ }
239
+
240
+
241
+ async function search(cql, opts = {}) {
242
+ const params = new URLSearchParams({ cql, limit: String(opts.limit || 20), start: String(opts.start || 0) });
243
+ if (opts.excerpt) params.set('excerptHighlight', 'true');
244
+ if (opts.order) params.set('order', opts.order);
245
+ return request('GET', `/rest/api/search?${params.toString()}`);
246
+ }
247
+
248
+
249
+ async function getSpaces(opts = {}) {
250
+ const params = new URLSearchParams({ limit: String(opts.limit || 100) });
251
+ if (opts.type) params.set('type', opts.type);
252
+ return request('GET', `/rest/api/space?${params.toString()}`);
253
+ }
254
+
255
+
256
+ async function getComments(pageId, opts = {}) {
257
+ const limit = opts.limit || 50;
258
+ return request('GET', `/rest/api/content/${pageId}/child/comment?limit=${limit}`);
259
+ }
260
+
261
+
262
+ async function getUser(opts = {}) {
263
+ if (opts.current) return request('GET', '/rest/api/user/current');
264
+ if (opts.accountId) return request('GET', `/rest/api/user?accountId=${encodeURIComponent(opts.accountId)}`);
265
+ if (opts.username) return request('GET', `/rest/api/user?username=${encodeURIComponent(opts.username)}`);
266
+ return { success: false, error: 'getUser 需要指定 --current、--account-id 或 --username' };
267
+ }
268
+
269
+
270
+ async function getVersions(pageId, opts = {}) {
271
+ const limit = opts.limit || 20;
272
+ return request('GET', `/rest/experimental/content/${pageId}/version?limit=${limit}`);
273
+ }
274
+
275
+
276
+ async function getAttachments(pageId, opts = {}) {
277
+ const limit = opts.limit || 50;
278
+ return request('GET', `/rest/api/content/${pageId}/child/attachment?limit=${limit}`);
279
+ }
280
+
281
+
282
+ async function createComment(pageId, xhtmlContent, parentCommentId) {
283
+ const payload = {
284
+ type: 'comment',
285
+ body: { storage: { value: xhtmlContent, representation: 'storage' } },
286
+ container: { type: 'page', id: String(pageId) },
287
+ };
288
+ if (parentCommentId) {
289
+ payload.container = { type: 'comment', id: String(parentCommentId) };
290
+ }
291
+ return request('POST', '/rest/api/content', payload);
292
+ }
293
+
294
+
295
+ async function uploadAttachment(pageId, filePath, comment) {
296
+ const auth = getAuthHeader();
297
+ if (!auth) {
298
+ return { success: false, error: '未配置 Wiki 认证' };
299
+ }
300
+ if (!fs.existsSync(filePath)) {
301
+ return { success: false, error: `文件不存在 ${filePath}` };
302
+ }
303
+
304
+ const boundary = '----wiki' + Math.random().toString(16).slice(2);
305
+ const fileName = path.basename(filePath);
306
+ const fileBuf = fs.readFileSync(filePath);
307
+ const parts = [];
308
+ parts.push(Buffer.from(`--${boundary}\r\nContent-Disposition: form-data; name="file"; filename="${fileName}"\r\nContent-Type: application/octet-stream\r\n\r\n`));
309
+ parts.push(fileBuf);
310
+ parts.push(Buffer.from('\r\n'));
311
+ if (comment) {
312
+ parts.push(Buffer.from(`--${boundary}\r\nContent-Disposition: form-data; name="comment"\r\n\r\n${comment}\r\n`));
313
+ }
314
+ parts.push(Buffer.from(`--${boundary}--\r\n`));
315
+ const rawBody = Buffer.concat(parts);
316
+
317
+ return request('POST', `/rest/api/content/${pageId}/child/attachment`, null, {
318
+ 'Content-Type': `multipart/form-data; boundary=${boundary}`,
319
+ }, rawBody);
320
+ }
321
+
322
+ /**
323
+ * 删除附件
324
+ * @param {string} attachmentId
325
+ * @returns {Promise<{success: boolean, error?: string}>}
326
+ */
327
+ async function deleteAttachment(attachmentId) {
328
+ return request('DELETE', `/rest/api/content/${attachmentId}`);
329
+ }
330
+
331
+ /**
332
+ * 根据页面数据构建 Wiki URL
333
+ * @param {{ id: string }} pageData
334
+ * @returns {string}
335
+ */
336
+ function buildPageUrl(pageData) {
337
+ return `https://${BASE_URL}/pages/viewpage.action?pageId=${pageData.id}`;
338
+ }
339
+
340
+
341
+
342
+ async function appendToPage(pageId, newXhtml, options = {}) {
343
+
344
+ const pageInfo = await getPage(pageId, { includeBody: true });
345
+ if (!pageInfo.success) return pageInfo;
346
+ const oldBody = pageInfo.data.bodyStorage || '';
347
+ const separator = options.separator || '\n\n';
348
+ const merged = oldBody + (oldBody && !oldBody.endsWith('\n') ? '\n' : '') + separator + newXhtml;
349
+
350
+
351
+ return updatePage(pageId, pageInfo.data.version, pageInfo.data.title, merged, !!options.minorEdit);
352
+ }
353
+
354
+ module.exports = {
355
+ getPage, createPage, updatePage, appendToPage, buildPageUrl, hasAuth,
356
+ getChildren, search, getSpaces, getComments, getUser,
357
+ getVersions, getAttachments, createComment, uploadAttachment, deleteAttachment,
358
+ };