@deepstorm/cli 0.10.2 → 0.11.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 (124) hide show
  1. package/README.md +8 -8
  2. package/dist/build-registry.js +11 -5
  3. package/dist/cli.js +1256 -634
  4. package/dist/mcp/code-hosting/github.json +14 -1
  5. package/dist/mcp/docs-reference/context7.json +1 -10
  6. package/dist/mcp-skills/deepstorm-mcp-feishu-wiki-read/SKILL.md +8 -5
  7. package/dist/mcp-skills/deepstorm-mcp-feishu-wiki-write/SKILL.md +10 -8
  8. package/dist/mcp-skills/deepstorm-mcp-figma-read/SKILL.md +21 -20
  9. package/dist/mcp-skills/deepstorm-mcp-github-read/SKILL.md +17 -17
  10. package/dist/mcp-skills/deepstorm-mcp-github-write/SKILL.md +20 -20
  11. package/dist/mcp-skills/deepstorm-mcp-jira-read/SKILL.md +8 -8
  12. package/dist/mcp-skills/deepstorm-mcp-jira-write/SKILL.md +13 -12
  13. package/dist/mcp-skills/deepstorm-mcp-playwright-read/SKILL.md +10 -10
  14. package/dist/registry.json +10 -0
  15. package/dist/skills/atoll-ops/SKILL.md +4 -0
  16. package/dist/skills/reef-commit/SKILL.md +9 -5
  17. package/dist/skills/reef-commit/scripts/branch-check.mjs +5 -11
  18. package/dist/skills/reef-commit/scripts/check-openspec-status.mjs +6 -12
  19. package/dist/skills/reef-commit/scripts/collect-git-context.mjs +13 -11
  20. package/dist/skills/reef-gen-backend/variants/java/steps.md +7 -7
  21. package/dist/skills/reef-gen-backend/variants/nodejs/steps.md +7 -7
  22. package/dist/skills/reef-gen-backend/variants/python/steps.md +7 -7
  23. package/dist/skills/reef-gen-frontend/variants/angular/steps.md +5 -5
  24. package/dist/skills/reef-gen-frontend/variants/react/steps.md +6 -6
  25. package/dist/skills/reef-gen-frontend/variants/vue/steps.md +6 -6
  26. package/dist/skills/reef-harden/EXAMPLES.md +12 -10
  27. package/dist/skills/reef-harden/SKILL.md +6 -0
  28. package/dist/skills/reef-harden/scripts/find-change-dir.mjs +13 -13
  29. package/dist/skills/reef-pr/SKILL.md +7 -0
  30. package/dist/skills/reef-pr/scripts/create-pr.mjs +31 -29
  31. package/dist/skills/reef-scope/SKILL.md +6 -6
  32. package/dist/skills/reef-start/references/jira-start-subagent.md +7 -7
  33. package/dist/skills/reef-start/references/risk-routing-card.md +28 -25
  34. package/dist/skills/reef-start/references/stage-4-implementation.md +33 -21
  35. package/dist/skills/reef-start/references/superpowers-gate.md +9 -9
  36. package/dist/skills/reef-style-backend/fragments/java/ai/spring-ai/examples/structured-output.md +2 -2
  37. package/dist/skills/reef-style-backend/fragments/java/ai/spring-ai/quick-reference.md +11 -0
  38. package/dist/skills/reef-style-backend/fragments/java/api-spec/jackson-polymorphism.md +36 -40
  39. package/dist/skills/reef-style-backend/fragments/java/api-spec/quick-reference.md +11 -10
  40. package/dist/skills/reef-style-backend/fragments/java/db-migration/liquibase/examples/database-migration.md +7 -7
  41. package/dist/skills/reef-style-backend/fragments/java/dependency-management/quick-reference.md +24 -21
  42. package/dist/skills/reef-style-backend/fragments/java/exception-handling/examples/error-code-enum.md +7 -7
  43. package/dist/skills/reef-style-backend/fragments/java/exception-handling/quick-reference.md +10 -8
  44. package/dist/skills/reef-style-backend/fragments/java/framework/spring-boot/examples/dto-mapper.md +2 -2
  45. package/dist/skills/reef-style-backend/fragments/java/framework/spring-boot/examples/service-entity.md +5 -5
  46. package/dist/skills/reef-style-backend/fragments/java/framework/spring-boot/examples/testing.md +1 -0
  47. package/dist/skills/reef-style-backend/fragments/java/framework/spring-boot/quick-reference.md +12 -12
  48. package/dist/skills/reef-style-backend/fragments/java/orm/hibernate/quick-reference.md +13 -12
  49. package/dist/skills/reef-style-backend/fragments/java/security-redlines/quick-reference.md +11 -9
  50. package/dist/skills/reef-style-backend/fragments/java/test/data-jpa-test/quick-reference.md +7 -7
  51. package/dist/skills/reef-style-backend/fragments/java/test/junit5/quick-reference.md +6 -6
  52. package/dist/skills/reef-style-backend/fragments/java/test/spring-mvc-test/quick-reference.md +8 -8
  53. package/dist/skills/reef-style-backend/fragments/java/test/spring-service-test/quick-reference.md +8 -7
  54. package/dist/skills/reef-style-backend/fragments/nodejs/eslint-config.json +1 -4
  55. package/dist/skills/reef-style-backend/fragments/nodejs/nestjs-structure.md +9 -9
  56. package/dist/skills/reef-style-backend/fragments/python/alembic-migration/quick-reference.md +2 -1
  57. package/dist/skills/reef-style-backend/fragments/python/api-spec/quick-reference.md +10 -10
  58. package/dist/skills/reef-style-backend/fragments/python/dependency-management/quick-reference.md +24 -22
  59. package/dist/skills/reef-style-backend/fragments/python/exception-handling/quick-reference.md +10 -9
  60. package/dist/skills/reef-style-backend/fragments/python/fastapi-quick-reference/quick-reference.md +3 -0
  61. package/dist/skills/reef-style-backend/fragments/python/langchain/quick-reference.md +10 -7
  62. package/dist/skills/reef-style-backend/fragments/python/pytest-testing/quick-reference.md +2 -0
  63. package/dist/skills/reef-style-backend/fragments/python/ruff-mypy-toolchain/quick-reference.md +2 -0
  64. package/dist/skills/reef-style-backend/fragments/python/security-redlines/quick-reference.md +11 -9
  65. package/dist/skills/reef-style-backend/fragments/python/sqlalchemy-orm/quick-reference.md +3 -0
  66. package/dist/skills/reef-style-backend/variants/java/examples/code-wrapping.md +3 -4
  67. package/dist/skills/reef-style-backend/variants/java/quick-reference.md +31 -29
  68. package/dist/skills/reef-style-backend/variants/nodejs/examples/module-example.md +2 -8
  69. package/dist/skills/reef-style-backend/variants/nodejs/quick-reference.md +35 -36
  70. package/dist/skills/reef-style-backend/variants/python/quick-reference.md +32 -31
  71. package/dist/skills/reef-style-frontend/fragments/css/tailwind/quick-reference.md +9 -7
  72. package/dist/skills/reef-style-frontend/fragments/test/vitest/examples/testing.md +9 -4
  73. package/dist/skills/reef-style-frontend/fragments/test/vitest/quick-reference.md +4 -4
  74. package/dist/skills/reef-style-frontend/fragments/test/vitest-react/examples/testing.md +135 -140
  75. package/dist/skills/reef-style-frontend/fragments/test/vitest-react/quick-reference.md +32 -32
  76. package/dist/skills/reef-style-frontend/fragments/test/vitest-vue/examples/testing.md +161 -162
  77. package/dist/skills/reef-style-frontend/fragments/test/vitest-vue/quick-reference.md +42 -42
  78. package/dist/skills/reef-style-frontend/fragments/ts-config/strict/quick-reference.md +16 -10
  79. package/dist/skills/reef-style-frontend/fragments/ui-lib/antd/examples/ui-components.md +72 -87
  80. package/dist/skills/reef-style-frontend/fragments/ui-lib/antd/quick-reference.md +14 -10
  81. package/dist/skills/reef-style-frontend/fragments/ui-lib/antd-vue/examples/ui-components.md +93 -111
  82. package/dist/skills/reef-style-frontend/fragments/ui-lib/antd-vue/quick-reference.md +29 -33
  83. package/dist/skills/reef-style-frontend/fragments/ui-lib/primeng/examples/ui-components.md +14 -23
  84. package/dist/skills/reef-style-frontend/variants/angular/examples/code-wrapping.md +15 -17
  85. package/dist/skills/reef-style-frontend/variants/angular/examples/component-types-pipes.md +22 -5
  86. package/dist/skills/reef-style-frontend/variants/angular/examples/entity-types.md +52 -22
  87. package/dist/skills/reef-style-frontend/variants/angular/examples/forms-layer.md +17 -20
  88. package/dist/skills/reef-style-frontend/variants/angular/examples/service-routing.md +8 -11
  89. package/dist/skills/reef-style-frontend/variants/angular/quick-reference.md +27 -27
  90. package/dist/skills/reef-style-frontend/variants/react/examples/code-wrapping.md +14 -27
  91. package/dist/skills/reef-style-frontend/variants/react/examples/component-types-pipes.md +45 -52
  92. package/dist/skills/reef-style-frontend/variants/react/examples/entity-types.md +44 -44
  93. package/dist/skills/reef-style-frontend/variants/react/examples/forms-layer.md +56 -85
  94. package/dist/skills/reef-style-frontend/variants/react/examples/service-routing.md +114 -88
  95. package/dist/skills/reef-style-frontend/variants/react/quick-reference.md +48 -42
  96. package/dist/skills/reef-style-frontend/variants/vue/examples/code-wrapping.md +24 -43
  97. package/dist/skills/reef-style-frontend/variants/vue/examples/component-types-pipes.md +48 -49
  98. package/dist/skills/reef-style-frontend/variants/vue/examples/entity-types.md +45 -45
  99. package/dist/skills/reef-style-frontend/variants/vue/examples/forms-layer.md +64 -68
  100. package/dist/skills/reef-style-frontend/variants/vue/examples/service-routing.md +85 -84
  101. package/dist/skills/reef-style-frontend/variants/vue/quick-reference.md +46 -51
  102. package/dist/skills/reef-testcase/SKILL.md +19 -19
  103. package/dist/skills/reef-testcase/references/coverage-dimensions.md +8 -0
  104. package/dist/skills/reef-testcase/references/test-case-template.md +16 -16
  105. package/dist/skills/sweep-explore/SKILL.md +824 -0
  106. package/dist/skills/sweep-explore/references/explore-flow-template.md +61 -0
  107. package/dist/skills/sweep-init/SKILL.md +4 -0
  108. package/dist/skills/sweep-init/scripts/flow-selector.mjs +6 -11
  109. package/dist/skills/sweep-init/scripts/init-project.mjs +68 -51
  110. package/dist/skills/sweep-plan/references/test-flow-template.md +12 -6
  111. package/dist/skills/sweep-record/SKILL.md +358 -0
  112. package/dist/skills/sweep-run/SKILL.md +59 -50
  113. package/dist/skills/sweep-run/scripts/env-manager.mjs +4 -5
  114. package/dist/skills/sweep-run/scripts/flow-parser.mjs +7 -6
  115. package/dist/skills/sweep-run/scripts/flow-selector.mjs +4 -15
  116. package/dist/skills/sweep-run/scripts/generate-report.mjs +1 -2
  117. package/dist/skills/sweep-run/scripts/mcp-manager.mjs +107 -14
  118. package/dist/skills/sweep-run/scripts/spec-compiler.mjs +19 -9
  119. package/dist/skills/tide-discuss/references/checklists.md +32 -32
  120. package/dist/skills/tide-discuss/references/data-format.md +85 -77
  121. package/dist/skills/tide-discuss/references/prd-template.md +21 -19
  122. package/dist/skills/tide-discuss/references/publish-flow.md +30 -11
  123. package/dist/skills/tide-discuss/references/role-prompts.md +5 -5
  124. package/package.json +5 -3
@@ -28,12 +28,12 @@ deepstorm:
28
28
 
29
29
  ### 执行模式说明
30
30
 
31
- | 模式 | 参数 | MCP 模式 | 说明 | 加速比 |
32
- |------|------|---------|------|--------|
31
+ | 模式 | 参数 | MCP 模式 | 说明 | 加速比 |
32
+ | -------------------- | ----------------- | -------- | -------------------------------------------------------------------------------- | ------- |
33
33
  | **混合执行(默认)** | (无)或 `--fast` | headless | 预编译 `.flow.spec.ts` → 原生 Playwright 执行(`--reporter=line`);失败 AI 自愈 | ~10-20x |
34
- | 纯原生(不自愈) | `--native` | 不启动 | 只用原生 Playwright 执行,失败不自愈 | ~20x |
35
- | 批量(单线程) | `--no-parallel` | headless | 原生执行 + 单线程顺序运行 | ~10x |
36
- | 浏览器调试 | `--browser` | headed | AI 逐步骤 MCP 调用 + 浏览器窗口可见 | 1x |
34
+ | 纯原生(不自愈) | `--native` | 不启动 | 只用原生 Playwright 执行,失败不自愈 | ~20x |
35
+ | 批量(单线程) | `--no-parallel` | headless | 原生执行 + 单线程顺序运行 | ~10x |
36
+ | 浏览器调试 | `--browser` | headed | AI 逐步骤 MCP 调用 + 浏览器窗口可见 | 1x |
37
37
 
38
38
  ---
39
39
 
@@ -48,7 +48,7 @@ node scripts/env-manager.mjs --framework
48
48
  ```
49
49
 
50
50
  ```json
51
- {"framework":"playwright","source":"deepstorm-settings"}
51
+ { "framework": "playwright", "source": "deepstorm-settings" }
52
52
  ```
53
53
 
54
54
  - **playwright** → 通过 Playwright MCP(`deepstorm-playwright`)执行浏览器操作
@@ -98,9 +98,9 @@ node scripts/env-manager.mjs --project-root
98
98
  ```markdown
99
99
  📋 可用测试模块:
100
100
 
101
- 1. user-system/register(3 个用例:L01-L03)
102
- 2. user-system/login(2 个用例:L01-L02)
103
- 3. tasks/crud(4 个用例:T01-T04)
101
+ 1. user-system/register(3 个用例:L01-L03)
102
+ 2. user-system/login(2 个用例:L01-L02)
103
+ 3. tasks/crud(4 个用例:T01-T04)
104
104
 
105
105
  请选择:全部执行 / 输入序号(如 1,3) / 输入模块名
106
106
  ```
@@ -124,19 +124,20 @@ node scripts/env-manager.mjs --project-root
124
124
 
125
125
  #### 2.2 直接参数模式
126
126
 
127
- | 参数 | 行为 | 示例 |
128
- |------|------|------|
129
- | `--all` | 执行 flows/ 下所有 .flow.md | `/sweep-run --all` |
130
- | `--path {module}` | 执行指定模块路径下的所有 .flow | `/sweep-run --path user-system/login` |
131
- | `{file-path}` | 执行指定文件 | `/sweep-run flows/login.flow.md` |
132
- | `{file-path} --flow {ID}` | 只执行文件中的某个 Flow | `/sweep-run login.flow.md --flow L02` |
133
- | `--env {env}` | 切换目标环境 | `/sweep-run --all --env staging` |
134
- | `--browser` | 打开浏览器窗口逐步骤调试 | `/sweep-run --all --browser` |
135
- | `--no-parallel` | 使用批量但不并行 | `/sweep-run --all --no-parallel` |
127
+ | 参数 | 行为 | 示例 |
128
+ | ------------------------- | ------------------------------ | ------------------------------------- |
129
+ | `--all` | 执行 flows/ 下所有 .flow.md | `/sweep-run --all` |
130
+ | `--path {module}` | 执行指定模块路径下的所有 .flow | `/sweep-run --path user-system/login` |
131
+ | `{file-path}` | 执行指定文件 | `/sweep-run flows/login.flow.md` |
132
+ | `{file-path} --flow {ID}` | 只执行文件中的某个 Flow | `/sweep-run login.flow.md --flow L02` |
133
+ | `--env {env}` | 切换目标环境 | `/sweep-run --all --env staging` |
134
+ | `--browser` | 打开浏览器窗口逐步骤调试 | `/sweep-run --all --browser` |
135
+ | `--no-parallel` | 使用批量但不并行 | `/sweep-run --all --no-parallel` |
136
136
 
137
137
  #### 2.3 文件不存在处理
138
138
 
139
139
  如指定的文件或 --path 不存在:
140
+
140
141
  - 提示"指定路径不存在"
141
142
  - 读取 topology.yaml 展示可用模块
142
143
  - 引导用户重新选择
@@ -165,6 +166,7 @@ Flows数组:
165
166
  ```
166
167
 
167
168
  提取每个 Flow 中的:
169
+
168
170
  - 前置条件(描述性文本,用于理解上下文)
169
171
  - 步骤列表(有序操作 + 验证点)
170
172
  - 环境要求(用于 --env 默认值)
@@ -184,10 +186,14 @@ node scripts/env-manager.mjs --env staging
184
186
  输出示例:
185
187
 
186
188
  ```json
187
- {"env":"staging","baseUrl":"https://staging.example.com","availableEnvs":[
188
- {"name":"test","key":"BASE_URL_TEST","url":"https://test.example.com"},
189
- {"name":"staging","key":"BASE_URL_STAGING","url":"https://staging.example.com"}
190
- ]}
189
+ {
190
+ "env": "staging",
191
+ "baseUrl": "https://staging.example.com",
192
+ "availableEnvs": [
193
+ { "name": "test", "key": "BASE_URL_TEST", "url": "https://test.example.com" },
194
+ { "name": "staging", "key": "BASE_URL_STAGING", "url": "https://staging.example.com" }
195
+ ]
196
+ }
191
197
  ```
192
198
 
193
199
  - `--env staging` → 从 `settings.json` 的 `sweep.environments.staging` 读取
@@ -201,6 +207,7 @@ export BASE_URL=$(node scripts/env-manager.mjs --env test --print 2>/dev/null |
201
207
  ```
202
208
 
203
209
  或从 JSON 结果提取:
210
+
204
211
  ```bash
205
212
  BASE_URL=$(node scripts/env-manager.mjs --env test | node -pe "JSON.parse(require('fs').readFileSync('/dev/stdin','utf8')).baseUrl")
206
213
  ```
@@ -246,8 +253,9 @@ node scripts/mcp-manager.mjs --mode=skip
246
253
  ```
247
254
 
248
255
  输出 JSON:
256
+
249
257
  ```json
250
- {"action":"started","mode":"headless","pid":12345,"port":54321}
258
+ { "action": "started", "mode": "headless", "pid": 12345, "port": 54321 }
251
259
  ```
252
260
 
253
261
  `action` 取值:`started`(新启动)/ `already-ok`(已在正确模式)/ `switched`(切换后重启)/ `skipped`(跳过)。
@@ -333,13 +341,13 @@ flowchart LR
333
341
 
334
342
  **操作类型:**
335
343
 
336
- | 操作 | MCP 调用 | 验证方式 |
337
- |------|---------|---------|
338
- | 导航 | navigate/goto | 检查页面标题/URL |
339
- | 点击 | click | 检查预期元素/状态变化 |
340
- | 输入 | fill | 检查输入回显或后续页面状态 |
341
- | 选择 | select | 检查选项是否生效 |
342
- | 等待 | waitFor | 检查超时或条件满足 |
344
+ | 操作 | MCP 调用 | 验证方式 |
345
+ | ---- | ------------- | -------------------------- |
346
+ | 导航 | navigate/goto | 检查页面标题/URL |
347
+ | 点击 | click | 检查预期元素/状态变化 |
348
+ | 输入 | fill | 检查输入回显或后续页面状态 |
349
+ | 选择 | select | 检查选项是否生效 |
350
+ | 等待 | waitFor | 检查超时或条件满足 |
343
351
 
344
352
  **执行完成后:** 杀掉 headed 进程,以 `--headless` 重启恢复默认模式。
345
353
 
@@ -364,12 +372,12 @@ flowchart LR
364
372
 
365
373
  **判定与处理:**
366
374
 
367
- | 判定结果 | 操作 | 报告中标注 |
368
- |----------|------|-----------|
369
- | **元素选择器失效**(元素存在但 CSS class/data-testid 变化) | 更新定位器;添加 `// auto-repaired: {timestamp} - {old} -> {new}` 注释;**重跑**确认 | 自愈成功 |
370
- | **数据累积**(多次运行导致同名元素重复,`.first()`/`.last()` 选错目标) | 通过 `page.evaluate()` 调用 API 清理历史残留数据后重跑 | 自愈成功 |
371
- | **真 Bug**(元素不存在、页面行为不符合预期) | 不修改 spec 文件 | 确认 Bug |
372
- | **修复无效**(更新后重跑仍然失败) | 放弃修复 | 自愈放弃 |
375
+ | 判定结果 | 操作 | 报告中标注 |
376
+ | ----------------------------------------------------------------------- | ------------------------------------------------------------------------------------ | ---------- |
377
+ | **元素选择器失效**(元素存在但 CSS class/data-testid 变化) | 更新定位器;添加 `// auto-repaired: {timestamp} - {old} -> {new}` 注释;**重跑**确认 | 自愈成功 |
378
+ | **数据累积**(多次运行导致同名元素重复,`.first()`/`.last()` 选错目标) | 通过 `page.evaluate()` 调用 API 清理历史残留数据后重跑 | 自愈成功 |
379
+ | **真 Bug**(元素不存在、页面行为不符合预期) | 不修改 spec 文件 | 确认 Bug |
380
+ | **修复无效**(更新后重跑仍然失败) | 放弃修复 | 自愈放弃 |
373
381
 
374
382
  **循环保护:** 同一 Flow 最多重试 **3 次**,超限则停止并保留最后一次失败报告。
375
383
 
@@ -417,16 +425,17 @@ Flow: L01 - 正常登录成功
417
425
 
418
426
  ## Flow: L01 - 正常登录成功 X
419
427
 
420
- | 步骤 | 操作 | 验证 | 结果 |
421
- |------|------|------|------|
422
- | 1/4 | 打开登录页面 | 页面加载完成 | OK |
423
- | 2/4 | 输入邮箱 | 输入框显示 | OK |
424
- | 3/4 | 点击登录按钮 | URL 跳转 /dashboard | X |
425
- | 4/4 | (跳过) | - | > |
428
+ | 步骤 | 操作 | 验证 | 结果 |
429
+ | ---- | ------------ | ------------------- | ---- |
430
+ | 1/4 | 打开登录页面 | 页面加载完成 | OK |
431
+ | 2/4 | 输入邮箱 | 输入框显示 | OK |
432
+ | 3/4 | 点击登录按钮 | URL 跳转 /dashboard | X |
433
+ | 4/4 | (跳过) | - | > |
426
434
 
427
435
  ### 失败详情
428
436
 
429
437
  **步骤 3/4:** 点击登录按钮
438
+
430
439
  - 期望:URL 跳转到 /dashboard
431
440
  - 实际:URL 保持在 /login
432
441
  - 可能原因:登录失败或页面未正确响应
@@ -435,14 +444,14 @@ Flow: L01 - 正常登录成功
435
444
 
436
445
  ## 汇总
437
446
 
438
- | 项目 | 值 |
439
- |------|-----|
440
- | 总 Flow 数 | 1 |
441
- | 总步骤数 | 3 |
442
- | 通过 | 2 |
443
- | 失败 | 1 |
444
- | 跳过 | 1 |
445
- | 通过率 | 66% |
447
+ | 项目 | 值 |
448
+ | ---------- | --- |
449
+ | 总 Flow 数 | 1 |
450
+ | 总步骤数 | 3 |
451
+ | 通过 | 2 |
452
+ | 失败 | 1 |
453
+ | 跳过 | 1 |
454
+ | 通过率 | 66% |
446
455
  ```
447
456
 
448
457
  #### 8.3 报告文件名格式
@@ -107,8 +107,7 @@ export function parseDotEnv(content) {
107
107
  const key = trimmed.slice(0, sepIdx).trim();
108
108
  let value = trimmed.slice(sepIdx + 1).trim();
109
109
  // Strip surrounding quotes
110
- if ((value.startsWith('"') && value.endsWith('"')) ||
111
- (value.startsWith("'") && value.endsWith("'"))) {
110
+ if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
112
111
  value = value.slice(1, -1);
113
112
  }
114
113
  env[key] = value;
@@ -306,7 +305,7 @@ if (process.argv[1] === import.meta.filename) {
306
305
  }
307
306
 
308
307
  if (args.includes('--check-mcp')) {
309
- const mcpName = args.find(a => a.startsWith('--mcp='))?.split('=')[1] || 'deepstorm-playwright';
308
+ const mcpName = args.find((a) => a.startsWith('--mcp='))?.split('=')[1] || 'deepstorm-playwright';
310
309
  const result = checkMcpAvailable(mcpName);
311
310
  console.log(JSON.stringify(result));
312
311
  process.exit(0);
@@ -333,7 +332,7 @@ if (process.argv[1] === import.meta.filename) {
333
332
  }
334
333
 
335
334
  // Default: resolve env
336
- const envFlag = args.find(a => a.startsWith('--env='));
335
+ const envFlag = args.find((a) => a.startsWith('--env='));
337
336
  const envName = envFlag ? envFlag.split('=')[1] : undefined;
338
337
  const result = resolveEnv(envName);
339
338
 
@@ -342,7 +341,7 @@ if (process.argv[1] === import.meta.filename) {
342
341
  console.log(`export BASE_URL=${result.baseUrl}`);
343
342
  } else {
344
343
  console.error(`# No base URL found for environment "${result.env}"`);
345
- console.error(`# Available: ${result.availableEnvs.map(e => e.name).join(', ')}`);
344
+ console.error(`# Available: ${result.availableEnvs.map((e) => e.name).join(', ')}`);
346
345
  process.exit(1);
347
346
  }
348
347
  } else {
@@ -62,7 +62,10 @@ function parseScenarios(content) {
62
62
  let inBody = false;
63
63
  for (const line of lines) {
64
64
  // Skip header and separator
65
- if (/^\|[-]+\|/.test(line) && line.includes('---')) { inBody = true; continue; }
65
+ if (/^\|[-]+\|/.test(line) && line.includes('---')) {
66
+ inBody = true;
67
+ continue;
68
+ }
66
69
  if (!inBody) continue;
67
70
 
68
71
  const m = line.match(SCENARIO_ROW_RE);
@@ -172,9 +175,7 @@ function parseFlows(content) {
172
175
 
173
176
  // Get content between this flow heading and the next, or end of file
174
177
  const startIdx = heading.index + heading[0].length;
175
- const endIdx = i + 1 < flowHeadings.length
176
- ? flowHeadings[i + 1].index
177
- : content.length;
178
+ const endIdx = i + 1 < flowHeadings.length ? flowHeadings[i + 1].index : content.length;
178
179
  const flowContent = content.slice(startIdx, endIdx);
179
180
 
180
181
  const preconditionsMatch = flowContent.match(PRECONDITIONS_RE);
@@ -186,8 +187,8 @@ function parseFlows(content) {
186
187
  if (preconditionsMatch) {
187
188
  const precondLines = preconditionsMatch[1]
188
189
  .split('\n')
189
- .map(l => l.replace(/^-\s*/, '').trim())
190
- .filter(l => l.length > 0);
190
+ .map((l) => l.replace(/^-\s*/, '').trim())
191
+ .filter((l) => l.length > 0);
191
192
  flow.preconditions = precondLines.join('\n');
192
193
  }
193
194
 
@@ -41,11 +41,7 @@ async function getCheckbox() {
41
41
  // ── TTY detection ───────────────────────────────────────────────────
42
42
 
43
43
  function isTtyAvailable() {
44
- return Boolean(
45
- process.stdout.isTTY &&
46
- process.stdin.isTTY &&
47
- process.stdin.setRawMode
48
- );
44
+ return Boolean(process.stdout.isTTY && process.stdin.isTTY && process.stdin.setRawMode);
49
45
  }
50
46
 
51
47
  // ── YAML 解析(轻量,不依赖 yaml 包) ──────────────────────────
@@ -98,14 +94,10 @@ function resolveFlowFile(modulePath, baseFlowsDir) {
98
94
 
99
95
  const parts = modulePath.split('/');
100
96
  const leafName = parts[parts.length - 1];
101
- const parentDir = parts.length > 1
102
- ? join(baseFlowsDir, parts.slice(0, -1).join('/'))
103
- : baseFlowsDir;
97
+ const parentDir = parts.length > 1 ? join(baseFlowsDir, parts.slice(0, -1).join('/')) : baseFlowsDir;
104
98
  if (existsSync(parentDir)) {
105
99
  const files = readdirSync(parentDir);
106
- const found = files.find(
107
- (f) => f.endsWith('.flow.md') && f.toLowerCase().includes(leafName.toLowerCase())
108
- );
100
+ const found = files.find((f) => f.endsWith('.flow.md') && f.toLowerCase().includes(leafName.toLowerCase()));
109
101
  if (found) return join(parentDir, found);
110
102
  }
111
103
 
@@ -442,10 +434,7 @@ async function main() {
442
434
  }
443
435
  }
444
436
 
445
- choices = [
446
- { name: '📋 全部执行', value: '__all__' },
447
- ...choices,
448
- ];
437
+ choices = [{ name: '📋 全部执行', value: '__all__' }, ...choices];
449
438
 
450
439
  // ── Decide mode: text vs checkbox ──────────────────────
451
440
 
@@ -30,8 +30,7 @@ import { resolve } from 'node:path';
30
30
  */
31
31
  export function generateReport(data) {
32
32
  const total = data.passed + data.failed + data.skipped;
33
- const passRate = total > 0
34
- ? Math.round((data.passed / total) * 100) : 0;
33
+ const passRate = total > 0 ? Math.round((data.passed / total) * 100) : 0;
35
34
 
36
35
  const lines = [];
37
36
  lines.push('# 测试执行报告\n');
@@ -13,6 +13,10 @@
13
13
  * node mcp-manager.mjs --status [--port 54321]
14
14
  * node mcp-manager.mjs --stop [--port 54321]
15
15
  * import { ensureMcp, stopMcp, getStatus } from './mcp-manager.mjs'
16
+ *
17
+ * Cross-platform note:
18
+ * 所有系统调用均兼容 macOS / Linux / Windows。
19
+ * 使用 Node.js 内置 API 替代 lsof / ps / sleep / SIGKILL 等 Unix-only 命令。
16
20
  */
17
21
 
18
22
  import { execSync, spawn } from 'node:child_process';
@@ -20,16 +24,39 @@ import { execSync, spawn } from 'node:child_process';
20
24
  const DEFAULT_PORT = 54321;
21
25
  const MCP_PACKAGE = '@playwright/mcp';
22
26
 
23
- // ── Process helpers ────────────────────────────────────────────────
27
+ // ── Cross-platform helpers ─────────────────────────────────────────
24
28
 
25
29
  /**
26
- * Find PID of process listening on a given TCP port.
27
- * Returns null if nothing is listening.
30
+ * 跨平台同步 sleep 使用 Atomics.wait 实现,不依赖 shell sleep 命令。
31
+ * @param {number} ms
32
+ */
33
+ function sleepSync(ms) {
34
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
35
+ }
36
+
37
+ // ── Cross-platform process helpers ──────────────────────────────────
38
+
39
+ /**
40
+ * 跨平台查找监听指定 TCP 端口的进程 PID。
41
+ *
42
+ * - macOS/Linux: lsof -ti tcp:<port>
43
+ * - Windows: netstat -ano | findstr LISTENING
44
+ *
45
+ * @param {number} port
46
+ * @returns {number|null}
28
47
  */
29
48
  export function findPidByPort(port) {
49
+ if (process.platform === 'win32') {
50
+ return findPidByPortWin(port);
51
+ }
52
+ return findPidByPortUnix(port);
53
+ }
54
+
55
+ function findPidByPortUnix(port) {
30
56
  try {
31
- const stdout = execSync(`lsof -ti tcp:${port} 2>/dev/null`, {
57
+ const stdout = execSync(`lsof -ti tcp:${port}`, {
32
58
  encoding: 'utf-8',
59
+ stdio: ['pipe', 'pipe', 'ignore'],
33
60
  timeout: 3000,
34
61
  }).trim();
35
62
  return stdout ? parseInt(stdout.split('\n')[0], 10) : null;
@@ -38,13 +65,47 @@ export function findPidByPort(port) {
38
65
  }
39
66
  }
40
67
 
68
+ function findPidByPortWin(port) {
69
+ try {
70
+ const stdout = execSync(`netstat -ano | findstr "LISTENING" | findstr ":${port} "`, {
71
+ encoding: 'utf-8',
72
+ stdio: ['pipe', 'pipe', 'ignore'],
73
+ timeout: 5000,
74
+ }).trim();
75
+ // netstat output: TCP 0.0.0.0:8080 0.0.0.0:0 LISTENING 12345
76
+ const lines = stdout.split('\n');
77
+ for (const line of lines) {
78
+ const parts = line.trim().split(/\s+/);
79
+ const pid = parseInt(parts[parts.length - 1], 10);
80
+ if (Number.isFinite(pid)) return pid;
81
+ }
82
+ return null;
83
+ } catch {
84
+ return null;
85
+ }
86
+ }
87
+
41
88
  /**
42
- * Get the command line of a process by PID.
89
+ * 跨平台获取进程命令行。
90
+ *
91
+ * - macOS/Linux: ps -p <pid> -o args=
92
+ * - Windows: wmic process where processid=<pid> get commandline
93
+ *
94
+ * @param {number} pid
95
+ * @returns {string}
43
96
  */
44
97
  export function getProcessCommand(pid) {
98
+ if (process.platform === 'win32') {
99
+ return getProcessCommandWin(pid);
100
+ }
101
+ return getProcessCommandUnix(pid);
102
+ }
103
+
104
+ function getProcessCommandUnix(pid) {
45
105
  try {
46
- return execSync(`ps -p ${pid} -o args= 2>/dev/null`, {
106
+ return execSync(`ps -p ${pid} -o args=`, {
47
107
  encoding: 'utf-8',
108
+ stdio: ['pipe', 'pipe', 'ignore'],
48
109
  timeout: 3000,
49
110
  }).trim();
50
111
  } catch {
@@ -52,6 +113,20 @@ export function getProcessCommand(pid) {
52
113
  }
53
114
  }
54
115
 
116
+ function getProcessCommandWin(pid) {
117
+ try {
118
+ const stdout = execSync(`wmic process where processid=${pid} get commandline`, {
119
+ encoding: 'utf-8',
120
+ stdio: ['pipe', 'pipe', 'ignore'],
121
+ timeout: 5000,
122
+ }).trim();
123
+ const lines = stdout.split('\n').filter(Boolean);
124
+ return lines.length >= 2 ? lines[1].trim() : '';
125
+ } catch {
126
+ return '';
127
+ }
128
+ }
129
+
55
130
  /**
56
131
  * Check if a process command indicates headless mode.
57
132
  */
@@ -60,12 +135,17 @@ export function isHeadless(cmd) {
60
135
  }
61
136
 
62
137
  /**
63
- * Kill a process by PID with escalation (SIGTERM -> SIGKILL).
138
+ * 跨平台终止进程。
139
+ *
140
+ * - macOS/Linux: SIGTERM → wait → SIGKILL (force)
141
+ * - Windows: process.kill SIGTERM → taskkill /F (force fallback)
142
+ *
143
+ * @param {number} pid
64
144
  */
65
145
  export function killProcess(pid) {
66
146
  try {
67
147
  process.kill(pid, 'SIGTERM');
68
- // Wait briefly for graceful shutdown
148
+ // Wait briefly for graceful shutdown — polling with setTimeout
69
149
  const start = Date.now();
70
150
  while (Date.now() - start < 2000) {
71
151
  try {
@@ -73,10 +153,23 @@ export function killProcess(pid) {
73
153
  } catch {
74
154
  return; // process is gone
75
155
  }
76
- execSync('sleep 0.2', { timeout: 1000 });
156
+ // Busy-wait with micro-pause to avoid CPU spin
157
+ const pollStart = Date.now();
158
+ while (Date.now() - pollStart < 50) {
159
+ /* yield */
160
+ }
77
161
  }
78
162
  // Force kill
79
- process.kill(pid, 'SIGKILL');
163
+ if (process.platform === 'win32') {
164
+ // Windows: SIGKILL unsupported, use taskkill /F
165
+ try {
166
+ execSync(`taskkill /F /PID ${pid}`, { stdio: 'ignore', timeout: 5000 });
167
+ } catch {
168
+ // already dead
169
+ }
170
+ } else {
171
+ process.kill(pid, 'SIGKILL');
172
+ }
80
173
  } catch {
81
174
  // already dead
82
175
  }
@@ -142,7 +235,7 @@ export function ensureMcp(options = {}) {
142
235
  }
143
236
  // Running in wrong mode -- restart
144
237
  stopMcp(port);
145
- execSync('sleep 1', { timeout: 2000 });
238
+ sleepSync(1000);
146
239
  }
147
240
 
148
241
  // Start MCP
@@ -161,7 +254,7 @@ export function ensureMcp(options = {}) {
161
254
  const deadline = Date.now() + 10000;
162
255
  let startedPid = null;
163
256
  while (Date.now() < deadline) {
164
- execSync('sleep 0.3', { timeout: 1000 });
257
+ sleep(300);
165
258
  const found = findPidByPort(port);
166
259
  if (found) {
167
260
  startedPid = found;
@@ -181,8 +274,8 @@ export function ensureMcp(options = {}) {
181
274
 
182
275
  if (process.argv[1] === import.meta.filename) {
183
276
  const args = process.argv.slice(2);
184
- const modeFlag = args.find(a => a.startsWith('--mode='));
185
- const portFlag = args.find(a => a.startsWith('--port='));
277
+ const modeFlag = args.find((a) => a.startsWith('--mode='));
278
+ const portFlag = args.find((a) => a.startsWith('--port='));
186
279
  const mode = modeFlag ? modeFlag.split('=')[1] : 'headless';
187
280
  const port = portFlag ? parseInt(portFlag.split('=')[1], 10) : DEFAULT_PORT;
188
281
 
@@ -24,7 +24,7 @@ function inferAction(description, index) {
24
24
  // Navigation
25
25
  if (/导航|打开|goto/i.test(desc)) {
26
26
  const urlMatch = desc.match(/`([^`]+)`/);
27
- const url = urlMatch ? urlMatch[1] : 'process.env.BASE_URL || \'/\'';
27
+ const url = urlMatch ? urlMatch[1] : "process.env.BASE_URL || '/'";
28
28
  return {
29
29
  comment: desc,
30
30
  action: 'page.goto',
@@ -48,7 +48,9 @@ function inferAction(description, index) {
48
48
  const valueMatch = desc.match(/`([^`]+)`/);
49
49
  const value = valueMatch ? valueMatch[1] : '';
50
50
  const targetMatch = desc.match(/[""「」](.+?)[""」』]/);
51
- const selector = targetMatch ? `[placeholder="${targetMatch[1]}" i]` : `input${index > 0 ? `:nth-child(${index})` : ''}`;
51
+ const selector = targetMatch
52
+ ? `[placeholder="${targetMatch[1]}" i]`
53
+ : `input${index > 0 ? `:nth-child(${index})` : ''}`;
52
54
  return {
53
55
  comment: desc,
54
56
  action: 'page.fill',
@@ -155,7 +157,9 @@ export function compile(parsed) {
155
157
  lines.push('');
156
158
 
157
159
  for (const flow of parsed.flows) {
158
- lines.push(`${indent}test('${escapeStr(parsed.featureName)} - ${escapeStr(flow.id + ' - ' + flow.title)}', async ({ page }) => {`);
160
+ lines.push(
161
+ `${indent}test('${escapeStr(parsed.featureName)} - ${escapeStr(flow.id + ' - ' + flow.title)}', async ({ page }) => {`,
162
+ );
159
163
 
160
164
  // Preconditions as comments
161
165
  if (flow.preconditions) {
@@ -166,8 +170,8 @@ export function compile(parsed) {
166
170
  }
167
171
 
168
172
  // AI_REQUIRED marker — skip entire flow if any step needs AI
169
- const hasAiRequired = flow.steps.some(s =>
170
- s.description.includes('AI_REQUIRED') || s.description.includes('<!-- AI_REQUIRED -->')
173
+ const hasAiRequired = flow.steps.some(
174
+ (s) => s.description.includes('AI_REQUIRED') || s.description.includes('<!-- AI_REQUIRED -->'),
171
175
  );
172
176
  if (hasAiRequired) {
173
177
  lines.push(`${indent}${indent}// AI_REQUIRED — cannot execute natively, AI will handle`);
@@ -181,7 +185,7 @@ export function compile(parsed) {
181
185
 
182
186
  const action = inferAction(desc, step.order);
183
187
  if (action.action) {
184
- const args = action.args.map(a => typeof a === 'string' ? `'${escapeStr(a)}'` : a).join(', ');
188
+ const args = action.args.map((a) => (typeof a === 'string' ? `'${escapeStr(a)}'` : a)).join(', ');
185
189
  lines.push(`${indent}${indent}await ${action.action}(${args});`);
186
190
  } else {
187
191
  // Cannot map to Playwright API — keep as TODO comment
@@ -196,11 +200,17 @@ export function compile(parsed) {
196
200
  if (va.assertion === 'toBeVisible') {
197
201
  lines.push(`${indent}${indent}await expect(page.locator('${escapeStr(va.args[0])}')).toBeVisible();`);
198
202
  } else if (va.assertion === 'toHaveText') {
199
- lines.push(`${indent}${indent}await expect(page.locator('${escapeStr(va.args[0])}')).toHaveText('${escapeStr(va.args[1])}');`);
203
+ lines.push(
204
+ `${indent}${indent}await expect(page.locator('${escapeStr(va.args[0])}')).toHaveText('${escapeStr(va.args[1])}');`,
205
+ );
200
206
  } else if (va.assertion === 'toHaveTitle') {
201
- lines.push(`${indent}${indent}await expect(page).toHaveTitle(${typeof va.args[0] === 'string' ? `'${escapeStr(va.args[0])}'` : va.args[0]});`);
207
+ lines.push(
208
+ `${indent}${indent}await expect(page).toHaveTitle(${typeof va.args[0] === 'string' ? `'${escapeStr(va.args[0])}'` : va.args[0]});`,
209
+ );
202
210
  } else if (va.assertion === 'toHaveURL') {
203
- lines.push(`${indent}${indent}await expect(page).toHaveURL(${typeof va.args[0] === 'string' ? `'${escapeStr(va.args[0])}'` : va.args[0]});`);
211
+ lines.push(
212
+ `${indent}${indent}await expect(page).toHaveURL(${typeof va.args[0] === 'string' ? `'${escapeStr(va.args[0])}'` : va.args[0]});`,
213
+ );
204
214
  }
205
215
  }
206
216