@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,383 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * inquirer 风格 checkbox 多选 UI。
5
+ *
6
+ * 键盘交互(TTY 模式):
7
+ * ↑ / ↓ 或 j / k — 移动光标
8
+ * Space — 切换当前项
9
+ * a — 全选 / 全不选(已全选时再按全不选)
10
+ * i — 反选所有项
11
+ * Enter — 确认(必须 ≥1 项已选)
12
+ * Ctrl+C — 取消并退出进程
13
+ *
14
+ * 降级路径(非 TTY,例如 CI / 管道):
15
+ * 输出列表后用 readline 接收按编号输入,多选用逗号分隔。
16
+ *
17
+ * 跨平台:
18
+ * - Windows 10 1607+ / Windows 11 原生支持 ANSI 与 raw mode
19
+ * - 颜色由 process.stdout.isTTY 与 NO_COLOR 环境变量联合控制
20
+ */
21
+
22
+ const readline = require('readline');
23
+
24
+ // ── ANSI 控制序列 ─────────────────────────────────────────
25
+
26
+ const ESC = '\x1b';
27
+ const CLEAR_LINE = `${ESC}[2K`; // 擦除整行
28
+ const CURSOR_UP = (n) => `${ESC}[${n}A`; // 光标上移 n 行
29
+ const CLEAR_TO_END = `${ESC}[J`; // 擦除从光标到屏幕底部
30
+ const HIDE_CURSOR = `${ESC}[?25l`;
31
+ const SHOW_CURSOR = `${ESC}[?25h`;
32
+
33
+ const { COLOR, COLOR_ENABLED, c } = require('./output');
34
+
35
+ // ── 纯函数:选中状态操作(独立可测)────────────────────
36
+
37
+ /** 切换某项选中状态,返回新 Set(不可变) */
38
+ function toggle(selected, idx) {
39
+ const next = new Set(selected);
40
+ if (next.has(idx)) next.delete(idx);
41
+ else next.add(idx);
42
+ return next;
43
+ }
44
+
45
+ /** 全选 / 全不选(已全选时再按则全不选) */
46
+ function toggleAll(selected, total) {
47
+ if (total <= 0) return new Set();
48
+ if (selected.size === total) return new Set();
49
+ return new Set(Array.from({ length: total }, (_, i) => i));
50
+ }
51
+
52
+ /** 反选所有 */
53
+ function invert(selected, total) {
54
+ const next = new Set();
55
+ for (let i = 0; i < total; i++) {
56
+ if (!selected.has(i)) next.add(i);
57
+ }
58
+ return next;
59
+ }
60
+
61
+ /**
62
+ * 解析非 TTY 模式下逗号 / 空格分隔的编号输入。
63
+ * @returns {Set<number>} 选中的索引(0-based),输入为空返回空 Set
64
+ */
65
+ function parseBulkInput(input, total) {
66
+ if (typeof input !== 'string' || !input.trim()) return new Set();
67
+ const indices = input.split(/[,,\s]+/)
68
+ .map(s => parseInt(s, 10) - 1)
69
+ .filter(i => Number.isInteger(i) && i >= 0 && i < total);
70
+ return new Set(indices);
71
+ }
72
+
73
+ // ── 渲染:把状态转成行数组(独立可测)────────────────────
74
+
75
+ /**
76
+ * @param {object} state
77
+ * @param {Array<{label:string, hint?:string}>} state.items
78
+ * @param {number} state.cursor
79
+ * @param {Set<number>} state.selected
80
+ * @param {string} [state.header]
81
+ * @param {string} [state.footer]
82
+ * @param {string} [state.emptyMsg]
83
+ * @returns {string[]} 行数组
84
+ */
85
+ function renderLines(state) {
86
+ const { items, cursor, selected, header, footer, emptyMsg } = state;
87
+ const lines = [];
88
+
89
+ if (header) {
90
+ lines.push(c('bold', header));
91
+ lines.push(c('gray', '─'.repeat(48)));
92
+ }
93
+
94
+ if (items.length === 0) {
95
+ lines.push(c('gray', emptyMsg || '(无可选项)'));
96
+ } else {
97
+ items.forEach((item, i) => {
98
+ const isCursor = i === cursor;
99
+ const isSelected = selected.has(i);
100
+ const cursorMark = isCursor ? c('cyan', '❯') : ' ';
101
+ const checkMark = isSelected ? c('green', '☑') : c('gray', '☐');
102
+ const raw = ` ${cursorMark} ${checkMark} ${item.label}`;
103
+ // 当前行整体加粗 + 反转视频:浅色终端也能识别
104
+ const line = isCursor && COLOR_ENABLED
105
+ ? `${COLOR.bold}${COLOR.reverse}${raw}${COLOR.reset}`
106
+ : (isCursor ? c('bold', raw) : raw);
107
+ lines.push(line);
108
+ if (item.hint) {
109
+ lines.push(c('gray', ' ' + item.hint));
110
+ }
111
+ });
112
+ }
113
+
114
+ if (footer) {
115
+ lines.push('');
116
+ lines.push(c('gray', footer));
117
+ }
118
+
119
+ return lines;
120
+ }
121
+
122
+ // ── TTY 模式:键盘交互主循环 ─────────────────────────────
123
+
124
+ const DEFAULT_FOOTER = '↑↓ 移动 · 空格 切换 · a 全选 · i 反选 · 回车 确认 · Ctrl+C 取消';
125
+
126
+ /**
127
+ * 探测 raw mode 是否真的可用。
128
+ *
129
+ * 返回 false 的场景(需要降级到 fallback):
130
+ * - process.stdin 不是 TTY(如 CI / 管道)
131
+ * - setRawMode(true) 抛错
132
+ * - setRawMode(true) 调用后 stdin.isRaw 不是 true(例如 Git Bash MINGW64 通过
133
+ * winpty 启动时:setRawMode 不报错但 isRaw 可能为 false,或实际键盘事件无法
134
+ * 传递给 Node.js 的 keypress 监听器)
135
+ *
136
+ * 探测完后还原原始 raw mode 状态,避免影响后续读取。
137
+ */
138
+ function canEnableRawMode(stdin) {
139
+ if (!stdin.isTTY) return false;
140
+ const wasRaw = stdin.isRaw === true;
141
+ try {
142
+ stdin.setRawMode(true);
143
+ const ok = stdin.isRaw === true;
144
+ if (wasRaw !== ok) {
145
+ try { stdin.setRawMode(wasRaw); } catch (_) { /* ignore */ }
146
+ }
147
+ return ok;
148
+ } catch (_) {
149
+ return false;
150
+ }
151
+ }
152
+
153
+ /**
154
+ * @param {object} options
155
+ * @param {Array<{label:string, hint?:string}>} options.items
156
+ * @param {string} [options.header]
157
+ * @param {string} [options.footer]
158
+ * @param {Set<number>} [options.preSelected] 预选项索引
159
+ * @returns {Promise<Set<number>>} 选中的索引集合
160
+ */
161
+ function promptCheckboxTty({ items, header, footer, preSelected }) {
162
+ return new Promise((resolve, reject) => {
163
+ const stdin = process.stdin;
164
+ const stdout = process.stdout;
165
+
166
+ // 探测:raw mode 不可用则立即降级,避免在 MINGW64 等环境下静默退出
167
+ if (!canEnableRawMode(stdin)) {
168
+ return promptCheckboxFallback({ items, header, footer, preSelected })
169
+ .then(resolve, reject);
170
+ }
171
+
172
+ const state = {
173
+ items,
174
+ cursor: 0,
175
+ selected: new Set(preSelected || []),
176
+ header,
177
+ footer: footer || DEFAULT_FOOTER,
178
+ emptyMsg: '(无可选项)',
179
+ };
180
+
181
+ let lastLineCount = 0;
182
+ let finished = false;
183
+
184
+ function rerender() {
185
+ if (lastLineCount > 0) {
186
+ stdout.write(CURSOR_UP(lastLineCount));
187
+ stdout.write(CLEAR_TO_END);
188
+ }
189
+ const lines = renderLines(state);
190
+ stdout.write(lines.join('\n') + '\n');
191
+ lastLineCount = lines.length;
192
+ }
193
+
194
+ function showWarning(text) {
195
+ // 在 UI 下方追加一行警告,下次 rerender 会被清除
196
+ stdout.write('\n' + c('yellow', ` ${text}`) + '\n');
197
+ lastLineCount += 2;
198
+ }
199
+
200
+ function finish(result, error) {
201
+ if (finished) return;
202
+ finished = true;
203
+ stdin.removeListener('keypress', onKeypress);
204
+ stdin.removeListener('end', onStdinEnd);
205
+ stdin.removeListener('close', onStdinEnd);
206
+ if (stdin.isRaw) {
207
+ try { stdin.setRawMode(false); } catch (_) { /* ignore */ }
208
+ }
209
+ // 恢复 paused 状态:避免 stdin 保持 flowing 导致 Node.js 事件循环
210
+ // 不退出(表现为进程运行完毕但光标停留在 shell 输入位置)
211
+ if (typeof stdin.pause === 'function') {
212
+ try { stdin.pause(); } catch (_) { /* ignore */ }
213
+ }
214
+ stdout.write(SHOW_CURSOR);
215
+ // 清理 UI:上移光标到 UI 开始位置后清除
216
+ if (lastLineCount > 0) {
217
+ stdout.write(CURSOR_UP(lastLineCount));
218
+ stdout.write(CLEAR_TO_END);
219
+ }
220
+ if (error) reject(error);
221
+ else resolve(result);
222
+ }
223
+
224
+ /**
225
+ * stdin 意外关闭降级处理。
226
+ *
227
+ * Git Bash MINGW64 通过 winpty 启动子进程时,raw mode 启用后 stdin 可能被
228
+ * 父进程立即关闭(表现为进程静默退出,UI 残留)。此时不能留白屏,降级到
229
+ * fallback 路径,用 preSelected 默认值确认,同时输出可见提示。
230
+ */
231
+ function onStdinEnd() {
232
+ if (finished) return;
233
+ stdin.removeListener('keypress', onKeypress);
234
+ stdin.removeListener('end', onStdinEnd);
235
+ stdin.removeListener('close', onStdinEnd);
236
+ if (stdin.isRaw) {
237
+ try { stdin.setRawMode(false); } catch (_) { /* ignore */ }
238
+ }
239
+ if (typeof stdin.pause === 'function') {
240
+ try { stdin.pause(); } catch (_) { /* ignore */ }
241
+ }
242
+ stdout.write(SHOW_CURSOR);
243
+ if (lastLineCount > 0) {
244
+ stdout.write(CURSOR_UP(lastLineCount));
245
+ stdout.write(CLEAR_TO_END);
246
+ }
247
+ stdout.write(c('yellow', '\n ⚠ stdin 已关闭,自动使用默认选择\n\n'));
248
+ promptCheckboxFallback({ items, header, footer, preSelected })
249
+ .then(resolve, reject);
250
+ }
251
+
252
+ function onKeypress(str, key) {
253
+ if (!key || finished) return;
254
+
255
+ // Ctrl+C 退出
256
+ if (key.ctrl && key.name === 'c') {
257
+ finish(new Set(), new Error('USER_CANCELLED'));
258
+ return;
259
+ }
260
+
261
+ switch (key.name) {
262
+ case 'up':
263
+ case 'k':
264
+ state.cursor = (state.cursor - 1 + items.length) % items.length;
265
+ rerender();
266
+ break;
267
+ case 'down':
268
+ case 'j':
269
+ state.cursor = (state.cursor + 1) % items.length;
270
+ rerender();
271
+ break;
272
+ case 'space':
273
+ state.selected = toggle(state.selected, state.cursor);
274
+ rerender();
275
+ break;
276
+ case 'a':
277
+ state.selected = toggleAll(state.selected, items.length);
278
+ rerender();
279
+ break;
280
+ case 'i':
281
+ state.selected = invert(state.selected, items.length);
282
+ rerender();
283
+ break;
284
+ case 'return':
285
+ case 'enter':
286
+ if (state.selected.size === 0) {
287
+ showWarning('⚠ 请至少选择 1 项');
288
+ } else {
289
+ finish(state.selected);
290
+ }
291
+ break;
292
+ default:
293
+ // 忽略其他按键
294
+ break;
295
+ }
296
+ }
297
+
298
+ // 启用 raw mode + 注册事件监听
299
+ // (探测阶段已通过 canEnableRawMode,这里理论上不会抛错;保留 try 防御)
300
+ try { stdin.setRawMode(true); } catch (_) { /* ignore */ }
301
+ readline.emitKeypressEvents(stdin);
302
+ stdin.on('keypress', onKeypress);
303
+ stdin.on('end', onStdinEnd);
304
+ stdin.on('close', onStdinEnd);
305
+ // 确保 stdin flowing:某些环境下 setRawMode 不会自动 resume
306
+ if (typeof stdin.resume === 'function') stdin.resume();
307
+
308
+ // 隐藏光标 + 首次渲染
309
+ stdout.write(HIDE_CURSOR);
310
+ rerender();
311
+ });
312
+ }
313
+
314
+ // ── 非 TTY 降级路径 ──────────────────────────────────────
315
+
316
+ function promptCheckboxFallback({ items, header, footer, preSelected }) {
317
+ return new Promise((resolve) => {
318
+ const state = {
319
+ items,
320
+ cursor: 0,
321
+ selected: new Set(preSelected || []),
322
+ header,
323
+ footer,
324
+ emptyMsg: '(无可选项)',
325
+ };
326
+
327
+ // 输出列表(无光标、无高亮,逐项标号)
328
+ console.log(c('bold', header) || '');
329
+ console.log(c('gray', '─'.repeat(48)));
330
+ items.forEach((item, i) => {
331
+ console.log(` ${c('cyan', String(i + 1) + '.')} ${item.label}`);
332
+ if (item.hint) console.log(c('gray', ' ' + item.hint));
333
+ });
334
+ console.log('');
335
+ if (footer) console.log(c('gray', footer));
336
+
337
+ const rl = readline.createInterface({
338
+ input: process.stdin,
339
+ output: process.stdout,
340
+ });
341
+
342
+ rl.question(`请输入编号(多选用逗号分隔): `, (answer) => {
343
+ rl.close();
344
+ const selected = parseBulkInput(answer, items.length);
345
+ resolve(selected);
346
+ });
347
+ });
348
+ }
349
+
350
+ // ── 主入口 ──────────────────────────────────────────────
351
+
352
+ /**
353
+ * 弹出 checkbox 列表,收集用户选择。
354
+ * @param {object} options
355
+ * @param {Array<{label:string, hint?:string}>} options.items
356
+ * @param {string} [options.header] 顶部标题(建议已含图标)
357
+ * @param {string} [options.footer] 底部操作提示;不传则使用默认模板
358
+ * @param {Set<number>|number[]} [options.preSelected] 预选项索引
359
+ * @returns {Promise<Set<number>>} 选中的索引集合(0-based,按原始顺序)
360
+ */
361
+ function promptCheckbox(options) {
362
+ const isTty = process.stdin.isTTY && process.stdout.isTTY;
363
+ const prompt = isTty ? promptCheckboxTty : promptCheckboxFallback;
364
+
365
+ // 兼容 number[] 形式的 preSelected
366
+ const normalized = { ...options };
367
+ if (Array.isArray(options.preSelected)) {
368
+ normalized.preSelected = new Set(options.preSelected);
369
+ }
370
+
371
+ return prompt(normalized);
372
+ }
373
+
374
+ module.exports = {
375
+ promptCheckbox,
376
+ // 暴露纯函数供单测与复用
377
+ toggle,
378
+ toggleAll,
379
+ invert,
380
+ parseBulkInput,
381
+ renderLines,
382
+ canEnableRawMode,
383
+ };
@@ -0,0 +1,69 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * .gitignore 管理的纯函数工具(无副作用,便于单测;由 init.js 的 I/O 层调用)。
5
+ *
6
+ * 安装时:若 .gitignore 已含某目录(认可带/不带尾斜杠两种形式),则不重复添加;
7
+ * 否则新增无尾斜杠形式(.claude / .qoder,更通用)。.cbb/ 维持带斜杠。
8
+ * 卸载时:移除本包曾写入的所有形式(新旧版本均覆盖)。
9
+ *
10
+ * 注意:.gitignore 条目无 _managedBy 之类的标记,卸载按"形式匹配"移除,
11
+ * 若用户恰好写了相同行也会被一并移除(与本包历史行为一致)。
12
+ */
13
+
14
+ // 各目录:add = 写入形式;accept = 视为"已存在 / 需卸载移除"的可接受形式(含尾斜杠变体)。
15
+ // .claude / .qoder 写无尾斜杠形式,但检测/卸载时同时认可带斜杠变体,避免重复添加、覆盖旧版本。
16
+ const GITIGNORE_RULES = [
17
+ { add: '.claude', accept: ['.claude', '.claude/'] },
18
+ { add: '.qoder', accept: ['.qoder', '.qoder/'] },
19
+ { add: '.cbb/', accept: ['.cbb/'] },
20
+ { add: '.code/', accept: ['.code/', '.code'] },
21
+ ];
22
+
23
+ /** 将 .gitignore 文本拆为行数组。 */
24
+ function toLines(content) {
25
+ return (content || '').split(/\r?\n/);
26
+ }
27
+
28
+ /**
29
+ * 给定 .gitignore 内容,返回需要新增的条目。
30
+ * 已含任一可接受形式(带/不带尾斜杠)的目录会被跳过;全部已存在时返回空数组。
31
+ * 返回的是写入形式(.claude / .qoder 无尾斜杠,.cbb/ 带尾斜杠)。
32
+ */
33
+ function entriesToAdd(content) {
34
+ const lines = toLines(content).map(l => l.trim());
35
+ return GITIGNORE_RULES
36
+ .filter(r => !r.accept.some(a => lines.includes(a)))
37
+ .map(r => r.add);
38
+ }
39
+
40
+ /**
41
+ * 给定 .gitignore 行数组,返回移除本包所有形式后的行数组(用于卸载)。
42
+ * 同时移除带/不带尾斜杠的形式,覆盖新旧版本写入的条目。
43
+ */
44
+ function filterManaged(lines) {
45
+ const removable = new Set(GITIGNORE_RULES.flatMap(r => r.accept));
46
+ return (lines || []).filter(l => !removable.has(l.trim()));
47
+ }
48
+
49
+ /**
50
+ * 将待新增条目追加到现有 .gitignore 内容,返回新内容字符串。
51
+ * 有内容时用一个空行分隔,使新增条目另起一行开始;空内容直接写(不产生前导空行)。
52
+ * 幂等:已含全部条目时调用方应先经 entriesToAdd 判定(返回空数组则不调用)。
53
+ */
54
+ function appendEntries(content, entries) {
55
+ const block = entries.join('\n');
56
+ if ((content || '').trim() === '') {
57
+ return block + '\n';
58
+ }
59
+ // 去掉末尾换行后:一个换行结束末行,再一个换行形成空行,然后是新增条目
60
+ const base = content.replace(/\n+$/, '');
61
+ return base + '\n\n' + block + '\n';
62
+ }
63
+
64
+ module.exports = {
65
+ GITIGNORE_RULES,
66
+ entriesToAdd,
67
+ filterManaged,
68
+ appendEntries,
69
+ };
@@ -0,0 +1,64 @@
1
+ // ── 颜色与输出辅助(公共模块)──────────────────────────────
2
+ // 合并自 lib/install/init.js、lib/utils/checkbox.js、lib/superpowers/index.js
3
+
4
+ const COLOR = {
5
+ reset: '\x1b[0m',
6
+ bold: '\x1b[1m',
7
+ dim: '\x1b[2m',
8
+ reverse: '\x1b[7m',
9
+ red: '\x1b[31m',
10
+ green: '\x1b[32m',
11
+ yellow: '\x1b[33m',
12
+ blue: '\x1b[34m',
13
+ cyan: '\x1b[36m',
14
+ gray: '\x1b[90m',
15
+ };
16
+
17
+ /** 是否启用颜色:仅 TTY 且未设置 NO_COLOR 时启用 */
18
+ const COLOR_ENABLED = process.stdout.isTTY === true && !process.env.NO_COLOR;
19
+
20
+ function c(color, text) {
21
+ if (!COLOR_ENABLED) return text;
22
+ return `${COLOR[color]}${text}${COLOR.reset}`;
23
+ }
24
+
25
+ // ── 工具函数 ──────────────────────────────────────────────
26
+
27
+ // 各类型的图标 + 颜色:icon 是前缀文字(2 字符对齐),color 是着色 key。
28
+ // 新增类型不会破坏旧调用;不识别的 type 退化为 info。
29
+ const LOG_STYLES = {
30
+ info: { icon: ' ', color: null },
31
+ success: { icon: '✓', color: 'green' },
32
+ error: { icon: '✗', color: 'red' },
33
+ warn: { icon: '!', color: 'yellow' },
34
+ dim: { icon: ' ', color: 'gray' },
35
+ accent: { icon: ' ', color: 'cyan' },
36
+ };
37
+
38
+ function log(msg, type = 'info') {
39
+ const s = LOG_STYLES[type] || LOG_STYLES.info;
40
+ const text = `${s.icon} ${msg}`;
41
+ console.log(s.color ? c(s.color, text) : text);
42
+ }
43
+
44
+ /** 输出一行分节标题(带图标 + 颜色),紧跟一行灰色分隔线 */
45
+ function section(icon, title) {
46
+ console.log('');
47
+ console.log(c('bold', `${icon} ${title}`));
48
+ console.log(c('gray', '─'.repeat(48)));
49
+ }
50
+
51
+ /** 以"label + value"两列对齐的方式输出一条字段记录 */
52
+ function field(label, value, valueColor) {
53
+ console.log(` ${c('gray', (label + ':').padEnd(8, ' '))} ${valueColor ? c(valueColor, value) : value}`);
54
+ }
55
+
56
+ module.exports = {
57
+ COLOR,
58
+ COLOR_ENABLED,
59
+ c,
60
+ LOG_STYLES,
61
+ log,
62
+ section,
63
+ field,
64
+ };
@@ -0,0 +1,119 @@
1
+ /**
2
+ * settings.json 读写工具
3
+ *
4
+ * 负责向 Qoder / Claude 的 settings.json 安全地写入 / 移除版本检查 hook,
5
+ * 通过 UserPromptSubmit 事件在用户发消息时自动检查本规范是否有新版本。
6
+ * 脚本内做会话级去重,每会话只提示一次。
7
+ *
8
+ * 所有本包写入的 hook 条目都带 `_managedBy: "cbb"` 标记,便于卸载时精确移除,
9
+ * 不影响用户自有的 hooks 配置。
10
+ *
11
+ * removeLegacyHooks 负责清理旧版本曾写入、现已弃用的 SessionStart hook:
12
+ * 当前仅安装 UserPromptSubmit,但升级自旧版本的用户 settings.json 中可能仍残留
13
+ * SessionStart 条目,升级 / 卸载时需主动移除,否则会永久残留。
14
+ */
15
+
16
+ const fs = require('fs');
17
+ const path = require('path');
18
+
19
+ const MANAGED_BY = 'cbb';
20
+
21
+ // 旧版本曾写入、现已弃用的 hook 事件。升级 / 卸载时需清理这些事件下本包标记的条目。
22
+ const LEGACY_HOOK_EVENTS = ['SessionStart'];
23
+
24
+ /** 返回 hook 要执行的命令(绝对路径调用本包的 check-update.js) */
25
+ function getCheckUpdateCommand() {
26
+ const scriptPath = path.join(__dirname, 'check-update.js');
27
+ return `node "${scriptPath}"`;
28
+ }
29
+
30
+ function readSettings(settingsPath) {
31
+ if (!fs.existsSync(settingsPath)) return {};
32
+ try {
33
+ return JSON.parse(fs.readFileSync(settingsPath, 'utf-8')) || {};
34
+ } catch (_) {
35
+ return {};
36
+ }
37
+ }
38
+
39
+ function writeSettings(settingsPath, data) {
40
+ fs.mkdirSync(path.dirname(settingsPath), { recursive: true });
41
+ fs.writeFileSync(settingsPath, JSON.stringify(data, null, 2) + '\n');
42
+ }
43
+
44
+ /** 移除某事件下本包标记的条目,返回过滤后的数组 */
45
+ function stripManaged(arr) {
46
+ return (Array.isArray(arr) ? arr : []).filter(
47
+ entry => !(entry && entry._managedBy === MANAGED_BY)
48
+ );
49
+ }
50
+
51
+ /**
52
+ * 向 settings.json 写入本包的版本检查 hook。
53
+ * @param {string} settingsPath settings.json 路径
54
+ * @param {string} command 要执行的命令
55
+ * @param {string[]} events 要写入的事件名数组,如 ['UserPromptSubmit']
56
+ * 已存在本包标记的条目会先移除再添加(保证命令为最新路径),不重复。
57
+ */
58
+ function addHooks(settingsPath, command, events) {
59
+ const settings = readSettings(settingsPath);
60
+ if (!settings.hooks || typeof settings.hooks !== 'object') settings.hooks = {};
61
+
62
+ events.forEach(event => {
63
+ settings.hooks[event] = stripManaged(settings.hooks[event]);
64
+ const entry = { _managedBy: MANAGED_BY, hooks: [{ type: 'command', command }] };
65
+ settings.hooks[event].push(entry);
66
+ });
67
+
68
+ writeSettings(settingsPath, settings);
69
+ return true;
70
+ }
71
+
72
+ /**
73
+ * 移除本包写入的版本检查 hook,保留用户其它配置。
74
+ * 清理变空的事件数组 / hooks。
75
+ * @param {string} settingsPath settings.json 路径
76
+ * @param {string[]} events 要清理的事件名数组
77
+ * 返回 true 表示有改动。
78
+ */
79
+ function removeHooks(settingsPath, events) {
80
+ if (!fs.existsSync(settingsPath)) return false;
81
+ const settings = readSettings(settingsPath);
82
+ if (!settings.hooks || typeof settings.hooks !== 'object') return false;
83
+
84
+ let changed = false;
85
+ events.forEach(event => {
86
+ if (!Array.isArray(settings.hooks[event])) return;
87
+ const before = settings.hooks[event].length;
88
+ settings.hooks[event] = stripManaged(settings.hooks[event]);
89
+ if (settings.hooks[event].length !== before) changed = true;
90
+ if (settings.hooks[event].length === 0) delete settings.hooks[event];
91
+ });
92
+
93
+ if (!changed) return false;
94
+
95
+ if (Object.keys(settings.hooks).length === 0) delete settings.hooks;
96
+
97
+ // 文件变成空对象则删除,否则写回
98
+ if (Object.keys(settings).length === 0) {
99
+ fs.unlinkSync(settingsPath);
100
+ } else {
101
+ writeSettings(settingsPath, settings);
102
+ }
103
+ return true;
104
+ }
105
+
106
+ /**
107
+ * 清理遗留事件下本包标记的 hook 条目(旧版本写入的 SessionStart 等)。
108
+ * 复用 removeHooks 的清理逻辑;返回 true 表示有改动。
109
+ */
110
+ function removeLegacyHooks(settingsPath) {
111
+ return removeHooks(settingsPath, LEGACY_HOOK_EVENTS);
112
+ }
113
+
114
+ module.exports = {
115
+ getCheckUpdateCommand,
116
+ addHooks,
117
+ removeHooks,
118
+ removeLegacyHooks,
119
+ };