@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.
Files changed (25) hide show
  1. package/README.md +58 -0
  2. package/codex/skills/e2e-audit-fixer/SKILL.md +76 -0
  3. package/codex/skills/e2e-audit-fixer/agents/openai.yaml +4 -0
  4. package/codex/skills/e2e-audit-fixer/scripts/e2e_e2e_audit.py +523 -0
  5. package/codex/skills/env-accessor-audit-fixer/SKILL.md +149 -0
  6. package/codex/skills/env-accessor-audit-fixer/agents/openai.yaml +7 -0
  7. package/codex/skills/env-accessor-audit-fixer/references/bootstrap-env-foundation.md +156 -0
  8. package/codex/skills/env-accessor-audit-fixer/scripts/env_accessor_audit.py +250 -0
  9. package/codex/skills/error-handling-audit-fixer/SKILL.md +150 -0
  10. package/codex/skills/error-handling-audit-fixer/agents/openai.yaml +7 -0
  11. package/codex/skills/error-handling-audit-fixer/references/error-handling-standard.md +152 -0
  12. package/codex/skills/error-handling-audit-fixer/references/foundation-bootstrap.md +85 -0
  13. package/codex/skills/error-handling-audit-fixer/scripts/error_handling_audit.py +537 -0
  14. package/codex/skills/pagination-dto-audit-fixer/SKILL.md +69 -0
  15. package/codex/skills/pagination-dto-audit-fixer/agents/openai.yaml +7 -0
  16. package/codex/skills/pagination-dto-audit-fixer/references/pagination-standard.md +67 -0
  17. package/codex/skills/pagination-dto-audit-fixer/scripts/pagination_dto_audit.py +244 -0
  18. package/lib/backend-artifact-deploy/config.js +11 -0
  19. package/lib/backend-artifact-deploy/remote-result.js +2 -0
  20. package/lib/backend-artifact-deploy/remote-script.js +57 -4
  21. package/lib/backend-artifact-deploy.js +30 -1
  22. package/lib/codex-initial.js +155 -3
  23. package/lib/exec.js +21 -2
  24. package/lib/run-with-version-env.js +2 -1
  25. 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,7 @@
1
+ interface:
2
+ display_name: "环境变量审计修复"
3
+ short_description: "审计并修复 process.env 直读,必要时补齐统一 env 访问基础设施"
4
+ default_prompt: "使用 $env-accessor-audit-fixer 扫描项目中的 process.env 直读,判断是否已具备统一 env 访问基础设施,并给出或实施修复方案。"
5
+
6
+ policy:
7
+ allow_implicit_invocation: true
@@ -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())