@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.
Files changed (25) hide show
  1. package/dist/cli.js +508 -24
  2. package/dist/registry.json +168 -0
  3. package/dist/skills/reef-commit/SKILL.md +27 -100
  4. package/dist/skills/reef-commit/scripts/branch-check.mjs +109 -0
  5. package/dist/skills/reef-commit/scripts/check-openspec-status.mjs +92 -0
  6. package/dist/skills/reef-commit/scripts/collect-git-context.mjs +186 -0
  7. package/dist/skills/reef-commit/scripts/run-tests.sh +91 -0
  8. package/dist/skills/reef-commit/scripts/stash-and-switch.sh +44 -0
  9. package/dist/skills/reef-gen-backend/variants/nodejs/steps.md +53 -0
  10. package/dist/skills/reef-harden/SKILL.md +11 -19
  11. package/dist/skills/reef-harden/scripts/find-change-dir.mjs +157 -0
  12. package/dist/skills/reef-pr/SKILL.md +7 -19
  13. package/dist/skills/reef-pr/scripts/create-pr.mjs +217 -0
  14. package/dist/skills/reef-style-backend/fragments/nodejs/eslint-config.json +30 -0
  15. package/dist/skills/reef-style-backend/fragments/nodejs/nestjs-structure.md +58 -0
  16. package/dist/skills/reef-style-backend/fragments/nodejs/prettier-config.json +19 -0
  17. package/dist/skills/reef-style-backend/variants/nodejs/examples/module-example.md +166 -0
  18. package/dist/skills/reef-style-backend/variants/nodejs/examples/prisma-example.md +115 -0
  19. package/dist/skills/reef-style-backend/variants/nodejs/quick-reference.md +116 -0
  20. package/dist/skills/sweep-init/SKILL.md +12 -125
  21. package/dist/skills/sweep-init/scripts/init-project.mjs +166 -0
  22. package/dist/skills/sweep-run/SKILL.md +11 -35
  23. package/dist/skills/sweep-run/scripts/env-manager.mjs +71 -2
  24. package/dist/skills/sweep-run/scripts/generate-report.mjs +116 -0
  25. 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
- `{TARGET_DIR}` 下创建 E2E 测试项目的目录骨架。
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
- # 计算目标目录:TARGET_DIR 为 "." 时用当前目录,否则用 TARGET_DIR
130
- if [ "$TARGET_DIR" != "." ]; then
131
- mkdir -p "$TARGET_DIR"/flows "$TARGET_DIR"/flows/reports "$TARGET_DIR"/scripts
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
- **当框架未知或未配置时**,跳过框架配置文件的生成,输出提示:"框架未配置,请运行 deepstorm setup 并选择 E2E 框架。"
191
-
192
- #### 3.3 tsconfig.json
193
-
194
- 写入 TypeScript 配置(框架无关):
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
- # 在目标目录中执行 npm install
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.3 输出完成信息
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 测试项目的位置,如非根目录则自动切换。支持从子目录向上查找以兼容用户在 E2E 项目子目录中执行的情况。
61
+ 从 `.deepstorm/settings.json` 读取 `sweep.e2eProjectPath`,确定 E2E 测试项目的位置。
62
62
 
63
63
  ```bash
64
- # 向上查找 .deepstorm/settings.json
65
- DEEPSTORM_DIR=""
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** `E2E_PATH` 不为空
85
- - **THEN** 判断路径值。注意 `E2E_PATH` 是基于 `DEEPSTORM_DIR`(settings.json 所在目录)的相对路径,若当前在子目录则需拼接:
70
+ - **WHEN** `--project-root` 输出 `found: true`
71
+ - **THEN** 读取 `sweep.e2eProjectPath`:
86
72
  ```bash
87
- if [ "$E2E_PATH" != "." ]; then
88
- # 若从子目录找到的 settings.json,E2E_PATH 相对于 DEEPSTORM_DIR
89
- TARGET_DIR="$E2E_PATH"
90
- if [ -n "$DEEPSTORM_DIR" ] && [ "$DEEPSTORM_DIR" != "$PWD" ]; then
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** `E2E_PATH` 为空
106
- - **THEN** 提示"❌ 未检测到 E2E 项目。请先运行 /sweep-init 初始化。"并退出
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;