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
@@ -0,0 +1,327 @@
1
+ import { chmod, mkdir, readFile, writeFile } from "node:fs/promises";
2
+ import { existsSync } from "node:fs";
3
+ import path from "node:path";
4
+
5
+ const START = "<!-- covgate:start -->";
6
+ const END = "<!-- covgate:end -->";
7
+
8
+ const AGENT_BLOCK = [
9
+ START,
10
+ "## CovGate 覆盖率门禁(完成定义)",
11
+ "",
12
+ "- 修改代码后必须运行 `npx --no-install covgate run --json`;退出码 0 才能结束任务。",
13
+ "- 客户端覆盖率必须 100%,服务端不低于 95%;不得降低 `covgate.config.json` 中的门槛。",
14
+ "- 禁止 `istanbul ignore`、`c8 ignore`、`v8 ignore`、`node:coverage ignore`。",
15
+ "- 确实不可达的代码使用 `covgate-ignore-next-line`、`covgate-ignore-start/end` 或 `covgate-ignore-file`,写具体原因、owner 和 expires。",
16
+ "- 禁止通过扩大 exclude、缩小 sources.include 或删除测试来让门禁通过;这类改动必须由人类评审。",
17
+ "- 失败详情在 `.covgate/report.json` 的 scopes/issues/waivers 中;修复后重跑。",
18
+ "",
19
+ "也可运行 `npm run coverage:gate -- --json`。Agent Stop hook 可调用 `scripts/covgate-agent-gate.ps1`(Windows)或 `scripts/covgate-agent-gate.sh`。",
20
+ "<!-- 由 covgate setup 生成;保留标记以便幂等更新。 -->",
21
+ END
22
+ ].join("\n");
23
+
24
+ const CURSOR_RULE = [
25
+ "---",
26
+ "description: CovGate coverage gate",
27
+ "alwaysApply: true",
28
+ "---",
29
+ "",
30
+ "# CovGate 覆盖率门禁",
31
+ "",
32
+ "任何代码修改后运行 `npx --no-install covgate run --json`,退出码 0 才能结束。",
33
+ "客户端必须 100%,服务端至少 95%。禁止原生 ignore;豁免必须写原因、owner、expires。",
34
+ ""
35
+ ].join("\n");
36
+
37
+ const SH_SCRIPT = [
38
+ "#!/bin/sh",
39
+ "# Generated by covgate setup. Use as an agent Stop hook or pre-commit step.",
40
+ "set -eu",
41
+ 'root="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"',
42
+ 'cd "$root"',
43
+ "exec npx --no-install covgate run --json"
44
+ ].join("\n") + "\n";
45
+
46
+ const PS1_SCRIPT = [
47
+ "# Generated by covgate setup. Use as an agent Stop hook or pre-commit step.",
48
+ "$ErrorActionPreference = 'Stop'",
49
+ "$root = git rev-parse --show-toplevel 2>$null",
50
+ "if (-not $root) { $root = (Get-Location).Path }",
51
+ "Set-Location $root",
52
+ "npx --no-install covgate run --json",
53
+ "exit $LASTEXITCODE"
54
+ ].join("\n") + "\n";
55
+
56
+ function hasManagedBlock(text) {
57
+ return text.includes(START) && text.includes(END);
58
+ }
59
+
60
+ /**
61
+ * @param {string} text
62
+ * @param {string} block
63
+ */
64
+ function replaceManagedBlock(text, block) {
65
+ const start = text.indexOf(START);
66
+ const end = text.indexOf(END);
67
+ if (start === -1 || end === -1 || end < start) return text + "\n\n" + block + "\n";
68
+ return text.slice(0, start) + block + text.slice(end + END.length);
69
+ }
70
+
71
+ /**
72
+ * @param {{
73
+ * root: string,
74
+ * file: string,
75
+ * block: string,
76
+ * force?: boolean,
77
+ * dryRun?: boolean,
78
+ * log: (message: string) => void,
79
+ * changes: string[]
80
+ * }} options
81
+ */
82
+ async function ensureManagedFile({ root, file, block, force, dryRun, log, changes }) {
83
+ const absolute = path.join(root, file);
84
+ let current = null;
85
+ try {
86
+ current = await readFile(absolute, "utf8");
87
+ } catch (error) {
88
+ if (!error || error.code !== "ENOENT") throw error;
89
+ }
90
+ if (current !== null && hasManagedBlock(current) && !force) {
91
+ log(" = " + file + " 已有 CovGate 区块,跳过");
92
+ return;
93
+ }
94
+ const next =
95
+ current === null
96
+ ? block + "\n"
97
+ : current.endsWith("\n")
98
+ ? current + "\n" + block + "\n"
99
+ : current + "\n\n" + block + "\n";
100
+ const finalContent =
101
+ current !== null && force && hasManagedBlock(current)
102
+ ? replaceManagedBlock(current, block)
103
+ : next;
104
+ if (!dryRun) {
105
+ await mkdir(path.dirname(absolute), { recursive: true });
106
+ await writeFile(absolute, finalContent, "utf8");
107
+ }
108
+ changes.push(file);
109
+ log(" + " + file);
110
+ }
111
+
112
+ /**
113
+ * @param {{
114
+ * root: string,
115
+ * file: string,
116
+ * content: string,
117
+ * force?: boolean,
118
+ * dryRun?: boolean,
119
+ * executable?: boolean,
120
+ * log: (message: string) => void,
121
+ * changes: string[]
122
+ * }} options
123
+ */
124
+ async function ensureNewFile({ root, file, content, force, dryRun, executable, log, changes }) {
125
+ const absolute = path.join(root, file);
126
+ if (existsSync(absolute) && !force) {
127
+ log(" = " + file + " 已存在,保留");
128
+ return;
129
+ }
130
+ if (!dryRun) {
131
+ await mkdir(path.dirname(absolute), { recursive: true });
132
+ await writeFile(absolute, content, "utf8");
133
+ if (executable && process.platform !== "win32") {
134
+ await chmod(absolute, 0o755).catch(() => {});
135
+ }
136
+ }
137
+ changes.push(file);
138
+ log(" + " + file);
139
+ }
140
+
141
+ /**
142
+ * @param {string} root
143
+ * @param {boolean} dryRun
144
+ * @param {(message: string) => void} log
145
+ * @param {string[]} changes
146
+ */
147
+ async function ensureGitignore(root, dryRun, log, changes) {
148
+ const file = path.join(root, ".gitignore");
149
+ let current = "";
150
+ try {
151
+ current = await readFile(file, "utf8");
152
+ } catch (error) {
153
+ if (!error || error.code !== "ENOENT") throw error;
154
+ }
155
+ const lines = current.split(/\n/);
156
+ const additions = ["/.covgate/", "/coverage/"].filter((entry) => !lines.includes(entry));
157
+ if (additions.length === 0) {
158
+ log(" = .gitignore 已包含覆盖率产物");
159
+ return;
160
+ }
161
+ const next =
162
+ current +
163
+ (current && !current.endsWith("\n") ? "\n" : "") +
164
+ additions.join("\n") +
165
+ "\n";
166
+ if (!dryRun) await writeFile(file, next, "utf8");
167
+ changes.push(".gitignore");
168
+ log(" + .gitignore(" + additions.join(", ") + ")");
169
+ }
170
+
171
+ /**
172
+ * @param {{
173
+ * root: string,
174
+ * packageManager: string,
175
+ * force?: boolean,
176
+ * dryRun?: boolean,
177
+ * log: (message:string) => void
178
+ * }} options
179
+ */
180
+ export async function scaffoldAgent({ root, packageManager, force = false, dryRun = false, log }) {
181
+ const changes = [];
182
+ const exists = (file) => existsSync(path.join(root, file));
183
+ await ensureManagedFile({
184
+ root,
185
+ file: "AGENTS.md",
186
+ block: AGENT_BLOCK,
187
+ force,
188
+ dryRun,
189
+ log,
190
+ changes
191
+ });
192
+
193
+ if (exists("CLAUDE.md") || exists(".claude")) {
194
+ await ensureManagedFile({
195
+ root,
196
+ file: "CLAUDE.md",
197
+ block: AGENT_BLOCK,
198
+ force,
199
+ dryRun,
200
+ log,
201
+ changes
202
+ });
203
+ }
204
+ if (exists(".cursor")) {
205
+ await ensureNewFile({
206
+ root,
207
+ file: ".cursor/rules/covgate.mdc",
208
+ content: CURSOR_RULE,
209
+ force,
210
+ dryRun,
211
+ log,
212
+ changes
213
+ });
214
+ }
215
+ if (exists(".github/copilot-instructions.md")) {
216
+ await ensureManagedFile({
217
+ root,
218
+ file: ".github/copilot-instructions.md",
219
+ block: AGENT_BLOCK,
220
+ force,
221
+ dryRun,
222
+ log,
223
+ changes
224
+ });
225
+ }
226
+
227
+ await ensureNewFile({
228
+ root,
229
+ file: "scripts/covgate-agent-gate.sh",
230
+ content: SH_SCRIPT,
231
+ force,
232
+ dryRun,
233
+ executable: true,
234
+ log,
235
+ changes
236
+ });
237
+ await ensureNewFile({
238
+ root,
239
+ file: "scripts/covgate-agent-gate.ps1",
240
+ content: PS1_SCRIPT,
241
+ force,
242
+ dryRun,
243
+ log,
244
+ changes
245
+ });
246
+
247
+ if (exists(".github")) {
248
+ await ensureNewFile({
249
+ root,
250
+ file: ".github/workflows/covgate.yml",
251
+ content: buildWorkflow(packageManager),
252
+ force,
253
+ dryRun,
254
+ log,
255
+ changes
256
+ });
257
+ }
258
+ await ensureGitignore(root, dryRun, log, changes);
259
+ return changes;
260
+ }
261
+
262
+ /**
263
+ * @param {string} packageManager
264
+ */
265
+ export function buildWorkflow(packageManager) {
266
+ const setup = [" - uses: actions/checkout@v4"];
267
+ let install = " - run: npm ci";
268
+ if (packageManager === "pnpm") {
269
+ setup.push(
270
+ " - uses: pnpm/action-setup@v4",
271
+ " with:",
272
+ " version: 9",
273
+ " - uses: actions/setup-node@v4",
274
+ " with:",
275
+ " node-version: 22",
276
+ " cache: pnpm"
277
+ );
278
+ install = " - run: pnpm install --frozen-lockfile";
279
+ } else if (packageManager === "yarn") {
280
+ setup.push(
281
+ " - uses: actions/setup-node@v4",
282
+ " with:",
283
+ " node-version: 22",
284
+ " cache: yarn"
285
+ );
286
+ install = " - run: corepack enable\n - run: yarn install --immutable";
287
+ } else if (packageManager === "bun") {
288
+ setup.push(" - uses: oven-sh/setup-bun@v2");
289
+ install = " - run: bun install --frozen-lockfile";
290
+ } else {
291
+ setup.push(
292
+ " - uses: actions/setup-node@v4",
293
+ " with:",
294
+ " node-version: 22",
295
+ " cache: npm"
296
+ );
297
+ }
298
+ return [
299
+ "name: coverage-gate",
300
+ "",
301
+ "on:",
302
+ " pull_request:",
303
+ " push:",
304
+ " branches: [main]",
305
+ "",
306
+ "permissions:",
307
+ " contents: read",
308
+ "",
309
+ "jobs:",
310
+ " coverage:",
311
+ " runs-on: ubuntu-latest",
312
+ " timeout-minutes: 30",
313
+ " steps:",
314
+ ...setup,
315
+ install,
316
+ " - name: Run CovGate",
317
+ " run: npx --no-install covgate run --json",
318
+ " - name: Upload audit report",
319
+ " if: always()",
320
+ " uses: actions/upload-artifact@v4",
321
+ " with:",
322
+ " name: covgate-report",
323
+ " path: .covgate/",
324
+ " if-no-files-found: ignore",
325
+ ""
326
+ ].join("\n");
327
+ }
@@ -0,0 +1,112 @@
1
+ import { expandFiles, matchGlob } from "../glob.js";
2
+ import { relativeDisplay } from "../util.js";
3
+
4
+ const TEST_EXCLUDE_CANDIDATES = [
5
+ {
6
+ pattern: "**/*.test.*",
7
+ reason: "测试文件,不是产品运行时代码"
8
+ },
9
+ {
10
+ pattern: "**/*.spec.*",
11
+ reason: "测试文件,不是产品运行时代码"
12
+ },
13
+ {
14
+ pattern: "**/__tests__/**",
15
+ reason: "测试目录,不是产品运行时代码"
16
+ },
17
+ {
18
+ pattern: "**/*.d.ts",
19
+ reason: "类型声明文件,没有运行时代码"
20
+ }
21
+ ];
22
+
23
+ /**
24
+ * Only add exclusions that currently match something. CovGate intentionally
25
+ * fails stale exclusions, so a starter config must not contain guesses that
26
+ * match zero files.
27
+ *
28
+ * @param {string} root
29
+ * @param {string} sourceGlob
30
+ */
31
+ async function pickExcludes(root, sourceGlob) {
32
+ let files = [];
33
+ try {
34
+ files = await expandFiles([sourceGlob], root);
35
+ } catch {
36
+ return [];
37
+ }
38
+ const relative = files.map((file) => relativeDisplay(file, root));
39
+ return TEST_EXCLUDE_CANDIDATES.filter((entry) =>
40
+ relative.some((file) => matchGlob(file, entry.pattern))
41
+ );
42
+ }
43
+
44
+ /**
45
+ * @param {{
46
+ * root: string,
47
+ * profile: "client" | "server",
48
+ * sourceGlob: string,
49
+ * command: string,
50
+ * coveragePath: string
51
+ * }} options
52
+ */
53
+ export async function buildStarterConfig({
54
+ root,
55
+ profile,
56
+ sourceGlob,
57
+ command,
58
+ coveragePath
59
+ }) {
60
+ const exclude = await pickExcludes(root, sourceGlob);
61
+ const label = profile === "client" ? "客户端" : "服务端";
62
+ return {
63
+ $schema: "./node_modules/covgate/schema/covgate.schema.json",
64
+ version: 1,
65
+ scopes: {
66
+ [profile]: {
67
+ profile,
68
+ label,
69
+ commands: [
70
+ {
71
+ name: label + "测试与覆盖率",
72
+ run: command,
73
+ timeoutSeconds: 900
74
+ }
75
+ ],
76
+ coverage: {
77
+ format: "auto",
78
+ paths: [coveragePath]
79
+ },
80
+ sources: {
81
+ include: [sourceGlob]
82
+ },
83
+ exclude
84
+ }
85
+ },
86
+ waivers: {
87
+ minReasonLength: 8,
88
+ requireOwner: false,
89
+ stale: "fail",
90
+ expiry: "fail"
91
+ },
92
+ reports: {
93
+ dir: ".covgate",
94
+ formats: ["json", "markdown"]
95
+ }
96
+ };
97
+ }
98
+
99
+ /**
100
+ * @param {any} config
101
+ */
102
+ export function configSummary(config) {
103
+ const scopes = Object.entries(config.scopes || {}).map(([name, scope]) => ({
104
+ name,
105
+ profile: scope.profile,
106
+ command: scope.commands?.[0]?.run,
107
+ source: scope.sources?.include?.[0],
108
+ coverage: scope.coverage?.paths?.[0],
109
+ exclusions: scope.exclude?.length || 0
110
+ }));
111
+ return scopes;
112
+ }
@@ -0,0 +1,242 @@
1
+ import { existsSync } from "node:fs";
2
+ import { mkdir, writeFile } from "node:fs/promises";
3
+ import path from "node:path";
4
+ import {
5
+ SetupError,
6
+ addScripts,
7
+ coverageScriptFor,
8
+ detectFramework,
9
+ detectPackageManager,
10
+ detectProfile,
11
+ detectSourceGlob,
12
+ displayPath,
13
+ installVitest,
14
+ readPackage,
15
+ writePackage
16
+ } from "./project.js";
17
+ import { buildStarterConfig } from "./config.js";
18
+ import { scaffoldAgent } from "./agent.js";
19
+ import { askYesNo, canPrompt } from "./prompt.js";
20
+
21
+ const DEFAULT_COVERAGE_PATH = "coverage/coverage-final.json";
22
+
23
+ /**
24
+ * @param {string} root
25
+ * @param {string} value
26
+ */
27
+ function normalizeGlob(root, value) {
28
+ return String(value || "")
29
+ .trim()
30
+ .replaceAll("\\", "/")
31
+ .replace(/^\.\//, "");
32
+ }
33
+
34
+ /**
35
+ * @param {{
36
+ * root?: string,
37
+ * yes?: boolean,
38
+ * installFramework?: boolean,
39
+ * profile?: "client" | "server",
40
+ * source?: string,
41
+ * coverage?: string,
42
+ * testCommand?: string,
43
+ * noAgent?: boolean,
44
+ * noConfig?: boolean,
45
+ * force?: boolean,
46
+ * dryRun?: boolean,
47
+ * postinstall?: boolean,
48
+ * log?: (message: string) => void,
49
+ * stderr?: NodeJS.WritableStream
50
+ * }} options
51
+ */
52
+ export async function runSetup(options = {}) {
53
+ const root = path.resolve(options.root || process.cwd());
54
+ const log = options.log || ((message) => process.stdout.write(message + String.fromCharCode(10)));
55
+ const writeError = options.stderr || process.stderr;
56
+ const project = await readPackage(root);
57
+ if (!project) {
58
+ throw new SetupError("当前目录没有 package.json。CovGate 的自动配置目前面向 Node.js 项目。");
59
+ }
60
+ let pkg = project.data;
61
+ const packageManager = detectPackageManager(root, pkg);
62
+ log("[covgate] 项目: " + root);
63
+ log("[covgate] 包管理器: " + packageManager);
64
+
65
+ let framework = detectFramework(pkg);
66
+ let installedFramework = false;
67
+
68
+ if (!framework.id && options.installFramework !== false) {
69
+ const approved =
70
+ options.yes === true
71
+ ? true
72
+ : await askYesNo(
73
+ "[covgate] 未检测到测试框架。是否安装 Vitest + @vitest/coverage-v8?",
74
+ { defaultYes: true, output: options.stderr || process.stderr }
75
+ );
76
+ if (approved) {
77
+ log("[covgate] 安装 Vitest 与 V8 覆盖率插件……");
78
+ const result = await installVitest(root, packageManager, options.dryRun === true, {
79
+ stdout: process.stdout,
80
+ stderr: writeError
81
+ });
82
+ if (result.ok) {
83
+ installedFramework = true;
84
+ framework = { id: "vitest", label: "Vitest", hasCoverageScript: true };
85
+ const refreshed = await readPackage(root);
86
+ if (refreshed) pkg = refreshed.data;
87
+ } else {
88
+ log("[covgate] 自动安装失败。可手动运行: " + result.command);
89
+ }
90
+ } else {
91
+ log("[covgate] 跳过测试框架安装。");
92
+ }
93
+ }
94
+
95
+ let packageChanged = false;
96
+ const scriptChanges = addScripts(pkg, framework.id, {
97
+ testCommand: options.testCommand,
98
+ force: options.force === true
99
+ });
100
+ if (scriptChanges.length > 0) {
101
+ packageChanged = true;
102
+ if (!options.dryRun) await writePackage(root, pkg);
103
+ log("[covgate] package.json 脚本: " + scriptChanges.join(", "));
104
+ }
105
+
106
+ let testCommand = options.testCommand;
107
+ if (!testCommand && pkg.scripts && pkg.scripts["test:coverage"]) {
108
+ testCommand = "npm run test:coverage";
109
+ }
110
+
111
+ const profile = options.profile || detectProfile(root, pkg);
112
+ if (!["client", "server"].includes(profile)) {
113
+ throw new SetupError("profile 必须是 client 或 server");
114
+ }
115
+ const sourceGlob = normalizeGlob(root, options.source || detectSourceGlob(root, profile) || "");
116
+ const coveragePath = normalizeGlob(root, options.coverage || DEFAULT_COVERAGE_PATH);
117
+ const configPath = path.join(root, "covgate.config.json");
118
+ let configWritten = false;
119
+
120
+ if (options.noConfig !== true && testCommand && sourceGlob) {
121
+ if (existsSync(configPath) && options.force !== true) {
122
+ log("[covgate] " + displayPath(root, configPath) + " 已存在,保留现有配置");
123
+ } else {
124
+ const config = await buildStarterConfig({
125
+ root,
126
+ profile,
127
+ sourceGlob,
128
+ command: testCommand,
129
+ coveragePath
130
+ });
131
+ if (!options.dryRun) {
132
+ await mkdir(path.dirname(configPath), { recursive: true });
133
+ await writeFile(configPath, JSON.stringify(config, null, 2) + String.fromCharCode(10), "utf8");
134
+ }
135
+ configWritten = true;
136
+ log(
137
+ "[covgate] 生成 " +
138
+ displayPath(root, configPath) +
139
+ "(" +
140
+ profile +
141
+ ",源码 " +
142
+ sourceGlob +
143
+ ")"
144
+ );
145
+ if (framework.id && !coverageScriptFor(framework.id) && !options.testCommand) {
146
+ log("[covgate] 注意:该测试框架的覆盖率命令需要你确认报告路径。");
147
+ }
148
+ }
149
+ } else if (options.noConfig !== true) {
150
+ if (!testCommand) {
151
+ log("[covgate] 未找到可用的覆盖率测试命令,跳过 covgate.config.json。");
152
+ }
153
+ if (!sourceGlob) {
154
+ log("[covgate] 未找到源码目录,跳过 covgate.config.json;请用 --source 指定。");
155
+ }
156
+ }
157
+
158
+ let agentChanges = [];
159
+ if (options.noAgent !== true) {
160
+ agentChanges = await scaffoldAgent({
161
+ root,
162
+ packageManager,
163
+ force: options.force === true,
164
+ dryRun: options.dryRun === true,
165
+ log
166
+ });
167
+ }
168
+
169
+ const summary = {
170
+ root,
171
+ packageManager,
172
+ framework: framework.id || null,
173
+ installedFramework,
174
+ profile,
175
+ sourceGlob: sourceGlob || null,
176
+ testCommand: testCommand || null,
177
+ coveragePath,
178
+ configPath: configWritten ? configPath : existsSync(configPath) ? configPath : null,
179
+ packageChanged,
180
+ packageScripts: scriptChanges,
181
+ agentChanges,
182
+ dryRun: options.dryRun === true,
183
+ postinstall: options.postinstall === true
184
+ };
185
+
186
+ log("");
187
+ log("[covgate] 完成。");
188
+ if (summary.configPath) {
189
+ log("[covgate] 配置: " + displayPath(root, summary.configPath));
190
+ }
191
+ if (summary.framework) {
192
+ log("[covgate] 测试框架: " + (framework.label || summary.framework));
193
+ }
194
+ log("[covgate] 运行门禁: " + (summary.dryRun ? "npx covgate run" : "npx --no-install covgate run"));
195
+ if (!options.noAgent) {
196
+ log("[covgate] AI 接入: AGENTS.md 与 scripts/covgate-agent-gate.*");
197
+ }
198
+ return summary;
199
+ }
200
+
201
+ /**
202
+ * @param {{
203
+ * root: string,
204
+ * stderr?: NodeJS.WritableStream
205
+ * }} options
206
+ */
207
+ export async function runPostinstall(options) {
208
+ const stderr = options.stderr || process.stderr;
209
+ const root = path.resolve(options.root || process.cwd());
210
+ const project = await readPackage(root).catch(() => null);
211
+ if (!project) return 0;
212
+ if (project.data?.name === "covgate") return 0;
213
+ if (process.env.COVGATE_SKIP_SETUP === "1") return 0;
214
+ if (process.env.CI) return 0;
215
+ if (process.env.npm_config_global === "true") return 0;
216
+ const hasDirectDependency =
217
+ project.data?.dependencies?.covgate ||
218
+ project.data?.devDependencies?.covgate ||
219
+ project.data?.optionalDependencies?.covgate;
220
+ if (!hasDirectDependency) return 0;
221
+
222
+ if (!canPrompt({ output: stderr }) && process.env.COVGATE_SETUP_ASSUME_YES !== "1") {
223
+ stderr.write(
224
+ "[covgate] 已安装。未检测到交互终端,未自动配置;可运行 npx covgate setup。\n"
225
+ );
226
+ return 0;
227
+ }
228
+
229
+ try {
230
+ await runSetup({
231
+ root,
232
+ postinstall: true,
233
+ installFramework: true,
234
+ stderr,
235
+ log: (message) => console.log(message)
236
+ });
237
+ } catch (error) {
238
+ stderr.write("[covgate] 安装后自动配置失败:" + (error?.message || error) + "\n");
239
+ stderr.write("[covgate] 可稍后运行 npx covgate setup。\n");
240
+ }
241
+ return 0;
242
+ }