covgate 0.0.0-stage → 0.2.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 (44) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +502 -2
  3. package/bin/covgate-postinstall.js +13 -0
  4. package/bin/covgate.js +5 -0
  5. package/docs/ai-workflow.md +196 -0
  6. package/docs/design.md +327 -0
  7. package/docs/setup.md +163 -0
  8. package/package.json +48 -4
  9. package/schema/covgate.schema.json +295 -0
  10. package/src/annotations.js +224 -0
  11. package/src/args.js +189 -0
  12. package/src/cli.js +486 -0
  13. package/src/config.js +442 -0
  14. package/src/coverage/index.js +85 -0
  15. package/src/coverage/istanbul.js +215 -0
  16. package/src/coverage/lcov.js +234 -0
  17. package/src/coverage/model.js +316 -0
  18. package/src/coverage/paths.js +40 -0
  19. package/src/errors.js +34 -0
  20. package/src/gate.js +203 -0
  21. package/src/glob.js +242 -0
  22. package/src/index.js +28 -0
  23. package/src/process.js +170 -0
  24. package/src/reason.js +77 -0
  25. package/src/report/build.js +135 -0
  26. package/src/report/index.js +29 -0
  27. package/src/report/markdown.js +343 -0
  28. package/src/report/text.js +267 -0
  29. package/src/reports.js +113 -0
  30. package/src/runner.js +95 -0
  31. package/src/scope.js +440 -0
  32. package/src/setup/agent.js +327 -0
  33. package/src/setup/config.js +112 -0
  34. package/src/setup/index.js +242 -0
  35. package/src/setup/project.js +316 -0
  36. package/src/setup/prompt.js +43 -0
  37. package/src/util.js +163 -0
  38. package/src/version.js +8 -0
  39. package/templates/AGENTS.md.snippet.md +9 -0
  40. package/templates/agent-stop-hook.sh +7 -0
  41. package/templates/covgate.config.json +98 -0
  42. package/templates/github-actions-coverage.yml +41 -0
  43. package/templates/gitignore +5 -0
  44. package/templates/pre-commit +10 -0
package/src/args.js ADDED
@@ -0,0 +1,189 @@
1
+ import { ConfigError } from "./errors.js";
2
+
3
+ const COMMANDS = new Set(["run", "check", "validate", "setup"]);
4
+ const NL = String.fromCharCode(10);
5
+
6
+ export const HELP_TEXT = [
7
+ "CovGate — AI 代码覆盖率门禁",
8
+ "",
9
+ "用法:",
10
+ " covgate setup [选项] 检测/安装测试框架并生成门禁与 AI 接入配置",
11
+ " covgate run [选项] 执行测试并检查覆盖率(默认命令)",
12
+ " covgate check [选项] 不执行测试,只检查已有覆盖率报告",
13
+ " covgate validate [选项] 只校验配置",
14
+ "",
15
+ "通用选项:",
16
+ " --config <path> 配置文件路径(默认 ./covgate.config.json)",
17
+ " --scope <name> 只检查指定 scope,可重复或用逗号分隔",
18
+ " --no-run 等同 check:不执行测试命令",
19
+ " --json 向 stdout 输出纯 JSON(日志走 stderr)",
20
+ " --no-files 不写 .covgate/report.json 和 report.md",
21
+ " --report-dir <path> 覆盖报告输出目录(相对当前目录)",
22
+ " --verbose 实时转发测试命令输出到 stderr",
23
+ " --color / --no-color 强制开启/关闭彩色输出",
24
+ " --version 显示版本",
25
+ " --help 显示帮助",
26
+ "",
27
+ "setup 选项:",
28
+ " --yes, -y 无框架时直接安装 Vitest,并接受默认配置",
29
+ " --no-install 不安装测试框架",
30
+ " --no-agent 不生成 AGENTS.md / Agent 脚本 / CI",
31
+ " --no-config 不生成 covgate.config.json",
32
+ " --profile <client|server> 强制 scope 类型,默认自动识别",
33
+ " --source <glob> 源码范围,默认自动识别",
34
+ " --coverage <path> 覆盖率报告路径,默认 coverage/coverage-final.json",
35
+ " --test-command <command> 覆盖测试命令",
36
+ " --force 覆盖已有配置和 setup 生成的文件",
37
+ " --dry-run 只打印计划,不写文件、不安装依赖",
38
+ "",
39
+ "硬性门槛(配置不可低于):",
40
+ " profile=client 行/语句/函数/分支覆盖率必须 100%",
41
+ " profile=server 行/语句/函数/分支覆盖率必须至少 95%",
42
+ "",
43
+ "覆盖不到的代码必须写 covgate-ignore-* 说明,例如:",
44
+ " // covgate-ignore-next-line: 平台灰度开关在 v1 永远不会开启 | owner=web | expires=2027-12-31",
45
+ " // covgate-ignore-file: OpenAPI 生成文件,不手工维护 | owner=api",
46
+ "",
47
+ "退出码:0 通过;1 覆盖率/审计不通过;2 配置或用法错误;3 测试失败/报告读取失败。",
48
+ "",
49
+ "参见 README.md 与 docs/design.md。"
50
+ ].join(NL);
51
+
52
+ /**
53
+ * @param {string[]} argv
54
+ */
55
+ export function parseArgs(argv) {
56
+ const options = {
57
+ command: "run",
58
+ configPath: undefined,
59
+ scopes: [],
60
+ json: false,
61
+ noRun: false,
62
+ noFiles: false,
63
+ reportDir: undefined,
64
+ verbose: false,
65
+ color: undefined,
66
+ help: false,
67
+ version: false,
68
+ yes: false,
69
+ installFramework: true,
70
+ noAgent: false,
71
+ noConfig: false,
72
+ force: false,
73
+ dryRun: false,
74
+ profile: undefined,
75
+ source: undefined,
76
+ coverage: undefined,
77
+ testCommand: undefined
78
+ };
79
+
80
+ let index = 0;
81
+ let explicitCommand = false;
82
+ if (argv[0] === "help") {
83
+ options.help = true;
84
+ return options;
85
+ }
86
+ if (argv[0] && COMMANDS.has(argv[0])) {
87
+ options.command = argv[0];
88
+ explicitCommand = true;
89
+ index = 1;
90
+ } else if (argv[0] && !argv[0].startsWith("-")) {
91
+ throw new ConfigError('未知命令 "' + argv[0] + '",可用命令:setup、run、check、validate');
92
+ }
93
+
94
+ for (; index < argv.length; index += 1) {
95
+ const argument = argv[index];
96
+ let key = argument;
97
+ let inlineValue;
98
+ if (argument.startsWith("--") && argument.includes("=")) {
99
+ const separator = argument.indexOf("=");
100
+ key = argument.slice(0, separator);
101
+ inlineValue = argument.slice(separator + 1);
102
+ }
103
+
104
+ const value = () => {
105
+ if (inlineValue !== undefined) return inlineValue;
106
+ index += 1;
107
+ if (index >= argv.length || argv[index].startsWith("--")) {
108
+ throw new ConfigError("选项 " + key + " 缺少值");
109
+ }
110
+ return argv[index];
111
+ };
112
+
113
+ if (argument === "help") {
114
+ options.help = true;
115
+ continue;
116
+ }
117
+ if (COMMANDS.has(argument) && !explicitCommand && !argument.startsWith("-")) {
118
+ options.command = argument;
119
+ explicitCommand = true;
120
+ continue;
121
+ }
122
+ if (COMMANDS.has(argument) && explicitCommand && index > 0) {
123
+ throw new ConfigError("命令只能指定一次");
124
+ }
125
+
126
+ if (key === "--config") {
127
+ options.configPath = value();
128
+ } else if (key === "--scope") {
129
+ const scopeValue = value();
130
+ for (const name of scopeValue.split(",")) {
131
+ const trimmed = name.trim();
132
+ if (trimmed && !options.scopes.includes(trimmed)) options.scopes.push(trimmed);
133
+ }
134
+ } else if (key === "--json") {
135
+ options.json = true;
136
+ } else if (key === "--no-run") {
137
+ options.noRun = true;
138
+ } else if (key === "--no-files") {
139
+ options.noFiles = true;
140
+ } else if (key === "--report-dir") {
141
+ options.reportDir = value();
142
+ } else if (key === "--verbose") {
143
+ options.verbose = true;
144
+ } else if (key === "--color") {
145
+ options.color = true;
146
+ } else if (key === "--no-color") {
147
+ options.color = false;
148
+ } else if (key === "--yes" || key === "-y") {
149
+ options.yes = true;
150
+ } else if (key === "--no-install") {
151
+ options.installFramework = false;
152
+ } else if (key === "--no-agent") {
153
+ options.noAgent = true;
154
+ } else if (key === "--no-config") {
155
+ options.noConfig = true;
156
+ } else if (key === "--force") {
157
+ options.force = true;
158
+ } else if (key === "--dry-run") {
159
+ options.dryRun = true;
160
+ } else if (key === "--profile") {
161
+ options.profile = value();
162
+ } else if (key === "--source") {
163
+ options.source = value();
164
+ } else if (key === "--coverage") {
165
+ options.coverage = value();
166
+ } else if (key === "--test-command") {
167
+ options.testCommand = value();
168
+ } else if (key === "--help" || key === "-h") {
169
+ options.help = true;
170
+ } else if (key === "--version") {
171
+ options.version = true;
172
+ } else {
173
+ throw new ConfigError('未知选项 "' + argument + '";运行 covgate --help 查看用法');
174
+ }
175
+ }
176
+
177
+ if (options.command === "check") options.noRun = true;
178
+ if (options.command !== "setup" && (options.yes || options.profile || options.source || options.coverage || options.testCommand || options.noAgent || options.noConfig || options.force || options.dryRun || !options.installFramework)) {
179
+ throw new ConfigError("setup 相关选项只能和 covgate setup 一起使用");
180
+ }
181
+ return options;
182
+ }
183
+
184
+ /**
185
+ * @param {string[]} argv
186
+ */
187
+ export function wantsJson(argv) {
188
+ return argv.includes("--json");
189
+ }
package/src/cli.js ADDED
@@ -0,0 +1,486 @@
1
+ import path from "node:path";
2
+ import { parseArgs, HELP_TEXT, wantsJson } from "./args.js";
3
+ import {
4
+ loadConfig,
5
+ resolveConfigPath,
6
+ selectScopes,
7
+ METRICS
8
+ } from "./config.js";
9
+ import { CovgateError, ExecutionError } from "./errors.js";
10
+ import { loadCoverage } from "./coverage/index.js";
11
+ import { gateScope } from "./gate.js";
12
+ import {
13
+ expandCoverageReportFiles,
14
+ findStaleReports,
15
+ snapshotReportFiles
16
+ } from "./reports.js";
17
+ import {
18
+ buildReport,
19
+ plannedArtifactPaths,
20
+ renderTextReport,
21
+ writeReportArtifacts
22
+ } from "./report/index.js";
23
+ import { printCommandFailure, runScopeCommands } from "./runner.js";
24
+ import { evaluateScope } from "./scope.js";
25
+ import { runSetup } from "./setup/index.js";
26
+ import { validateReason } from "./reason.js";
27
+ import {
28
+ formatThreshold,
29
+ relativeDisplay,
30
+ tail
31
+ } from "./util.js";
32
+ import { VERSION } from "./version.js";
33
+
34
+ const METRIC_LABELS = {
35
+ lines: "行",
36
+ statements: "语句",
37
+ functions: "函数",
38
+ branches: "分支"
39
+ };
40
+
41
+ const NL = String.fromCharCode(10);
42
+
43
+ function logger() {
44
+ return {
45
+ info(message) {
46
+ process.stderr.write(message + "\n");
47
+ },
48
+ warn(message) {
49
+ process.stderr.write("警告: " + message + "\n");
50
+ },
51
+ error(message) {
52
+ process.stderr.write("错误: " + message + "\n");
53
+ }
54
+ };
55
+ }
56
+
57
+ /**
58
+ * @param {unknown} error
59
+ * @param {boolean} json
60
+ * @param {boolean} includeStack
61
+ */
62
+ function printEarlyError(error, json, includeStack = false) {
63
+ const message = error instanceof Error ? error.message : String(error);
64
+ const exitCode = error instanceof CovgateError ? error.exitCode : 2;
65
+ if (includeStack && error instanceof Error && error.stack) {
66
+ process.stderr.write(error.stack + "\n");
67
+ }
68
+ if (json) {
69
+ const body = {
70
+ schemaVersion: 1,
71
+ tool: { name: "covgate", version: VERSION },
72
+ status: "error",
73
+ exitCode,
74
+ generatedAt: new Date().toISOString(),
75
+ issues: [
76
+ {
77
+ code: error instanceof CovgateError ? error.kind : "error",
78
+ severity: "error",
79
+ message
80
+ }
81
+ ]
82
+ };
83
+ process.stdout.write(JSON.stringify(body, null, 2) + "\n");
84
+ } else {
85
+ process.stderr.write("CovGate 错误: " + message + "\n");
86
+ }
87
+ return exitCode;
88
+ }
89
+
90
+ /**
91
+ * @param {any} options
92
+ */
93
+ async function runSetupCommand(options) {
94
+ const log = options.json
95
+ ? (message) => process.stderr.write(message + NL)
96
+ : (message) => process.stdout.write(message + NL);
97
+ try {
98
+ const summary = await runSetup({
99
+ root: process.cwd(),
100
+ yes: options.yes,
101
+ installFramework: options.installFramework,
102
+ profile: options.profile,
103
+ source: options.source,
104
+ coverage: options.coverage,
105
+ testCommand: options.testCommand,
106
+ noAgent: options.noAgent,
107
+ noConfig: options.noConfig,
108
+ force: options.force,
109
+ dryRun: options.dryRun,
110
+ log
111
+ });
112
+ if (options.json) {
113
+ process.stdout.write(JSON.stringify({
114
+ schemaVersion: 1,
115
+ tool: { name: "covgate", version: VERSION },
116
+ status: "ok",
117
+ ...summary
118
+ }, null, 2) + NL);
119
+ }
120
+ return 0;
121
+ } catch (error) {
122
+ return printEarlyError(error, options.json, options.verbose);
123
+ }
124
+ }
125
+
126
+ /**
127
+ * @param {any} config
128
+ * @param {any[]} scopes
129
+ * @param {any} options
130
+ */
131
+ function printValidate(config, scopes, options) {
132
+ const problems = [];
133
+ for (const scope of scopes) {
134
+ for (const entry of scope.exclude) {
135
+ const validation = validateReason(
136
+ entry.reason,
137
+ config.waivers,
138
+ { owner: entry.owner, expires: entry.expires },
139
+ new Date()
140
+ );
141
+ for (const message of validation.errors) {
142
+ problems.push({
143
+ code: "invalid-exclusion",
144
+ severity: "error",
145
+ scope: scope.name,
146
+ message: "exclude \"" + entry.pattern + "\" 的说明无效:" + message
147
+ });
148
+ }
149
+ if (validation.expired) {
150
+ problems.push({
151
+ code: "expired-exclusion",
152
+ severity: config.waivers.expiry === "fail" ? "error" : "warning",
153
+ scope: scope.name,
154
+ message: "exclude \"" + entry.pattern + "\" 已于 " + entry.expires + " 过期"
155
+ });
156
+ }
157
+ }
158
+ }
159
+ if (problems.some((problem) => problem.severity === "error")) {
160
+ if (options.json) {
161
+ process.stdout.write(
162
+ JSON.stringify(
163
+ {
164
+ schemaVersion: 1,
165
+ tool: { name: "covgate", version: VERSION },
166
+ status: "error",
167
+ exitCode: 2,
168
+ config: relativeDisplay(config.configPath, config.root),
169
+ issues: problems
170
+ },
171
+ null,
172
+ 2
173
+ ) + "\n"
174
+ );
175
+ } else {
176
+ for (const problem of problems) {
177
+ process.stderr.write("配置错误: " + problem.message + "\n");
178
+ }
179
+ }
180
+ return 2;
181
+ }
182
+ for (const problem of problems) {
183
+ process.stderr.write("警告: " + problem.message + "\n");
184
+ }
185
+
186
+ const rows = scopes.map((scope) => ({
187
+ name: scope.name,
188
+ label: scope.label,
189
+ profile: scope.profile,
190
+ thresholds: Object.fromEntries(
191
+ METRICS.map((metric) => [metric, scope.thresholds[metric].value])
192
+ ),
193
+ commands: scope.commands.length,
194
+ coverageFormat: scope.coverage.format,
195
+ coveragePaths: scope.coverage.paths,
196
+ sourceInclude: scope.sources.include,
197
+ excludeCount: scope.exclude.length
198
+ }));
199
+ if (options.json) {
200
+ process.stdout.write(
201
+ JSON.stringify(
202
+ {
203
+ schemaVersion: 1,
204
+ tool: { name: "covgate", version: VERSION },
205
+ status: "ok",
206
+ config: relativeDisplay(config.configPath, config.root),
207
+ scopes: rows
208
+ },
209
+ null,
210
+ 2
211
+ ) + "\n"
212
+ );
213
+ return 0;
214
+ }
215
+ process.stdout.write("✓ 配置有效: " + config.configPath + "\n");
216
+ for (const row of rows) {
217
+ process.stdout.write("\n[" + row.name + "] " + row.label + " (" + row.profile + ")\n");
218
+ process.stdout.write(
219
+ " 门槛: " +
220
+ METRICS.map(
221
+ (metric) =>
222
+ METRIC_LABELS[metric] + " ≥ " + formatThreshold(row.thresholds[metric]) + "%"
223
+ ).join(" · ") +
224
+ "\n"
225
+ );
226
+ process.stdout.write(
227
+ " 测试命令: " +
228
+ row.commands +
229
+ " 条 · 覆盖率格式: " +
230
+ row.coverageFormat +
231
+ " · 报告: " +
232
+ row.coveragePaths.join(", ") +
233
+ "\n"
234
+ );
235
+ process.stdout.write(
236
+ " 源码: " +
237
+ row.sourceInclude.join(", ") +
238
+ " · 配置排除: " +
239
+ row.excludeCount +
240
+ " 条\n"
241
+ );
242
+ }
243
+ process.stdout.write("\n报告目录: " + config.reports.dir + "\n");
244
+ return 0;
245
+ }
246
+
247
+ /**
248
+ * @param {any} report
249
+ * @param {any} options
250
+ * @param {{ json?: string, markdown?: string }} paths
251
+ */
252
+ async function emitReport(report, options, paths) {
253
+ const writeErrors = await writeReportArtifacts(report, paths);
254
+ for (const message of writeErrors) {
255
+ process.stderr.write("警告: " + message + "\n");
256
+ }
257
+ if (options.json) {
258
+ process.stdout.write(JSON.stringify(report, null, 2) + "\n");
259
+ } else {
260
+ const color =
261
+ options.color === false
262
+ ? false
263
+ : options.color === true
264
+ ? true
265
+ : Boolean(process.stdout.isTTY);
266
+ process.stdout.write(renderTextReport(report, { color }));
267
+ }
268
+ return report.exitCode;
269
+ }
270
+
271
+ /**
272
+ * @param {unknown} error
273
+ */
274
+ function commandRecordFromError(error) {
275
+ if (!(error instanceof Error)) return null;
276
+ const details = /** @type {any} */ (error).details;
277
+ if (details && typeof details.stdout === "string" && typeof details.stderr === "string") {
278
+ return details;
279
+ }
280
+ return null;
281
+ }
282
+
283
+ /**
284
+ * @param {{
285
+ * config: any,
286
+ * scopes: any[],
287
+ * options: any,
288
+ * startedAt: string,
289
+ * startedMs: number,
290
+ * log: any
291
+ * }} context
292
+ */
293
+ async function executeGate(context) {
294
+ const { config, scopes, options, startedAt, startedMs, log } = context;
295
+ const mode = options.noRun ? "check" : "run";
296
+ const commands = [];
297
+ const scopeResults = [];
298
+ const topLevelIssues = [];
299
+ const paths = plannedArtifactPaths(config, options.noFiles);
300
+
301
+ const finish = async () => {
302
+ const report = buildReport({
303
+ config,
304
+ mode,
305
+ scopes: scopeResults,
306
+ commands,
307
+ topLevelIssues,
308
+ durationMs: Date.now() - startedMs,
309
+ startedAt,
310
+ artifactPaths: paths
311
+ });
312
+ return emitReport(report, options, paths);
313
+ };
314
+
315
+ try {
316
+ log.info(
317
+ "CovGate " +
318
+ VERSION +
319
+ " · " +
320
+ (mode === "run" ? "执行测试" : "只检查已有报告") +
321
+ " · " +
322
+ relativeDisplay(config.configPath, config.root)
323
+ );
324
+ for (const scope of scopes) {
325
+ log.info("\n[" + scope.name + "] " + scope.label + " (" + scope.profile + ")");
326
+ /** @type {Map<string, { path: string, mtimeMs: number, size: number }>} */
327
+ let before = new Map();
328
+ if (mode === "run") {
329
+ const existingReports = await expandCoverageReportFiles(scope, config.root);
330
+ before = await snapshotReportFiles(existingReports);
331
+ const ran = await runScopeCommands(scope, {
332
+ verbose: options.verbose,
333
+ root: config.root,
334
+ logger: log
335
+ });
336
+ commands.push(...ran);
337
+ }
338
+
339
+ const reportFiles = await expandCoverageReportFiles(scope, config.root);
340
+ if (reportFiles.length === 0) {
341
+ throw new ExecutionError(
342
+ "scope " +
343
+ scope.name +
344
+ " 没有找到覆盖率报告。检查 coverage.paths:" +
345
+ scope.coverage.paths.join(", ")
346
+ );
347
+ }
348
+
349
+ if (mode === "run") {
350
+ const stale = await findStaleReports(before, reportFiles);
351
+ if (stale.length > 0) {
352
+ throw new ExecutionError(
353
+ "以下覆盖率报告在本次测试后没有更新,拒绝使用旧数据:\n " +
354
+ stale.map((file) => relativeDisplay(file, config.root)).join("\n "),
355
+ {
356
+ details: {
357
+ staleReports: stale.map((file) => relativeDisplay(file, config.root))
358
+ }
359
+ }
360
+ );
361
+ }
362
+ }
363
+
364
+ log.info(
365
+ " 覆盖率报告: " +
366
+ reportFiles
367
+ .map((file) => relativeDisplay(file, config.root))
368
+ .join(", ")
369
+ );
370
+ const coverage = await loadCoverage({
371
+ reportFiles,
372
+ format: scope.coverage.format,
373
+ root: config.root
374
+ });
375
+ const evaluated = await evaluateScope({
376
+ scope,
377
+ root: config.root,
378
+ coverage,
379
+ waiverPolicy: config.waivers
380
+ });
381
+ const gated = gateScope(evaluated);
382
+ scopeResults.push(gated);
383
+ log.info(
384
+ " " +
385
+ (gated.passed ? "✓" : "✗") +
386
+ " " +
387
+ METRICS.map((metric) => {
388
+ const value = gated.metrics[metric];
389
+ return (
390
+ METRIC_LABELS[metric] +
391
+ " " +
392
+ (value.applicable
393
+ ? (value.percent === null ? "N/A" : value.percent.toFixed(2) + "%")
394
+ : "N/A")
395
+ );
396
+ }).join(" · ")
397
+ );
398
+ }
399
+ return await finish();
400
+ } catch (error) {
401
+ const record = commandRecordFromError(error);
402
+ if (record) {
403
+ printCommandFailure(record);
404
+ if (Array.isArray(record.priorCommands)) commands.push(...record.priorCommands);
405
+ commands.push(record);
406
+ }
407
+ const issue = {
408
+ code: error instanceof CovgateError ? error.kind : "run-error",
409
+ severity: "error",
410
+ message: error instanceof Error ? error.message : String(error)
411
+ };
412
+ if (record) {
413
+ issue.outputTail = tail(
414
+ ((record.stdout ?? "") + "\n" + (record.stderr ?? "")).trim(),
415
+ 12_000
416
+ );
417
+ }
418
+ const details = /** @type {any} */ (error)?.details;
419
+ if (Array.isArray(details?.staleReports)) issue.files = details.staleReports;
420
+ topLevelIssues.push(issue);
421
+ if (options.verbose && error instanceof Error && error.stack) {
422
+ log.error(error.stack);
423
+ } else if (error instanceof Error) {
424
+ log.error(error.message);
425
+ }
426
+ return await finish();
427
+ }
428
+ }
429
+
430
+ /**
431
+ * @param {string[]} argv
432
+ */
433
+ export async function main(argv) {
434
+ const startedAt = new Date().toISOString();
435
+ const startedMs = Date.now();
436
+ const jsonRequested = wantsJson(argv);
437
+ let options;
438
+ try {
439
+ options = parseArgs(argv);
440
+ } catch (error) {
441
+ return printEarlyError(error, jsonRequested);
442
+ }
443
+ if (options.help) {
444
+ process.stdout.write(HELP_TEXT + "\n");
445
+ return 0;
446
+ }
447
+ if (options.version) {
448
+ process.stdout.write(VERSION + "\n");
449
+ return 0;
450
+ }
451
+
452
+ if (options.command === "setup") {
453
+ return runSetupCommand(options);
454
+ }
455
+
456
+ let config;
457
+ let scopes;
458
+ try {
459
+ const configPath = resolveConfigPath(process.cwd(), options.configPath);
460
+ config = await loadConfig(configPath);
461
+ if (options.reportDir) {
462
+ config.reports.dir = path.resolve(process.cwd(), options.reportDir);
463
+ }
464
+ scopes = selectScopes(config, options.scopes);
465
+ } catch (error) {
466
+ return printEarlyError(error, options.json, options.verbose);
467
+ }
468
+
469
+ if (options.command === "validate") {
470
+ return printValidate(config, scopes, options);
471
+ }
472
+
473
+ const log = logger();
474
+ try {
475
+ return await executeGate({
476
+ config,
477
+ scopes,
478
+ options,
479
+ startedAt,
480
+ startedMs,
481
+ log
482
+ });
483
+ } catch (error) {
484
+ return printEarlyError(error, options.json, options.verbose);
485
+ }
486
+ }