@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.
- package/README.md +8 -8
- package/dist/build-registry.js +11 -5
- package/dist/cli.js +1256 -634
- package/dist/mcp/code-hosting/github.json +14 -1
- package/dist/mcp/docs-reference/context7.json +1 -10
- package/dist/mcp-skills/deepstorm-mcp-feishu-wiki-read/SKILL.md +8 -5
- package/dist/mcp-skills/deepstorm-mcp-feishu-wiki-write/SKILL.md +10 -8
- package/dist/mcp-skills/deepstorm-mcp-figma-read/SKILL.md +21 -20
- package/dist/mcp-skills/deepstorm-mcp-github-read/SKILL.md +17 -17
- package/dist/mcp-skills/deepstorm-mcp-github-write/SKILL.md +20 -20
- package/dist/mcp-skills/deepstorm-mcp-jira-read/SKILL.md +8 -8
- package/dist/mcp-skills/deepstorm-mcp-jira-write/SKILL.md +13 -12
- package/dist/mcp-skills/deepstorm-mcp-playwright-read/SKILL.md +10 -10
- package/dist/registry.json +10 -0
- package/dist/skills/atoll-ops/SKILL.md +4 -0
- package/dist/skills/reef-commit/SKILL.md +9 -5
- package/dist/skills/reef-commit/scripts/branch-check.mjs +5 -11
- package/dist/skills/reef-commit/scripts/check-openspec-status.mjs +6 -12
- package/dist/skills/reef-commit/scripts/collect-git-context.mjs +13 -11
- package/dist/skills/reef-gen-backend/variants/java/steps.md +7 -7
- package/dist/skills/reef-gen-backend/variants/nodejs/steps.md +7 -7
- package/dist/skills/reef-gen-backend/variants/python/steps.md +7 -7
- package/dist/skills/reef-gen-frontend/variants/angular/steps.md +5 -5
- package/dist/skills/reef-gen-frontend/variants/react/steps.md +6 -6
- package/dist/skills/reef-gen-frontend/variants/vue/steps.md +6 -6
- package/dist/skills/reef-harden/EXAMPLES.md +12 -10
- package/dist/skills/reef-harden/SKILL.md +6 -0
- package/dist/skills/reef-harden/scripts/find-change-dir.mjs +13 -13
- package/dist/skills/reef-pr/SKILL.md +7 -0
- package/dist/skills/reef-pr/scripts/create-pr.mjs +31 -29
- package/dist/skills/reef-scope/SKILL.md +6 -6
- package/dist/skills/reef-start/references/jira-start-subagent.md +7 -7
- package/dist/skills/reef-start/references/risk-routing-card.md +28 -25
- package/dist/skills/reef-start/references/stage-4-implementation.md +33 -21
- package/dist/skills/reef-start/references/superpowers-gate.md +9 -9
- package/dist/skills/reef-style-backend/fragments/java/ai/spring-ai/examples/structured-output.md +2 -2
- package/dist/skills/reef-style-backend/fragments/java/ai/spring-ai/quick-reference.md +11 -0
- package/dist/skills/reef-style-backend/fragments/java/api-spec/jackson-polymorphism.md +36 -40
- package/dist/skills/reef-style-backend/fragments/java/api-spec/quick-reference.md +11 -10
- package/dist/skills/reef-style-backend/fragments/java/db-migration/liquibase/examples/database-migration.md +7 -7
- package/dist/skills/reef-style-backend/fragments/java/dependency-management/quick-reference.md +24 -21
- package/dist/skills/reef-style-backend/fragments/java/exception-handling/examples/error-code-enum.md +7 -7
- package/dist/skills/reef-style-backend/fragments/java/exception-handling/quick-reference.md +10 -8
- package/dist/skills/reef-style-backend/fragments/java/framework/spring-boot/examples/dto-mapper.md +2 -2
- package/dist/skills/reef-style-backend/fragments/java/framework/spring-boot/examples/service-entity.md +5 -5
- package/dist/skills/reef-style-backend/fragments/java/framework/spring-boot/examples/testing.md +1 -0
- package/dist/skills/reef-style-backend/fragments/java/framework/spring-boot/quick-reference.md +12 -12
- package/dist/skills/reef-style-backend/fragments/java/orm/hibernate/quick-reference.md +13 -12
- package/dist/skills/reef-style-backend/fragments/java/security-redlines/quick-reference.md +11 -9
- package/dist/skills/reef-style-backend/fragments/java/test/data-jpa-test/quick-reference.md +7 -7
- package/dist/skills/reef-style-backend/fragments/java/test/junit5/quick-reference.md +6 -6
- package/dist/skills/reef-style-backend/fragments/java/test/spring-mvc-test/quick-reference.md +8 -8
- package/dist/skills/reef-style-backend/fragments/java/test/spring-service-test/quick-reference.md +8 -7
- package/dist/skills/reef-style-backend/fragments/nodejs/eslint-config.json +1 -4
- package/dist/skills/reef-style-backend/fragments/nodejs/nestjs-structure.md +9 -9
- package/dist/skills/reef-style-backend/fragments/python/alembic-migration/quick-reference.md +2 -1
- package/dist/skills/reef-style-backend/fragments/python/api-spec/quick-reference.md +10 -10
- package/dist/skills/reef-style-backend/fragments/python/dependency-management/quick-reference.md +24 -22
- package/dist/skills/reef-style-backend/fragments/python/exception-handling/quick-reference.md +10 -9
- package/dist/skills/reef-style-backend/fragments/python/fastapi-quick-reference/quick-reference.md +3 -0
- package/dist/skills/reef-style-backend/fragments/python/langchain/quick-reference.md +10 -7
- package/dist/skills/reef-style-backend/fragments/python/pytest-testing/quick-reference.md +2 -0
- package/dist/skills/reef-style-backend/fragments/python/ruff-mypy-toolchain/quick-reference.md +2 -0
- package/dist/skills/reef-style-backend/fragments/python/security-redlines/quick-reference.md +11 -9
- package/dist/skills/reef-style-backend/fragments/python/sqlalchemy-orm/quick-reference.md +3 -0
- package/dist/skills/reef-style-backend/variants/java/examples/code-wrapping.md +3 -4
- package/dist/skills/reef-style-backend/variants/java/quick-reference.md +31 -29
- package/dist/skills/reef-style-backend/variants/nodejs/examples/module-example.md +2 -8
- package/dist/skills/reef-style-backend/variants/nodejs/quick-reference.md +35 -36
- package/dist/skills/reef-style-backend/variants/python/quick-reference.md +32 -31
- package/dist/skills/reef-style-frontend/fragments/css/tailwind/quick-reference.md +9 -7
- package/dist/skills/reef-style-frontend/fragments/test/vitest/examples/testing.md +9 -4
- package/dist/skills/reef-style-frontend/fragments/test/vitest/quick-reference.md +4 -4
- package/dist/skills/reef-style-frontend/fragments/test/vitest-react/examples/testing.md +135 -140
- package/dist/skills/reef-style-frontend/fragments/test/vitest-react/quick-reference.md +32 -32
- package/dist/skills/reef-style-frontend/fragments/test/vitest-vue/examples/testing.md +161 -162
- package/dist/skills/reef-style-frontend/fragments/test/vitest-vue/quick-reference.md +42 -42
- package/dist/skills/reef-style-frontend/fragments/ts-config/strict/quick-reference.md +16 -10
- package/dist/skills/reef-style-frontend/fragments/ui-lib/antd/examples/ui-components.md +72 -87
- package/dist/skills/reef-style-frontend/fragments/ui-lib/antd/quick-reference.md +14 -10
- package/dist/skills/reef-style-frontend/fragments/ui-lib/antd-vue/examples/ui-components.md +93 -111
- package/dist/skills/reef-style-frontend/fragments/ui-lib/antd-vue/quick-reference.md +29 -33
- package/dist/skills/reef-style-frontend/fragments/ui-lib/primeng/examples/ui-components.md +14 -23
- package/dist/skills/reef-style-frontend/variants/angular/examples/code-wrapping.md +15 -17
- package/dist/skills/reef-style-frontend/variants/angular/examples/component-types-pipes.md +22 -5
- package/dist/skills/reef-style-frontend/variants/angular/examples/entity-types.md +52 -22
- package/dist/skills/reef-style-frontend/variants/angular/examples/forms-layer.md +17 -20
- package/dist/skills/reef-style-frontend/variants/angular/examples/service-routing.md +8 -11
- package/dist/skills/reef-style-frontend/variants/angular/quick-reference.md +27 -27
- package/dist/skills/reef-style-frontend/variants/react/examples/code-wrapping.md +14 -27
- package/dist/skills/reef-style-frontend/variants/react/examples/component-types-pipes.md +45 -52
- package/dist/skills/reef-style-frontend/variants/react/examples/entity-types.md +44 -44
- package/dist/skills/reef-style-frontend/variants/react/examples/forms-layer.md +56 -85
- package/dist/skills/reef-style-frontend/variants/react/examples/service-routing.md +114 -88
- package/dist/skills/reef-style-frontend/variants/react/quick-reference.md +48 -42
- package/dist/skills/reef-style-frontend/variants/vue/examples/code-wrapping.md +24 -43
- package/dist/skills/reef-style-frontend/variants/vue/examples/component-types-pipes.md +48 -49
- package/dist/skills/reef-style-frontend/variants/vue/examples/entity-types.md +45 -45
- package/dist/skills/reef-style-frontend/variants/vue/examples/forms-layer.md +64 -68
- package/dist/skills/reef-style-frontend/variants/vue/examples/service-routing.md +85 -84
- package/dist/skills/reef-style-frontend/variants/vue/quick-reference.md +46 -51
- package/dist/skills/reef-testcase/SKILL.md +19 -19
- package/dist/skills/reef-testcase/references/coverage-dimensions.md +8 -0
- package/dist/skills/reef-testcase/references/test-case-template.md +16 -16
- package/dist/skills/sweep-explore/SKILL.md +824 -0
- package/dist/skills/sweep-explore/references/explore-flow-template.md +61 -0
- package/dist/skills/sweep-init/SKILL.md +4 -0
- package/dist/skills/sweep-init/scripts/flow-selector.mjs +6 -11
- package/dist/skills/sweep-init/scripts/init-project.mjs +68 -51
- package/dist/skills/sweep-plan/references/test-flow-template.md +12 -6
- package/dist/skills/sweep-record/SKILL.md +358 -0
- package/dist/skills/sweep-run/SKILL.md +59 -50
- package/dist/skills/sweep-run/scripts/env-manager.mjs +4 -5
- package/dist/skills/sweep-run/scripts/flow-parser.mjs +7 -6
- package/dist/skills/sweep-run/scripts/flow-selector.mjs +4 -15
- package/dist/skills/sweep-run/scripts/generate-report.mjs +1 -2
- package/dist/skills/sweep-run/scripts/mcp-manager.mjs +107 -14
- package/dist/skills/sweep-run/scripts/spec-compiler.mjs +19 -9
- package/dist/skills/tide-discuss/references/checklists.md +32 -32
- package/dist/skills/tide-discuss/references/data-format.md +85 -77
- package/dist/skills/tide-discuss/references/prd-template.md +21 -19
- package/dist/skills/tide-discuss/references/publish-flow.md +30 -11
- package/dist/skills/tide-discuss/references/role-prompts.md +5 -5
- package/package.json +5 -3
|
@@ -28,12 +28,12 @@ deepstorm:
|
|
|
28
28
|
|
|
29
29
|
### 执行模式说明
|
|
30
30
|
|
|
31
|
-
| 模式
|
|
32
|
-
|
|
31
|
+
| 模式 | 参数 | MCP 模式 | 说明 | 加速比 |
|
|
32
|
+
| -------------------- | ----------------- | -------- | -------------------------------------------------------------------------------- | ------- |
|
|
33
33
|
| **混合执行(默认)** | (无)或 `--fast` | headless | 预编译 `.flow.spec.ts` → 原生 Playwright 执行(`--reporter=line`);失败 AI 自愈 | ~10-20x |
|
|
34
|
-
| 纯原生(不自愈)
|
|
35
|
-
| 批量(单线程)
|
|
36
|
-
| 浏览器调试
|
|
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
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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`
|
|
130
|
-
| `--path {module}`
|
|
131
|
-
| `{file-path}`
|
|
132
|
-
| `{file-path} --flow {ID}` | 只执行文件中的某个 Flow
|
|
133
|
-
| `--env {env}`
|
|
134
|
-
| `--browser`
|
|
135
|
-
| `--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
|
-
{
|
|
188
|
-
|
|
189
|
-
|
|
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 变化)
|
|
370
|
-
| **数据累积**(多次运行导致同名元素重复,`.first()`/`.last()` 选错目标) | 通过 `page.evaluate()` 调用 API 清理历史残留数据后重跑
|
|
371
|
-
| **真 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
|
|
423
|
-
| 2/4
|
|
424
|
-
| 3/4
|
|
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
|
-
| 总步骤数
|
|
442
|
-
| 通过
|
|
443
|
-
| 失败
|
|
444
|
-
| 跳过
|
|
445
|
-
| 通过率
|
|
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('---')) {
|
|
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
|
-
// ──
|
|
27
|
+
// ── Cross-platform helpers ─────────────────────────────────────────
|
|
24
28
|
|
|
25
29
|
/**
|
|
26
|
-
*
|
|
27
|
-
*
|
|
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}
|
|
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
|
-
*
|
|
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
|
|
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
|
-
*
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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] :
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|