@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,464 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Markdown → Confluence Storage XHTML 转换器。
5
+ *
6
+ * 将 design.md 风格的 Markdown 转换为 Confluence Storage Format XHTML,
7
+ * 支持 mermaid/plantuml 图表宏、代码块、表格、列表等常用元素。
8
+ *
9
+ * 采用"提取→处理→还原"的分阶段流水线架构:
10
+ * 1. 提取保护:代码块、行内代码 → 占位符
11
+ * 2. 结构转换:标题、表格、列表、分割线
12
+ * 3. 内联转换:粗体、斜体、链接、图片
13
+ * 4. 还原注入:占位符 → Confluence 宏 / XHTML 标签
14
+ * 5. 收尾:段落包裹、XHTML 清理
15
+ */
16
+
17
+ const { applyMermaidGuard } = require('./mermaid-guard');
18
+
19
+ // ── 语言映射表(参考 vai-cli extract-code.js) ─────────
20
+
21
+ const LANG_MAP = {
22
+ json: 'js', jsonc: 'js', js: 'js', javascript: 'js', jsx: 'js', mjs: 'js', cjs: 'js',
23
+ ts: 'js', typescript: 'js', tsx: 'js', mts: 'js',
24
+ bash: 'bash', sh: 'bash', shell: 'bash', zsh: 'bash', dockerfile: 'bash', makefile: 'bash',
25
+ py: 'py', python: 'py', python3: 'py',
26
+ yml: 'yaml', yaml: 'yaml',
27
+ html: 'html', htm: 'html', xhtml: 'html', xml: 'xml', svg: 'xml', vue: 'html',
28
+ css: 'css', scss: 'css', sass: 'css', less: 'css',
29
+ java: 'java', kt: 'java', kotlin: 'java', groovy: 'groovy', gradle: 'groovy',
30
+ c: 'c++', cpp: 'c++', 'c++': 'c++', h: 'c++', hpp: 'c++',
31
+ cs: 'c#', csharp: 'c#', 'c#': 'c#',
32
+ rb: 'ruby', ruby: 'ruby',
33
+ php: 'php',
34
+ pl: 'perl', perl: 'perl',
35
+ sql: 'sql', mysql: 'sql', pgsql: 'sql', plsql: 'sql',
36
+ scala: 'scala',
37
+ vb: 'vb', vbnet: 'vb',
38
+ ps1: 'powershell', powershell: 'powershell', pwsh: 'powershell',
39
+ diff: 'diff', patch: 'diff',
40
+ md: 'none', markdown: 'none', txt: 'none', text: 'none',
41
+ go: 'none', golang: 'none', rust: 'none', rs: 'none', swift: 'none', dart: 'none',
42
+ r: 'none', lua: 'none', haskell: 'none', elixir: 'none',
43
+ terraform: 'none', tf: 'none', graphql: 'none',
44
+ };
45
+
46
+ function resolveLang(lang) {
47
+ const lower = (lang || '').toLowerCase();
48
+ return LANG_MAP[lower] || 'none';
49
+ }
50
+
51
+ // ── 工具函数 ──────────────────────────────────────────
52
+
53
+ function escapeXml(str) {
54
+ return str
55
+ .replace(/&/g, '&')
56
+ .replace(/</g, '&lt;')
57
+ .replace(/>/g, '&gt;')
58
+ .replace(/"/g, '&quot;');
59
+ }
60
+
61
+ function escapeCdata(str) {
62
+ // CDATA 不能包含 ]]> 序列
63
+ return str.replace(/]]>/g, ']]]]><![CDATA[>');
64
+ }
65
+
66
+ // ── 主转换函数 ────────────────────────────────────────
67
+
68
+ /**
69
+ * 将 Markdown 转换为 Confluence Storage XHTML
70
+ * @param {string} markdown - Markdown 源码
71
+ * @returns {string} Confluence Storage XHTML
72
+ */
73
+ function markdownToXhtml(markdown) {
74
+ // 统一换行符
75
+ let html = markdown.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
76
+
77
+ // ── 阶段 1: 提取代码块 ──
78
+ const codeBlocks = [];
79
+ html = html.replace(/```(\w*)\n([\s\S]*?)```/g, (match, lang, code) => {
80
+ codeBlocks.push({ language: lang || '', content: code });
81
+ return `__CODE_BLOCK_${codeBlocks.length - 1}__`;
82
+ });
83
+
84
+ // ── 阶段 2: 提取行内代码 ──
85
+ const inlineCodes = [];
86
+ html = html.replace(/`([^`]+)`/g, (match, code) => {
87
+ inlineCodes.push(code);
88
+ return `__INLINE_CODE_${inlineCodes.length - 1}__`;
89
+ });
90
+
91
+ // ── 阶段 3: 提取链接和图片(保护 URL 中的特殊字符) ──
92
+ // 图片必须在链接之前提取,因为 ![...](...) 会先被链接正则匹配
93
+ const links = [];
94
+ html = html.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, (match, alt, url) => {
95
+ links.push({ text: alt, url, type: 'image' });
96
+ return `__IMAGE_${links.length - 1}__`;
97
+ });
98
+ html = html.replace(/\[([^\]]*)\]\(([^)]+)\)/g, (match, text, url) => {
99
+ links.push({ text, url, type: 'link' });
100
+ return `__LINK_${links.length - 1}__`;
101
+ });
102
+
103
+ // ── 阶段 3.4: 提取 Confluence 安全 HTML 标签(白名单) ──
104
+ // Confluence Storage Format 支持少量行内 HTML 标签(如 <font>、<span>),
105
+ // 在 Stage 3.5 全量转义前先提取为占位符,避免被错误转义。
106
+ const SAFE_HTML_TAGS = ['font', 'span', 'b', 'i', 'strong', 'em', 'del', 'sub', 'sup', 'u', 'br', 'mark'];
107
+ const safeTags = [];
108
+ const safeTagRegex = new RegExp(`</?(?:${SAFE_HTML_TAGS.join('|')})(?:\\s[^>]*?)?/?>`, 'gi');
109
+ html = html.replace(safeTagRegex, (match) => {
110
+ safeTags.push(match);
111
+ return `__SAFE_HTML_${safeTags.length - 1}__`;
112
+ });
113
+
114
+ // ── 阶段 3.5: 转义原始文本中的 & 和 < ──
115
+ // 此时代码块、行内代码、链接、图片、安全 HTML 标签均已提取为占位符,
116
+ // 剩余文本中的 & 和 < 均为字面字符,需要转义
117
+ html = html.replace(/&/g, '&amp;');
118
+ html = html.replace(/</g, '&lt;');
119
+
120
+ // ── 阶段 3.5.1: 还原安全 HTML 标签 ──
121
+ for (let i = safeTags.length - 1; i >= 0; i--) {
122
+ html = html.split(`__SAFE_HTML_${i}__`).join(safeTags[i]);
123
+ }
124
+
125
+ // ── 阶段 3.6: 包裹 @ 注解(避免被 Confluence 解析为用户提及) ──
126
+ // 仅匹配行首或空白后的 @Word,避免误处理邮箱(name@domain 中的 @ 前是 . 或字母)
127
+ html = html.replace(/(^|\s)@([A-Za-z]\w*)/g, '$1<code>@$2</code>');
128
+
129
+ // ── 阶段 4: 处理表格 ──
130
+ html = processTables(html);
131
+
132
+ // ── 阶段 5: 粗体/斜体/删除线 ──
133
+ html = html.replace(/\*\*\*(.+?)\*\*\*/g, '<strong><em>$1</em></strong>');
134
+ html = html.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>');
135
+ html = html.replace(/(?<!\*)\*([^*\n]+?)\*(?!\*)/g, '<em>$1</em>');
136
+ html = html.replace(/~~(.+?)~~/g, '<del>$1</del>');
137
+
138
+ // ── 阶段 6: 标题(# → <h1>) ──
139
+ html = html.replace(/^#### (.+)$/gm, '<h4>$1</h4>');
140
+ html = html.replace(/^### (.+)$/gm, '<h3>$1</h3>');
141
+ html = html.replace(/^## (.+)$/gm, '<h2>$1</h2>');
142
+ html = html.replace(/^# (.+)$/gm, '<h1>$1</h1>');
143
+
144
+ // ── 阶段 7: 列表 ──
145
+ html = processLists(html);
146
+
147
+ // ── 阶段 8: 分割线 ──
148
+ html = html.replace(/^[-*_]{3,}\s*$/gm, '<hr/>');
149
+
150
+ // ── 阶段 9: 还原链接和图片 ──
151
+ for (let i = links.length - 1; i >= 0; i--) {
152
+ const link = links[i];
153
+ if (link.type === 'image') {
154
+ html = html.split(`__IMAGE_${i}__`).join(`<ac:image><ri:url ri:value="${escapeXml(link.url)}"/></ac:image>`);
155
+ } else {
156
+ html = html.split(`__LINK_${i}__`).join(`<a href="${escapeXml(link.url)}">${escapeXml(link.text)}</a>`);
157
+ }
158
+ }
159
+
160
+ // ── 阶段 10: 还原行内代码 ──
161
+ for (let i = inlineCodes.length - 1; i >= 0; i--) {
162
+ html = html.split(`__INLINE_CODE_${i}__`).join(`<code>${escapeXml(inlineCodes[i])}</code>`);
163
+ }
164
+
165
+ // ── 阶段 11: 还原代码块为 Confluence 宏 ──
166
+ for (let i = codeBlocks.length - 1; i >= 0; i--) {
167
+ const block = codeBlocks[i];
168
+ const langLower = block.language.toLowerCase();
169
+ let macro;
170
+
171
+ if (langLower === 'mermaid') {
172
+ const guardResult = applyMermaidGuard(block.content.trim());
173
+ // 品牌主色:未自带 %%{init 时注入(尊重作者自定义)
174
+ const brandInit = "%%{init: {'themeVariables': {'primaryColor': '#2698F7'}}}%%";
175
+ const finalContent = guardResult.fixed.includes('%%{init')
176
+ ? guardResult.fixed
177
+ : `${brandInit}\n${guardResult.fixed}`;
178
+ macro = `<ac:structured-macro ac:name="mermaid-macro" ac:schema-version="1"><ac:plain-text-body><![CDATA[${escapeCdata(finalContent)}]]></ac:plain-text-body></ac:structured-macro>`;
179
+ } else if (langLower === 'plantuml') {
180
+ const content = block.content.trim();
181
+ const skinparamLine = 'skinparam backgroundColor #2698F7';
182
+ let finalContent;
183
+ if (content.includes('skinparam backgroundColor')) {
184
+ // 用户已自定义 skinparam,尊重作者选择
185
+ finalContent = content;
186
+ } else if (/@startuml\b/.test(content)) {
187
+ // 在 @startuml 之后插入品牌 skinparam
188
+ finalContent = content.replace(/(@startuml\b[^\n]*)/, `$1\n${skinparamLine}`);
189
+ } else {
190
+ // 无 @startuml,直接放顶部
191
+ finalContent = `${skinparamLine}\n${content}`;
192
+ }
193
+ macro = `<ac:structured-macro ac:name="plantuml" ac:schema-version="1"><ac:parameter ac:name="type">plantuml</ac:parameter><ac:plain-text-body><![CDATA[${escapeCdata(finalContent)}]]></ac:plain-text-body></ac:structured-macro>`;
194
+ } else {
195
+ const confluenceLang = resolveLang(block.language);
196
+ macro = `<ac:structured-macro ac:name="code" ac:schema-version="1"><ac:parameter ac:name="language">${confluenceLang}</ac:parameter><ac:parameter ac:name="theme">Midnight</ac:parameter><ac:parameter ac:name="linenumbers">true</ac:parameter><ac:plain-text-body><![CDATA[${escapeCdata(block.content.trim())}]]></ac:plain-text-body></ac:structured-macro>`;
197
+ }
198
+
199
+ html = html.split(`__CODE_BLOCK_${i}__`).join(macro);
200
+ }
201
+
202
+ // ─ 阶段 11.5: 块级元素隔离 ──
203
+ // 在列表、表格等块级元素前后加空行,防止段落包裹时 <p> 内嵌套块级元素
204
+ html = html.replace(/(<\/?(?:ul|ol|table|thead|tbody|tr|th|td)\b[^>]*>)/g, '\n$1\n');
205
+ html = html.replace(/\n{3,}/g, '\n\n');
206
+
207
+ // ─ 阶段 12: 段落包裹 ─
208
+ // 将剩余的非空行(未被其他标签包裹的)用 <p> 包裹
209
+ const lines = html.split('\n');
210
+ const resultLines = [];
211
+ let paraLines = [];
212
+
213
+ function flushPara() {
214
+ if (paraLines.length > 0) {
215
+ resultLines.push('<p>' + paraLines.join('\n') + '</p>');
216
+ paraLines = [];
217
+ }
218
+ }
219
+
220
+ let insideMacro = false;
221
+
222
+ for (let i = 0; i < lines.length; i++) {
223
+ const line = lines[i];
224
+ const trimmed = line.trim();
225
+
226
+ // 在 ac:structured-macro 块内部,直接输出,不做段落包裹
227
+ if (insideMacro) {
228
+ resultLines.push(line);
229
+ if (trimmed.includes('</ac:structured-macro>')) {
230
+ insideMacro = false;
231
+ }
232
+ continue;
233
+ }
234
+
235
+ // 检测 ac:structured-macro 开标签
236
+ if (/^<ac:structured-macro\b/.test(trimmed)) {
237
+ flushPara();
238
+ resultLines.push(line);
239
+ if (!trimmed.includes('</ac:structured-macro>')) {
240
+ insideMacro = true;
241
+ }
242
+ } else if (/^<\/?(h[1-6]|table|thead|tbody|tr|th|td|ul|ol|li|hr|ac:image|blockquote|div|pre|ac:link)\b/.test(trimmed) ||
243
+ /^<hr\/>$/.test(trimmed)) {
244
+ // 已经是块级标签的行,直接输出并刷新段落缓冲
245
+ flushPara();
246
+ resultLines.push(line);
247
+ } else if (trimmed === '') {
248
+ flushPara();
249
+ resultLines.push('');
250
+ } else {
251
+ paraLines.push(line);
252
+ }
253
+ }
254
+ flushPara();
255
+
256
+ html = resultLines.join('\n');
257
+
258
+ // ─ 阶段 12.5: 去除标签间多余空白 ─
259
+ // Stage 11.5 为了避免 <p> 嵌套块级元素,强制在块级标签前后加换行
260
+ // 段落包裹完成后,需要把这些"占位换行"清掉,让 XHTML 输出更紧凑
261
+ html = html.replace(/>\s+</g, '><');
262
+ // 同时清理"行内文本\n<块级标签"之间的换行(如 <li>foo\n<ul>...)
263
+ html = html.replace(/(\S)\n+(<ul\b|<ol\b|<table\b)/g, '$1$2');
264
+
265
+ // ─ 阶段 13: XHTML 清理 ─
266
+ // 清理多余空行
267
+ html = html.replace(/\n{3,}/g, '\n\n');
268
+ // 确保 self-closing 标签正确
269
+ html = html.replace(/<hr>/g, '<hr/>');
270
+ html = html.replace(/<br>/g, '<br/>');
271
+
272
+ return html.trim();
273
+ }
274
+
275
+ // ── 表格处理 ──────────────────────────────────────────
276
+
277
+ function processTables(html) {
278
+ // 匹配 Markdown 表格:至少一行表头 + 一行分隔符 + 零或多行数据
279
+ const tableRegex = /(?:^\|.+\|$\n?)+/gm;
280
+
281
+ return html.replace(tableRegex, (tableBlock) => {
282
+ const lines = tableBlock.trim().split('\n');
283
+ if (lines.length < 2) return tableBlock; // 至少需要表头和分隔行
284
+
285
+ // 检查是否有分隔行(第二行必须包含 |---| 模式)
286
+ const sepLine = lines[1] || '';
287
+ if (!/^\|[\s\-:]+\|/.test(sepLine) && !/^\|[-:\s|]+\|$/.test(sepLine)) {
288
+ return tableBlock;
289
+ }
290
+
291
+ // 解析对齐方式
292
+ const aligns = sepLine.split('|').filter(c => c.trim() !== '')
293
+ .map(c => {
294
+ const trimmed = c.trim();
295
+ if (trimmed.startsWith(':') && trimmed.endsWith(':')) return 'center';
296
+ if (trimmed.endsWith(':')) return 'right';
297
+ return 'left';
298
+ });
299
+
300
+ // 表头行
301
+ const headerCells = (lines[0] || '').split('|').filter(c => c.trim() !== '')
302
+ .map(c => c.trim());
303
+
304
+ // 数据行
305
+ const dataRows = lines.slice(2);
306
+
307
+ let tableHtml = '<table><thead><tr>';
308
+ headerCells.forEach((cell, idx) => {
309
+ const align = aligns[idx] ? ` style="text-align:${aligns[idx]}"` : '';
310
+ tableHtml += `<th${align}>${cell}</th>`;
311
+ });
312
+ tableHtml += '</tr></thead><tbody>';
313
+
314
+ dataRows.forEach(row => {
315
+ const cells = row.split('|').filter(c => c.trim() !== '').map(c => c.trim());
316
+ tableHtml += '<tr>';
317
+ cells.forEach((cell, idx) => {
318
+ const align = aligns[idx] ? ` style="text-align:${aligns[idx]}"` : '';
319
+ tableHtml += `<td${align}>${cell}</td>`;
320
+ });
321
+ tableHtml += '</tr>';
322
+ });
323
+
324
+ tableHtml += '</tbody></table>';
325
+ return tableHtml;
326
+ });
327
+ }
328
+
329
+ // ── 列表处理 ──────────────────────────────────────────
330
+
331
+ /**
332
+ * 解析列表块为树形结构
333
+ * @param {string[]} blockLines - 列表块的所有行(含缩进)
334
+ * @returns {{type: string, items: object[]}|null}
335
+ */
336
+ function parseListBlock(blockLines) {
337
+ // 栈:每个条目 { indent, container, listType }
338
+ // container 是父节点(item 或 root)
339
+ const root = { children: null };
340
+ const stack = [{ indent: -1, container: root }];
341
+ let currentParaIdx = -1;
342
+
343
+ for (const line of blockLines) {
344
+ if (line.trim() === '') {
345
+ // 空行:当前段结束
346
+ currentParaIdx = -1;
347
+ continue;
348
+ }
349
+
350
+ const itemMatch = line.match(/^(\s*)([-*+]|\d+\.)\s+(.*)$/);
351
+ if (itemMatch) {
352
+ const indent = itemMatch[1].length;
353
+ const marker = itemMatch[2];
354
+ const content = itemMatch[3];
355
+ const listType = /^\d+\.$/.test(marker) ? 'ol' : 'ul';
356
+
357
+ // 弹出缩进 ≥ 当前的所有栈帧
358
+ while (stack.length > 1 && stack[stack.length - 1].indent >= indent) {
359
+ stack.pop();
360
+ }
361
+
362
+ const parent = stack[stack.length - 1].container;
363
+ // 复用或创建父节点的 list
364
+ let list = null;
365
+ if (parent.children && parent.children.type === listType) {
366
+ list = parent.children;
367
+ } else {
368
+ list = { type: listType, items: [] };
369
+ parent.children = list;
370
+ }
371
+
372
+ const item = { content, paragraphs: [], children: null };
373
+ list.items.push(item);
374
+
375
+ stack.push({ indent, container: item });
376
+ currentParaIdx = -1;
377
+ } else {
378
+ // 延续行(缩进但不是列表标记)
379
+ const indent = line.match(/^(\s*)/)[1].length;
380
+ const text = line.trim();
381
+
382
+ // 找最近一个 indent < current 的栈帧(其 container 是 item)
383
+ let target = null;
384
+ for (let k = stack.length - 1; k > 0; k--) {
385
+ if (stack[k].indent < indent) {
386
+ target = stack[k].container;
387
+ break;
388
+ }
389
+ }
390
+ if (!target) continue;
391
+
392
+ if (currentParaIdx === -1) {
393
+ target.paragraphs.push(text);
394
+ currentParaIdx = target.paragraphs.length - 1;
395
+ } else {
396
+ target.paragraphs[currentParaIdx] += '\n' + text;
397
+ }
398
+ }
399
+ }
400
+
401
+ return root.children;
402
+ }
403
+
404
+ function renderListHtml(list) {
405
+ if (!list) return '';
406
+ const items = list.items.map(item => {
407
+ let html = '<li>' + item.content;
408
+ if (item.paragraphs.length > 0) {
409
+ html += item.paragraphs.map(p => '<p>' + p + '</p>').join('');
410
+ }
411
+ if (item.children) {
412
+ html += renderListHtml(item.children);
413
+ }
414
+ html += '</li>';
415
+ return html;
416
+ }).join('');
417
+ return `<${list.type}>${items}</${list.type}>`;
418
+ }
419
+
420
+ function processLists(html) {
421
+ const lines = html.split('\n');
422
+ const result = [];
423
+ let i = 0;
424
+
425
+ while (i < lines.length) {
426
+ const line = lines[i];
427
+ if (!/^\s*([-*+]|\d+\.)\s+/.test(line)) {
428
+ result.push(line);
429
+ i++;
430
+ continue;
431
+ }
432
+
433
+ // 收集整个列表块(含子项、延续行、空行)
434
+ const block = [line];
435
+ let j = i + 1;
436
+ while (j < lines.length) {
437
+ const l = lines[j];
438
+ if (l.trim() === '') {
439
+ // 空行:前看是否仍是列表(含延续行)
440
+ let k = j + 1;
441
+ while (k < lines.length && lines[k].trim() === '') k++;
442
+ if (k < lines.length && (/^\s*([-*+]|\d+\.)\s+/.test(lines[k]) || /^\s+\S/.test(lines[k]))) {
443
+ block.push(l);
444
+ j = k;
445
+ } else {
446
+ break;
447
+ }
448
+ } else if (/^\s*([-*+]|\d+\.)\s+/.test(l) || /^\s+\S/.test(l)) {
449
+ block.push(l);
450
+ j++;
451
+ } else {
452
+ break;
453
+ }
454
+ }
455
+
456
+ const list = parseListBlock(block);
457
+ result.push(list ? renderListHtml(list) : block.join('\n'));
458
+ i = j;
459
+ }
460
+
461
+ return result.join('\n');
462
+ }
463
+
464
+ module.exports = { markdownToXhtml };
@@ -0,0 +1,218 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Wiki 发布命令 — 将 Markdown 文件发布到Confluence Wiki。
5
+ *
6
+ * 用法:
7
+ * cbb wp <file> --page-id <id> 更新已有页面
8
+ * cbb wp <file> --parent-id <id> --title "..." 创建新页面
9
+ * cbb wp <file> --page-id <id> --dry-run 仅输出 XHTML,不发布
10
+ *
11
+ * 认证:
12
+ * 环境变量 CBB_WIKI_EMAIL + CBB_WIKI_TOKEN,或 ~/.cbb/wiki-config.json
13
+ * 凭据与 it-wiki-mcp / vai-wiki 相同(个人访问令牌 + 邮箱格式用户名)
14
+ */
15
+
16
+ const fs = require('fs');
17
+ const path = require('path');
18
+ const os = require('os');
19
+ const { markdownToXhtml } = require('./convert');
20
+ const { getPage, createPage, updatePage, buildPageUrl } = require('./api-client');
21
+
22
+ // ── 参数解析 ──────────────────────────────────────────
23
+
24
+ function parseArgs(argv) {
25
+ // --help / -h 优先处理,避免误触发"缺少 file"或"未知选项"错误
26
+ if (argv.length === 1 && (argv[0] === '--help' || argv[0] === '-h')) {
27
+ return { help: true, pos: [], opts: {} };
28
+ }
29
+
30
+ const pos = [];
31
+ const opts = {};
32
+ let i = 0;
33
+
34
+ while (i < argv.length) {
35
+ const arg = argv[i];
36
+ // --flag=value 形式:单参数内同时包含名和值,避免下面误把 --page-id=value 中的 "=value" 当成位置参数
37
+ if (arg.startsWith('--') && arg.includes('=')) {
38
+ const eq = arg.indexOf('=');
39
+ const key = arg.slice(2, eq);
40
+ const value = arg.slice(eq + 1);
41
+ applyKnownOption(key, value, opts);
42
+ i++;
43
+ continue;
44
+ }
45
+ if (arg === '--page-id') {
46
+ opts.pageId = argv[++i];
47
+ } else if (arg === '--parent-id') {
48
+ opts.parentId = argv[++i];
49
+ } else if (arg === '--title') {
50
+ opts.title = argv[++i];
51
+ } else if (arg === '--file') {
52
+ opts.file = argv[++i];
53
+ } else if (arg === '--dry-run') {
54
+ opts.dryRun = true;
55
+ } else if (!arg.startsWith('--')) {
56
+ pos.push(arg);
57
+ } else {
58
+ // 未知选项:若像 --key=value 形式但 value 缺失,提示用户用空格分隔
59
+ if (arg.startsWith('--') && /=.?$/.test(arg)) {
60
+ const m = arg.match(/^--([\w-]+)=?$/);
61
+ const key = m ? m[1] : arg.slice(2);
62
+ log(`未知选项: ${arg}`, 'warn');
63
+ log(`提示:选项 --${key} 后面接的是另一个选项名或为空。`, 'warn');
64
+ log(` --flag 的值必须用空格分隔:--${key} <value>(或写为 --${key}=<value>,但不要漏 value)`, 'warn');
65
+ log(` 执行 \`cbb wp --help\` 查看完整用法`, 'warn');
66
+ process.exit(1);
67
+ }
68
+ log(`未知选项: ${arg}`, 'warn');
69
+ log('提示:参数格式为 --flag <value> 或 --flag=<value>,执行 `cbb wp --help` 查看完整用法', 'warn');
70
+ process.exit(1);
71
+ }
72
+ i++;
73
+ }
74
+
75
+ // 位置参数 <file> 仅在用户未显式 --file 时生效,避免覆盖解析结果
76
+ if (opts.file === undefined) {
77
+ opts.file = pos[0];
78
+ }
79
+
80
+ if (!opts.file) {
81
+ log('用法: cbb wp <file> [--page-id <id> | --parent-id <id> --title "..."] [--dry-run]', 'warn');
82
+ log('提示:参数格式为 --flag <value> 或 --flag=<value>,执行 `cbb wp --help` 查看完整用法', 'warn');
83
+ process.exit(1);
84
+ }
85
+
86
+ return { pos, opts };
87
+ }
88
+
89
+ // 处理 --key=value 形式的非位置类选项(--file/page-id/parent-id/title 仍以位置形式解析,保持原行为)
90
+ function applyKnownOption(key, value, opts) {
91
+ if (key === 'page-id') opts.pageId = value;
92
+ else if (key === 'parent-id') opts.parentId = value;
93
+ else if (key === 'title') opts.title = value;
94
+ else if (key === 'file') opts.file = value;
95
+ else if (key === 'dry-run') opts.dryRun = value === 'false' ? false : true;
96
+ else log(`未知选项: --${key}`, 'warn');
97
+ }
98
+
99
+ // ── 工具 ──────────────────────────────────────────────
100
+
101
+ function showHelp() {
102
+ log(`cbb wp — 将 Markdown 发布到Confluence Wiki
103
+
104
+ 参数格式:
105
+ 本工具同时支持两种写法,请按习惯任选其一:
106
+ --flag <value> GNU 风格(推荐,shell 提示更友好)
107
+ --flag=<value> 等号风格
108
+
109
+ 带值选项:--file / --page-id / --parent-id / --title
110
+ 布尔开关:--dry-run(出现即视为 true,不能带值)
111
+
112
+ 用法:
113
+ cbb wp <file> --page-id <pageId> # 更新已有页面
114
+ cbb wp <file> --page-id=<pageId> # 等价写法
115
+ cbb wp <file> --parent-id <parentId> --title "..." # 在父页面下创建新页面
116
+ cbb wp <file> --parent-id=<parentId> --title="..." # 等价写法
117
+ cbb wp <file> --page-id <pageId> --dry-run # 仅输出 XHTML,不发布
118
+
119
+ 示例:
120
+ cbb wp design.md --page-id 123456
121
+ cbb wp "D:\\path with space\\design.md" --page-id 123456 --dry-run
122
+
123
+ 认证:环境变量 CBB_WIKI_EMAIL + CBB_WIKI_TOKEN,或 ~/.cbb/wiki-config.json
124
+ 令牌获取: https://wiki.amh-group.com/plugins/tokens/view.action`, 'info');
125
+ }
126
+
127
+ // ── 工具 ──────────────────────────────────────────────
128
+
129
+ function log(msg, level) {
130
+ const colors = { warn: '\x1b[33m', error: '\x1b[31m', success: '\x1b[32m', info: '\x1b[36m' };
131
+ const prefix = { warn: '⚠', error: '✗', success: '✓', info: 'ℹ' };
132
+ const color = colors[level] || '';
133
+ const p = prefix[level] || '';
134
+ const reset = '\x1b[0m';
135
+ // 所有日志统一输出到 stderr,确保 stdout 可用于管道/dry-run 纯净输出
136
+ process.stderr.write(Buffer.from(`${color}${p} ${msg}${reset}\n`, 'utf8'));
137
+ }
138
+
139
+ // ── 主流程 ────────────────────────────────────────────
140
+
141
+ async function publish(argv) {
142
+ const parsed = parseArgs(argv);
143
+ if (parsed.help) {
144
+ showHelp();
145
+ return;
146
+ }
147
+ const { opts } = parsed;
148
+
149
+ // 1. 验证参数
150
+ if (!opts.pageId && !(opts.parentId && opts.title)) {
151
+ log('必须指定 --page-id(更新)或 --parent-id + --title(创建)', 'error');
152
+ log('用法: cbb wp <file> [--page-id <id> | --parent-id <id> --title "..."] [--dry-run]', 'warn');
153
+ process.exit(1);
154
+ }
155
+
156
+ // 2. 读取 Markdown 文件
157
+ const filePath = path.resolve(opts.file);
158
+ if (!fs.existsSync(filePath)) {
159
+ log(`文件不存在: ${filePath}`, 'error');
160
+ process.exit(1);
161
+ }
162
+ const markdown = fs.readFileSync(filePath, 'utf-8');
163
+ if (!markdown.trim()) {
164
+ log(`文件为空: ${filePath}`, 'error');
165
+ process.exit(1);
166
+ }
167
+
168
+ // 3. 转换 Markdown → XHTML
169
+ log('转换 Markdown → Confluence Storage XHTML...', 'info');
170
+ const xhtml = markdownToXhtml(markdown);
171
+ log(`转换完成,${xhtml.length} 字符`, 'info');
172
+
173
+ // 4. Dry-run 模式:仅输出 XHTML
174
+ if (opts.dryRun) {
175
+ // 写入临时文件
176
+ const tmpFile = path.join(os.tmpdir(), `cbb-dryrun-${Date.now()}.xml`);
177
+ fs.writeFileSync(tmpFile, xhtml, 'utf-8');
178
+ process.stdout.write(Buffer.from(xhtml + '\n', 'utf8'));
179
+ log(`\nXHTML 已保存到: ${tmpFile}`, 'info');
180
+ return;
181
+ }
182
+
183
+ // 5. 发布
184
+ let result;
185
+ if (opts.pageId) {
186
+ // 更新模式
187
+ log(`获取页面 ${opts.pageId} 信息...`, 'info');
188
+ const pageInfo = await getPage(opts.pageId);
189
+ if (!pageInfo.success) {
190
+ log(`获取页面失败: ${pageInfo.error}`, 'error');
191
+ process.exit(1);
192
+ }
193
+
194
+ log(`更新页面: ${pageInfo.data.title} (v${pageInfo.data.version} → v${pageInfo.data.version + 1})`, 'info');
195
+ result = await updatePage(opts.pageId, pageInfo.data.version, pageInfo.data.title, xhtml);
196
+ } else {
197
+ // 创建模式
198
+ log(`创建页面 "${opts.title}" 在父页面 ${opts.parentId} 下...`, 'info');
199
+ result = await createPage(opts.title, opts.parentId, xhtml);
200
+ }
201
+
202
+ if (!result.success) {
203
+ log(`发布失败: ${result.error}`, 'error');
204
+ if (result.statusCode === 401 || result.statusCode === 403) {
205
+ log('认证失败。请检查 CBB_WIKI_EMAIL / CBB_WIKI_TOKEN 环境变量或 ~/.cbb/wiki-config.json 配置文件。', 'warn');
206
+ log('用户名应为邮箱格式(如 jinchuang.zhang@example.com),令牌从 https://wiki.amh-group.com/plugins/tokens/view.action 获取。', 'warn');
207
+ } else if (result.statusCode === 409) {
208
+ log('版本冲突 (409),页面可能已被他人修改,请重试。', 'warn');
209
+ }
210
+ process.exit(1);
211
+ }
212
+
213
+ const url = buildPageUrl(result.data);
214
+ log(`发布成功!页面 ID: ${result.data.id}`, 'success');
215
+ log(`访问: ${url}`, 'success');
216
+ }
217
+
218
+ module.exports = { publish, parseArgs, applyKnownOption };