@deepstorm/cli 0.1.0

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 (148) hide show
  1. package/README.md +72 -0
  2. package/dist/agents/reef-inspect-figma.md +77 -0
  3. package/dist/agents/reef-review-backend.md.tmpl +112 -0
  4. package/dist/agents/reef-review-frontend.md.tmpl +78 -0
  5. package/dist/agents/reef-review-infra.md +47 -0
  6. package/dist/agents/reef-review-security.md.tmpl +80 -0
  7. package/dist/agents/reef-scope-analysis.md +64 -0
  8. package/dist/build-registry.js +375 -0
  9. package/dist/cli.js +8581 -0
  10. package/dist/config-schema.json +133 -0
  11. package/dist/env-examples/context7.env-example +19 -0
  12. package/dist/env-examples/feishu-wiki.env-example +16 -0
  13. package/dist/env-examples/figma.env-example +16 -0
  14. package/dist/env-examples/github.env-example +20 -0
  15. package/dist/env-examples/jira.env-example +20 -0
  16. package/dist/hooks/mcp-hook.sh +77 -0
  17. package/dist/hooks/reef-auto-format.sh.tmpl +72 -0
  18. package/dist/hooks/reef-block-dangerous.sh +70 -0
  19. package/dist/hooks/reef-hooks.json +72 -0
  20. package/dist/hooks/reef-intent-detect.sh +129 -0
  21. package/dist/hooks/reef-protect-files.sh +55 -0
  22. package/dist/hooks/reef-run-tests.sh +84 -0
  23. package/dist/hooks/reef-scope-check.sh +386 -0
  24. package/dist/hooks/reef-scope-ci.sh +28 -0
  25. package/dist/hooks/reef-scope-gate.sh +115 -0
  26. package/dist/hooks/reef-scope-pre-commit.sh.tmpl +28 -0
  27. package/dist/hooks/reef-scope-setup.sh +204 -0
  28. package/dist/hooks/reef-scope-split.sh +203 -0
  29. package/dist/hooks/sweep-hooks.json +14 -0
  30. package/dist/hooks/sweep-mcp-hook.sh +77 -0
  31. package/dist/hooks/tide-hooks.json +14 -0
  32. package/dist/hooks/tide-session-preload.sh +17 -0
  33. package/dist/mcp/code-hosting/github.json +20 -0
  34. package/dist/mcp/design-tools/figma.json +19 -0
  35. package/dist/mcp/docs-reference/context7.json +28 -0
  36. package/dist/mcp/e2e-testing/playwright.json +13 -0
  37. package/dist/mcp/knowledge-base/feishu-wiki.json +19 -0
  38. package/dist/mcp/project-management/jira.json +27 -0
  39. package/dist/mcp-skills/deepflow-mcp-feishu-wiki-read/SKILL.md +65 -0
  40. package/dist/mcp-skills/deepflow-mcp-feishu-wiki-write/SKILL.md +63 -0
  41. package/dist/mcp-skills/deepflow-mcp-figma-read/SKILL.md +98 -0
  42. package/dist/mcp-skills/deepflow-mcp-github-read/SKILL.md +62 -0
  43. package/dist/mcp-skills/deepflow-mcp-github-write/SKILL.md +63 -0
  44. package/dist/mcp-skills/deepflow-mcp-jira-read/SKILL.md +80 -0
  45. package/dist/mcp-skills/deepflow-mcp-jira-write/SKILL.md +74 -0
  46. package/dist/mcp-skills/deepflow-mcp-playwright-read/SKILL.md +79 -0
  47. package/dist/mcp-skills/deepstorm-mcp-feishu-wiki-read/SKILL.md +65 -0
  48. package/dist/mcp-skills/deepstorm-mcp-feishu-wiki-write/SKILL.md +63 -0
  49. package/dist/mcp-skills/deepstorm-mcp-figma-read/SKILL.md +98 -0
  50. package/dist/mcp-skills/deepstorm-mcp-github-read/SKILL.md +62 -0
  51. package/dist/mcp-skills/deepstorm-mcp-github-write/SKILL.md +63 -0
  52. package/dist/mcp-skills/deepstorm-mcp-jira-read/SKILL.md +80 -0
  53. package/dist/mcp-skills/deepstorm-mcp-jira-write/SKILL.md +74 -0
  54. package/dist/mcp-skills/deepstorm-mcp-playwright-read/SKILL.md +79 -0
  55. package/dist/registry.json +818 -0
  56. package/dist/skills/atoll-ops/SKILL.md +46 -0
  57. package/dist/skills/reef-commit/SKILL.md +127 -0
  58. package/dist/skills/reef-gen-backend/SKILL.md.tmpl +87 -0
  59. package/dist/skills/reef-gen-backend/variants/java/steps.md +28 -0
  60. package/dist/skills/reef-gen-backend/variants/python/steps.md +70 -0
  61. package/dist/skills/reef-gen-frontend/SKILL.md.tmpl +83 -0
  62. package/dist/skills/reef-gen-frontend/variants/angular/steps.md +30 -0
  63. package/dist/skills/reef-harden/EXAMPLES.md +89 -0
  64. package/dist/skills/reef-harden/SKILL.md +136 -0
  65. package/dist/skills/reef-pr/SKILL.md +97 -0
  66. package/dist/skills/reef-review/SKILL.md.tmpl +107 -0
  67. package/dist/skills/reef-scope/SKILL.md +134 -0
  68. package/dist/skills/reef-start/SKILL.md.tmpl +562 -0
  69. package/dist/skills/reef-start/references/jira-start-subagent.md +60 -0
  70. package/dist/skills/reef-style-backend/SKILL.md.tmpl +134 -0
  71. package/dist/skills/reef-style-backend/fragments/java/ai/spring-ai/examples/chat-client.md +96 -0
  72. package/dist/skills/reef-style-backend/fragments/java/ai/spring-ai/examples/rag.md +94 -0
  73. package/dist/skills/reef-style-backend/fragments/java/ai/spring-ai/examples/structured-output.md +62 -0
  74. package/dist/skills/reef-style-backend/fragments/java/ai/spring-ai/examples/tool-calling.md +68 -0
  75. package/dist/skills/reef-style-backend/fragments/java/ai/spring-ai/quick-reference.md +220 -0
  76. package/dist/skills/reef-style-backend/fragments/java/api-spec/quick-reference.md +148 -0
  77. package/dist/skills/reef-style-backend/fragments/java/db-migration/liquibase/examples/database-migration.md +131 -0
  78. package/dist/skills/reef-style-backend/fragments/java/db-migration/liquibase/quick-reference.md +103 -0
  79. package/dist/skills/reef-style-backend/fragments/java/dependency-management/quick-reference.md +119 -0
  80. package/dist/skills/reef-style-backend/fragments/java/exception-handling/examples/error-code-enum.md +101 -0
  81. package/dist/skills/reef-style-backend/fragments/java/exception-handling/quick-reference.md +181 -0
  82. package/dist/skills/reef-style-backend/fragments/java/framework/spring-boot/examples/controller.md +95 -0
  83. package/dist/skills/reef-style-backend/fragments/java/framework/spring-boot/examples/dto-mapper.md +121 -0
  84. package/dist/skills/reef-style-backend/fragments/java/framework/spring-boot/examples/infrastructure.md +179 -0
  85. package/dist/skills/reef-style-backend/fragments/java/framework/spring-boot/examples/service-entity.md +202 -0
  86. package/dist/skills/reef-style-backend/fragments/java/framework/spring-boot/examples/testing.md +107 -0
  87. package/dist/skills/reef-style-backend/fragments/java/framework/spring-boot/quick-reference.md +83 -0
  88. package/dist/skills/reef-style-backend/fragments/java/orm/hibernate/quick-reference.md +150 -0
  89. package/dist/skills/reef-style-backend/fragments/java/security-redlines/quick-reference.md +197 -0
  90. package/dist/skills/reef-style-backend/fragments/java/test/data-jpa-test/examples/user-repository-test.md +69 -0
  91. package/dist/skills/reef-style-backend/fragments/java/test/data-jpa-test/quick-reference.md +101 -0
  92. package/dist/skills/reef-style-backend/fragments/java/test/junit5/examples/user-service-test.md +61 -0
  93. package/dist/skills/reef-style-backend/fragments/java/test/junit5/quick-reference.md +100 -0
  94. package/dist/skills/reef-style-backend/fragments/java/test/spring-mvc-test/examples/user-controller-test.md +61 -0
  95. package/dist/skills/reef-style-backend/fragments/java/test/spring-mvc-test/quick-reference.md +85 -0
  96. package/dist/skills/reef-style-backend/fragments/java/test/spring-service-test/examples/user-service-integration-test.md +56 -0
  97. package/dist/skills/reef-style-backend/fragments/java/test/spring-service-test/quick-reference.md +83 -0
  98. package/dist/skills/reef-style-backend/fragments/python/alembic-migration/quick-reference.md +77 -0
  99. package/dist/skills/reef-style-backend/fragments/python/api-spec/quick-reference.md +164 -0
  100. package/dist/skills/reef-style-backend/fragments/python/dependency-management/quick-reference.md +139 -0
  101. package/dist/skills/reef-style-backend/fragments/python/exception-handling/quick-reference.md +177 -0
  102. package/dist/skills/reef-style-backend/fragments/python/fastapi-quick-reference/quick-reference.md +101 -0
  103. package/dist/skills/reef-style-backend/fragments/python/langchain/quick-reference.md +135 -0
  104. package/dist/skills/reef-style-backend/fragments/python/pytest-testing/quick-reference.md +111 -0
  105. package/dist/skills/reef-style-backend/fragments/python/ruff-mypy-toolchain/quick-reference.md +83 -0
  106. package/dist/skills/reef-style-backend/fragments/python/security-redlines/quick-reference.md +207 -0
  107. package/dist/skills/reef-style-backend/fragments/python/sqlalchemy-orm/quick-reference.md +91 -0
  108. package/dist/skills/reef-style-backend/variants/java/examples/code-wrapping.md +227 -0
  109. package/dist/skills/reef-style-backend/variants/java/examples/contributor-pattern.md +97 -0
  110. package/dist/skills/reef-style-backend/variants/java/quick-reference.md +117 -0
  111. package/dist/skills/reef-style-backend/variants/python/examples/crud-router.md +71 -0
  112. package/dist/skills/reef-style-backend/variants/python/examples/pydantic-schema.md +45 -0
  113. package/dist/skills/reef-style-backend/variants/python/examples/pytest-fixture.md +60 -0
  114. package/dist/skills/reef-style-backend/variants/python/examples/sqlalchemy-model.md +31 -0
  115. package/dist/skills/reef-style-backend/variants/python/quick-reference.md +111 -0
  116. package/dist/skills/reef-style-frontend/SKILL.md.tmpl +70 -0
  117. package/dist/skills/reef-style-frontend/fragments/css/tailwind/quick-reference.md +79 -0
  118. package/dist/skills/reef-style-frontend/fragments/test/vitest/examples/testing.md +150 -0
  119. package/dist/skills/reef-style-frontend/fragments/test/vitest/quick-reference.md +141 -0
  120. package/dist/skills/reef-style-frontend/fragments/ts-config/strict/quick-reference.md +112 -0
  121. package/dist/skills/reef-style-frontend/fragments/ui-lib/primeng/examples/ui-components.md +134 -0
  122. package/dist/skills/reef-style-frontend/fragments/ui-lib/primeng/quick-reference.md +66 -0
  123. package/dist/skills/reef-style-frontend/variants/angular/examples/code-wrapping.md +252 -0
  124. package/dist/skills/reef-style-frontend/variants/angular/examples/component-types-pipes.md +56 -0
  125. package/dist/skills/reef-style-frontend/variants/angular/examples/entity-types.md +100 -0
  126. package/dist/skills/reef-style-frontend/variants/angular/examples/forms-layer.md +119 -0
  127. package/dist/skills/reef-style-frontend/variants/angular/examples/service-routing.md +95 -0
  128. package/dist/skills/reef-style-frontend/variants/angular/quick-reference.md +100 -0
  129. package/dist/skills/reef-testcase/SKILL.md +138 -0
  130. package/dist/skills/reef-testcase/references/coverage-dimensions.md +99 -0
  131. package/dist/skills/reef-testcase/references/test-case-template.md +63 -0
  132. package/dist/skills/sweep-init/SKILL.md +286 -0
  133. package/dist/skills/sweep-init/scripts/flow-selector.mjs +305 -0
  134. package/dist/skills/sweep-plan/SKILL.md.tmpl +312 -0
  135. package/dist/skills/sweep-plan/references/test-flow-template.md +46 -0
  136. package/dist/skills/sweep-run/SKILL.md +437 -0
  137. package/dist/skills/sweep-run/scripts/env-manager.mjs +208 -0
  138. package/dist/skills/sweep-run/scripts/flow-parser.mjs +329 -0
  139. package/dist/skills/sweep-run/scripts/flow-selector.mjs +483 -0
  140. package/dist/skills/sweep-run/scripts/mcp-manager.mjs +208 -0
  141. package/dist/skills/sweep-run/scripts/spec-compiler.mjs +303 -0
  142. package/dist/skills/tide-discuss/SKILL.md.tmpl +449 -0
  143. package/dist/skills/tide-discuss/references/checklists.md +88 -0
  144. package/dist/skills/tide-discuss/references/data-format.md +237 -0
  145. package/dist/skills/tide-discuss/references/prd-template.md +134 -0
  146. package/dist/skills/tide-discuss/references/publish-flow.md +167 -0
  147. package/dist/skills/tide-discuss/references/role-prompts.md +105 -0
  148. package/package.json +38 -0
@@ -0,0 +1,483 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Sweep Flow Selector — 基于 @inquirer/checkbox 的层级选择工具
5
+ *
6
+ * 支持选择粒度:
7
+ * 1. 模块级别(eg. user-system)
8
+ * 2. 文件级别(eg. user-system/login.flow.md)
9
+ * 3. 用例级别(eg. L01 - 正常登录)
10
+ *
11
+ * 读取 config(topology.yaml 或文件系统扫描),展示可勾选界面。
12
+ * 当 TTY 不可用或使用 --text 标志时,自动回退到 readline 文本选择模式。
13
+ * 将选中结果写入 .sweep-selection.json 并输出到 stdout。
14
+ *
15
+ * 使用方式:
16
+ * node scripts/flow-selector.mjs # 自动检测 TTY(checkbox 或文本回退)
17
+ * node scripts/flow-selector.mjs --tui # 强制 TUI checkbox 模式
18
+ * node scripts/flow-selector.mjs --text # 强制文本输入模式
19
+ */
20
+
21
+ import { readFileSync, readdirSync, existsSync, writeFileSync } from 'node:fs';
22
+ import { join, relative } from 'node:path';
23
+ import { createInterface } from 'node:readline';
24
+
25
+ // ── Lazy @inquirer/checkbox ─────────────────────────────────────────
26
+
27
+ let checkboxModule = null;
28
+
29
+ async function getCheckbox() {
30
+ if (checkboxModule === undefined) {
31
+ try {
32
+ checkboxModule = (await import('@inquirer/checkbox')).default;
33
+ } catch {
34
+ checkboxModule = null;
35
+ }
36
+ }
37
+ return checkboxModule;
38
+ }
39
+
40
+ // ── TTY detection ───────────────────────────────────────────────────
41
+
42
+ function isTtyAvailable() {
43
+ return Boolean(
44
+ process.stdout.isTTY &&
45
+ process.stdin.isTTY &&
46
+ process.stdin.setRawMode
47
+ );
48
+ }
49
+
50
+ // ── YAML 解析(轻量,不依赖 yaml 包) ──────────────────────────
51
+
52
+ function parseTopology(yaml) {
53
+ const modules = [];
54
+ const lines = yaml.split('\n');
55
+ let stack = [];
56
+
57
+ for (const raw of lines) {
58
+ const trimmed = raw.trimEnd();
59
+ if (!trimmed.trim() || trimmed.trimStart().startsWith('#')) continue;
60
+
61
+ const indent = trimmed.length - trimmed.trimStart().length;
62
+ while (stack.length > 0 && stack[stack.length - 1].indent >= indent) {
63
+ stack.pop();
64
+ }
65
+
66
+ const lineTrimmed = trimmed.trimStart();
67
+ const listMatch = lineTrimmed.match(/^-\s*name:\s*(.+)$/);
68
+ if (!listMatch) continue;
69
+
70
+ const name = listMatch[1];
71
+ const parentList = stack.length > 0 ? stack[stack.length - 1].list : modules;
72
+ const existing = parentList.find((m) => m.name === name);
73
+ if (!existing) {
74
+ parentList.push({ name, children: [] });
75
+ }
76
+ stack.push({ indent, list: parentList[parentList.length - 1].children });
77
+ }
78
+
79
+ return modules;
80
+ }
81
+
82
+ // ── Flow 文件解析 ────────────────────────────────────────────
83
+
84
+ function parseFlows(content) {
85
+ const flows = [];
86
+ const regex = /^## Flow:\s*(\S+)\s*-\s*(.+)$/gm;
87
+ let match;
88
+ while ((match = regex.exec(content)) !== null) {
89
+ flows.push({ id: match[1], title: match[2].trim() });
90
+ }
91
+ return flows;
92
+ }
93
+
94
+ function resolveFlowFile(modulePath, baseFlowsDir) {
95
+ const directPath = join(baseFlowsDir, `${modulePath}.flow.md`);
96
+ if (existsSync(directPath)) return directPath;
97
+
98
+ const parts = modulePath.split('/');
99
+ const leafName = parts[parts.length - 1];
100
+ const parentDir = parts.length > 1
101
+ ? join(baseFlowsDir, parts.slice(0, -1).join('/'))
102
+ : baseFlowsDir;
103
+ if (existsSync(parentDir)) {
104
+ const files = readdirSync(parentDir);
105
+ const found = files.find(
106
+ (f) => f.endsWith('.flow.md') && f.toLowerCase().includes(leafName.toLowerCase())
107
+ );
108
+ if (found) return join(parentDir, found);
109
+ }
110
+
111
+ const dirPath = join(baseFlowsDir, modulePath);
112
+ if (existsSync(dirPath)) {
113
+ const files = readdirSync(dirPath);
114
+ const found = files.find((f) => f.endsWith('.flow.md'));
115
+ if (found) return join(dirPath, found);
116
+ }
117
+
118
+ return null;
119
+ }
120
+
121
+ // ── 文件系统扫描 ────────────────────────────────────────────
122
+
123
+ function scanFlowFiles(dir) {
124
+ const result = [];
125
+ let entries;
126
+ try {
127
+ entries = readdirSync(dir, { withFileTypes: true });
128
+ } catch {
129
+ return result;
130
+ }
131
+
132
+ for (const entry of entries) {
133
+ const fullPath = join(dir, entry.name);
134
+ if (entry.isDirectory()) {
135
+ if (entry.name.startsWith('.')) continue;
136
+ result.push(...scanFlowFiles(fullPath));
137
+ } else if (entry.isFile() && entry.name.endsWith('.flow.md')) {
138
+ result.push(fullPath);
139
+ }
140
+ }
141
+
142
+ return result;
143
+ }
144
+
145
+ // ── 构建 Choice 树 ──────────────────────────────────────────
146
+
147
+ function buildChoicesWithFlows(modules, baseFlowsDir, prefix = '') {
148
+ const choices = [];
149
+
150
+ for (const mod of modules) {
151
+ const path = prefix ? `${prefix}/${mod.name}` : mod.name;
152
+
153
+ if (mod.children && mod.children.length > 0) {
154
+ choices.push(...buildChoicesWithFlows(mod.children, baseFlowsDir, path));
155
+ } else {
156
+ const flowFile = resolveFlowFile(path, baseFlowsDir);
157
+ if (flowFile) {
158
+ const content = readFileSync(flowFile, 'utf-8');
159
+ const flows = parseFlows(content);
160
+
161
+ choices.push({
162
+ name: `📁 ${path}`,
163
+ value: `__file:${flowFile}`,
164
+ description: `${flows.length} 个用例`,
165
+ });
166
+
167
+ for (const f of flows) {
168
+ choices.push({
169
+ name: ` ${f.id} - ${f.title}`,
170
+ value: JSON.stringify({ file: flowFile, flowId: f.id }),
171
+ description: flowFile,
172
+ });
173
+ }
174
+ } else {
175
+ choices.push({
176
+ name: `📁 ${path}`,
177
+ value: `__module:${path}`,
178
+ description: '未找到对应 .flow.md 文件',
179
+ });
180
+ }
181
+ }
182
+ }
183
+
184
+ return choices;
185
+ }
186
+
187
+ function nameFromPath(p) {
188
+ const parts = p.split('/');
189
+ return parts[parts.length - 1];
190
+ }
191
+
192
+ // ── 处理选中结果 ────────────────────────────────────────────
193
+
194
+ function normalizeSelection(answer) {
195
+ if (answer.includes('__all__')) {
196
+ return { type: 'all' };
197
+ }
198
+
199
+ const fileMap = new Map();
200
+
201
+ for (const item of answer) {
202
+ if (item.startsWith('__file:')) {
203
+ const filePath = item.slice('__file:'.length);
204
+ if (!fileMap.has(filePath)) fileMap.set(filePath, []);
205
+ } else if (item.startsWith('__module:')) {
206
+ // ignore
207
+ } else {
208
+ try {
209
+ const { file, flowId } = JSON.parse(item);
210
+ if (!fileMap.has(file)) fileMap.set(file, []);
211
+ const flows = fileMap.get(file);
212
+ if (!flows.includes(flowId)) flows.push(flowId);
213
+ } catch {
214
+ // ignore
215
+ }
216
+ }
217
+ }
218
+
219
+ const files = [];
220
+ for (const [filePath, selectedFlows] of fileMap) {
221
+ const content = readFileSync(filePath, 'utf-8');
222
+ const allFlows = parseFlows(content);
223
+ const allIds = allFlows.map((f) => f.id);
224
+
225
+ if (selectedFlows.length === 0 || selectedFlows.length >= allIds.length) {
226
+ files.push({ file: filePath, flows: [], all: true });
227
+ } else {
228
+ files.push({ file: filePath, flows: selectedFlows, all: false });
229
+ }
230
+ }
231
+
232
+ for (const item of answer) {
233
+ if (item.startsWith('__file:')) {
234
+ const filePath = item.slice('__file:'.length);
235
+ const entry = files.find((f) => f.file === filePath);
236
+ if (entry) entry.all = true;
237
+ }
238
+ }
239
+
240
+ return { type: 'selection', files };
241
+ }
242
+
243
+ function writeSelection(data, selectionPath = '.sweep-selection.json') {
244
+ const json = JSON.stringify(data, null, 2);
245
+ writeFileSync(selectionPath, json, 'utf-8');
246
+ console.log(json);
247
+ }
248
+
249
+ // ── 文本选择模式 ────────────────────────────────────────────
250
+
251
+ /**
252
+ * 将 choices 数组转换为扁平索引列表,供文本模式选择。
253
+ * 只包含文件级条目(__file:),不展开到单个 flow 级别。
254
+ * 返回 [{index, value, label, description}, ...]
255
+ */
256
+ function buildFlatFileChoices(choices) {
257
+ const flat = [];
258
+ let idx = 0;
259
+
260
+ for (const c of choices) {
261
+ // Skip "全部执行" — it's handled separately
262
+ if (c.value === '__all__') continue;
263
+ // Only include file-level entries
264
+ if (c.value.startsWith('__file:')) {
265
+ idx++;
266
+ flat.push({ index: idx, value: c.value, label: c.name.replace(/^📁\s*/, ''), description: c.description });
267
+ }
268
+ }
269
+
270
+ return flat;
271
+ }
272
+
273
+ /**
274
+ * 文本模式选择:使用 readline 接收用户输入。
275
+ * 用户输入序号(逗号分隔)、范围(1-3)、或 all 来全选。
276
+ */
277
+ async function askTextSelection(flatChoices) {
278
+ const rl = createInterface({
279
+ input: process.stdin,
280
+ output: process.stdout,
281
+ });
282
+
283
+ try {
284
+ // Print options
285
+ console.log('\n可用文件模块:');
286
+ for (const f of flatChoices) {
287
+ const label = f.label.padEnd(30);
288
+ console.log(` ${String(f.index).padStart(2)}. ${label} ${f.description}`);
289
+ }
290
+ console.log(' a. 全部执行\n');
291
+
292
+ const answer = await new Promise((resolve) => {
293
+ rl.question('输入序号选择(逗号分隔可多选,例如 1,3;范围 1-3;或 a 全选): ', resolve);
294
+ });
295
+
296
+ const trimmed = answer.trim().toLowerCase();
297
+ if (trimmed === 'a' || trimmed === 'all' || trimmed === '') {
298
+ return ['__all__'];
299
+ }
300
+
301
+ // Parse comma-separated values and ranges
302
+ const selected = new Set();
303
+ const parts = trimmed.split(/[,,\s]+/).filter(Boolean);
304
+
305
+ for (const part of parts) {
306
+ const rangeMatch = part.match(/^(\d+)\s*-\s*(\d+)$/);
307
+ if (rangeMatch) {
308
+ const start = parseInt(rangeMatch[1], 10);
309
+ const end = parseInt(rangeMatch[2], 10);
310
+ for (let i = start; i <= end; i++) {
311
+ const match = flatChoices.find((f) => f.index === i);
312
+ if (match) selected.add(match.value);
313
+ }
314
+ } else {
315
+ const num = parseInt(part, 10);
316
+ if (!isNaN(num)) {
317
+ const match = flatChoices.find((f) => f.index === num);
318
+ if (match) selected.add(match.value);
319
+ }
320
+ }
321
+ }
322
+
323
+ if (selected.size === 0) {
324
+ console.log('未选择任何有效选项,将执行全部测试。');
325
+ return ['__all__'];
326
+ }
327
+
328
+ return [...selected];
329
+ } finally {
330
+ rl.close();
331
+ }
332
+ }
333
+
334
+ // ── 主流程 ──────────────────────────────────────────────────
335
+
336
+ async function main() {
337
+ const BASE_FLOWS_DIR = join(process.cwd(), 'flows');
338
+ const TOPOLOGY_PATH = join(BASE_FLOWS_DIR, 'topology.yaml');
339
+ const SELECTION_PATH = join(process.cwd(), '.sweep-selection.json');
340
+
341
+ const cliArgs = process.argv.slice(2);
342
+ const forceText = cliArgs.includes('--text');
343
+ const forceTui = cliArgs.includes('--tui');
344
+
345
+ try {
346
+ let modules = [];
347
+
348
+ if (existsSync(TOPOLOGY_PATH)) {
349
+ const yaml = readFileSync(TOPOLOGY_PATH, 'utf-8');
350
+ modules = parseTopology(yaml);
351
+ }
352
+
353
+ if (!modules.length && !existsSync(BASE_FLOWS_DIR)) {
354
+ console.error('⚠ flows/ 目录不存在');
355
+ process.exit(1);
356
+ }
357
+
358
+ let choices;
359
+
360
+ if (modules.length > 0) {
361
+ choices = buildChoicesWithFlows(modules, BASE_FLOWS_DIR);
362
+ } else {
363
+ const files = scanFlowFiles(BASE_FLOWS_DIR);
364
+ if (files.length === 0) {
365
+ console.error('⚠ flows/ 目录下没有找到任何 .flow.md 文件');
366
+ process.exit(1);
367
+ }
368
+
369
+ choices = [{ name: '📁 全部执行', value: '__all__' }];
370
+ for (const file of files) {
371
+ const rel = relative(BASE_FLOWS_DIR, file);
372
+ const content = readFileSync(file, 'utf-8');
373
+ const flows = parseFlows(content);
374
+ choices.push({
375
+ name: `📄 ${rel}`,
376
+ value: `__file:${file}`,
377
+ description: `${flows.length} 个用例`,
378
+ });
379
+ for (const f of flows) {
380
+ choices.push({
381
+ name: ` ${f.id} - ${f.title}`,
382
+ value: JSON.stringify({ file, flowId: f.id }),
383
+ description: rel,
384
+ });
385
+ }
386
+ }
387
+ }
388
+
389
+ choices = [
390
+ { name: '📋 全部执行', value: '__all__' },
391
+ ...choices,
392
+ ];
393
+
394
+ // ── Decide mode: text vs checkbox ──────────────────────
395
+
396
+ let answer;
397
+
398
+ if (forceText) {
399
+ // Force text mode
400
+ const flat = buildFlatFileChoices(choices);
401
+ answer = await askTextSelection(flat);
402
+ } else if (forceTui) {
403
+ // Force TUI checkbox mode
404
+ const checkbox = await getCheckbox();
405
+ if (!checkbox) {
406
+ console.error('⚠ @inquirer/checkbox 不可用,回退到文本选择模式。');
407
+ const flat = buildFlatFileChoices(choices);
408
+ answer = await askTextSelection(flat);
409
+ } else {
410
+ answer = await checkbox({
411
+ message: '选择要执行的测试用例(空格勾选,回车确认,a=全选):',
412
+ pageSize: 24,
413
+ loop: false,
414
+ choices,
415
+ shortcuts: { all: 'a', invert: 'i' },
416
+ });
417
+ }
418
+ } else {
419
+ // Auto-detect: prefer checkbox if TTY is fully available
420
+ const ttyOk = isTtyAvailable();
421
+ if (ttyOk) {
422
+ const checkbox = await getCheckbox();
423
+ if (checkbox) {
424
+ try {
425
+ answer = await checkbox({
426
+ message: '选择要执行的测试用例(空格勾选,回车确认,a=全选):',
427
+ pageSize: 24,
428
+ loop: false,
429
+ choices,
430
+ shortcuts: { all: 'a', invert: 'i' },
431
+ });
432
+ } catch (checkboxErr) {
433
+ // Checkbox failed at runtime — fall back to text mode
434
+ if (checkboxErr.name === 'ExitPromptError') {
435
+ process.exit(0);
436
+ }
437
+ console.error('\n⚠ 交互式选择器不可用,切换到文本选择模式。');
438
+ const flat = buildFlatFileChoices(choices);
439
+ answer = await askTextSelection(flat);
440
+ }
441
+ } else {
442
+ const flat = buildFlatFileChoices(choices);
443
+ answer = await askTextSelection(flat);
444
+ }
445
+ } else {
446
+ const flat = buildFlatFileChoices(choices);
447
+ answer = await askTextSelection(flat);
448
+ }
449
+ }
450
+
451
+ const selection = normalizeSelection(answer);
452
+ writeSelection(selection, SELECTION_PATH);
453
+ } catch (e) {
454
+ if (e.name === 'ExitPromptError') {
455
+ process.exit(0);
456
+ }
457
+ // 出错时默认全量执行
458
+ const fallback = { type: 'all' };
459
+ writeSelection(fallback, SELECTION_PATH);
460
+ process.exit(0);
461
+ }
462
+ }
463
+
464
+ // ── Exports (for testing) ────────────────────────────────────
465
+
466
+ export {
467
+ parseTopology,
468
+ parseFlows,
469
+ resolveFlowFile,
470
+ scanFlowFiles,
471
+ buildChoicesWithFlows,
472
+ normalizeSelection,
473
+ buildFlatFileChoices,
474
+ askTextSelection,
475
+ isTtyAvailable,
476
+ getCheckbox,
477
+ };
478
+
479
+ // ── CLI entry point ──────────────────────────────────────────
480
+
481
+ if (process.argv[1] === import.meta.filename) {
482
+ main();
483
+ }
@@ -0,0 +1,208 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * MCP Manager — Playwright MCP 服务生命周期管理
5
+ *
6
+ * 根据执行模式自动管理 Playwright MCP 进程:
7
+ * - headless(默认):混合执行,MCP 仅用于自愈诊断
8
+ * - headed:浏览器调试模式,需要可见窗口
9
+ * - skip:不启动 MCP(--native 模式)
10
+ *
11
+ * Usage:
12
+ * node mcp-manager.mjs [--mode headless|headed|skip] [--port 54321]
13
+ * node mcp-manager.mjs --status [--port 54321]
14
+ * node mcp-manager.mjs --stop [--port 54321]
15
+ * import { ensureMcp, stopMcp, getStatus } from './mcp-manager.mjs'
16
+ */
17
+
18
+ import { execSync, spawn } from 'node:child_process';
19
+
20
+ const DEFAULT_PORT = 54321;
21
+ const MCP_PACKAGE = '@playwright/mcp';
22
+
23
+ // ── Process helpers ────────────────────────────────────────────────
24
+
25
+ /**
26
+ * Find PID of process listening on a given TCP port.
27
+ * Returns null if nothing is listening.
28
+ */
29
+ export function findPidByPort(port) {
30
+ try {
31
+ const stdout = execSync(`lsof -ti tcp:${port} 2>/dev/null`, {
32
+ encoding: 'utf-8',
33
+ timeout: 3000,
34
+ }).trim();
35
+ return stdout ? parseInt(stdout.split('\n')[0], 10) : null;
36
+ } catch {
37
+ return null;
38
+ }
39
+ }
40
+
41
+ /**
42
+ * Get the command line of a process by PID.
43
+ */
44
+ export function getProcessCommand(pid) {
45
+ try {
46
+ return execSync(`ps -p ${pid} -o args= 2>/dev/null`, {
47
+ encoding: 'utf-8',
48
+ timeout: 3000,
49
+ }).trim();
50
+ } catch {
51
+ return '';
52
+ }
53
+ }
54
+
55
+ /**
56
+ * Check if a process command indicates headless mode.
57
+ */
58
+ export function isHeadless(cmd) {
59
+ return cmd.includes('--headless');
60
+ }
61
+
62
+ /**
63
+ * Kill a process by PID with escalation (SIGTERM -> SIGKILL).
64
+ */
65
+ export function killProcess(pid) {
66
+ try {
67
+ process.kill(pid, 'SIGTERM');
68
+ // Wait briefly for graceful shutdown
69
+ const start = Date.now();
70
+ while (Date.now() - start < 2000) {
71
+ try {
72
+ process.kill(pid, 0); // probe if alive
73
+ } catch {
74
+ return; // process is gone
75
+ }
76
+ execSync('sleep 0.2', { timeout: 1000 });
77
+ }
78
+ // Force kill
79
+ process.kill(pid, 'SIGKILL');
80
+ } catch {
81
+ // already dead
82
+ }
83
+ }
84
+
85
+ // ── Public API ─────────────────────────────────────────────────────
86
+
87
+ /**
88
+ * Get current MCP service status.
89
+ * @param {number} [port=DEFAULT_PORT]
90
+ * @returns {{ running: boolean, pid: number|null, mode: string|null }}
91
+ */
92
+ export function getStatus(port = DEFAULT_PORT) {
93
+ const pid = findPidByPort(port);
94
+ if (!pid) {
95
+ return { running: false, pid: null, mode: null };
96
+ }
97
+ const cmd = getProcessCommand(pid);
98
+ const mode = isHeadless(cmd) ? 'headless' : 'headed';
99
+ return { running: true, pid, mode };
100
+ }
101
+
102
+ /**
103
+ * Stop MCP service if running.
104
+ * @param {number} [port=DEFAULT_PORT]
105
+ * @returns {{ stopped: boolean, pid: number|null }}
106
+ */
107
+ export function stopMcp(port = DEFAULT_PORT) {
108
+ const status = getStatus(port);
109
+ if (!status.running) {
110
+ return { stopped: false, pid: null };
111
+ }
112
+ killProcess(status.pid);
113
+ return { stopped: true, pid: status.pid };
114
+ }
115
+
116
+ /**
117
+ * Ensure MCP service is running in the desired mode.
118
+ *
119
+ * @param {object} options
120
+ * @param {'headless'|'headed'} [options.mode='headless'] - Desired mode
121
+ * @param {number} [options.port=DEFAULT_PORT] - MCP port
122
+ * @returns {{ action: string, mode: string, pid: number, port: number }}
123
+ *
124
+ * Possible action values:
125
+ * - 'started' -- MCP was not running, started fresh
126
+ * - 'switched' -- MCP was running in wrong mode, restarted
127
+ * - 'already-ok' -- MCP already running in correct mode
128
+ */
129
+ export function ensureMcp(options = {}) {
130
+ const mode = options.mode || 'headless';
131
+ const port = options.port || DEFAULT_PORT;
132
+
133
+ if (mode === 'skip') {
134
+ return { action: 'skipped', mode: 'skip', pid: null, port };
135
+ }
136
+
137
+ const status = getStatus(port);
138
+
139
+ if (status.running) {
140
+ if (status.mode === mode) {
141
+ return { action: 'already-ok', mode, pid: status.pid, port };
142
+ }
143
+ // Running in wrong mode -- restart
144
+ stopMcp(port);
145
+ execSync('sleep 1', { timeout: 2000 });
146
+ }
147
+
148
+ // Start MCP
149
+ const args = ['--port', String(port)];
150
+ if (mode === 'headless') {
151
+ args.push('--headless');
152
+ }
153
+
154
+ const child = spawn('npx', [MCP_PACKAGE, ...args], {
155
+ stdio: 'inherit',
156
+ detached: true,
157
+ });
158
+ child.unref();
159
+
160
+ // Wait for it to be listening
161
+ const deadline = Date.now() + 10000;
162
+ let startedPid = null;
163
+ while (Date.now() < deadline) {
164
+ execSync('sleep 0.3', { timeout: 1000 });
165
+ const found = findPidByPort(port);
166
+ if (found) {
167
+ startedPid = found;
168
+ break;
169
+ }
170
+ }
171
+
172
+ if (!startedPid) {
173
+ throw new Error(`MCP failed to start on port ${port} within 10s`);
174
+ }
175
+
176
+ const action = status.running ? 'switched' : 'started';
177
+ return { action, mode, pid: startedPid, port };
178
+ }
179
+
180
+ // ── CLI entry point ────────────────────────────────────────────────
181
+
182
+ if (process.argv[1] === import.meta.filename) {
183
+ const args = process.argv.slice(2);
184
+ const modeFlag = args.find(a => a.startsWith('--mode='));
185
+ const portFlag = args.find(a => a.startsWith('--port='));
186
+ const mode = modeFlag ? modeFlag.split('=')[1] : 'headless';
187
+ const port = portFlag ? parseInt(portFlag.split('=')[1], 10) : DEFAULT_PORT;
188
+
189
+ if (args.includes('--stop')) {
190
+ const result = stopMcp(port);
191
+ console.log(JSON.stringify(result));
192
+ process.exit(0);
193
+ }
194
+
195
+ if (args.includes('--status')) {
196
+ const result = getStatus(port);
197
+ console.log(JSON.stringify(result));
198
+ process.exit(0);
199
+ }
200
+
201
+ try {
202
+ const result = ensureMcp({ mode, port });
203
+ console.log(JSON.stringify(result));
204
+ } catch (err) {
205
+ console.error(JSON.stringify({ error: err.message }));
206
+ process.exit(1);
207
+ }
208
+ }