@deepstorm/cli 0.9.2 → 0.10.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.
- package/dist/cli.js +508 -24
- package/dist/registry.json +168 -0
- package/dist/skills/reef-commit/SKILL.md +27 -100
- package/dist/skills/reef-commit/scripts/branch-check.mjs +109 -0
- package/dist/skills/reef-commit/scripts/check-openspec-status.mjs +92 -0
- package/dist/skills/reef-commit/scripts/collect-git-context.mjs +186 -0
- package/dist/skills/reef-commit/scripts/run-tests.sh +91 -0
- package/dist/skills/reef-commit/scripts/stash-and-switch.sh +44 -0
- package/dist/skills/reef-gen-backend/variants/nodejs/steps.md +53 -0
- package/dist/skills/reef-harden/SKILL.md +11 -19
- package/dist/skills/reef-harden/scripts/find-change-dir.mjs +157 -0
- package/dist/skills/reef-pr/SKILL.md +7 -19
- package/dist/skills/reef-pr/scripts/create-pr.mjs +217 -0
- package/dist/skills/reef-style-backend/fragments/nodejs/eslint-config.json +30 -0
- package/dist/skills/reef-style-backend/fragments/nodejs/nestjs-structure.md +58 -0
- package/dist/skills/reef-style-backend/fragments/nodejs/prettier-config.json +19 -0
- package/dist/skills/reef-style-backend/variants/nodejs/examples/module-example.md +166 -0
- package/dist/skills/reef-style-backend/variants/nodejs/examples/prisma-example.md +115 -0
- package/dist/skills/reef-style-backend/variants/nodejs/quick-reference.md +116 -0
- package/dist/skills/sweep-init/SKILL.md +12 -125
- package/dist/skills/sweep-init/scripts/init-project.mjs +166 -0
- package/dist/skills/sweep-run/SKILL.md +11 -35
- package/dist/skills/sweep-run/scripts/env-manager.mjs +71 -2
- package/dist/skills/sweep-run/scripts/generate-report.mjs +116 -0
- package/package.json +2 -2
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# 后端编码快速参考 — Node.js / NestJS
|
|
2
|
+
|
|
3
|
+
按需加载。仅当你需要编写对应组件类型时阅读相关章节。
|
|
4
|
+
|
|
5
|
+
> 跨维度规范(适用所有后端代码):
|
|
6
|
+
> - [API 规范](api-spec.md) — RESTful 命名、统一响应体、OpenAPI、版本策略
|
|
7
|
+
> - [依赖管理规范](dependency-management.md) — 版本一致性、CVE
|
|
8
|
+
> - [异常处理深度规范](exception-handling.md) — 异常层次、错误码、全局过滤
|
|
9
|
+
> - [安全红线](security-redlines.md) — P0/P1 安全规则(必须遵守)
|
|
10
|
+
|
|
11
|
+
## 速查
|
|
12
|
+
|
|
13
|
+
| 场景 | 决策 |
|
|
14
|
+
| --- | --- |
|
|
15
|
+
| 模块结构 | 每个业务模块一个 NestJS Module,独立 Controller/Service/DTO/Entity |
|
|
16
|
+
| 依赖注入 | 构造函数注入,`@Injectable()` 装饰器,禁止 `@Inject()` 字段注入 |
|
|
17
|
+
| 参数验证 | DTO 使用 `class-validator` 装饰器(`@IsNotEmpty`、`@IsString`) |
|
|
18
|
+
| 响应格式 | 统一使用 Controller 返回值,禁止在 Service 中直接返回 Response 对象 |
|
|
19
|
+
| 异步处理 | 所有数据库/IO 操作用 `async/await`,禁止裸 `.subscribe()` 或 `.then()` |
|
|
20
|
+
| 配置管理 | 使用 `@nestjs/config` 的 `ConfigService`,禁止 `process.env` 直读(测试不可 mock) |
|
|
21
|
+
| 异常处理 | 使用 NestJS 全局异常过滤器(`ExceptionFilter`),禁止在 Controller 中 try-catch 吞异常 |
|
|
22
|
+
| 日志 | 使用 `@nestjs/common` 的 `Logger`,构造函数中注入:`private readonly logger = new Logger(XxxService.name)` |
|
|
23
|
+
| 类型安全 | 禁止 `any` 类型,优先用 `unknown` + 类型守卫 |
|
|
24
|
+
| 模块注册 | 动态模块用 `forRoot()/forFeature()` 模式,禁止在 Module 中直接 `new Provider()` |
|
|
25
|
+
|
|
26
|
+
## 代码风格
|
|
27
|
+
|
|
28
|
+
### LLM 常犯错误
|
|
29
|
+
|
|
30
|
+
- DTO 验证装饰器必须与 Swagger 装饰器同时存在(`@ApiProperty()` + `@IsString()`),禁止遗漏 Swagger
|
|
31
|
+
- Controller 方法用 `@HttpCode()` 显式声明状态码,不依赖默认 200
|
|
32
|
+
- Service 方法中数据库查询用 `findFirstOrThrow()` / `findUniqueOrThrow()` 替代手动 `if (!result) throw`
|
|
33
|
+
- Prisma 事务用 `$transaction` 包裹,禁止手动 `prisma.$executeRaw` 拼事务
|
|
34
|
+
- 枚举值用 `@nestjs/common` 的 `EnumValidationPipe` 校验,禁止手动 `if (!Object.values(E).includes(v))`
|
|
35
|
+
- 不在 Controller 中直接实例化 Service(由 DI 注入),不在 Service 中直接实例化 Repository(由 DI 注入)
|
|
36
|
+
- 所有 `catch` 块不能为空:要么 `throw` 重新抛出,要么 `this.logger.error()` + 返回 fallback
|
|
37
|
+
|
|
38
|
+
### TypeScript Decorator 使用规范
|
|
39
|
+
|
|
40
|
+
| 组件类型 | 必用装饰器 | 说明 |
|
|
41
|
+
|---------|-----------|------|
|
|
42
|
+
| Controller | `@Controller('prefix')`、`@Get()`/`@Post()`/`@Put()`/`@Delete()` | 路由定义 |
|
|
43
|
+
| DTO | `@ApiProperty()`、`@IsString()`/`@IsNumber()`/`@IsOptional()` | 验证 + Swagger |
|
|
44
|
+
| Service | `@Injectable()` | DI 可注入 |
|
|
45
|
+
| Module | `@Module()` | NestJS 模块定义 |
|
|
46
|
+
| Entity (Prisma) | 使用 Prisma Schema 定义,不额外装饰 | TypeScript 类型由 `prisma generate` 生成 |
|
|
47
|
+
|
|
48
|
+
### 控件能力声明模式
|
|
49
|
+
|
|
50
|
+
NestJS 中通过 Interface 和 Provider 令牌实现能力声明:
|
|
51
|
+
|
|
52
|
+
```typescript
|
|
53
|
+
// 定义能力接口
|
|
54
|
+
export interface ToolCapability {
|
|
55
|
+
readonly name: string;
|
|
56
|
+
supports(context: ExecutionContext): boolean;
|
|
57
|
+
execute(input: unknown): Promise<unknown>;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// 注册 Provider
|
|
61
|
+
@Module({
|
|
62
|
+
providers: [
|
|
63
|
+
{ provide: 'TOOL_CAPABILITIES', useClass: TextToolCapability, multi: true },
|
|
64
|
+
],
|
|
65
|
+
})
|
|
66
|
+
export class ToolsModule {}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## 注释规则
|
|
70
|
+
|
|
71
|
+
| 文件类型 | 注释要求 |
|
|
72
|
+
|---------|---------|
|
|
73
|
+
| **Entity / Prisma Schema** | Prisma Schema 中每个 model 加 `/// 注释`;生成类型不修改 |
|
|
74
|
+
| **DTO** | 类 JSDoc `/** 用途说明 */`;字段装饰器自带文档(`@ApiProperty({ description: '...' })`) |
|
|
75
|
+
| **Service** | 每个 public 方法加 JSDoc `/** 功能、@param、@returns */` |
|
|
76
|
+
| **Controller** | 每个端点加 `@ApiOperation({ summary: '...', description: '...' })` |
|
|
77
|
+
| **Module** | 类 JSDoc `/** 模块职责 */`;`@Module({})` 中 imports/providers/exports 按字母排序 |
|
|
78
|
+
|
|
79
|
+
## 项目目录结构
|
|
80
|
+
|
|
81
|
+
```
|
|
82
|
+
src/
|
|
83
|
+
├── main.ts # 入口文件
|
|
84
|
+
├── app.module.ts # 根模块
|
|
85
|
+
├── app.controller.ts # 根 Controller(健康检查)
|
|
86
|
+
├── prisma/
|
|
87
|
+
│ ├── prisma.module.ts # Prisma 全局模块
|
|
88
|
+
│ └── prisma.service.ts # Prisma Client 封装
|
|
89
|
+
├── modules/
|
|
90
|
+
│ └── {module}/
|
|
91
|
+
│ ├── {module}.module.ts
|
|
92
|
+
│ ├── {module}.controller.ts
|
|
93
|
+
│ ├── {module}.service.ts
|
|
94
|
+
│ ├── dto/
|
|
95
|
+
│ │ ├── create-{entity}.dto.ts
|
|
96
|
+
│ │ └── update-{entity}.dto.ts
|
|
97
|
+
│ └── entities/
|
|
98
|
+
│ └── {entity}.entity.ts # Prisma 生成类型的二次封装(可选)
|
|
99
|
+
├── common/
|
|
100
|
+
│ ├── filters/ # 全局异常过滤器
|
|
101
|
+
│ ├── pipes/ # 全局管道
|
|
102
|
+
│ ├── interceptors/ # 全局拦截器
|
|
103
|
+
│ └── guards/ # 全局守卫
|
|
104
|
+
└── config/
|
|
105
|
+
└── configuration.ts # 配置模块
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## 常见坑
|
|
109
|
+
|
|
110
|
+
| 场景 | 问题 | 正确做法 |
|
|
111
|
+
|------|------|---------|
|
|
112
|
+
| 循环依赖 | Module A imports Module B,Module B imports Module A | 用 `forwardRef(() => ModuleB)` |
|
|
113
|
+
| 异步初始化 | Service 的 `constructor` 中 await Prisma 连接 | 实现 `OnModuleInit` 接口,在 `onModuleInit()` 中初始化 |
|
|
114
|
+
| 环境变量直读 | `process.env.DB_URL` 在代码中硬编码 | 通过 `ConfigService.get('DB_URL')` 读取 |
|
|
115
|
+
| DTO 缺少装饰器 | `class-validator` 装饰器缺失导致验证不生效 | 每个 DTO 字段同时加 `@ApiProperty()` 和验证装饰器 |
|
|
116
|
+
| 事务边界 | 事务内调用外部 HTTP 服务 | Prisma `$transaction` 中禁止非数据库操作 |
|
|
@@ -103,107 +103,21 @@ E2E_PATH=$(cat .deepstorm/settings.json 2>/dev/null | grep -o '"e2eProjectPath"[
|
|
|
103
103
|
|
|
104
104
|
---
|
|
105
105
|
|
|
106
|
-
### 步骤 2
|
|
106
|
+
### 步骤 2+3:创建项目目录与配置文件
|
|
107
107
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
#### 目录结构(当 TARGET_DIR 为 `"."` 时)
|
|
111
|
-
|
|
112
|
-
```
|
|
113
|
-
.
|
|
114
|
-
├── flows/ # 测试意图文档目录
|
|
115
|
-
│ ├── topology.yaml # 功能模块拓扑定义(待用户编辑)
|
|
116
|
-
│ └── reports/ # 执行报告持久化目录
|
|
117
|
-
├── scripts/
|
|
118
|
-
│ └── flow-selector.mjs # 层级选择工具(预置)
|
|
119
|
-
├── .env # 环境变量(gitignored,即模板)
|
|
120
|
-
├── package.json # 依赖声明
|
|
121
|
-
└── tsconfig.json # TypeScript 配置
|
|
122
|
-
```
|
|
123
|
-
|
|
124
|
-
> 如果 TARGET_DIR 为 `"e2e"`,以上所有文件在 `e2e/` 下生成。如果框架为 `playwright`,还会额外生成 `playwright.config.ts`。
|
|
125
|
-
|
|
126
|
-
#### 执行
|
|
108
|
+
使用 `init-project.mjs` 一键创建目录结构并生成所有配置文件:
|
|
127
109
|
|
|
128
110
|
```bash
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
echo "📂 将在 $TARGET_DIR/ 目录下生成 E2E 测试项目"
|
|
133
|
-
else
|
|
134
|
-
mkdir -p flows flows/reports scripts
|
|
135
|
-
fi
|
|
136
|
-
```
|
|
137
|
-
|
|
138
|
-
---
|
|
139
|
-
|
|
140
|
-
### 步骤 3:生成项目配置文件(框架感知)
|
|
141
|
-
|
|
142
|
-
根据步骤 0 读取的框架配置,在 `{TARGET_DIR}` 下生成对应的配置文件。
|
|
143
|
-
|
|
144
|
-
> 所有文件写入路径以 `{TARGET_DIR}` 为前缀。当 `TARGET_DIR` 为 `"."` 时,路径不变。
|
|
145
|
-
|
|
146
|
-
#### 3.1 package.json
|
|
147
|
-
|
|
148
|
-
写入基础 `package.json`。如果框架为 `playwright`,包含 Playwright 依赖:
|
|
149
|
-
|
|
150
|
-
```json
|
|
151
|
-
{
|
|
152
|
-
"name": "sweep-e2e",
|
|
153
|
-
"version": "1.0.0",
|
|
154
|
-
"private": true,
|
|
155
|
-
"type": "module",
|
|
156
|
-
"scripts": {
|
|
157
|
-
"test": "playwright test",
|
|
158
|
-
"report": "playwright show-report"
|
|
159
|
-
},
|
|
160
|
-
"devDependencies": {
|
|
161
|
-
"@playwright/test": "^1.50.0",
|
|
162
|
-
"@inquirer/checkbox": "^4.0.0",
|
|
163
|
-
"@types/node": "^22.0.0"
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
```
|
|
167
|
-
|
|
168
|
-
> 当 E2E 框架未知或未配置时,`package.json` 中仅包含 `@inquirer/checkbox` 和 `@types/node` 依赖,**不包含** `@playwright/test`。
|
|
169
|
-
|
|
170
|
-
#### 3.2 框架配置文件
|
|
171
|
-
|
|
172
|
-
**当框架为 `playwright` 时**,写入 `{TARGET_DIR}/playwright.config.ts`,包含多环境 baseURL 设置:
|
|
173
|
-
|
|
174
|
-
```ts
|
|
175
|
-
import { defineConfig } from '@playwright/test';
|
|
176
|
-
|
|
177
|
-
export default defineConfig({
|
|
178
|
-
use: {
|
|
179
|
-
baseURL: process.env.BASE_URL || 'http://localhost:3000',
|
|
180
|
-
},
|
|
181
|
-
timeout: 30000,
|
|
182
|
-
retries: 0,
|
|
183
|
-
reporter: [['line'], ['html', { outputFolder: 'flows/reports' }]],
|
|
184
|
-
projects: [
|
|
185
|
-
{ name: 'chromium', use: { browserName: 'chromium' } },
|
|
186
|
-
],
|
|
187
|
-
});
|
|
111
|
+
node packages/sweep/skills/sweep-init/scripts/init-project.mjs \
|
|
112
|
+
--dir "$TARGET_DIR" \
|
|
113
|
+
--framework "$(node scripts/env-manager.mjs --framework | node -pe "JSON.parse(require('fs').readFileSync('/dev/stdin','utf8')).framework || ''")"
|
|
188
114
|
```
|
|
189
115
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
```json
|
|
197
|
-
{
|
|
198
|
-
"compilerOptions": {
|
|
199
|
-
"target": "ES2022",
|
|
200
|
-
"module": "ESNext",
|
|
201
|
-
"moduleResolution": "bundler",
|
|
202
|
-
"strict": true,
|
|
203
|
-
"esModuleInterop": true
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
```
|
|
116
|
+
脚本会自动:
|
|
117
|
+
- 创建 `flows/`、`flows/reports/`、`scripts/` 目录
|
|
118
|
+
- 生成 `package.json`(含框架对应依赖)、`tsconfig.json`、`playwright.config.ts`(仅 playwright)
|
|
119
|
+
- 不覆盖已有的 `topology.yaml`
|
|
120
|
+
- 执行 `npm install`
|
|
207
121
|
|
|
208
122
|
---
|
|
209
123
|
|
|
@@ -312,38 +226,11 @@ cat .mcp.json 2>/dev/null | grep -c "deepstorm-playwright"
|
|
|
312
226
|
|
|
313
227
|
#### 8.1 写入 e2eProjectPath 到 settings.json
|
|
314
228
|
|
|
315
|
-
将 `sweep.e2eProjectPath` 写入 `.deepstorm/settings.json`。**不再创建 `.sweep-init` 标记文件。**
|
|
316
|
-
|
|
317
|
-
```bash
|
|
318
|
-
# 值由步骤 0A 确定:
|
|
319
|
-
# 独立项目 → "."
|
|
320
|
-
# 混放项目 → 用户选择的路径(如 "e2e")
|
|
321
|
-
```
|
|
322
|
-
|
|
323
|
-
写入后的 settings.json 结构示例:
|
|
324
|
-
|
|
325
|
-
```json
|
|
326
|
-
{
|
|
327
|
-
"sweep": {
|
|
328
|
-
"e2eFramework": "playwright",
|
|
329
|
-
"e2eProjectPath": "e2e",
|
|
330
|
-
"environments": { ... }
|
|
331
|
-
}
|
|
332
|
-
}
|
|
333
|
-
```
|
|
334
|
-
|
|
335
|
-
#### 8.2 安装依赖
|
|
336
|
-
|
|
337
229
|
```bash
|
|
338
|
-
|
|
339
|
-
if [ "$TARGET_DIR" != "." ]; then
|
|
340
|
-
cd "$TARGET_DIR" && npm install
|
|
341
|
-
else
|
|
342
|
-
npm install
|
|
343
|
-
fi
|
|
230
|
+
node scripts/env-manager.mjs --set-e2e-path "$TARGET_DIR"
|
|
344
231
|
```
|
|
345
232
|
|
|
346
|
-
#### 8.
|
|
233
|
+
#### 8.2 输出完成信息
|
|
347
234
|
|
|
348
235
|
```
|
|
349
236
|
✅ Sweep E2E 测试项目初始化完成!
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* init-project.mjs
|
|
5
|
+
* 初始化 E2E 测试项目骨架。
|
|
6
|
+
*
|
|
7
|
+
* 用法:
|
|
8
|
+
* node init-project.mjs # 在当前目录
|
|
9
|
+
* node init-project.mjs --dir e2e # 指定子目录
|
|
10
|
+
* node init-project.mjs --framework playwright
|
|
11
|
+
* node init-project.mjs --help
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { mkdirSync, writeFileSync, existsSync } from 'node:fs';
|
|
15
|
+
import { resolve, join } from 'node:path';
|
|
16
|
+
import { execSync } from 'node:child_process';
|
|
17
|
+
|
|
18
|
+
// ── Template writers ──────────────────────────────────────────────
|
|
19
|
+
|
|
20
|
+
function writePackageJson(targetDir, framework) {
|
|
21
|
+
const deps = {
|
|
22
|
+
'@inquirer/checkbox': '^4.0.0',
|
|
23
|
+
'@types/node': '^22.0.0',
|
|
24
|
+
};
|
|
25
|
+
if (framework === 'playwright') {
|
|
26
|
+
deps['@playwright/test'] = '^1.50.0';
|
|
27
|
+
}
|
|
28
|
+
const scripts = { report: 'playwright show-report' };
|
|
29
|
+
if (framework === 'playwright') {
|
|
30
|
+
scripts.test = 'playwright test';
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
writeFileSync(join(targetDir, 'package.json'), JSON.stringify({
|
|
34
|
+
name: 'sweep-e2e',
|
|
35
|
+
version: '1.0.0',
|
|
36
|
+
private: true,
|
|
37
|
+
type: 'module',
|
|
38
|
+
scripts,
|
|
39
|
+
devDependencies: deps,
|
|
40
|
+
}, null, 2) + '\n');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function writePlaywrightConfig(targetDir) {
|
|
44
|
+
writeFileSync(join(targetDir, 'playwright.config.ts'), [
|
|
45
|
+
"import { defineConfig } from '@playwright/test';",
|
|
46
|
+
'',
|
|
47
|
+
'export default defineConfig({',
|
|
48
|
+
' use: {',
|
|
49
|
+
" baseURL: process.env.BASE_URL || 'http://localhost:3000',",
|
|
50
|
+
' },',
|
|
51
|
+
' timeout: 30000,',
|
|
52
|
+
' retries: 0,',
|
|
53
|
+
" reporter: [['line'], ['html', { outputFolder: 'flows/reports' }]],",
|
|
54
|
+
' projects: [',
|
|
55
|
+
" { name: 'chromium', use: { browserName: 'chromium' } },",
|
|
56
|
+
' ],',
|
|
57
|
+
'});',
|
|
58
|
+
'',
|
|
59
|
+
].join('\n'));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function writeTsconfig(targetDir) {
|
|
63
|
+
writeFileSync(join(targetDir, 'tsconfig.json'), JSON.stringify({
|
|
64
|
+
compilerOptions: {
|
|
65
|
+
target: 'ES2022',
|
|
66
|
+
module: 'ESNext',
|
|
67
|
+
moduleResolution: 'bundler',
|
|
68
|
+
strict: true,
|
|
69
|
+
esModuleInterop: true,
|
|
70
|
+
},
|
|
71
|
+
}, null, 2) + '\n');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function writeTopologyYaml(targetDir) {
|
|
75
|
+
writeFileSync(join(targetDir, 'flows', 'topology.yaml'), [
|
|
76
|
+
'# flows/topology.yaml',
|
|
77
|
+
'name: E2E 测试拓扑',
|
|
78
|
+
'version: 1',
|
|
79
|
+
'modules:',
|
|
80
|
+
' - name: example',
|
|
81
|
+
' description: 示例模块',
|
|
82
|
+
' children:',
|
|
83
|
+
' - name: feature1',
|
|
84
|
+
' description: 功能 1',
|
|
85
|
+
' features: []',
|
|
86
|
+
'',
|
|
87
|
+
].join('\n'));
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// ── Main ──────────────────────────────────────────────────────────
|
|
91
|
+
|
|
92
|
+
export function initProject(opts = {}) {
|
|
93
|
+
const {
|
|
94
|
+
framework = null, // 'playwright' | null
|
|
95
|
+
dir = '.',
|
|
96
|
+
} = opts;
|
|
97
|
+
|
|
98
|
+
const targetDir = resolve(process.cwd(), dir);
|
|
99
|
+
const flowsDir = join(targetDir, 'flows');
|
|
100
|
+
const reportsDir = join(flowsDir, 'reports');
|
|
101
|
+
const scriptsDir = join(targetDir, 'scripts');
|
|
102
|
+
|
|
103
|
+
// 创建目录
|
|
104
|
+
mkdirSync(flowsDir, { recursive: true });
|
|
105
|
+
mkdirSync(reportsDir, { recursive: true });
|
|
106
|
+
mkdirSync(scriptsDir, { recursive: true });
|
|
107
|
+
|
|
108
|
+
// 写入配置文件
|
|
109
|
+
writePackageJson(targetDir, framework);
|
|
110
|
+
writeTsconfig(targetDir);
|
|
111
|
+
|
|
112
|
+
if (framework === 'playwright') {
|
|
113
|
+
writePlaywrightConfig(targetDir);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (!existsSync(join(flowsDir, 'topology.yaml'))) {
|
|
117
|
+
writeTopologyYaml(targetDir);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// npm install
|
|
121
|
+
try {
|
|
122
|
+
execSync('npm install', {
|
|
123
|
+
cwd: targetDir,
|
|
124
|
+
stdio: 'pipe',
|
|
125
|
+
timeout: 60000,
|
|
126
|
+
});
|
|
127
|
+
} catch {
|
|
128
|
+
// npm install 失败不阻塞
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const created = [
|
|
132
|
+
'flows/', 'flows/reports/', 'scripts/',
|
|
133
|
+
'package.json', 'tsconfig.json',
|
|
134
|
+
];
|
|
135
|
+
if (framework === 'playwright') created.push('playwright.config.ts');
|
|
136
|
+
if (!existsSync(join(flowsDir, 'topology.yaml'))) {
|
|
137
|
+
created.push('flows/topology.yaml');
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return { dir: targetDir, created, framework };
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// ── CLI entry ─────────────────────────────────────────────────────
|
|
144
|
+
|
|
145
|
+
if (import.meta.filename === process.argv[1]) {
|
|
146
|
+
if (process.argv.includes('--help') || process.argv.includes('-h')) {
|
|
147
|
+
console.log(`
|
|
148
|
+
init-project.mjs — E2E 测试项目初始化
|
|
149
|
+
|
|
150
|
+
用法:
|
|
151
|
+
node init-project.mjs 当前目录
|
|
152
|
+
node init-project.mjs --dir e2e 指定子目录
|
|
153
|
+
node init-project.mjs --framework playwright 指定框架
|
|
154
|
+
node init-project.mjs --help
|
|
155
|
+
`);
|
|
156
|
+
process.exit(0);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const dirIdx = process.argv.indexOf('--dir');
|
|
160
|
+
const fwIdx = process.argv.indexOf('--framework');
|
|
161
|
+
const result = initProject({
|
|
162
|
+
dir: dirIdx >= 0 ? process.argv[dirIdx + 1] : '.',
|
|
163
|
+
framework: fwIdx >= 0 ? process.argv[fwIdx + 1] : null,
|
|
164
|
+
});
|
|
165
|
+
process.stdout.write(JSON.stringify(result, null, 2) + '\n');
|
|
166
|
+
}
|
|
@@ -58,52 +58,28 @@ node scripts/env-manager.mjs --framework
|
|
|
58
58
|
|
|
59
59
|
### 步骤 1:检查初始化状态与路径导航
|
|
60
60
|
|
|
61
|
-
从 `.deepstorm/settings.json` 读取 `sweep.e2eProjectPath`,确定 E2E
|
|
61
|
+
从 `.deepstorm/settings.json` 读取 `sweep.e2eProjectPath`,确定 E2E 测试项目的位置。
|
|
62
62
|
|
|
63
63
|
```bash
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
CUR="$PWD"
|
|
67
|
-
while [ "$CUR" != "/" ]; do
|
|
68
|
-
if [ -f "$CUR/.deepstorm/settings.json" ]; then
|
|
69
|
-
DEEPSTORM_DIR="$CUR"
|
|
70
|
-
break
|
|
71
|
-
fi
|
|
72
|
-
CUR=$(dirname "$CUR")
|
|
73
|
-
done
|
|
74
|
-
|
|
75
|
-
if [ -n "$DEEPSTORM_DIR" ]; then
|
|
76
|
-
E2E_PATH=$(grep -o '"e2eProjectPath"[^,]*' "$DEEPSTORM_DIR/.deepstorm/settings.json" | head -1 | cut -d'"' -f4)
|
|
77
|
-
else
|
|
78
|
-
E2E_PATH=""
|
|
79
|
-
fi
|
|
64
|
+
node scripts/env-manager.mjs --project-root
|
|
65
|
+
# 输出: {"found":true,"path":"/abs/path/to/project"}
|
|
80
66
|
```
|
|
81
67
|
|
|
82
68
|
#### 1.1 配置存在 → 路径导航
|
|
83
69
|
|
|
84
|
-
- **WHEN** `
|
|
85
|
-
- **THEN**
|
|
70
|
+
- **WHEN** `--project-root` 输出 `found: true`
|
|
71
|
+
- **THEN** 读取 `sweep.e2eProjectPath`:
|
|
86
72
|
```bash
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
TARGET_DIR="$DEEPSTORM_DIR/$E2E_PATH"
|
|
92
|
-
fi
|
|
93
|
-
if [ -d "$TARGET_DIR" ]; then
|
|
94
|
-
echo "📂 切换到 E2E 项目目录: $E2E_PATH"
|
|
95
|
-
cd "$TARGET_DIR"
|
|
96
|
-
else
|
|
97
|
-
echo "❌ E2E 项目目录不存在: $E2E_PATH,请重新运行 /sweep-init"
|
|
98
|
-
exit 1
|
|
99
|
-
fi
|
|
100
|
-
fi
|
|
73
|
+
E2E_PATH=$(grep -o '"e2eProjectPath"[^,]*' \
|
|
74
|
+
"$(node scripts/env-manager.mjs --project-root | \
|
|
75
|
+
node -pe "JSON.parse(require('fs').readFileSync('/dev/stdin','utf8')).path")/.deepstorm/settings.json" \
|
|
76
|
+
| head -1 | cut -d'"' -f4)
|
|
101
77
|
```
|
|
102
78
|
|
|
103
79
|
#### 1.2 配置不存在 → 报错退出
|
|
104
80
|
|
|
105
|
-
- **WHEN** `
|
|
106
|
-
- **THEN** 提示"❌
|
|
81
|
+
- **WHEN** `--project-root` 输出 `found: false`
|
|
82
|
+
- **THEN** 提示"❌ 未检测到深风项目。请先运行 deepstorm setup。"并退出
|
|
107
83
|
|
|
108
84
|
---
|
|
109
85
|
|
|
@@ -20,8 +20,8 @@
|
|
|
20
20
|
* import { resolveEnv, listEnvs, readFramework, checkMcpAvailable } from './env-manager.mjs'
|
|
21
21
|
*/
|
|
22
22
|
|
|
23
|
-
import { readFileSync, existsSync } from 'node:fs';
|
|
24
|
-
import { resolve } from 'node:path';
|
|
23
|
+
import { readFileSync, writeFileSync, existsSync } from 'node:fs';
|
|
24
|
+
import { resolve, dirname, sep } from 'node:path';
|
|
25
25
|
|
|
26
26
|
// ── Path helpers (lazy, respect process.cwd() changes) ────────────
|
|
27
27
|
|
|
@@ -217,6 +217,55 @@ export function readFramework() {
|
|
|
217
217
|
|
|
218
218
|
// ── MCP availability ──────────────────────────────────────────────
|
|
219
219
|
|
|
220
|
+
// ── Project root discovery ──────────────────────────────────────────
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Walk up from startDir to find .deepstorm/settings.json
|
|
224
|
+
*
|
|
225
|
+
* @param {string} [startDir] - defaults to process.cwd()
|
|
226
|
+
* @returns {string|null} resolved dir containing settings.json, or null
|
|
227
|
+
*/
|
|
228
|
+
export function resolveProjectRoot(startDir) {
|
|
229
|
+
let dir = startDir ? resolve(startDir) : process.cwd();
|
|
230
|
+
while (true) {
|
|
231
|
+
if (existsSync(resolve(dir, '.deepstorm', 'settings.json'))) {
|
|
232
|
+
return dir;
|
|
233
|
+
}
|
|
234
|
+
const parent = dirname(dir);
|
|
235
|
+
if (parent === dir) return null;
|
|
236
|
+
dir = parent;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Write or update a key in .deepstorm/settings.json.
|
|
242
|
+
*
|
|
243
|
+
* @param {string} keyPath dot-separated path, e.g. "sweep.e2eProjectPath"
|
|
244
|
+
* @param {*} value
|
|
245
|
+
* @returns {boolean}
|
|
246
|
+
*/
|
|
247
|
+
export function writeDeepstormConfig(keyPath, value) {
|
|
248
|
+
const filePath = resolve(process.cwd(), '.deepstorm', 'settings.json');
|
|
249
|
+
if (!existsSync(filePath)) return false;
|
|
250
|
+
try {
|
|
251
|
+
const content = readFileSync(filePath, 'utf-8');
|
|
252
|
+
const config = JSON.parse(content);
|
|
253
|
+
const keys = keyPath.split('.');
|
|
254
|
+
let obj = config;
|
|
255
|
+
for (let i = 0; i < keys.length - 1; i++) {
|
|
256
|
+
if (!obj[keys[i]] || typeof obj[keys[i]] !== 'object') {
|
|
257
|
+
obj[keys[i]] = {};
|
|
258
|
+
}
|
|
259
|
+
obj = obj[keys[i]];
|
|
260
|
+
}
|
|
261
|
+
obj[keys[keys.length - 1]] = value;
|
|
262
|
+
writeFileSync(filePath, JSON.stringify(config, null, 2) + '\n');
|
|
263
|
+
return true;
|
|
264
|
+
} catch {
|
|
265
|
+
return false;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
220
269
|
/**
|
|
221
270
|
* Check if the required MCP service is configured in .mcp.json.
|
|
222
271
|
*
|
|
@@ -263,6 +312,26 @@ if (process.argv[1] === import.meta.filename) {
|
|
|
263
312
|
process.exit(0);
|
|
264
313
|
}
|
|
265
314
|
|
|
315
|
+
if (args.includes('--project-root')) {
|
|
316
|
+
const startIdx = args.indexOf('--project-root');
|
|
317
|
+
const startDir = startIdx + 1 < args.length ? args[startIdx + 1] : undefined;
|
|
318
|
+
const result = resolveProjectRoot(startDir);
|
|
319
|
+
if (result) {
|
|
320
|
+
console.log(JSON.stringify({ found: true, path: result }));
|
|
321
|
+
} else {
|
|
322
|
+
console.log(JSON.stringify({ found: false }));
|
|
323
|
+
}
|
|
324
|
+
process.exit(0);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
if (args.includes('--set-e2e-path')) {
|
|
328
|
+
const idx = args.indexOf('--set-e2e-path');
|
|
329
|
+
const path = idx + 1 < args.length ? args[idx + 1] : '.';
|
|
330
|
+
const ok = writeDeepstormConfig('sweep.e2eProjectPath', path);
|
|
331
|
+
console.log(JSON.stringify({ ok, path }));
|
|
332
|
+
process.exit(0);
|
|
333
|
+
}
|
|
334
|
+
|
|
266
335
|
// Default: resolve env
|
|
267
336
|
const envFlag = args.find(a => a.startsWith('--env='));
|
|
268
337
|
const envName = envFlag ? envFlag.split('=')[1] : undefined;
|