@ranger1/dx 0.1.84 → 0.1.86
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 +58 -0
- package/codex/skills/e2e-audit-fixer/SKILL.md +76 -0
- package/codex/skills/e2e-audit-fixer/agents/openai.yaml +4 -0
- package/codex/skills/e2e-audit-fixer/scripts/e2e_e2e_audit.py +523 -0
- package/codex/skills/env-accessor-audit-fixer/SKILL.md +149 -0
- package/codex/skills/env-accessor-audit-fixer/agents/openai.yaml +7 -0
- package/codex/skills/env-accessor-audit-fixer/references/bootstrap-env-foundation.md +156 -0
- package/codex/skills/env-accessor-audit-fixer/scripts/env_accessor_audit.py +250 -0
- package/codex/skills/error-handling-audit-fixer/SKILL.md +150 -0
- package/codex/skills/error-handling-audit-fixer/agents/openai.yaml +7 -0
- package/codex/skills/error-handling-audit-fixer/references/error-handling-standard.md +152 -0
- package/codex/skills/error-handling-audit-fixer/references/foundation-bootstrap.md +85 -0
- package/codex/skills/error-handling-audit-fixer/scripts/error_handling_audit.py +537 -0
- package/codex/skills/pagination-dto-audit-fixer/SKILL.md +69 -0
- package/codex/skills/pagination-dto-audit-fixer/agents/openai.yaml +7 -0
- package/codex/skills/pagination-dto-audit-fixer/references/pagination-standard.md +67 -0
- package/codex/skills/pagination-dto-audit-fixer/scripts/pagination_dto_audit.py +244 -0
- package/lib/backend-artifact-deploy/config.js +11 -0
- package/lib/backend-artifact-deploy/remote-result.js +2 -0
- package/lib/backend-artifact-deploy/remote-script.js +57 -4
- package/lib/backend-artifact-deploy.js +30 -1
- package/lib/codex-initial.js +155 -3
- package/lib/exec.js +21 -2
- package/lib/run-with-version-env.js +2 -1
- package/package.json +1 -1
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: env-accessor-audit-fixer
|
|
3
|
+
description: Use when backend、NestJS、配置模块、E2E 测试或脚本中需要审计或修复 `process.env` 直读,判断项目是否已具备统一环境访问基础设施,并在缺少 EnvAccessor、EnvService、EnvModule 或统一配置接入时补齐最小可用实现。
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# 环境变量访问审计与修复
|
|
7
|
+
|
|
8
|
+
## 概览
|
|
9
|
+
|
|
10
|
+
先扫描 `process.env` 直读点,再判断仓库是否已有统一 env 访问基础设施。
|
|
11
|
+
|
|
12
|
+
- 若已有 `EnvService`、`createEnvAccessor`、`defaultEnvAccessor` 或等价封装,优先复用现有方案并逐项迁移。
|
|
13
|
+
- 若缺少统一入口,先补齐最小基础设施,再收口剩余直读点。
|
|
14
|
+
|
|
15
|
+
## 快速开始
|
|
16
|
+
|
|
17
|
+
先运行扫描脚本:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
CODEX_HOME="${CODEX_HOME:-$HOME/.codex}"
|
|
21
|
+
python "$CODEX_HOME/skills/env-accessor-audit-fixer/scripts/env_accessor_audit.py" \
|
|
22
|
+
--workspace /Users/a1/work/ai-monorepo
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
需要结构化结果时输出 JSON:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
python "$CODEX_HOME/skills/env-accessor-audit-fixer/scripts/env_accessor_audit.py" \
|
|
29
|
+
--workspace /Users/a1/work/ai-monorepo \
|
|
30
|
+
--output-json /tmp/env-accessor-audit.json
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
若脚本显示尚未形成统一 env 入口,再读取 [references/bootstrap-env-foundation.md](./references/bootstrap-env-foundation.md)。
|
|
34
|
+
|
|
35
|
+
## 执行流程
|
|
36
|
+
|
|
37
|
+
1. 扫描 `apps/backend/src` 与 `apps/backend/e2e` 中的 `process.env`。
|
|
38
|
+
2. 仅排除真正的基础封装文件:
|
|
39
|
+
- `env.accessor.ts`
|
|
40
|
+
- `env.service.ts`
|
|
41
|
+
3. 不排除 `apps/backend/src/config/**/*.ts`,确保 `registerAs` 配置层也纳入审计。
|
|
42
|
+
4. 优先识别当前基础设施状态:
|
|
43
|
+
- 是否存在 `createEnvAccessor`
|
|
44
|
+
- 是否存在 `defaultEnvAccessor`
|
|
45
|
+
- 是否存在 `EnvService`
|
|
46
|
+
- 是否存在 `EnvModule`
|
|
47
|
+
- 是否已有 `registerAs` 或配置层通过统一 env 入口读取变量
|
|
48
|
+
5. 按场景为每个直读点选择迁移方式,而不是机械替换。
|
|
49
|
+
6. 修复后重新扫描,并补跑最小验证,确认配置读取与运行行为一致。
|
|
50
|
+
|
|
51
|
+
## 快速复核命令
|
|
52
|
+
|
|
53
|
+
项目具备 `rg` 时,优先用下列命令复核:
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
rg "process\\.env" apps/backend/src apps/backend/e2e \
|
|
57
|
+
--glob '!*env.accessor.ts' \
|
|
58
|
+
--glob '!*env.service.ts'
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## 修复准则
|
|
62
|
+
|
|
63
|
+
### 配置层与 `registerAs`
|
|
64
|
+
|
|
65
|
+
- 优先使用 `defaultEnvAccessor`
|
|
66
|
+
- 仅在必须显式传入环境对象时使用 `createEnvAccessor(process.env)`
|
|
67
|
+
- 不要继续裸读 `process.env.FOO`
|
|
68
|
+
|
|
69
|
+
```typescript
|
|
70
|
+
import { registerAs } from '@nestjs/config'
|
|
71
|
+
import { defaultEnvAccessor } from '@/common/env/env.accessor'
|
|
72
|
+
|
|
73
|
+
const env = defaultEnvAccessor
|
|
74
|
+
|
|
75
|
+
export const redisConfig = registerAs('redis', () => ({
|
|
76
|
+
host: env.str('REDIS_HOST', 'localhost'),
|
|
77
|
+
}))
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### 运行期服务、控制器、提供者
|
|
81
|
+
|
|
82
|
+
- 注入 `EnvService`
|
|
83
|
+
- 优先使用 `getString`、`getInt`、`getBoolean`、`isProd`、`isE2E` 等 typed getter
|
|
84
|
+
- 若模块尚未暴露 `EnvModule`,先补模块依赖,再迁移读取逻辑
|
|
85
|
+
|
|
86
|
+
```typescript
|
|
87
|
+
@Injectable()
|
|
88
|
+
export class ExampleService {
|
|
89
|
+
constructor(private readonly env: EnvService) {}
|
|
90
|
+
|
|
91
|
+
getRedisHost() {
|
|
92
|
+
return this.env.getString('REDIS_HOST', 'localhost')
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### 独立脚本与 CLI
|
|
98
|
+
|
|
99
|
+
- 在完成 dotenv 装载后显式创建 accessor
|
|
100
|
+
|
|
101
|
+
```typescript
|
|
102
|
+
const env = createEnvAccessor(process.env)
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### 必须读取原始值
|
|
106
|
+
|
|
107
|
+
- 使用 `EnvService.getAccessor().raw(key)`,或 accessor 的 `raw(key)`
|
|
108
|
+
- 在代码中简短说明为何 typed getter 不适用
|
|
109
|
+
|
|
110
|
+
## 缺失基础设施时的补齐顺序
|
|
111
|
+
|
|
112
|
+
若扫描结果显示项目尚未形成统一 env 访问方案,按以下顺序补齐:
|
|
113
|
+
|
|
114
|
+
1. 创建 `env.accessor.ts`
|
|
115
|
+
- 提供 `createEnvAccessor`
|
|
116
|
+
- 提供 `defaultEnvAccessor`
|
|
117
|
+
- 至少支持 `str`、`bool`、`int`、`num`、`raw`、`appEnv`、`snapshot`
|
|
118
|
+
2. 创建 `env.service.ts`
|
|
119
|
+
- 包装 `ConfigService`
|
|
120
|
+
- 提供 typed getter 与常用环境判断方法
|
|
121
|
+
3. 创建 `env.module.ts`
|
|
122
|
+
- 暴露 `EnvService`
|
|
123
|
+
4. 将配置层迁移到 `registerAs + defaultEnvAccessor`
|
|
124
|
+
5. 将运行期服务迁移到 `EnvService`
|
|
125
|
+
6. 重新扫描剩余 `process.env` 直读点
|
|
126
|
+
|
|
127
|
+
优先复用现有命名、目录与模块结构;若无统一基础设施,再参考 [references/bootstrap-env-foundation.md](./references/bootstrap-env-foundation.md)。
|
|
128
|
+
|
|
129
|
+
## 例外与判断原则
|
|
130
|
+
|
|
131
|
+
- `env.accessor.ts`、`env.service.ts` 本身允许访问 `process.env`
|
|
132
|
+
- 负责 dotenv 装载、环境注入、测试环境临时覆写的底层入口可保留少量受控访问
|
|
133
|
+
- 测试里对 `process.env` 的显式设值可视为受控例外,但优先复用公共 fixture 或 helper
|
|
134
|
+
- 若某文件同时负责“装载环境”和“消费环境”,优先拆分职责,避免例外扩大
|
|
135
|
+
|
|
136
|
+
## 输出要求
|
|
137
|
+
|
|
138
|
+
最终输出至少包含:
|
|
139
|
+
|
|
140
|
+
1. 基础设施状态
|
|
141
|
+
2. `process.env` 直读文件清单
|
|
142
|
+
3. 每个问题的推荐迁移方式
|
|
143
|
+
4. 是否需要补齐基础设施
|
|
144
|
+
5. 已修改内容、验证结果与剩余风险
|
|
145
|
+
|
|
146
|
+
## 资源
|
|
147
|
+
|
|
148
|
+
- 扫描脚本:`scripts/env_accessor_audit.py`
|
|
149
|
+
- 补齐模板:`references/bootstrap-env-foundation.md`
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
# 缺失统一 env 基础设施时的最小补齐模板
|
|
2
|
+
|
|
3
|
+
当扫描结果显示项目尚未具备 `createEnvAccessor`、`defaultEnvAccessor`、`EnvService` 等统一入口时,可按下面的最小骨架补齐。优先复用项目现有工具链、路径别名与公共类型;只有在仓库确实没有对应能力时,才照此新建。
|
|
4
|
+
|
|
5
|
+
## 1. `env.accessor.ts`
|
|
6
|
+
|
|
7
|
+
目标:
|
|
8
|
+
|
|
9
|
+
- 为配置层、脚本、工具函数提供无依赖的统一读取入口
|
|
10
|
+
- 允许显式传入 `process.env` 或自定义 env record
|
|
11
|
+
- 提供 typed getter 与 `raw()` 回退口
|
|
12
|
+
|
|
13
|
+
最小示例:
|
|
14
|
+
|
|
15
|
+
```typescript
|
|
16
|
+
export interface EnvAccessor {
|
|
17
|
+
str(key: string, defaultValue?: string): string | undefined
|
|
18
|
+
bool(key: string, defaultValue?: boolean): boolean
|
|
19
|
+
int(key: string, defaultValue?: number): number
|
|
20
|
+
raw(key: string): string | undefined
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
type EnvSource = Record<string, string | undefined> | NodeJS.ProcessEnv | undefined
|
|
24
|
+
|
|
25
|
+
function resolveEnv(source?: EnvSource): Record<string, string | undefined> {
|
|
26
|
+
return source ? { ...source } : process.env
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function createEnvAccessor(source?: EnvSource): EnvAccessor {
|
|
30
|
+
const env = resolveEnv(source)
|
|
31
|
+
return {
|
|
32
|
+
str(key, defaultValue) {
|
|
33
|
+
const value = env[key]
|
|
34
|
+
return value === undefined || value === '' ? defaultValue : value
|
|
35
|
+
},
|
|
36
|
+
bool(key, defaultValue = false) {
|
|
37
|
+
const value = String(env[key] ?? '').toLowerCase()
|
|
38
|
+
if (!value) return defaultValue
|
|
39
|
+
return value === 'true' || value === '1'
|
|
40
|
+
},
|
|
41
|
+
int(key, defaultValue = 0) {
|
|
42
|
+
const value = Number.parseInt(String(env[key] ?? ''), 10)
|
|
43
|
+
return Number.isFinite(value) ? value : defaultValue
|
|
44
|
+
},
|
|
45
|
+
raw(key) {
|
|
46
|
+
return env[key]
|
|
47
|
+
},
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export const defaultEnvAccessor = createEnvAccessor()
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## 2. `env.service.ts`
|
|
55
|
+
|
|
56
|
+
目标:
|
|
57
|
+
|
|
58
|
+
- 为 NestJS 运行期服务提供注入式访问方式
|
|
59
|
+
- 统一接入 `ConfigService`
|
|
60
|
+
- 预留缓存、阈值裁剪、调试开关等运行期逻辑
|
|
61
|
+
|
|
62
|
+
最小示例:
|
|
63
|
+
|
|
64
|
+
```typescript
|
|
65
|
+
@Injectable()
|
|
66
|
+
export class EnvService {
|
|
67
|
+
constructor(private readonly config: ConfigService) {}
|
|
68
|
+
|
|
69
|
+
getString(key: string, defaultValue?: string): string | undefined {
|
|
70
|
+
const value = this.config.get<string>(key)
|
|
71
|
+
return value === undefined || value === null || value === '' ? defaultValue : String(value)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
getBoolean(key: string, defaultValue = false): boolean {
|
|
75
|
+
const value = String(this.config.get<string>(key) ?? '').toLowerCase()
|
|
76
|
+
if (!value) return defaultValue
|
|
77
|
+
return value === 'true' || value === '1'
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
getInt(key: string, defaultValue = 0): number {
|
|
81
|
+
const value = Number.parseInt(String(this.config.get<string>(key) ?? ''), 10)
|
|
82
|
+
return Number.isFinite(value) ? value : defaultValue
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
isProd(): boolean {
|
|
86
|
+
return this.getString('APP_ENV', this.getString('NODE_ENV', 'development')) === 'production'
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
getAccessor() {
|
|
90
|
+
return createEnvAccessor(process.env)
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## 3. `env.module.ts`
|
|
96
|
+
|
|
97
|
+
目标:
|
|
98
|
+
|
|
99
|
+
- 统一导出 `EnvService`
|
|
100
|
+
- 供业务模块直接导入
|
|
101
|
+
|
|
102
|
+
最小示例:
|
|
103
|
+
|
|
104
|
+
```typescript
|
|
105
|
+
@Module({
|
|
106
|
+
providers: [EnvService],
|
|
107
|
+
exports: [EnvService],
|
|
108
|
+
})
|
|
109
|
+
export class EnvModule {}
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## 4. 配置层接入
|
|
113
|
+
|
|
114
|
+
配置文件中不要继续写 `process.env.REDIS_HOST`,改为:
|
|
115
|
+
|
|
116
|
+
```typescript
|
|
117
|
+
import { registerAs } from '@nestjs/config'
|
|
118
|
+
import { defaultEnvAccessor } from '@/common/env/env.accessor'
|
|
119
|
+
|
|
120
|
+
const env = defaultEnvAccessor
|
|
121
|
+
|
|
122
|
+
export const redisConfig = registerAs('redis', () => ({
|
|
123
|
+
host: env.str('REDIS_HOST', 'localhost'),
|
|
124
|
+
port: env.int('REDIS_PORT', 6379),
|
|
125
|
+
}))
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
## 5. 运行期服务接入
|
|
129
|
+
|
|
130
|
+
业务代码中不要继续裸读环境变量,改为:
|
|
131
|
+
|
|
132
|
+
```typescript
|
|
133
|
+
@Injectable()
|
|
134
|
+
export class ExampleService {
|
|
135
|
+
constructor(private readonly env: EnvService) {}
|
|
136
|
+
|
|
137
|
+
shouldEnableFeature(): boolean {
|
|
138
|
+
return this.env.getBoolean('FEATURE_FLAG_ENABLED', false)
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
## 6. 迁移顺序建议
|
|
144
|
+
|
|
145
|
+
1. 先落 `env.accessor.ts`
|
|
146
|
+
2. 再落 `env.service.ts` 与 `env.module.ts`
|
|
147
|
+
3. 先迁配置层,再迁服务层
|
|
148
|
+
4. 最后清理残留 `process.env` 直读
|
|
149
|
+
|
|
150
|
+
## 7. 允许保留的少量例外
|
|
151
|
+
|
|
152
|
+
- 负责加载 `.env` 文件的底层入口
|
|
153
|
+
- 用于测试注入的 setup / fixture
|
|
154
|
+
- 必须读取原始未加工字符串的极少数路径
|
|
155
|
+
|
|
156
|
+
即便属于例外,也优先把“装载环境”和“消费环境”拆开,避免把业务逻辑继续锁在 `process.env` 上。
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import argparse
|
|
5
|
+
import json
|
|
6
|
+
import subprocess
|
|
7
|
+
import sys
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
DEFAULT_SCAN_DIRS = ["apps/backend/src", "apps/backend/e2e"]
|
|
13
|
+
EXCLUDED_GLOBS = ["!*env.accessor.ts", "!*env.service.ts"]
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def run_rg(args: list[str], cwd: Path) -> str:
|
|
17
|
+
result = subprocess.run(
|
|
18
|
+
["rg", *args],
|
|
19
|
+
cwd=str(cwd),
|
|
20
|
+
text=True,
|
|
21
|
+
capture_output=True,
|
|
22
|
+
)
|
|
23
|
+
if result.returncode not in (0, 1):
|
|
24
|
+
raise RuntimeError(result.stderr.strip() or "rg failed")
|
|
25
|
+
return result.stdout
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def rg_has_matches(pattern: str, paths: list[str], cwd: Path) -> bool:
|
|
29
|
+
output = run_rg(["-l", pattern, *paths], cwd)
|
|
30
|
+
return bool(output.strip())
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def detect_foundation(workspace: Path) -> dict[str, Any]:
|
|
34
|
+
search_paths = ["apps/backend/src", "apps/backend/e2e"]
|
|
35
|
+
file_hits = {
|
|
36
|
+
"env_accessor_file": run_rg(["-l", r"export function createEnvAccessor", *search_paths], workspace)
|
|
37
|
+
.strip()
|
|
38
|
+
.splitlines(),
|
|
39
|
+
"default_accessor_usage": run_rg(["-l", r"defaultEnvAccessor", *search_paths], workspace)
|
|
40
|
+
.strip()
|
|
41
|
+
.splitlines(),
|
|
42
|
+
"env_service_file": run_rg(["-l", r"export class EnvService", *search_paths], workspace)
|
|
43
|
+
.strip()
|
|
44
|
+
.splitlines(),
|
|
45
|
+
"env_service_usage": run_rg(["-l", r"\bEnvService\b", *search_paths], workspace)
|
|
46
|
+
.strip()
|
|
47
|
+
.splitlines(),
|
|
48
|
+
"config_registeras_usage": run_rg(["-l", r"registerAs\(", "apps/backend/src"], workspace)
|
|
49
|
+
.strip()
|
|
50
|
+
.splitlines(),
|
|
51
|
+
}
|
|
52
|
+
file_hits = {key: [line for line in value if line] for key, value in file_hits.items()}
|
|
53
|
+
|
|
54
|
+
foundation = {
|
|
55
|
+
"has_create_env_accessor": bool(file_hits["env_accessor_file"]),
|
|
56
|
+
"has_default_env_accessor": bool(file_hits["default_accessor_usage"]),
|
|
57
|
+
"has_env_service": bool(file_hits["env_service_file"]),
|
|
58
|
+
"has_register_as_config": bool(file_hits["config_registeras_usage"]),
|
|
59
|
+
"recommended_mode": "reuse-existing-foundation"
|
|
60
|
+
if (
|
|
61
|
+
bool(file_hits["env_accessor_file"])
|
|
62
|
+
or bool(file_hits["default_accessor_usage"])
|
|
63
|
+
or bool(file_hits["env_service_file"])
|
|
64
|
+
)
|
|
65
|
+
else "bootstrap-foundation",
|
|
66
|
+
"evidence": file_hits,
|
|
67
|
+
}
|
|
68
|
+
return foundation
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def collect_process_env_findings(workspace: Path, scan_dirs: list[str]) -> list[dict[str, Any]]:
|
|
72
|
+
args = ["-n", "--column", "process\\.env", *scan_dirs]
|
|
73
|
+
for glob in EXCLUDED_GLOBS:
|
|
74
|
+
args.extend(["--glob", glob])
|
|
75
|
+
output = run_rg(args, workspace)
|
|
76
|
+
|
|
77
|
+
findings: list[dict[str, Any]] = []
|
|
78
|
+
for line in output.strip().splitlines():
|
|
79
|
+
if not line:
|
|
80
|
+
continue
|
|
81
|
+
parts = line.split(":", 3)
|
|
82
|
+
if len(parts) != 4:
|
|
83
|
+
continue
|
|
84
|
+
file_path, line_no, column_no, snippet = parts
|
|
85
|
+
if is_allowed_process_env_usage(file_path, snippet):
|
|
86
|
+
continue
|
|
87
|
+
findings.append(
|
|
88
|
+
{
|
|
89
|
+
"file": file_path,
|
|
90
|
+
"line": int(line_no),
|
|
91
|
+
"column": int(column_no),
|
|
92
|
+
"snippet": snippet.strip(),
|
|
93
|
+
"kind": classify_finding(file_path),
|
|
94
|
+
"recommended_fix": recommend_fix(file_path),
|
|
95
|
+
}
|
|
96
|
+
)
|
|
97
|
+
return findings
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def is_allowed_process_env_usage(file_path: str, snippet: str) -> bool:
|
|
101
|
+
normalized = snippet.replace(" ", "")
|
|
102
|
+
if file_path.endswith("test-env.helper.ts"):
|
|
103
|
+
return True
|
|
104
|
+
if file_path.endswith("load-environment.ts"):
|
|
105
|
+
return True
|
|
106
|
+
if file_path.endswith("export-openapi.ts"):
|
|
107
|
+
return True
|
|
108
|
+
if file_path.endswith("setup-e2e.ts"):
|
|
109
|
+
return True
|
|
110
|
+
if file_path.endswith("fixtures.ts"):
|
|
111
|
+
return True
|
|
112
|
+
if file_path.endswith("stream-session.guard.e2e-spec.ts") and "{process.env." in snippet:
|
|
113
|
+
return True
|
|
114
|
+
if "createEnvAccessor(process.env)" in normalized:
|
|
115
|
+
return True
|
|
116
|
+
if "defaultEnvAccessor=createEnvAccessor(process.env)" in normalized:
|
|
117
|
+
return True
|
|
118
|
+
return False
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def classify_finding(file_path: str) -> str:
|
|
122
|
+
if "/e2e/" in file_path or file_path.endswith(".spec.ts"):
|
|
123
|
+
return "test-or-e2e"
|
|
124
|
+
if "/config/" in file_path or file_path.endswith(".config.ts"):
|
|
125
|
+
return "config"
|
|
126
|
+
if file_path.endswith(".ts"):
|
|
127
|
+
return "runtime"
|
|
128
|
+
return "unknown"
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def recommend_fix(file_path: str) -> str:
|
|
132
|
+
kind = classify_finding(file_path)
|
|
133
|
+
if kind == "config":
|
|
134
|
+
return "使用 defaultEnvAccessor 或 createEnvAccessor(process.env)"
|
|
135
|
+
if kind == "runtime":
|
|
136
|
+
return "注入 EnvService 并改用 getString/getInt/getBoolean/isProd 等方法"
|
|
137
|
+
if kind == "test-or-e2e":
|
|
138
|
+
return "优先收敛到公共 fixture/helper;若属于测试注入,可保留为受控例外并说明原因"
|
|
139
|
+
return "根据上下文改为 EnvService 或 EnvAccessor"
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def summarize_findings(findings: list[dict[str, Any]]) -> dict[str, Any]:
|
|
143
|
+
by_kind: dict[str, int] = {}
|
|
144
|
+
by_file: dict[str, int] = {}
|
|
145
|
+
for item in findings:
|
|
146
|
+
by_kind[item["kind"]] = by_kind.get(item["kind"], 0) + 1
|
|
147
|
+
by_file[item["file"]] = by_file.get(item["file"], 0) + 1
|
|
148
|
+
return {
|
|
149
|
+
"total_findings": len(findings),
|
|
150
|
+
"by_kind": by_kind,
|
|
151
|
+
"files": sorted(
|
|
152
|
+
[{"file": file_path, "count": count} for file_path, count in by_file.items()],
|
|
153
|
+
key=lambda item: (-item["count"], item["file"]),
|
|
154
|
+
),
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def build_report(workspace: Path, scan_dirs: list[str]) -> dict[str, Any]:
|
|
159
|
+
foundation = detect_foundation(workspace)
|
|
160
|
+
findings = collect_process_env_findings(workspace, scan_dirs)
|
|
161
|
+
return {
|
|
162
|
+
"workspace": str(workspace),
|
|
163
|
+
"scan_dirs": scan_dirs,
|
|
164
|
+
"excluded_globs": EXCLUDED_GLOBS,
|
|
165
|
+
"foundation": foundation,
|
|
166
|
+
"summary": summarize_findings(findings),
|
|
167
|
+
"findings": findings,
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def print_text_report(report: dict[str, Any]) -> None:
|
|
172
|
+
foundation = report["foundation"]
|
|
173
|
+
summary = report["summary"]
|
|
174
|
+
|
|
175
|
+
print("== env-accessor-audit-fixer ==")
|
|
176
|
+
print(f"workspace: {report['workspace']}")
|
|
177
|
+
print(f"scan dirs: {', '.join(report['scan_dirs'])}")
|
|
178
|
+
print(f"recommended mode: {foundation['recommended_mode']}")
|
|
179
|
+
print("")
|
|
180
|
+
print("foundation:")
|
|
181
|
+
print(f"- has createEnvAccessor: {foundation['has_create_env_accessor']}")
|
|
182
|
+
print(f"- has defaultEnvAccessor: {foundation['has_default_env_accessor']}")
|
|
183
|
+
print(f"- has EnvService: {foundation['has_env_service']}")
|
|
184
|
+
print(f"- has registerAs config: {foundation['has_register_as_config']}")
|
|
185
|
+
print("")
|
|
186
|
+
print("summary:")
|
|
187
|
+
print(f"- total findings: {summary['total_findings']}")
|
|
188
|
+
for kind, count in sorted(summary["by_kind"].items()):
|
|
189
|
+
print(f"- {kind}: {count}")
|
|
190
|
+
|
|
191
|
+
if not report["findings"]:
|
|
192
|
+
return
|
|
193
|
+
|
|
194
|
+
print("")
|
|
195
|
+
print("findings:")
|
|
196
|
+
for item in report["findings"]:
|
|
197
|
+
print(
|
|
198
|
+
f"- {item['file']}:{item['line']}:{item['column']} "
|
|
199
|
+
f"[{item['kind']}] {item['recommended_fix']}"
|
|
200
|
+
)
|
|
201
|
+
print(f" {item['snippet']}")
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def parse_args() -> argparse.Namespace:
|
|
205
|
+
parser = argparse.ArgumentParser(
|
|
206
|
+
description="审计项目中的 process.env 直读,并判断是否缺少统一 env 访问基础设施。"
|
|
207
|
+
)
|
|
208
|
+
parser.add_argument(
|
|
209
|
+
"--workspace",
|
|
210
|
+
default=".",
|
|
211
|
+
help="仓库根目录,默认当前目录",
|
|
212
|
+
)
|
|
213
|
+
parser.add_argument(
|
|
214
|
+
"--scan-dir",
|
|
215
|
+
dest="scan_dirs",
|
|
216
|
+
action="append",
|
|
217
|
+
help="附加扫描目录;默认扫描 apps/backend/src 与 apps/backend/e2e",
|
|
218
|
+
)
|
|
219
|
+
parser.add_argument(
|
|
220
|
+
"--output-json",
|
|
221
|
+
help="将结构化结果输出到指定文件",
|
|
222
|
+
)
|
|
223
|
+
return parser.parse_args()
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def main() -> int:
|
|
227
|
+
args = parse_args()
|
|
228
|
+
workspace = Path(args.workspace).resolve()
|
|
229
|
+
scan_dirs = args.scan_dirs or DEFAULT_SCAN_DIRS
|
|
230
|
+
|
|
231
|
+
try:
|
|
232
|
+
report = build_report(workspace, scan_dirs)
|
|
233
|
+
except FileNotFoundError:
|
|
234
|
+
print("未找到 rg,请先安装 ripgrep", file=sys.stderr)
|
|
235
|
+
return 2
|
|
236
|
+
except RuntimeError as exc:
|
|
237
|
+
print(str(exc), file=sys.stderr)
|
|
238
|
+
return 2
|
|
239
|
+
|
|
240
|
+
print_text_report(report)
|
|
241
|
+
|
|
242
|
+
if args.output_json:
|
|
243
|
+
output_path = Path(args.output_json)
|
|
244
|
+
output_path.write_text(json.dumps(report, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
|
245
|
+
|
|
246
|
+
return 0
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
if __name__ == "__main__":
|
|
250
|
+
raise SystemExit(main())
|