dependfix 0.1.0 → 0.3.2

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/dist/cli.cjs DELETED
@@ -1,2812 +0,0 @@
1
- //#region \0rolldown/runtime.js
2
- var __create = Object.create;
3
- var __defProp = Object.defineProperty;
4
- var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
- var __getOwnPropNames = Object.getOwnPropertyNames;
6
- var __getProtoOf = Object.getPrototypeOf;
7
- var __hasOwnProp = Object.prototype.hasOwnProperty;
8
- var __commonJSMin = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports);
9
- var __copyProps = (to, from, except, desc) => {
10
- if (from && typeof from === "object" || typeof from === "function") {
11
- for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
12
- key = keys[i];
13
- if (!__hasOwnProp.call(to, key) && key !== except) {
14
- __defProp(to, key, {
15
- get: ((k) => from[k]).bind(null, key),
16
- enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
17
- });
18
- }
19
- }
20
- }
21
- return to;
22
- };
23
- var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
24
- value: mod,
25
- enumerable: true
26
- }) : target, mod));
27
-
28
- //#endregion
29
- let citty = require("citty");
30
- let _dependfix_core = require("@dependfix/core");
31
- let _octokit_rest = require("@octokit/rest");
32
- let node_child_process = require("node:child_process");
33
- let node_crypto = require("node:crypto");
34
- let node_fs = require("node:fs");
35
- let node_path = require("node:path");
36
- let node_readline = require("node:readline");
37
- let yaml = require("yaml");
38
- yaml = __toESM(yaml);
39
-
40
- //#region src/github/client.ts
41
- /**
42
- * 创建已认证的 Octokit 实例。
43
- *
44
- * 调用方直接使用 `octokit.rest.*` 访问所有已类型化的 GitHub REST API。
45
- * 分页使用 `octokit.paginate()` 自动合并多页结果。
46
- *
47
- * @example
48
- * ```typescript
49
- * const octokit = createGitHubClient({ token: 'ghp_xxxx' })
50
- *
51
- * // 仓库信息
52
- * const { data: repo } = await octokit.rest.repos.get({ owner: 'foo', repo: 'bar' })
53
- *
54
- * // Dependabot 告警(自动分页)
55
- * const alerts = await octokit.paginate(
56
- * octokit.rest.dependabot.listAlertsForRepo,
57
- * { owner: 'foo', repo: 'bar', state: 'open', per_page: 100 },
58
- * )
59
- * ```
60
- */
61
- function createGitHubClient(options) {
62
- return new _octokit_rest.Octokit({
63
- auth: options.token,
64
- baseUrl: options.baseUrl ?? "https://api.github.com"
65
- });
66
- }
67
-
68
- //#endregion
69
- //#region ../../node_modules/.pnpm/deprecation@2.3.1/node_modules/deprecation/dist-node/index.js
70
- var require_dist_node$1 = /* @__PURE__ */ __commonJSMin(((exports) => {
71
- Object.defineProperty(exports, "__esModule", { value: true });
72
- var Deprecation = class extends Error {
73
- constructor(message) {
74
- super(message);
75
- /* istanbul ignore next */
76
- if (Error.captureStackTrace) Error.captureStackTrace(this, this.constructor);
77
- this.name = "Deprecation";
78
- }
79
- };
80
- exports.Deprecation = Deprecation;
81
- }));
82
-
83
- //#endregion
84
- //#region ../../node_modules/.pnpm/wrappy@1.0.2/node_modules/wrappy/wrappy.js
85
- var require_wrappy = /* @__PURE__ */ __commonJSMin(((exports, module) => {
86
- module.exports = wrappy;
87
- function wrappy(fn, cb) {
88
- if (fn && cb) return wrappy(fn)(cb);
89
- if (typeof fn !== "function") throw new TypeError("need wrapper function");
90
- Object.keys(fn).forEach(function(k) {
91
- wrapper[k] = fn[k];
92
- });
93
- return wrapper;
94
- function wrapper() {
95
- var args = new Array(arguments.length);
96
- for (var i = 0; i < args.length; i++) args[i] = arguments[i];
97
- var ret = fn.apply(this, args);
98
- var cb = args[args.length - 1];
99
- if (typeof ret === "function" && ret !== cb) Object.keys(cb).forEach(function(k) {
100
- ret[k] = cb[k];
101
- });
102
- return ret;
103
- }
104
- }
105
- }));
106
-
107
- //#endregion
108
- //#region ../../node_modules/.pnpm/once@1.4.0/node_modules/once/once.js
109
- var require_once = /* @__PURE__ */ __commonJSMin(((exports, module) => {
110
- var wrappy = require_wrappy();
111
- module.exports = wrappy(once);
112
- module.exports.strict = wrappy(onceStrict);
113
- once.proto = once(function() {
114
- Object.defineProperty(Function.prototype, "once", {
115
- value: function() {
116
- return once(this);
117
- },
118
- configurable: true
119
- });
120
- Object.defineProperty(Function.prototype, "onceStrict", {
121
- value: function() {
122
- return onceStrict(this);
123
- },
124
- configurable: true
125
- });
126
- });
127
- function once(fn) {
128
- var f = function() {
129
- if (f.called) return f.value;
130
- f.called = true;
131
- return f.value = fn.apply(this, arguments);
132
- };
133
- f.called = false;
134
- return f;
135
- }
136
- function onceStrict(fn) {
137
- var f = function() {
138
- if (f.called) throw new Error(f.onceError);
139
- f.called = true;
140
- return f.value = fn.apply(this, arguments);
141
- };
142
- f.onceError = (fn.name || "Function wrapped with `once`") + " shouldn't be called more than once";
143
- f.called = false;
144
- return f;
145
- }
146
- }));
147
-
148
- //#endregion
149
- //#region ../../node_modules/.pnpm/@octokit+request-error@5.1.1/node_modules/@octokit/request-error/dist-node/index.js
150
- var require_dist_node = /* @__PURE__ */ __commonJSMin(((exports, module) => {
151
- var __create = Object.create;
152
- var __defProp = Object.defineProperty;
153
- var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
154
- var __getOwnPropNames = Object.getOwnPropertyNames;
155
- var __getProtoOf = Object.getPrototypeOf;
156
- var __hasOwnProp = Object.prototype.hasOwnProperty;
157
- var __export = (target, all) => {
158
- for (var name in all) __defProp(target, name, {
159
- get: all[name],
160
- enumerable: true
161
- });
162
- };
163
- var __copyProps = (to, from, except, desc) => {
164
- if (from && typeof from === "object" || typeof from === "function") {
165
- for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
166
- get: () => from[key],
167
- enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
168
- });
169
- }
170
- return to;
171
- };
172
- var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
173
- value: mod,
174
- enumerable: true
175
- }) : target, mod));
176
- var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
177
- var dist_src_exports = {};
178
- __export(dist_src_exports, { RequestError: () => RequestError });
179
- module.exports = __toCommonJS(dist_src_exports);
180
- var import_deprecation = require_dist_node$1();
181
- var import_once = __toESM(require_once());
182
- var logOnceCode = (0, import_once.default)((deprecation) => console.warn(deprecation));
183
- var logOnceHeaders = (0, import_once.default)((deprecation) => console.warn(deprecation));
184
- var RequestError = class extends Error {
185
- constructor(message, statusCode, options) {
186
- super(message);
187
- if (Error.captureStackTrace) Error.captureStackTrace(this, this.constructor);
188
- this.name = "HttpError";
189
- this.status = statusCode;
190
- let headers;
191
- if ("headers" in options && typeof options.headers !== "undefined") headers = options.headers;
192
- if ("response" in options) {
193
- this.response = options.response;
194
- headers = options.response.headers;
195
- }
196
- const requestCopy = Object.assign({}, options.request);
197
- if (options.request.headers.authorization) requestCopy.headers = Object.assign({}, options.request.headers, { authorization: options.request.headers.authorization.replace(/(?<! ) .*$/, " [REDACTED]") });
198
- requestCopy.url = requestCopy.url.replace(/\bclient_secret=\w+/g, "client_secret=[REDACTED]").replace(/\baccess_token=\w+/g, "access_token=[REDACTED]");
199
- this.request = requestCopy;
200
- Object.defineProperty(this, "code", { get() {
201
- logOnceCode(new import_deprecation.Deprecation("[@octokit/request-error] `error.code` is deprecated, use `error.status`."));
202
- return statusCode;
203
- } });
204
- Object.defineProperty(this, "headers", { get() {
205
- logOnceHeaders(new import_deprecation.Deprecation("[@octokit/request-error] `error.headers` is deprecated, use `error.response.headers`."));
206
- return headers || {};
207
- } });
208
- }
209
- };
210
- }));
211
-
212
- //#endregion
213
- //#region src/github/errors.ts
214
- var import_dist_node = require_dist_node();
215
- /**
216
- * 将 Octokit 请求错误映射为 `AppError`。
217
- *
218
- * @param error - 原始错误(`RequestError` 或网络异常)
219
- * @param context - 调用上下文描述(如 `'fetch repo info for foo/bar'`),会附加到错误消息中
220
- */
221
- function mapGitHubError(error, context) {
222
- if (error instanceof import_dist_node.RequestError) {
223
- const code = resolveErrorCode(error);
224
- const details = collectErrorDetails(error);
225
- return new _dependfix_core.AppError(code, `${context}: ${error.message}`, {
226
- cause: error,
227
- details
228
- });
229
- }
230
- if (error instanceof Error) return new _dependfix_core.AppError("NETWORK_ERROR", `${context}: ${error.message}`, { cause: error });
231
- return new _dependfix_core.AppError("NETWORK_ERROR", `${context}: unknown error`, { details: { raw: (0, _dependfix_core.toErrorMessage)(error) } });
232
- }
233
- function resolveErrorCode(error) {
234
- switch (error.status) {
235
- case 401: return "AUTHENTICATION_FAILED";
236
- case 403: {
237
- const remaining = error.response?.headers["x-ratelimit-remaining"];
238
- if (remaining !== void 0 && remaining === "0") return "RATE_LIMITED";
239
- return "PERMISSION_DENIED";
240
- }
241
- case 404: return "REPO_NOT_FOUND";
242
- default: return "GITHUB_API_ERROR";
243
- }
244
- }
245
- function collectErrorDetails(error) {
246
- const details = {
247
- status: error.status,
248
- requestUrl: error.request?.url,
249
- requestMethod: error.request?.method
250
- };
251
- const resetAt = error.response?.headers["x-ratelimit-reset"];
252
- if (resetAt) details.rateLimitReset = resetAt;
253
- return details;
254
- }
255
-
256
- //#endregion
257
- //#region src/github/dependabot-fetcher.ts
258
- /**
259
- * 拉取指定仓库的 Dependabot 告警并映射为标准化模型。
260
- *
261
- * - 自动分页(`octokit.paginate()`),返回所有匹配的告警
262
- * - 仅处理 `state=open` 的告警(可通过 `params.state` 覆盖)
263
- * - 异常通过 `mapGitHubError` 转为 `AppError`
264
- * - 空仓库返回 `[]`,不抛异常
265
- *
266
- * @param client - 已认证的 Octokit 实例(来自 `createGitHubClient`)
267
- * @param params - 仓库标识与过滤参数
268
- * @returns 标准化告警列表
269
- *
270
- * @example
271
- * ```typescript
272
- * const octokit = createGitHubClient({ token: 'ghp_xxxx' })
273
- * const alerts = await fetchDependabotAlerts(octokit, { owner: 'foo', repo: 'bar' })
274
- * ```
275
- */
276
- async function fetchDependabotAlerts(client, params) {
277
- const { owner, repo, state = "open", perPage = 100 } = params;
278
- try {
279
- return (await client.paginate(client.rest.dependabot.listAlertsForRepo, {
280
- owner,
281
- repo,
282
- state,
283
- per_page: perPage
284
- })).map((alert) => normalizeAlert(alert, owner, repo));
285
- } catch (error) {
286
- throw mapGitHubError(error, `fetch dependabot alerts for ${owner}/${repo}`);
287
- }
288
- }
289
- /**
290
- * 将单条 Dependabot API 原始告警转换为标准化模型。
291
- *
292
- * 映射逻辑:
293
- * - severity 直接透传(Dependabot 的 `low|medium|high|critical` 与 `AlertSeverity` 对齐)
294
- * - fixable 判定:`security_vulnerability.first_patched_version !== null`
295
- * - fixStrategy:有修复版本 → `'upgrade'`,否则 `null`
296
- * - recommendedVersion 取自 `first_patched_version.identifier`
297
- * - 可选字段兜底:`packageName`/`packageEcosystem` → `'unknown'`,`manifestPath` → `''`
298
- * - `defaultBranch` 不在此层填充(由上层调用方获取后注入)
299
- */
300
- function normalizeAlert(alert, owner, repo) {
301
- const firstPatched = alert.security_vulnerability.first_patched_version;
302
- const fixable = firstPatched !== null;
303
- return {
304
- id: alert.number,
305
- source: "dependabot",
306
- repository: `${owner}/${repo}`,
307
- defaultBranch: "",
308
- severity: alert.security_advisory.severity,
309
- packageEcosystem: alert.dependency.package?.ecosystem ?? "unknown",
310
- packageName: alert.dependency.package?.name ?? "unknown",
311
- manifestPath: alert.dependency.manifest_path ?? "",
312
- ruleId: alert.security_advisory.ghsa_id,
313
- summary: alert.security_advisory.summary,
314
- htmlUrl: alert.html_url,
315
- fixable,
316
- fixStrategy: fixable ? "upgrade" : null,
317
- recommendedVersion: firstPatched?.identifier ?? "",
318
- dependencyType: normalizeDependencyRelationship(alert.dependency.relationship)
319
- };
320
- }
321
- /**
322
- * 将 Dependabot API 的 `dependency.relationship` 映射为 `DependencyType`。
323
- *
324
- * GitHub API 返回的合法值:
325
- * - `'direct'` → 直接依赖
326
- * - `'indirect'` → 间接依赖(transitive)
327
- * - `null`/`undefined` → 未知
328
- */
329
- function normalizeDependencyRelationship(relationship) {
330
- if (relationship === "direct") return "direct";
331
- if (relationship === "indirect") return "transitive";
332
- }
333
-
334
- //#endregion
335
- //#region src/github/pr-creator.ts
336
- const BOT_NAME = "dependfix[bot]";
337
- const BOT_EMAIL = "dependfix[bot]@users.noreply.github.com";
338
- /** 自动修复分支统一前缀(T210 起分支名 = 前缀 + 内容指纹 8 位) */
339
- const BRANCH_PREFIX = "dependfix/auto-fix-";
340
- /**
341
- * 计算修复内容指纹(sha256 前 8 位)。
342
- *
343
- * 指纹基于**结构化修复结果**而非 git diff:
344
- * - 成功升级集:`pkg@toVersion`(排序拼接)
345
- * - 修复失败集:失败包名(排序拼接)
346
- * - lockfile 修复状态(成功/失败)
347
- *
348
- * 同告警集 → 同修复结果 → 同指纹 → 幂等跳过;
349
- * 内容变化(升级集/失败集/锁文件修复变化)→ 指纹变化 → 关旧开新。
350
- * 不用 diff hash 是因为 pnpm 版本等非确定性因素会使锁文件内容漂移,
351
- * 导致"告警没变但指纹变"。
352
- */
353
- function computeFixFingerprint(actions) {
354
- const isUpgrade = (a) => a.type === "dependency-upgrade" && !!a.target && !a.target.startsWith("PR #");
355
- const upgrades = actions.filter((a) => isUpgrade(a) && a.success && a.toVersion).map((a) => `${a.target}@${a.toVersion}`).sort();
356
- const failures = actions.filter((a) => isUpgrade(a) && !a.success).map((a) => a.target).sort();
357
- const repairs = actions.filter((a) => a.type === "lockfile-repair").map((a) => `${a.target}:${a.success}`).sort();
358
- const payload = JSON.stringify([
359
- upgrades,
360
- failures,
361
- repairs
362
- ]);
363
- return (0, node_crypto.createHash)("sha256").update(payload).digest("hex").slice(0, 8);
364
- }
365
- /**
366
- * 从分支名提取内容指纹(`dependfix/auto-fix-{fp8}` → `{fp8}`)。
367
- * 指纹格式为 8 位 hex;分支名不含 `-` 后的内容、前缀不匹配或格式不合法时返回 null
368
- * (如旧版 runId 分支,会被 supersede)。
369
- */
370
- function extractFingerprintFromBranch(branchName) {
371
- if (!branchName.startsWith(BRANCH_PREFIX)) return null;
372
- const fingerprint = branchName.slice(19);
373
- return /^[0-9a-f]{8}$/.test(fingerprint) ? fingerprint : null;
374
- }
375
- /**
376
- * 计算 fix-and-pr 去重决策:
377
- * - 存在同指纹 open PR → skip(仅需关闭其余异指纹 PR)
378
- * - 否则 → create(关闭全部旧 PR,先建新后关旧由调用方保证)
379
- */
380
- function computeFixAndPrPlan(existingPRs, fingerprint) {
381
- const sameContentPR = existingPRs.find((pr) => extractFingerprintFromBranch(pr.headRef) === fingerprint);
382
- if (sameContentPR) return {
383
- action: "skip",
384
- sameContentPR,
385
- supersedePRs: existingPRs.filter((pr) => pr !== sameContentPR)
386
- };
387
- return {
388
- action: "create",
389
- supersedePRs: existingPRs
390
- };
391
- }
392
- /**
393
- * 在工作目录创建修复分支。
394
- *
395
- * - 分支名由调用方传入(T210 起为 `dependfix/auto-fix-{内容指纹}`,不再依赖 runId)
396
- * - 如果分支已存在(如重跑),切换到该分支
397
- *
398
- * @returns 分支名和是否为新创建
399
- */
400
- function createFixBranch(branchName, workDir) {
401
- if (branchExists(branchName, workDir)) {
402
- (0, node_child_process.execSync)(`git checkout ${branchName}`, {
403
- cwd: workDir,
404
- stdio: "pipe"
405
- });
406
- return {
407
- branchName,
408
- created: false
409
- };
410
- }
411
- (0, node_child_process.execSync)(`git checkout -b ${branchName}`, {
412
- cwd: workDir,
413
- stdio: "pipe"
414
- });
415
- return {
416
- branchName,
417
- created: true
418
- };
419
- }
420
- /**
421
- * 暂存所有变更并提交。
422
- * 自动设置 git user.name / user.email(如未设置)。
423
- */
424
- function stageAndCommit(message, workDir) {
425
- ensureGitConfig(workDir);
426
- (0, node_child_process.execSync)("git add .", {
427
- cwd: workDir,
428
- stdio: "pipe"
429
- });
430
- (0, node_child_process.execSync)(`git commit -m "${escapeShell(message)}"`, {
431
- cwd: workDir,
432
- stdio: "pipe"
433
- });
434
- }
435
- /**
436
- * 推送分支到远程 origin。
437
- */
438
- function pushBranch(branchName, workDir) {
439
- (0, node_child_process.execSync)(`git push origin ${branchName}`, {
440
- cwd: workDir,
441
- stdio: "pipe"
442
- });
443
- }
444
- /**
445
- * 查找目标仓库中所有 open 状态的 dependfix 自动修复 PR。
446
- *
447
- * GitHub 的 `pulls.list` 的 `head` 参数不支持通配符,因此拉取 open PR 列表后
448
- * 按 head 分支前缀 `dependfix/auto-fix-` 过滤。假设 PR 数量不多(单页 100 足够),
449
- * 未来 PR 数量增大时可引入 label 索引(见 backlog B1)。
450
- */
451
- async function findDependfixOpenPR(octokit, owner, repo) {
452
- const { data } = await octokit.rest.pulls.list({
453
- owner,
454
- repo,
455
- state: "open",
456
- per_page: 100
457
- });
458
- return data.filter((pr) => pr.head.ref.startsWith(BRANCH_PREFIX)).map((pr) => ({
459
- number: pr.number,
460
- htmlUrl: pr.html_url,
461
- headRef: pr.head.ref
462
- }));
463
- }
464
- /**
465
- * 关闭 Pull Request(用于 supersede 旧 PR)。
466
- */
467
- async function closePullRequest(octokit, owner, repo, pullNumber) {
468
- await octokit.rest.pulls.update({
469
- owner,
470
- repo,
471
- pull_number: pullNumber,
472
- state: "closed"
473
- });
474
- }
475
- /**
476
- * 通过 GitHub API 创建 Pull Request。
477
- */
478
- async function createPullRequest(params) {
479
- const { octokit, owner, repo, headBranch, baseBranch, title, body } = params;
480
- const { data } = await octokit.rest.pulls.create({
481
- owner,
482
- repo,
483
- title,
484
- body,
485
- head: headBranch,
486
- base: baseBranch
487
- });
488
- return {
489
- number: data.number,
490
- htmlUrl: data.html_url
491
- };
492
- }
493
- /**
494
- * 列出远端所有 `dependfix/` 前缀分支(git.listMatchingRefs 精确前缀匹配)。
495
- */
496
- async function listDependfixBranches(octokit, owner, repo) {
497
- const { data } = await octokit.rest.git.listMatchingRefs({
498
- owner,
499
- repo,
500
- ref: "heads/dependfix",
501
- per_page: 100
502
- });
503
- return data.map((ref) => ref.ref.replace(/^refs\/heads\//, "")).filter((name) => name.startsWith("dependfix/"));
504
- }
505
- /**
506
- * 查询分支对应的最近 PR 状态(pulls.list head 精确匹配,按最近更新取 1 条)。
507
- */
508
- async function getBranchPrStatus(octokit, owner, repo, branch) {
509
- const { data } = await octokit.rest.pulls.list({
510
- owner,
511
- repo,
512
- head: `${owner}:${branch}`,
513
- state: "all",
514
- sort: "updated",
515
- direction: "desc",
516
- per_page: 1
517
- });
518
- const pr = data[0];
519
- if (!pr) return {
520
- branch,
521
- prNumber: null,
522
- merged: false,
523
- closed: false
524
- };
525
- return {
526
- branch,
527
- prNumber: pr.number,
528
- merged: pr.state === "closed" && pr.merged_at !== null,
529
- closed: pr.state === "closed"
530
- };
531
- }
532
- /**
533
- * 删除远端分支(git ref delete)。失败(分支保护、不存在)由调用方捕获处理。
534
- */
535
- async function deleteRemoteBranch(octokit, owner, repo, branch) {
536
- await octokit.rest.git.deleteRef({
537
- owner,
538
- repo,
539
- ref: `heads/${branch}`
540
- });
541
- }
542
- /**
543
- * 判定交互确认回答是否为"是"(y/yes,大小写不敏感;空输入默认拒绝)。
544
- */
545
- function isConfirmAnswer(answer) {
546
- return /^y(es)?$/i.test(answer.trim());
547
- }
548
- /**
549
- * 从 RunResult 生成 PR body(Markdown)。
550
- *
551
- * @param supersededNumbers 被本 PR 取代并已关闭的旧 PR 编号列表(用于 Supersedes 声明)
552
- */
553
- function generatePRBody(result, supersededNumbers) {
554
- const { summary, actions, errors } = result;
555
- const lines = [
556
- "## 🔒 Dependfix Auto Fix",
557
- "",
558
- `**Run ID**: \`${result.runId}\``,
559
- `**Mode**: ${result.config.mode}`,
560
- `**Time**: ${result.startedAt}`,
561
- "",
562
- "### 📊 Summary",
563
- "",
564
- "| Metric | Value |",
565
- "|--------|-------|",
566
- `| Repositories scanned | ${summary.repositoriesScanned} |`,
567
- `| Alerts found | ${summary.alertsFound} |`,
568
- `| Alerts fixable | ${summary.alertsFixable} |`,
569
- `| Dependencies upgraded | ${summary.alertsFixed} |`,
570
- `| Upgrades failed | ${summary.alertsFailed} |`,
571
- `| Lockfile repairs | ${summary.lockfileRepairs} |`,
572
- `| Verifications passed | ${summary.verificationsPassed} |`,
573
- `| Verifications failed | ${summary.verificationsFailed} |`,
574
- ""
575
- ];
576
- const upgrades = actions.filter((a) => a.type === "dependency-upgrade" && a.success);
577
- if (upgrades.length > 0) {
578
- lines.push("### 📦 Upgraded Dependencies", "");
579
- lines.push("| Package | From | To | Major |");
580
- lines.push("|---------|------|----|-------|");
581
- for (const a of upgrades) {
582
- const major = a.isMajor ? "⚠️ Yes" : "No";
583
- lines.push(`| \`${a.target}\` | ${a.fromVersion ?? "-"} | ${a.toVersion ?? "-"} | ${major} |`);
584
- }
585
- lines.push("");
586
- }
587
- const failures = actions.filter((a) => a.type === "dependency-upgrade" && !a.success);
588
- if (failures.length > 0) {
589
- lines.push("### ⚠️ Failed Upgrades", "");
590
- for (const a of failures) lines.push(`- **\`${a.target}\`**: ${a.error ?? "unknown error"}`);
591
- lines.push("");
592
- }
593
- const verifications = actions.filter((a) => a.type === "verification");
594
- if (verifications.length > 0) {
595
- lines.push("### ✅ Verification", "");
596
- for (const v of verifications) {
597
- const icon = v.success ? "✅" : "❌";
598
- lines.push(`- ${icon} \`${v.target}\` ${v.error ? `(${v.error})` : ""}`);
599
- }
600
- lines.push("");
601
- }
602
- if (errors.length > 0) {
603
- lines.push("### 🚨 Errors", "");
604
- for (const e of errors) lines.push(`- **${e.stage}**: ${e.message}`);
605
- lines.push("");
606
- }
607
- lines.push("---", "", "*This PR was automatically created by [dependfix](https://github.com/dependfix/dependfix).*");
608
- if (supersededNumbers && supersededNumbers.length > 0) lines.push("", `> **Supersedes**: ${supersededNumbers.map((n) => `#${n}`).join(", ")}(内容已更新,将取代旧 PR)`);
609
- return lines.join("\n");
610
- }
611
- function branchExists(branchName, workDir) {
612
- try {
613
- (0, node_child_process.execSync)(`git rev-parse --verify ${branchName}`, {
614
- cwd: workDir,
615
- stdio: "pipe"
616
- });
617
- return true;
618
- } catch {
619
- return false;
620
- }
621
- }
622
- function ensureGitConfig(workDir) {
623
- const hasName = gitConfigExists("user.name", workDir);
624
- const hasEmail = gitConfigExists("user.email", workDir);
625
- if (!hasName) (0, node_child_process.execSync)(`git config user.name "${BOT_NAME}"`, {
626
- cwd: workDir,
627
- stdio: "pipe"
628
- });
629
- if (!hasEmail) (0, node_child_process.execSync)(`git config user.email "${BOT_EMAIL}"`, {
630
- cwd: workDir,
631
- stdio: "pipe"
632
- });
633
- }
634
- function gitConfigExists(key, workDir) {
635
- try {
636
- (0, node_child_process.execSync)(`git config ${key}`, {
637
- cwd: workDir,
638
- stdio: "pipe"
639
- });
640
- return true;
641
- } catch {
642
- return false;
643
- }
644
- }
645
- function escapeShell(str) {
646
- return str.replace(/"/g, "\\\"");
647
- }
648
-
649
- //#endregion
650
- //#region src/fixers/dependency/index.ts
651
- /**
652
- * 升级单个依赖到指定版本。
653
- *
654
- * - 在 `workDir` 中查找 `package.json` 并修改目标包的版本声明(保留原始前缀)
655
- * - 执行 `pnpm install --no-frozen-lockfile` 更新 lockfile
656
- * - 失败时自动回滚 `package.json` 和 `pnpm-lock.yaml`
657
- * - 不执行验证(由 T107 负责)
658
- *
659
- * @param params - 包名、目标版本、工作目录
660
- * @returns 修复结果
661
- */
662
- async function upgradeDependency(params) {
663
- const { packageName, targetVersion, workDir } = params;
664
- const pkgPath = (0, node_path.join)(workDir, "package.json");
665
- const lockfilePath = (0, node_path.join)(workDir, "pnpm-lock.yaml");
666
- if (!(0, node_fs.existsSync)(pkgPath)) return failResult(packageName, targetVersion, "", `${pkgPath}: file not found`);
667
- let pkg;
668
- try {
669
- pkg = JSON.parse((0, node_fs.readFileSync)(pkgPath, "utf-8"));
670
- } catch {
671
- return failResult(packageName, targetVersion, "", `${pkgPath}: invalid JSON`);
672
- }
673
- const dep = findDependencyVersion(pkg, packageName);
674
- if (!dep) return failResult(packageName, targetVersion, "", `package "${packageName}" not found in dependencies / devDependencies / optionalDependencies`);
675
- const fromVersion = dep.version;
676
- const toVersion = `${extractPrefix(fromVersion)}${targetVersion}`;
677
- const isMajor = parseMajorVersion(fromVersion) !== parseMajorVersion(toVersion) && parseMajorVersion(fromVersion) !== -1 && parseMajorVersion(toVersion) !== -1;
678
- const pkgBackup = `${pkgPath}.bak`;
679
- const lockBackup = `${lockfilePath}.bak`;
680
- try {
681
- (0, node_fs.copyFileSync)(pkgPath, pkgBackup);
682
- if ((0, node_fs.existsSync)(lockfilePath)) (0, node_fs.copyFileSync)(lockfilePath, lockBackup);
683
- } catch {
684
- return failResult(packageName, targetVersion, fromVersion, "failed to create backup files");
685
- }
686
- const deps = pkg[dep.group];
687
- if (!deps) return failResult(packageName, targetVersion, fromVersion, `dependency group "${dep.group}" is missing`);
688
- deps[packageName] = toVersion;
689
- (0, node_fs.writeFileSync)(pkgPath, `${JSON.stringify(pkg, null, 2)}\n`, "utf-8");
690
- try {
691
- await execPnpmInstall(workDir);
692
- } catch (installErr) {
693
- rollback$1(pkgPath, pkgBackup, lockfilePath, lockBackup);
694
- cleanupBackups({
695
- pkgBackup,
696
- lockBackup
697
- });
698
- return {
699
- packageName,
700
- fromVersion,
701
- toVersion,
702
- isMajor,
703
- success: false,
704
- error: `pnpm install failed: ${getStderr(installErr)}`
705
- };
706
- }
707
- cleanupBackups({
708
- pkgBackup,
709
- lockBackup
710
- });
711
- return {
712
- packageName,
713
- fromVersion,
714
- toVersion,
715
- isMajor,
716
- success: true
717
- };
718
- }
719
- /**
720
- * 通过 pnpm `overrides` 升级间接(transitive)依赖。
721
- *
722
- * 间接依赖不在 `package.json` 的 `dependencies` / `devDependencies` 中列出,
723
- * 因此无法通过常规 `upgradeDependency` 处理。本函数根据项目类型选择覆盖写入位置:
724
- *
725
- * - **有 `pnpm-workspace.yaml`**(monorepo / workspace)→ 写入其中的 `overrides` 字段(pnpm v10+ 推荐)
726
- * - **无 `pnpm-workspace.yaml`**(单包项目)→ 写入 `package.json` 的 `pnpm.overrides`
727
- *
728
- * 失败时自动回滚被修改的文件和 `pnpm-lock.yaml`。不执行验证(由 T107 负责)。
729
- *
730
- * @param params - 包名、目标版本、工作目录
731
- * @returns 修复结果
732
- */
733
- async function overrideTransitiveDependency(params) {
734
- const { packageName, targetVersion, workDir } = params;
735
- const pkgPath = (0, node_path.join)(workDir, "package.json");
736
- const workspaceYamlPath = (0, node_path.join)(workDir, "pnpm-workspace.yaml");
737
- const lockfilePath = (0, node_path.join)(workDir, "pnpm-lock.yaml");
738
- const usesWorkspaceYaml = (0, node_fs.existsSync)(workspaceYamlPath);
739
- if (!(0, node_fs.existsSync)(pkgPath)) return failResult(packageName, targetVersion, "", `${pkgPath}: file not found`);
740
- let pkg;
741
- try {
742
- pkg = JSON.parse((0, node_fs.readFileSync)(pkgPath, "utf-8"));
743
- } catch {
744
- return failResult(packageName, targetVersion, "", `${pkgPath}: invalid JSON`);
745
- }
746
- const directDep = findDependencyVersion(pkg, packageName);
747
- if (directDep) return failResult(packageName, targetVersion, directDep.version, `package "${packageName}" is a direct dependency (${directDep.group}), use upgradeDependency instead`);
748
- const fromVersion = readLockfileVersion(lockfilePath, packageName) ?? "unknown";
749
- const toVersion = `${extractPrefix(fromVersion)}${targetVersion}`;
750
- const isMajor = parseMajorVersion(fromVersion) !== parseMajorVersion(toVersion) && parseMajorVersion(fromVersion) !== -1 && parseMajorVersion(toVersion) !== -1;
751
- const pkgBackup = `${pkgPath}.bak`;
752
- const workspaceBackup = usesWorkspaceYaml ? `${workspaceYamlPath}.bak` : null;
753
- const lockBackup = `${lockfilePath}.bak`;
754
- try {
755
- (0, node_fs.copyFileSync)(pkgPath, pkgBackup);
756
- if (usesWorkspaceYaml) (0, node_fs.copyFileSync)(workspaceYamlPath, workspaceBackup);
757
- if ((0, node_fs.existsSync)(lockfilePath)) (0, node_fs.copyFileSync)(lockfilePath, lockBackup);
758
- } catch {
759
- return failResult(packageName, targetVersion, fromVersion, "failed to create backup files");
760
- }
761
- let oldOverride;
762
- if (usesWorkspaceYaml) oldOverride = writeWorkspaceOverride(workspaceYamlPath, packageName, toVersion);
763
- else {
764
- const overrides = ensurePnpmOverrides(pkg);
765
- oldOverride = overrides[packageName];
766
- overrides[packageName] = toVersion;
767
- (0, node_fs.writeFileSync)(pkgPath, `${JSON.stringify(pkg, null, 2)}\n`, "utf-8");
768
- }
769
- try {
770
- await execPnpmInstall(workDir);
771
- } catch (installErr) {
772
- rollbackOverrides({
773
- usesWorkspaceYaml,
774
- workspaceYamlPath,
775
- packageName,
776
- oldOverride,
777
- pkg,
778
- pkgPath
779
- });
780
- rollback$1(pkgPath, pkgBackup, lockfilePath, lockBackup);
781
- if (usesWorkspaceYaml) rollback$1(workspaceYamlPath, workspaceBackup, lockfilePath, null);
782
- cleanupBackups({
783
- pkgBackup,
784
- lockBackup,
785
- workspaceBackup
786
- });
787
- return {
788
- packageName,
789
- fromVersion,
790
- toVersion,
791
- isMajor,
792
- success: false,
793
- error: `pnpm install failed: ${getStderr(installErr)}`
794
- };
795
- }
796
- cleanupBackups({
797
- pkgBackup,
798
- lockBackup,
799
- workspaceBackup
800
- });
801
- return {
802
- packageName,
803
- fromVersion,
804
- toVersion,
805
- isMajor,
806
- success: true
807
- };
808
- }
809
- /**
810
- * 从版本声明中提取前缀(^ / ~ / 空)。
811
- *
812
- * @example
813
- * extractPrefix('^4.17.20') // '^'
814
- * extractPrefix('~4.17.0') // '~'
815
- * extractPrefix('4.17.20') // ''
816
- * extractPrefix('*') // '^' (默认)
817
- */
818
- function extractPrefix(version) {
819
- const trimmed = version.trim();
820
- if (/^\d/.test(trimmed)) return "";
821
- if (trimmed.startsWith("^")) return "^";
822
- if (trimmed.startsWith("~")) return "~";
823
- return "^";
824
- }
825
- /**
826
- * 从版本声明中提取主版本号(major)。
827
- * 去除前缀后匹配第一个 semver 段。
828
- *
829
- * @example
830
- * parseMajorVersion('^4.17.20') // 4
831
- * parseMajorVersion('~2.0.0') // 2
832
- * parseMajorVersion('1.2.3') // 1
833
- * parseMajorVersion('*') // -1
834
- */
835
- function parseMajorVersion(version) {
836
- const cleaned = version.replace(/^\s*[\^~>=<]*\s*/, "");
837
- const match = /^(\d+)\.\d+\.\d+/.exec(cleaned);
838
- return match ? Number.parseInt(match[1], 10) : -1;
839
- }
840
- /**
841
- * 在 `package.json` 的 `dependencies` / `devDependencies` / `optionalDependencies` 中按顺序查找包。
842
- */
843
- function findDependencyVersion(pkg, packageName) {
844
- for (const group of [
845
- "dependencies",
846
- "devDependencies",
847
- "optionalDependencies"
848
- ]) {
849
- const deps = pkg[group];
850
- if (deps && typeof deps === "object" && packageName in deps) return {
851
- group,
852
- version: deps[packageName]
853
- };
854
- }
855
- return null;
856
- }
857
- /**
858
- * 确保 `package.json` 中存在 `pnpm.overrides` 对象,若不存在则创建。
859
- * 返回 `overrides` 对象的引用,调用方可直接写入。
860
- */
861
- function ensurePnpmOverrides(pkg) {
862
- if (!pkg.pnpm) pkg.pnpm = {};
863
- if (!pkg.pnpm.overrides) pkg.pnpm.overrides = {};
864
- return pkg.pnpm.overrides;
865
- }
866
- /**
867
- * 从 `pnpm-lock.yaml` 中读取指定包的当前锁定版本。
868
- * 通过简单的正则匹配查找,不解析完整 YAML(性能优先)。
869
- *
870
- * pnpm v9+ lockfile 格式(v9.0 起):
871
- * /package-name/version:
872
- * /@scope/package-name/version:
873
- *
874
- * @returns 锁定版本字符串(如 `'5.0.0'`),未找到返回 `null`
875
- */
876
- function readLockfileVersion(lockfilePath, packageName) {
877
- if (!(0, node_fs.existsSync)(lockfilePath)) return null;
878
- try {
879
- const content = (0, node_fs.readFileSync)(lockfilePath, "utf-8");
880
- const escapedName = escapeRegExp(packageName);
881
- const match = new RegExp(`^/${escapedName}/(\\d+(?:\\.\\d+(?:\\.\\d+)?(?:-[a-zA-Z0-9.]+)?)):`, "m").exec(content);
882
- return match ? match[1] : null;
883
- } catch {
884
- return null;
885
- }
886
- }
887
- /**
888
- * 转义正则表达式特殊字符。
889
- */
890
- function escapeRegExp(str) {
891
- return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
892
- }
893
- /**
894
- * 执行 `pnpm install --no-frozen-lockfile`。
895
- * 将同步 `execSync` 包装为 Promise,使上层 `upgradeDependency` 保持 async 语义。
896
- */
897
- function execPnpmInstall(workDir) {
898
- return new Promise((resolve, reject) => {
899
- try {
900
- (0, node_child_process.execSync)("pnpm install --no-frozen-lockfile", {
901
- cwd: workDir,
902
- encoding: "utf-8",
903
- stdio: "pipe",
904
- timeout: 12e4
905
- });
906
- resolve();
907
- } catch (err) {
908
- reject(err instanceof Error ? err : new Error(String(err)));
909
- }
910
- });
911
- }
912
- function failResult(packageName, targetVersion, fromVersion, error) {
913
- return {
914
- packageName,
915
- fromVersion,
916
- toVersion: targetVersion,
917
- isMajor: false,
918
- success: false,
919
- error
920
- };
921
- }
922
- function rollback$1(pkgPath, pkgBackup, lockfilePath, lockBackup) {
923
- try {
924
- if ((0, node_fs.existsSync)(pkgBackup)) (0, node_fs.copyFileSync)(pkgBackup, pkgPath);
925
- if (lockBackup !== null && (0, node_fs.existsSync)(lockBackup)) (0, node_fs.copyFileSync)(lockBackup, lockfilePath);
926
- } catch {}
927
- }
928
- /**
929
- * 安全删除备份文件,失败静默忽略。
930
- */
931
- function safeUnlink(filePath) {
932
- try {
933
- if ((0, node_fs.existsSync)(filePath)) (0, node_fs.unlinkSync)(filePath);
934
- } catch {}
935
- }
936
- /**
937
- * 清理所有备份文件。
938
- */
939
- function cleanupBackups(paths) {
940
- safeUnlink(paths.pkgBackup);
941
- if (paths.lockBackup !== null) safeUnlink(paths.lockBackup);
942
- if (paths.workspaceBackup) safeUnlink(paths.workspaceBackup);
943
- }
944
- /**
945
- * 在 `pnpm-workspace.yaml` 中写入一条 override。
946
- *
947
- * 读取现有 YAML → 设置 `overrides[packageName] = version` → 原样写回。
948
- *
949
- * @returns 写入前该包的旧值(`undefined` 表示原先不存在该 override)
950
- */
951
- function writeWorkspaceOverride(workspaceYamlPath, packageName, version) {
952
- let doc;
953
- try {
954
- const raw = (0, node_fs.readFileSync)(workspaceYamlPath, "utf-8");
955
- doc = yaml.default.parse(raw) ?? {};
956
- } catch {
957
- doc = {};
958
- }
959
- const overrides = doc.overrides ?? {};
960
- const old = overrides[packageName];
961
- overrides[packageName] = version;
962
- doc.overrides = overrides;
963
- (0, node_fs.writeFileSync)(workspaceYamlPath, yaml.default.stringify(doc), "utf-8");
964
- return old;
965
- }
966
- /**
967
- * 回滚 override 写入操作。
968
- *
969
- * - `pnpm-workspace.yaml` 模式:删除刚写入的条目,或恢复旧值
970
- * - `package.json` 模式:同理操作 `pkg.pnpm.overrides`
971
- */
972
- function rollbackOverrides(params) {
973
- const { usesWorkspaceYaml, workspaceYamlPath, packageName, oldOverride, pkg, pkgPath } = params;
974
- if (usesWorkspaceYaml) try {
975
- const raw = (0, node_fs.readFileSync)(workspaceYamlPath, "utf-8");
976
- const doc = yaml.default.parse(raw) ?? {};
977
- const overrides = doc.overrides ?? {};
978
- if (oldOverride === void 0) {
979
- const next = Object.fromEntries(Object.entries(overrides).filter(([key]) => key !== packageName));
980
- if (Object.keys(next).length === 0) delete doc.overrides;
981
- else doc.overrides = next;
982
- } else overrides[packageName] = oldOverride;
983
- (0, node_fs.writeFileSync)(workspaceYamlPath, yaml.default.stringify(doc), "utf-8");
984
- } catch {}
985
- else {
986
- if (pkg.pnpm?.overrides) {
987
- const overrides = pkg.pnpm.overrides;
988
- if (oldOverride === void 0) {
989
- const next = Object.fromEntries(Object.entries(overrides).filter(([key]) => key !== packageName));
990
- if (Object.keys(next).length === 0) {
991
- delete pkg.pnpm.overrides;
992
- if (Object.keys(pkg.pnpm).length === 0) delete pkg.pnpm;
993
- } else pkg.pnpm.overrides = next;
994
- } else overrides[packageName] = oldOverride;
995
- }
996
- (0, node_fs.writeFileSync)(pkgPath, `${JSON.stringify(pkg, null, 2)}\n`, "utf-8");
997
- }
998
- }
999
- function getStderr(err) {
1000
- if (err instanceof Error && "stderr" in err) {
1001
- const stderr = err.stderr;
1002
- if (typeof stderr === "string") return stderr.trim();
1003
- if (Buffer.isBuffer(stderr)) return stderr.toString("utf-8").trim();
1004
- return typeof stderr === "object" && stderr !== null ? JSON.stringify(stderr) : String(stderr);
1005
- }
1006
- if (err instanceof Error) return err.message;
1007
- return typeof err === "object" && err !== null ? JSON.stringify(err) : String(err);
1008
- }
1009
-
1010
- //#endregion
1011
- //#region src/fixers/pnpm/index.ts
1012
- /**
1013
- * 执行 `pnpm i --frozen-lockfile` 并解析失败分类。
1014
- * 成功时返回 { ok: true }。
1015
- */
1016
- function classifyLockfileFailure(workDir) {
1017
- try {
1018
- (0, node_child_process.execSync)("pnpm install --frozen-lockfile", {
1019
- cwd: workDir,
1020
- stdio: "pipe",
1021
- timeout: 12e4
1022
- });
1023
- return { ok: true };
1024
- } catch (err) {
1025
- const stderr = getStderrText(err);
1026
- if (!stderr) return {
1027
- ok: false,
1028
- category: "UNKNOWN",
1029
- stderr: ""
1030
- };
1031
- return {
1032
- ok: false,
1033
- category: parseFailureCategory(stderr),
1034
- stderr
1035
- };
1036
- }
1037
- }
1038
- const FAILURE_PATTERNS = [
1039
- ["LOCKFILE_NOT_FOUND", /no lockfile|Cannot find.*pnpm-lock\.yaml|ENOENT.*lockfile/i],
1040
- ["MANIFEST_MISMATCH", /ERR_PNPM_OUTDATED_LOCKFILE|out of sync|needs update/i],
1041
- ["CREDENTIAL_ERROR", /E401|ERR_PNPM_FETCH_401|ERR_PNPM_FETCH_403|authentication failed|Authorization/i],
1042
- ["CORRUPTED_LOCKFILE", /\b(broken|corrupted|Cannot read).*lockfile|ERR_PNPM_BROKEN_LOCKFILE/i],
1043
- ["LOCKFILE_VERSION_MISMATCH", /lockfileVersion.*incompatible|unsupported lockfile version|lockfile had been generated with/i],
1044
- ["RESOLVE_ERROR", /ERR_PNPM_NO_MATCHING_VERSION|resolution|resolve.*failed|ERR_PNPM_PEER_DEP_ISSUES/i]
1045
- ];
1046
- /**
1047
- * 按优先级匹配 stderr,返回首个命中的 FailureCategory。
1048
- * 不匹配任何特征时返回 'UNKNOWN'。
1049
- */
1050
- function parseFailureCategory(stderr) {
1051
- for (const [category, pattern] of FAILURE_PATTERNS) if (pattern.test(stderr)) return category;
1052
- return "UNKNOWN";
1053
- }
1054
- function getStderrText(err) {
1055
- if (!err.stderr) return err.message ?? "";
1056
- if (typeof err.stderr === "string") return err.stderr;
1057
- return err.stderr.toString("utf-8");
1058
- }
1059
- function execRepair(command, workDir, timeoutMs = 18e4) {
1060
- (0, node_child_process.execSync)(command, {
1061
- cwd: workDir,
1062
- stdio: "pipe",
1063
- timeout: timeoutMs
1064
- });
1065
- }
1066
- /**
1067
- * 确定某个分类下适用的修复策略链(从首到末)。
1068
- * CREDENTIAL_ERROR 返回空链(不可修复)。
1069
- */
1070
- function getStrategyChain(category) {
1071
- switch (category) {
1072
- case "LOCKFILE_NOT_FOUND":
1073
- case "MANIFEST_MISMATCH": return ["REGENERATE", "REINSTALL"];
1074
- case "CORRUPTED_LOCKFILE": return [
1075
- "FIX_ENTRIES",
1076
- "REGENERATE",
1077
- "REINSTALL"
1078
- ];
1079
- case "LOCKFILE_VERSION_MISMATCH": return [
1080
- "PIN_TOOLCHAIN",
1081
- "REGENERATE",
1082
- "REINSTALL"
1083
- ];
1084
- case "RESOLVE_ERROR": return [
1085
- "REGENERATE",
1086
- "FIX_ENTRIES",
1087
- "REINSTALL"
1088
- ];
1089
- case "UNKNOWN": return ["REGENERATE", "REINSTALL"];
1090
- case "CREDENTIAL_ERROR": return [];
1091
- }
1092
- }
1093
- function getStrategyCommand(strategy) {
1094
- switch (strategy) {
1095
- case "REGENERATE": return "pnpm install --lockfile-only";
1096
- case "FIX_ENTRIES": return "pnpm install --fix-lockfile --lockfile-only";
1097
- case "REINSTALL": return "pnpm install --no-frozen-lockfile";
1098
- case "PIN_TOOLCHAIN": return "pnpm install --lockfile-only";
1099
- default: return "";
1100
- }
1101
- }
1102
- /**
1103
- * 比较修复前后 lockfile,返回行数与包数量变化摘要。
1104
- */
1105
- function computeLockfileDiff(beforePath, afterPath) {
1106
- const beforeLines = countLockfileLines(beforePath);
1107
- const afterLines = countLockfileLines(afterPath);
1108
- const beforePackages = countLockfilePackages(beforePath);
1109
- const afterPackages = countLockfilePackages(afterPath);
1110
- const linesChanged = afterLines - beforeLines;
1111
- const packagesChanged = afterPackages - beforePackages;
1112
- const parts = [];
1113
- if (linesChanged !== 0) parts.push(`${linesChanged > 0 ? "+" : ""}${linesChanged} lines`);
1114
- if (packagesChanged !== 0) parts.push(`${packagesChanged > 0 ? "+" : ""}${packagesChanged} packages`);
1115
- return {
1116
- linesChanged,
1117
- packagesChanged,
1118
- summary: parts.length > 0 ? `lockfile updated: ${parts.join(", ")}` : "lockfile unchanged"
1119
- };
1120
- }
1121
- function countLockfileLines(path) {
1122
- if (!(0, node_fs.existsSync)(path)) return 0;
1123
- return (0, node_fs.readFileSync)(path, "utf-8").split("\n").length;
1124
- }
1125
- /**
1126
- * 统计 pnpm-lock.yaml 中 `packages:` 块下的条目数。
1127
- * packages 条目以缩进 + `/` 开头(如 ` /lodash@4.17.21:`)。
1128
- */
1129
- function countLockfilePackages(path) {
1130
- if (!(0, node_fs.existsSync)(path)) return 0;
1131
- const lines = (0, node_fs.readFileSync)(path, "utf-8").split("\n");
1132
- let inPackages = false;
1133
- let count = 0;
1134
- for (const line of lines) {
1135
- if (!inPackages) {
1136
- if (/^packages:\s*$/.test(line)) inPackages = true;
1137
- continue;
1138
- }
1139
- if (/^[a-zA-Z]/.test(line)) break;
1140
- if (/^\s+\//.test(line)) count++;
1141
- }
1142
- return count;
1143
- }
1144
- /**
1145
- * 从 package.json 的 `packageManager` 字段解析 pnpm 版本。
1146
- * 优先级: toolchain.pnpmVersion > packageManager > null
1147
- */
1148
- function resolvePnpmVersion(workDir, toolchain) {
1149
- if (toolchain?.pnpmVersion) return toolchain.pnpmVersion;
1150
- const pkgPath = (0, node_path.join)(workDir, "package.json");
1151
- if (!(0, node_fs.existsSync)(pkgPath)) return null;
1152
- try {
1153
- const pm = JSON.parse((0, node_fs.readFileSync)(pkgPath, "utf-8")).packageManager;
1154
- if (typeof pm === "string") {
1155
- const match = /pnpm@(.+)$/.exec(pm);
1156
- if (match?.[1]) return match[1];
1157
- }
1158
- } catch {}
1159
- return null;
1160
- }
1161
- const LOCKFILE_NAME = "pnpm-lock.yaml";
1162
- /**
1163
- * 在 workDir 中使用 `pnpm install --frozen-lockfile` 验证 lockfile 合规性。
1164
- * 验证通过 → 返回 ret:0;失败 → 返回 ret:1 + stderr。
1165
- * 不抛异常——调用方通过 ret 判断结果。
1166
- */
1167
- function verifyFrozenLockfile(workDir) {
1168
- try {
1169
- (0, node_child_process.execSync)("pnpm install --frozen-lockfile", {
1170
- cwd: workDir,
1171
- stdio: "pipe",
1172
- timeout: 12e4
1173
- });
1174
- return {
1175
- ret: 0,
1176
- stderr: ""
1177
- };
1178
- } catch (err) {
1179
- return {
1180
- ret: 1,
1181
- stderr: getStderrText(err)
1182
- };
1183
- }
1184
- }
1185
- /**
1186
- * 备份 pnpm-lock.yaml → `<name>.bak`,返回备份路径。
1187
- * lockfile 不存在时会创建空 .bak(回滚时删除即可)。
1188
- */
1189
- function backupLockfilePath(workDir) {
1190
- const src = (0, node_path.join)(workDir, LOCKFILE_NAME);
1191
- const bak = `${src}.bak`;
1192
- if ((0, node_fs.existsSync)(src)) (0, node_fs.copyFileSync)(src, bak);
1193
- return bak;
1194
- }
1195
- /** 从 .bak 还原 lockfile,并删除 .bak。 */
1196
- function rollback(bakPath, workDir) {
1197
- const target = (0, node_path.join)(workDir, LOCKFILE_NAME);
1198
- try {
1199
- if ((0, node_fs.existsSync)(bakPath)) (0, node_fs.copyFileSync)(bakPath, target);
1200
- } finally {
1201
- try {
1202
- (0, node_fs.rmSync)(bakPath);
1203
- } catch {}
1204
- }
1205
- }
1206
- /**
1207
- * 检测并修复 pnpm frozen-lockfile 漂移。
1208
- *
1209
- * 流程:
1210
- * 1. 先跑 `pnpm i --frozen-lockfile` 诊断
1211
- * 2. 若已合规 → 直接返回 success
1212
- * 3. 若为 CREDENTIAL_ERROR → SKIP
1213
- * 4. 按分类选取策略链,逐级尝试修复:
1214
- * - 每个策略执行后立即用 `pnpm i --frozen-lockfile` 验证
1215
- * - 验证通过 → 记录 diff、返回 success
1216
- * - 验证失败 → 继续下一个策略
1217
- * 5. 所有策略失败 → 回滚 lockfile、返回失败结果
1218
- */
1219
- function repairLockfile(params) {
1220
- const { workDir } = params;
1221
- const workDir_ = workDir;
1222
- const attempts = [];
1223
- const bakPath = backupLockfilePath(workDir_);
1224
- const classification = classifyLockfileFailure(workDir_);
1225
- if (classification.ok) {
1226
- try {
1227
- (0, node_fs.rmSync)(bakPath);
1228
- } catch {}
1229
- return {
1230
- success: true,
1231
- diff: null,
1232
- attemptHistory: attempts
1233
- };
1234
- }
1235
- const category = classification.category ?? "UNKNOWN";
1236
- const stderr = classification.stderr ?? "";
1237
- if (category === "CREDENTIAL_ERROR") {
1238
- try {
1239
- (0, node_fs.rmSync)(bakPath);
1240
- } catch {}
1241
- return {
1242
- success: false,
1243
- failureCategory: "CREDENTIAL_ERROR",
1244
- failureDetail: `registry authentication error: ${truncate(stderr, 500)}`,
1245
- strategy: "SKIP",
1246
- attemptHistory: attempts
1247
- };
1248
- }
1249
- const strategyChain = getStrategyChain(category);
1250
- let lastError = stderr;
1251
- const startTimestamp = Date.now();
1252
- for (const strategy of strategyChain) {
1253
- const command = getStrategyCommand(strategy);
1254
- const t0 = Date.now();
1255
- let strategySuccess = false;
1256
- let strategyError;
1257
- try {
1258
- execRepair(command, workDir_);
1259
- strategySuccess = true;
1260
- } catch (err) {
1261
- strategyError = getStderrText(err) || err.message;
1262
- }
1263
- const durationMs = Date.now() - t0;
1264
- attempts.push({
1265
- strategy,
1266
- command,
1267
- success: strategySuccess,
1268
- error: strategyError,
1269
- durationMs
1270
- });
1271
- if (!strategySuccess) {
1272
- lastError = strategyError ?? lastError;
1273
- continue;
1274
- }
1275
- const verify = verifyFrozenLockfile(workDir_);
1276
- if (verify.ret === 0) {
1277
- const diff = computeLockfileDiff(bakPath, (0, node_path.join)(workDir_, LOCKFILE_NAME));
1278
- try {
1279
- (0, node_fs.rmSync)(bakPath);
1280
- } catch {}
1281
- return {
1282
- success: true,
1283
- strategy,
1284
- diff,
1285
- attemptHistory: attempts
1286
- };
1287
- }
1288
- lastError = verify.stderr;
1289
- }
1290
- rollback(bakPath, workDir_);
1291
- const durationMs = Date.now() - startTimestamp;
1292
- attempts.push({
1293
- strategy: "SKIP",
1294
- command: "rollback",
1295
- success: true,
1296
- durationMs
1297
- });
1298
- return {
1299
- success: false,
1300
- failureCategory: category,
1301
- failureDetail: truncate(lastError, 500),
1302
- attemptHistory: attempts
1303
- };
1304
- }
1305
- function truncate(text, maxLen) {
1306
- if (text.length <= maxLen) return text;
1307
- return `${text.slice(0, maxLen)}…`;
1308
- }
1309
-
1310
- //#endregion
1311
- //#region src/runners/verification-runner.ts
1312
- const DEFAULT_COMMANDS = [
1313
- "pnpm install --frozen-lockfile",
1314
- "pnpm lint",
1315
- "pnpm build"
1316
- ];
1317
- const MAX_OUTPUT_LINES = 200;
1318
- /**
1319
- * 按顺序在工作目录执行命令序列,任一命令失败则停止。
1320
- *
1321
- * @example
1322
- * ```ts
1323
- * const result = await runVerification({ workDir: '/repo' })
1324
- * if (!result.success) {
1325
- * console.error(result.failure)
1326
- * }
1327
- * ```
1328
- */
1329
- async function runVerification(params) {
1330
- const commands = params.commands ?? DEFAULT_COMMANDS;
1331
- const commandResults = [];
1332
- for (const command of commands) {
1333
- const result = await execCommand(command, params.workDir);
1334
- commandResults.push(result);
1335
- if (result.exitCode !== 0) return {
1336
- success: false,
1337
- commandResults,
1338
- failedCommand: command,
1339
- failure: `command "${command}" exited with code ${result.exitCode}`
1340
- };
1341
- }
1342
- return {
1343
- success: true,
1344
- commandResults
1345
- };
1346
- }
1347
- /**
1348
- * 执行单条 shell 命令,捕获 stdout/stderr(截断到 200 行)并脱敏。
1349
- */
1350
- function execCommand(command, workDir) {
1351
- return new Promise((resolve) => {
1352
- const startTime = Date.now();
1353
- const cp = (0, node_child_process.spawn)(command, {
1354
- cwd: workDir,
1355
- shell: true,
1356
- stdio: "pipe"
1357
- });
1358
- const stdoutChunks = [];
1359
- const stderrChunks = [];
1360
- cp.stdout.on("data", (data) => {
1361
- stdoutChunks.push(data.toString("utf-8"));
1362
- });
1363
- cp.stderr.on("data", (data) => {
1364
- stderrChunks.push(data.toString("utf-8"));
1365
- });
1366
- cp.on("error", (err) => {
1367
- resolve({
1368
- command,
1369
- exitCode: -1,
1370
- durationMs: Date.now() - startTime,
1371
- stdout: "",
1372
- stderr: sanitizeOutput(err.message)
1373
- });
1374
- });
1375
- cp.on("close", (code) => {
1376
- const stdout = truncateLines(stdoutChunks.join(""), MAX_OUTPUT_LINES);
1377
- const stderr = truncateLines(stderrChunks.join(""), MAX_OUTPUT_LINES);
1378
- resolve({
1379
- command,
1380
- exitCode: code ?? -1,
1381
- durationMs: Date.now() - startTime,
1382
- stdout: sanitizeOutput(stdout),
1383
- stderr: sanitizeOutput(stderr)
1384
- });
1385
- });
1386
- });
1387
- }
1388
- /** 截断文本到指定行数 */
1389
- function truncateLines(text, maxLines) {
1390
- const lines = text.split("\n");
1391
- if (lines.length <= maxLines) return text;
1392
- return `${lines.slice(0, maxLines).join("\n")}\n... (truncated, ${lines.length - maxLines} more lines)`;
1393
- }
1394
- /**
1395
- * 脱敏处理:移除命令输出中可能包含的敏感信息。
1396
- *
1397
- * 过滤规则:
1398
- * - 包含 `token=` / `secret=` / `password=` 的行
1399
- * - `npm_config_` / `GITHUB_TOKEN` / `NPM_TOKEN` 等环境变量暴露
1400
- * - URL 中的 user:password@ 模式
1401
- */
1402
- const SECRET_PATTERNS = [
1403
- [/^(GITHUB_TOKEN|NPM_TOKEN|NODE_AUTH_TOKEN|DOCKER_PASSWORD)=.+/gim, "$1=***"],
1404
- [/\b(token|secret|password|api[_-]?key)\s*[=:]\s*\S+/gi, "$1=***"],
1405
- [/\/\/[^@\n]+@/g, "//***@"],
1406
- [/npm_config_[a-z_]+=\S+/g, "npm_config_***=***"]
1407
- ];
1408
- /**
1409
- * 对外暴露以支持单独测试
1410
- */
1411
- function sanitizeOutput(output) {
1412
- let result = output;
1413
- for (const [pattern, replacement] of SECRET_PATTERNS) result = result.replace(pattern, replacement);
1414
- return result;
1415
- }
1416
-
1417
- //#endregion
1418
- //#region src/app-helpers.ts
1419
- const DEFAULT_VERIFY_COMMANDS = [
1420
- "pnpm install --frozen-lockfile",
1421
- "pnpm lint",
1422
- "pnpm build"
1423
- ];
1424
- /** 匹配 `pnpm <singleWord>` 模式的命令(可能是 package.json script 引用) */
1425
- const PNPM_SCRIPT_RE = /^pnpm\s+([a-zA-Z][a-zA-Z0-9:_-]*)$/;
1426
- /** 自动修复提交的标准消息(本地 --commit 与 fix-and-pr 共用) */
1427
- const FIX_COMMIT_MESSAGE = "fix(deps): automated dependfix security repair";
1428
- /**
1429
- * 升级单个告警对应的依赖(dry-run 仅记录)。
1430
- * 优先直接升级,失败且为间接依赖时回退到 pnpm overrides。
1431
- */
1432
- async function upgradeAlert(ctx, alert) {
1433
- const { config, logger, workDir } = ctx;
1434
- const startMs = Date.now();
1435
- if (config.dryRun) {
1436
- logger.info(`[dry-run] Would upgrade ${alert.packageName} to ${alert.recommendedVersion}`);
1437
- return {
1438
- type: "dependency-upgrade",
1439
- repository: alert.repository,
1440
- target: alert.packageName,
1441
- fromVersion: alert.recommendedVersion ? `< ${alert.recommendedVersion}` : void 0,
1442
- toVersion: alert.recommendedVersion,
1443
- isMajor: false,
1444
- success: true,
1445
- durationMs: 0
1446
- };
1447
- }
1448
- try {
1449
- let result = await upgradeDependency({
1450
- packageName: alert.packageName,
1451
- targetVersion: alert.recommendedVersion,
1452
- workDir
1453
- });
1454
- if (!result.success && result.error?.includes("not found in dependencies")) {
1455
- result = await overrideTransitiveDependency({
1456
- packageName: alert.packageName,
1457
- targetVersion: alert.recommendedVersion,
1458
- workDir
1459
- });
1460
- logger.info(result.success ? `Upgraded ${result.packageName}: ${result.fromVersion} → ${result.toVersion} (pnpm overrides)` : `Failed to upgrade ${result.packageName}: ${result.error}`);
1461
- } else logger.info(result.success ? `Upgraded ${result.packageName}: ${result.fromVersion} → ${result.toVersion}` : `Failed to upgrade ${result.packageName}: ${result.error}`);
1462
- return {
1463
- type: "dependency-upgrade",
1464
- repository: alert.repository,
1465
- target: alert.packageName,
1466
- fromVersion: result.fromVersion,
1467
- toVersion: result.toVersion,
1468
- isMajor: result.isMajor,
1469
- success: result.success,
1470
- error: result.error,
1471
- durationMs: Date.now() - startMs
1472
- };
1473
- } catch (error) {
1474
- const message = (0, _dependfix_core.toErrorMessage)(error);
1475
- logger.error(`Upgrade error for ${alert.packageName}: ${message}`);
1476
- return {
1477
- type: "dependency-upgrade",
1478
- repository: alert.repository,
1479
- target: alert.packageName,
1480
- toVersion: alert.recommendedVersion,
1481
- success: false,
1482
- error: message,
1483
- durationMs: Date.now() - startMs
1484
- };
1485
- }
1486
- }
1487
- /** 尝试修复 pnpm-lock.yaml(dry-run 仅记录)。 */
1488
- function tryLockfileRepair(ctx, repo) {
1489
- const { config, logger, workDir } = ctx;
1490
- const startMs = Date.now();
1491
- if (config.dryRun) {
1492
- logger.info(`[dry-run] Would attempt lockfile repair for ${repo}`);
1493
- return {
1494
- type: "lockfile-repair",
1495
- repository: repo,
1496
- target: "pnpm-lock.yaml",
1497
- success: true,
1498
- durationMs: 0
1499
- };
1500
- }
1501
- try {
1502
- const result = repairLockfile({ workDir });
1503
- logger.info(result.success ? `Lockfile repaired for ${repo} (strategy: ${result.strategy ?? "N/A"})` : `Lockfile repair failed for ${repo}: ${result.failureDetail ?? "unknown"}`);
1504
- return {
1505
- type: "lockfile-repair",
1506
- repository: repo,
1507
- target: "pnpm-lock.yaml",
1508
- success: result.success,
1509
- error: result.success ? void 0 : result.failureDetail ?? "Lockfile repair failed",
1510
- strategy: result.strategy,
1511
- durationMs: Date.now() - startMs,
1512
- diff: result.diff?.summary
1513
- };
1514
- } catch (error) {
1515
- const message = (0, _dependfix_core.toErrorMessage)(error);
1516
- logger.error(`Lockfile repair error for ${repo}: ${message}`);
1517
- return {
1518
- type: "lockfile-repair",
1519
- repository: repo,
1520
- target: "pnpm-lock.yaml",
1521
- success: false,
1522
- error: message,
1523
- durationMs: Date.now() - startMs
1524
- };
1525
- }
1526
- }
1527
- /** 执行验证命令链,返回逐命令结果。 */
1528
- async function verifyProject(ctx, repo) {
1529
- const { customCommands, logger, workDir, allErrors } = ctx;
1530
- const rawCommands = customCommands ?? DEFAULT_VERIFY_COMMANDS;
1531
- const { valid, skipped } = !customCommands ? validateVerifyCommands(rawCommands, workDir) : {
1532
- valid: rawCommands,
1533
- skipped: []
1534
- };
1535
- for (const cmd of skipped) {
1536
- logger.info(`Skipping command "${cmd}": script not found in package.json`);
1537
- allErrors.push({
1538
- repository: repo,
1539
- target: cmd,
1540
- stage: "verify",
1541
- category: "SCRIPT_NOT_FOUND",
1542
- message: `Skipped: no matching script in package.json for "${cmd}"`
1543
- });
1544
- }
1545
- if (valid.length === 0) {
1546
- logger.info(`No verification commands to run for ${repo}`);
1547
- return [];
1548
- }
1549
- try {
1550
- return (await runVerification({
1551
- workDir,
1552
- commands: valid
1553
- })).commandResults.map((cr) => ({
1554
- type: "verification",
1555
- repository: repo,
1556
- target: cr.command,
1557
- success: cr.exitCode === 0,
1558
- error: cr.exitCode !== 0 ? `exit code ${cr.exitCode}` : void 0,
1559
- durationMs: cr.durationMs
1560
- }));
1561
- } catch (error) {
1562
- const message = (0, _dependfix_core.toErrorMessage)(error);
1563
- logger.error(`Verification error for ${repo}: ${message}`);
1564
- return [{
1565
- type: "verification",
1566
- repository: repo,
1567
- target: "verification",
1568
- success: false,
1569
- error: message
1570
- }];
1571
- }
1572
- }
1573
- /**
1574
- * 校验默认命令链中的脚本引用是否存在。
1575
- *
1576
- * - `pnpm install --frozen-lockfile` 等非脚本命令 → 直接保留
1577
- * - `pnpm lint` 等脚本命令 → 检查 `package.json#scripts` 是否存在对应键
1578
- * - 用户自定义命令(`--commands`)不经过此校验
1579
- */
1580
- function validateVerifyCommands(commands, workDir) {
1581
- const pkgJsonPath = (0, node_path.join)(workDir, "package.json");
1582
- let pkgScripts = {};
1583
- if ((0, node_fs.existsSync)(pkgJsonPath)) try {
1584
- pkgScripts = JSON.parse((0, node_fs.readFileSync)(pkgJsonPath, "utf-8")).scripts ?? {};
1585
- } catch {
1586
- return {
1587
- valid: commands,
1588
- skipped: []
1589
- };
1590
- }
1591
- const valid = [];
1592
- const skipped = [];
1593
- for (const cmd of commands) {
1594
- const match = PNPM_SCRIPT_RE.exec(cmd);
1595
- if (match) {
1596
- const scriptName = match[1];
1597
- if (pkgScripts[scriptName]) valid.push(cmd);
1598
- else skipped.push(cmd);
1599
- } else valid.push(cmd);
1600
- }
1601
- return {
1602
- valid,
1603
- skipped
1604
- };
1605
- }
1606
- /**
1607
- * 将修复产生的变更提交到本地当前分支。
1608
- *
1609
- * - 无任何变更(含已暂存变更)时跳过
1610
- * - config 校验已保证 `commit` 与 `dryRun` / `createPullRequest` 互斥,
1611
- * 因此这里不需要再检查这两个开关
1612
- */
1613
- function commitLocalChanges(ctx) {
1614
- const { logger, workDir } = ctx;
1615
- if (!hasGitChanges(workDir)) {
1616
- logger.info("No changes to commit — skipping local commit");
1617
- return;
1618
- }
1619
- ensureGitignore(workDir);
1620
- stageAndCommit(FIX_COMMIT_MESSAGE, workDir);
1621
- logger.info(`Committed fix changes to current branch: ${FIX_COMMIT_MESSAGE}`);
1622
- }
1623
- /**
1624
- * 检查工作目录是否有未提交的变更(含未暂存与已暂存)。
1625
- */
1626
- function hasGitChanges(workDir) {
1627
- try {
1628
- (0, node_child_process.execSync)("git diff --quiet", {
1629
- cwd: workDir,
1630
- stdio: "pipe"
1631
- });
1632
- (0, node_child_process.execSync)("git diff --cached --quiet", {
1633
- cwd: workDir,
1634
- stdio: "pipe"
1635
- });
1636
- return false;
1637
- } catch {
1638
- return true;
1639
- }
1640
- }
1641
- /**
1642
- * 确保目标仓库的 `.gitignore` 中包含 `dependfix-reports/`。
1643
- *
1644
- * - 仅在 workDir 是 git 仓库时执行
1645
- * - 已存在该条目时幂等跳过
1646
- * - 失败(权限、磁盘满等)静默降级
1647
- */
1648
- function ensureGitignore(workDir) {
1649
- try {
1650
- if (!(0, node_fs.existsSync)((0, node_path.join)(workDir, ".git"))) return;
1651
- const gitignorePath = (0, node_path.join)(workDir, ".gitignore");
1652
- const entry = "dependfix-reports/";
1653
- let content = "";
1654
- if ((0, node_fs.existsSync)(gitignorePath)) content = (0, node_fs.readFileSync)(gitignorePath, "utf-8");
1655
- if (content.split("\n").some((l) => l.trim() === entry)) return;
1656
- const block = `${content.endsWith("\n") || content.length === 0 ? "" : "\n"}# dependfix\n${entry}\n`;
1657
- (0, node_fs.writeFileSync)(gitignorePath, content + block, "utf-8");
1658
- } catch {}
1659
- }
1660
- /**
1661
- * 关闭被取代的旧 PR。单条关闭失败不中断其余 PR 的关闭,
1662
- * 失败记录为 `PR_CLOSE_FAILED`(与 PR 创建失败区分)。
1663
- */
1664
- async function closeSupersededPRs(ctx, client, owner, repo, supersedePRs) {
1665
- const { logger, allErrors } = ctx;
1666
- for (const old of supersedePRs) try {
1667
- await closePullRequest(client, owner, repo, old.number);
1668
- logger.info(`Closed superseded PR #${old.number}`);
1669
- } catch (error) {
1670
- const message = (0, _dependfix_core.toErrorMessage)(error);
1671
- logger.error(`Failed to close superseded PR #${old.number}: ${message}`);
1672
- allErrors.push({
1673
- repository: `${owner}/${repo}`,
1674
- stage: "report",
1675
- category: "PR_CLOSE_FAILED",
1676
- message: `Failed to close PR #${old.number}: ${message}`
1677
- });
1678
- }
1679
- }
1680
- /**
1681
- * (fix-and-pr + --cleanup-branches)将已合并的 dependfix 分支列为待清理清单,
1682
- * 记录到报告与日志,不执行删除。
1683
- */
1684
- async function reportCleanupCandidates(ctx, client) {
1685
- const { config, logger, allActions, allErrors } = ctx;
1686
- for (const repo of config.repositories) {
1687
- const [owner, name] = repo.split("/");
1688
- try {
1689
- const branches = await listDependfixBranches(client, owner, name);
1690
- for (const branch of branches) if ((await getBranchPrStatus(client, owner, name, branch)).merged) {
1691
- logger.info(`[cleanup] merged branch awaiting manual cleanup: ${branch}`);
1692
- allActions.push({
1693
- type: "branch-cleanup",
1694
- repository: repo,
1695
- target: branch,
1696
- success: true,
1697
- diff: "merged; run `dependfix cleanup-branches` to delete",
1698
- durationMs: 0
1699
- });
1700
- }
1701
- } catch (error) {
1702
- const message = (0, _dependfix_core.toErrorMessage)(error);
1703
- logger.error(`[cleanup] detection failed for ${repo}: ${message}`);
1704
- allErrors.push({
1705
- repository: repo,
1706
- stage: "report",
1707
- category: "CLEANUP_DETECT_FAILED",
1708
- message
1709
- });
1710
- }
1711
- }
1712
- }
1713
- /**
1714
- * 交互式确认删除。非 TTY(CI/管道)时直接拒绝。
1715
- */
1716
- function confirmCleanup(ctx, repo, candidates) {
1717
- const { logger } = ctx;
1718
- if (!process.stdin.isTTY) {
1719
- logger.warn("[cleanup] non-TTY environment — deletion requires interactive confirmation, aborting");
1720
- return Promise.resolve(false);
1721
- }
1722
- const rl = (0, node_readline.createInterface)({
1723
- input: process.stdin,
1724
- output: process.stdout
1725
- });
1726
- return new Promise((resolve) => {
1727
- rl.question(`Delete ${candidates.length} branch(es) from ${repo}? [y/N] `, (answer) => {
1728
- rl.close();
1729
- resolve(isConfirmAnswer(answer));
1730
- });
1731
- });
1732
- }
1733
- /** 汇总所有动作到 summary(alertsSkipped 已在 processRepoForFix 中累加)。 */
1734
- function computeSummary(ctx) {
1735
- const { config, allActions, allAlerts, summary } = ctx;
1736
- let fixed = 0;
1737
- let failed = 0;
1738
- let lockfileRepairs = 0;
1739
- let verificationsPassed = 0;
1740
- let verificationsFailed = 0;
1741
- for (const action of allActions) {
1742
- if (action.type === "dependency-upgrade") if (action.success) fixed++;
1743
- else failed++;
1744
- if (action.type === "lockfile-repair" && action.success) lockfileRepairs++;
1745
- if (action.type === "verification") if (action.success) verificationsPassed++;
1746
- else verificationsFailed++;
1747
- }
1748
- const fixable = allAlerts.filter((a) => a.fixable).length;
1749
- summary.repositoriesScanned = config.repositories.length;
1750
- summary.alertsFound = allAlerts.length;
1751
- summary.alertsFixable = fixable;
1752
- summary.alertsFixed = fixed;
1753
- summary.alertsFailed = failed;
1754
- summary.lockfileRepairs = lockfileRepairs;
1755
- summary.verificationsPassed = verificationsPassed;
1756
- summary.verificationsFailed = verificationsFailed;
1757
- }
1758
- /** 组装最终运行结果。 */
1759
- function buildRunResult(ctx) {
1760
- const reportConfig = {
1761
- mode: ctx.config.mode,
1762
- severityThreshold: ctx.config.severityThreshold,
1763
- repositories: ctx.config.repositories,
1764
- dryRun: ctx.config.dryRun,
1765
- createPullRequest: ctx.config.createPullRequest,
1766
- maxAlertsPerRepository: ctx.config.maxAlertsPerRepository
1767
- };
1768
- return {
1769
- runId: ctx.runId,
1770
- startedAt: ctx.startedAt,
1771
- finishedAt: ctx.finishedAt,
1772
- config: reportConfig,
1773
- summary: ctx.summary,
1774
- repositories: ctx.repoResults,
1775
- alerts: ctx.allAlerts,
1776
- actions: ctx.allActions,
1777
- errors: ctx.allErrors
1778
- };
1779
- }
1780
- /**
1781
- * 计算退出码:
1782
- * - 0: 全部仓库处理成功(无 failed actions、无 errors)
1783
- * - 1: 部分仓库失败
1784
- * - 2: 全部仓库失败(或无仓库被成功处理)
1785
- */
1786
- function computeExitCode(ctx) {
1787
- const { config, allErrors, allActions, repoResults } = ctx;
1788
- const hasErrors = allErrors.length > 0;
1789
- const hasFailures = allActions.some((a) => !a.success);
1790
- const hasRepoSuccess = repoResults.length > 0 && repoResults.some((r) => r.alertsCount > 0 || r.fixed > 0 || r.verificationPassed === true);
1791
- const hasCleanupSuccess = config.mode === "cleanup-branches" && allActions.some((a) => a.success && a.type === "branch-cleanup");
1792
- const hasSuccess = hasRepoSuccess || hasCleanupSuccess;
1793
- if (config.mode === "fix-and-pr") return 0;
1794
- if (!hasErrors && !hasFailures) return 0;
1795
- if (hasSuccess) return 1;
1796
- return 2;
1797
- }
1798
-
1799
- //#endregion
1800
- //#region src/app.ts
1801
- var DependfixApp = class {
1802
- config;
1803
- workDir;
1804
- logger;
1805
- verbose;
1806
- customCommands;
1807
- runId;
1808
- allAlerts = [];
1809
- allActions = [];
1810
- allErrors = [];
1811
- repoResults = [];
1812
- summary = (0, _dependfix_core.createEmptyRunSummary)();
1813
- startedAt = "";
1814
- finishedAt = "";
1815
- constructor(options) {
1816
- this.config = options.config;
1817
- this.workDir = options.workDir ?? process.cwd();
1818
- this.verbose = options.verbose ?? false;
1819
- this.customCommands = options.commands;
1820
- this.runId = `dependfix-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
1821
- this.logger = (0, _dependfix_core.createLogger)({
1822
- name: "dependfix",
1823
- minLevel: this.verbose ? "debug" : "info"
1824
- });
1825
- }
1826
- /** 供辅助方法使用的状态切片。 */
1827
- get ctx() {
1828
- return {
1829
- config: this.config,
1830
- workDir: this.workDir,
1831
- logger: this.logger,
1832
- customCommands: this.customCommands,
1833
- runId: this.runId,
1834
- allAlerts: this.allAlerts,
1835
- allActions: this.allActions,
1836
- allErrors: this.allErrors,
1837
- repoResults: this.repoResults,
1838
- summary: this.summary,
1839
- startedAt: this.startedAt,
1840
- finishedAt: this.finishedAt
1841
- };
1842
- }
1843
- /** 执行完整的编排流程,返回结构化结果和退出码。 */
1844
- async run() {
1845
- this.startedAt = (/* @__PURE__ */ new Date()).toISOString();
1846
- this.logger.info(`Starting dependfix run ${this.runId}`, {
1847
- mode: this.config.mode,
1848
- repositories: this.config.repositories
1849
- });
1850
- try {
1851
- await this.executeMode();
1852
- } catch (error) {
1853
- const message = (0, _dependfix_core.toErrorMessage)(error);
1854
- this.logger.error(`Unexpected error: ${message}`);
1855
- this.allErrors.push({
1856
- repository: "*",
1857
- stage: "report",
1858
- category: "FATAL",
1859
- message
1860
- });
1861
- }
1862
- this.finishedAt = (/* @__PURE__ */ new Date()).toISOString();
1863
- computeSummary(this.ctx);
1864
- const runResult = buildRunResult(this.ctx);
1865
- const exitCode = computeExitCode(this.ctx);
1866
- try {
1867
- (0, _dependfix_core.writeReport)((0, _dependfix_core.generateMarkdownReport)(runResult), (0, _dependfix_core.generateJsonReport)(runResult), this.startedAt, this.runId);
1868
- this.logger.info(`Reports written to ./dependfix-reports/`);
1869
- } catch (reportError) {
1870
- const message = (0, _dependfix_core.toErrorMessage)(reportError);
1871
- this.logger.error(`Failed to write reports: ${message}`);
1872
- }
1873
- ensureGitignore(this.workDir);
1874
- this.logger.info(`Run ${this.runId} completed`, { exitCode });
1875
- return {
1876
- result: runResult,
1877
- exitCode
1878
- };
1879
- }
1880
- async executeMode() {
1881
- switch (this.config.mode) {
1882
- case "report-only":
1883
- await this.executeReportMode();
1884
- break;
1885
- case "fix":
1886
- await this.executeFixMode();
1887
- break;
1888
- case "fix-and-pr":
1889
- await this.executeFixAndPrMode();
1890
- break;
1891
- case "cleanup-branches":
1892
- await this.executeCleanupBranchesMode();
1893
- break;
1894
- }
1895
- }
1896
- async executeReportMode() {
1897
- const client = this.createClient();
1898
- for (const repo of this.config.repositories) await this.processRepoForReport(client, repo);
1899
- }
1900
- async processRepoForReport(client, repo) {
1901
- const startTime = Date.now();
1902
- const [owner, name] = repo.split("/");
1903
- try {
1904
- const { filtered } = (0, _dependfix_core.filterAlerts)(await fetchDependabotAlerts(client, {
1905
- owner,
1906
- repo: name
1907
- }), { severityThreshold: this.config.severityThreshold });
1908
- const { limited } = (0, _dependfix_core.limitAlerts)((0, _dependfix_core.prioritizeAlerts)(filtered), this.config.maxAlertsPerRepository);
1909
- const defaultBranch = await this.fetchDefaultBranch(client, owner, name);
1910
- this.allAlerts.push(...limited);
1911
- this.repoResults.push({
1912
- repository: repo,
1913
- defaultBranch,
1914
- alertsCount: limited.length,
1915
- fixable: limited.filter((a) => a.fixable).length,
1916
- fixed: 0,
1917
- failed: 0,
1918
- lockfileRepaired: false,
1919
- durationMs: Date.now() - startTime
1920
- });
1921
- this.logger.info(`Fetched ${limited.length} alerts for ${repo}`);
1922
- } catch (error) {
1923
- const message = (0, _dependfix_core.toErrorMessage)(error);
1924
- this.logger.error(`Failed to fetch alerts for ${repo}: ${message}`);
1925
- this.allErrors.push({
1926
- repository: repo,
1927
- stage: "fetch",
1928
- category: "FETCH_FAILED",
1929
- message
1930
- });
1931
- this.repoResults.push({
1932
- repository: repo,
1933
- defaultBranch: "",
1934
- alertsCount: 0,
1935
- fixable: 0,
1936
- fixed: 0,
1937
- failed: 0,
1938
- lockfileRepaired: false,
1939
- durationMs: Date.now() - startTime
1940
- });
1941
- }
1942
- }
1943
- async executeFixMode() {
1944
- const client = this.createClient();
1945
- for (const repo of this.config.repositories) await this.processRepoForFix(client, repo);
1946
- if (this.config.commit) try {
1947
- commitLocalChanges(this.ctx);
1948
- } catch (error) {
1949
- const message = (0, _dependfix_core.toErrorMessage)(error);
1950
- this.logger.error(`Local commit failed: ${message}`);
1951
- this.allErrors.push({
1952
- repository: "*",
1953
- stage: "fix",
1954
- category: "COMMIT_FAILED",
1955
- message
1956
- });
1957
- }
1958
- }
1959
- async processRepoForFix(client, repo) {
1960
- const startTime = Date.now();
1961
- const [owner, name] = repo.split("/");
1962
- let alertsCount = 0;
1963
- let fixable = 0;
1964
- let fixed = 0;
1965
- let failed = 0;
1966
- let lockfileRepaired = false;
1967
- let verificationPassed;
1968
- let defaultBranch = "";
1969
- try {
1970
- const { filtered } = (0, _dependfix_core.filterAlerts)(await fetchDependabotAlerts(client, {
1971
- owner,
1972
- repo: name
1973
- }), { severityThreshold: this.config.severityThreshold });
1974
- const { limited } = (0, _dependfix_core.limitAlerts)((0, _dependfix_core.prioritizeAlerts)(filtered), this.config.maxAlertsPerRepository);
1975
- alertsCount = limited.length;
1976
- fixable = limited.filter((a) => a.fixable).length;
1977
- defaultBranch = await this.fetchDefaultBranch(client, owner, name);
1978
- this.allAlerts.push(...limited);
1979
- const fixableAlerts = limited.filter((a) => a.fixable && a.recommendedVersion);
1980
- for (const alert of fixableAlerts) {
1981
- const action = await upgradeAlert(this.ctx, alert);
1982
- this.allActions.push(action);
1983
- if (action.success) fixed++;
1984
- else failed++;
1985
- }
1986
- const skippedCount = limited.length - fixableAlerts.length;
1987
- this.summary.alertsSkipped += skippedCount;
1988
- const repairAction = tryLockfileRepair(this.ctx, repo);
1989
- this.allActions.push(repairAction);
1990
- if (repairAction.success) lockfileRepaired = true;
1991
- if (!this.config.dryRun) {
1992
- const verifyActions = await verifyProject(this.ctx, repo);
1993
- this.allActions.push(...verifyActions);
1994
- verificationPassed = verifyActions.every((a) => a.success);
1995
- } else {
1996
- this.logger.info(`[dry-run] Skipping verification for ${repo}`);
1997
- verificationPassed = void 0;
1998
- }
1999
- } catch (error) {
2000
- const message = (0, _dependfix_core.toErrorMessage)(error);
2001
- this.logger.error(`Failed to process ${repo}: ${message}`);
2002
- this.allErrors.push({
2003
- repository: repo,
2004
- stage: "fix",
2005
- category: "PROCESS_FAILED",
2006
- message
2007
- });
2008
- }
2009
- this.repoResults.push({
2010
- repository: repo,
2011
- defaultBranch,
2012
- alertsCount,
2013
- fixable,
2014
- fixed,
2015
- failed,
2016
- lockfileRepaired,
2017
- verificationPassed,
2018
- durationMs: Date.now() - startTime
2019
- });
2020
- }
2021
- /**
2022
- * 修复 → 计算内容指纹 → 查重 →(同指纹跳过 / 异指纹关旧开新)。
2023
- *
2024
- * 去重语义(T210):
2025
- * - 同告警集 → 同修复结果 → 同指纹 → 跳过,不重复提交相同 PR
2026
- * - 内容变化 → 先创建新 PR(body 注明 Supersedes),成功后关闭旧 PR;
2027
- * 新 PR 创建失败时保留旧 PR,避免出现"无 PR 窗口"
2028
- */
2029
- async executeFixAndPrMode() {
2030
- const client = this.createClient();
2031
- for (const repo of this.config.repositories) await this.processRepoForFix(client, repo);
2032
- if (this.config.cleanupBranches) await reportCleanupCandidates(this.ctx, client);
2033
- if (this.config.dryRun) {
2034
- this.logger.info("[dry-run] Would create branch, commit, push, and PR");
2035
- return;
2036
- }
2037
- if (!hasGitChanges(this.workDir)) {
2038
- this.logger.info("No changes to commit — skipping PR creation");
2039
- return;
2040
- }
2041
- if (this.config.repositories.length === 0) return;
2042
- const firstRepo = this.config.repositories[0];
2043
- const [owner, repo] = firstRepo.split("/");
2044
- try {
2045
- const fingerprint = computeFixFingerprint(this.allActions);
2046
- this.logger.info(`Fix fingerprint: ${fingerprint}`);
2047
- const plan = computeFixAndPrPlan(await findDependfixOpenPR(client, owner, repo), fingerprint);
2048
- if (plan.action === "skip" && plan.sameContentPR) {
2049
- this.logger.info(`Open PR #${plan.sameContentPR.number} already contains identical fixes — skipping PR creation`);
2050
- this.allActions.push({
2051
- type: "dependency-upgrade",
2052
- repository: firstRepo,
2053
- target: `PR #${plan.sameContentPR.number} (existing)`,
2054
- fromVersion: void 0,
2055
- toVersion: plan.sameContentPR.htmlUrl,
2056
- isMajor: false,
2057
- success: true,
2058
- durationMs: 0
2059
- });
2060
- await closeSupersededPRs(this.ctx, client, owner, repo, plan.supersedePRs);
2061
- return;
2062
- }
2063
- if (plan.supersedePRs.length > 0) this.logger.info(`Existing PR(s) with different content will be superseded: ${plan.supersedePRs.map((pr) => `#${pr.number}`).join(", ")}`);
2064
- const branchName = `dependfix/auto-fix-${fingerprint}`;
2065
- createFixBranch(branchName, this.workDir);
2066
- this.logger.info(`Creating fix branch: ${branchName}`);
2067
- this.logger.info("Staging and committing changes");
2068
- stageAndCommit(FIX_COMMIT_MESSAGE, this.workDir);
2069
- this.logger.info(`Pushing branch: ${branchName}`);
2070
- pushBranch(branchName, this.workDir);
2071
- const defaultBranch = await this.fetchDefaultBranch(client, owner, repo);
2072
- computeSummary(this.ctx);
2073
- const prBody = generatePRBody(buildRunResult(this.ctx), plan.supersedePRs.map((pr) => pr.number));
2074
- const pr = await createPullRequest({
2075
- octokit: client,
2076
- owner,
2077
- repo,
2078
- headBranch: branchName,
2079
- baseBranch: defaultBranch,
2080
- title: `fix(deps): automated security fix — ${this.summary.alertsFixed} upgrades`,
2081
- body: prBody
2082
- });
2083
- this.logger.info(`PR created: ${pr.htmlUrl}`);
2084
- this.allActions.push({
2085
- type: "dependency-upgrade",
2086
- repository: firstRepo,
2087
- target: `PR #${pr.number}`,
2088
- fromVersion: void 0,
2089
- toVersion: pr.htmlUrl,
2090
- isMajor: false,
2091
- success: true,
2092
- durationMs: 0
2093
- });
2094
- await closeSupersededPRs(this.ctx, client, owner, repo, plan.supersedePRs);
2095
- } catch (error) {
2096
- const message = (0, _dependfix_core.toErrorMessage)(error);
2097
- this.logger.error(`PR creation failed: ${message}`);
2098
- this.allErrors.push({
2099
- repository: this.config.repositories[0] ?? "*",
2100
- stage: "report",
2101
- category: "PR_CREATION_FAILED",
2102
- message
2103
- });
2104
- }
2105
- }
2106
- /**
2107
- * 清理已合并(或已关闭)的 dependfix 分支。
2108
- *
2109
- * - 分类:已合并(安全清理)/ 已关闭未合并(supersede 孤儿)/ open(跳过)
2110
- * - 删除前必须交互式确认(y/N);非 TTY 环境(CI)默认拒绝
2111
- * - 只删 `dependfix/` 前缀分支,绝不触碰 open PR 对应分支
2112
- * - dry-run 模式仅列清单并打印 "would delete",不执行删除
2113
- */
2114
- async executeCleanupBranchesMode() {
2115
- const client = this.createClient();
2116
- for (const repo of this.config.repositories) {
2117
- const [owner, name] = repo.split("/");
2118
- try {
2119
- const branches = await listDependfixBranches(client, owner, name);
2120
- if (branches.length === 0) {
2121
- this.logger.info(`[cleanup] ${repo}: no dependfix branches found`);
2122
- continue;
2123
- }
2124
- const statuses = [];
2125
- for (const branch of branches) statuses.push(await getBranchPrStatus(client, owner, name, branch));
2126
- const merged = statuses.filter((s) => s.merged);
2127
- const orphaned = statuses.filter((s) => s.closed && !s.merged);
2128
- const open = statuses.filter((s) => !s.closed);
2129
- this.logger.info(`[cleanup] ${repo}: ${merged.length} merged, ${orphaned.length} closed, ${open.length} kept`);
2130
- for (const s of merged) this.logger.info(` [merged] ${s.branch}${s.prNumber ? ` (PR #${s.prNumber})` : ""}`);
2131
- for (const s of orphaned) this.logger.info(` [closed] ${s.branch}${s.prNumber ? ` (PR #${s.prNumber})` : ""}`);
2132
- for (const s of open) {
2133
- const label = s.prNumber ? `[open — kept]` : `[no PR — kept]`;
2134
- this.logger.info(` ${label} ${s.branch}${s.prNumber ? ` (PR #${s.prNumber})` : ""}`);
2135
- }
2136
- const candidates = [...merged, ...orphaned];
2137
- if (candidates.length === 0) {
2138
- this.logger.info("[cleanup] nothing to delete");
2139
- continue;
2140
- }
2141
- if (this.config.dryRun) {
2142
- this.logger.info(`[dry-run] Would delete ${candidates.length} branch(es): ${candidates.map((s) => s.branch).join(", ")}`);
2143
- continue;
2144
- }
2145
- if (!await confirmCleanup(this.ctx, repo, candidates)) {
2146
- this.logger.info("[cleanup] cancelled by user");
2147
- continue;
2148
- }
2149
- for (const s of candidates) try {
2150
- await deleteRemoteBranch(client, owner, name, s.branch);
2151
- this.logger.info(`Deleted branch: ${s.branch}`);
2152
- this.allActions.push({
2153
- type: "branch-cleanup",
2154
- repository: repo,
2155
- target: s.branch,
2156
- success: true,
2157
- diff: s.merged ? "merged" : "closed",
2158
- durationMs: 0
2159
- });
2160
- } catch (error) {
2161
- const message = (0, _dependfix_core.toErrorMessage)(error);
2162
- this.logger.error(`Failed to delete branch ${s.branch}: ${message}`);
2163
- this.allErrors.push({
2164
- repository: repo,
2165
- stage: "report",
2166
- category: "BRANCH_DELETE_FAILED",
2167
- message: `Failed to delete ${s.branch}: ${message}`
2168
- });
2169
- }
2170
- } catch (error) {
2171
- const message = (0, _dependfix_core.toErrorMessage)(error);
2172
- this.logger.error(`[cleanup] failed for ${repo}: ${message}`);
2173
- this.allErrors.push({
2174
- repository: repo,
2175
- stage: "report",
2176
- category: "CLEANUP_FAILED",
2177
- message
2178
- });
2179
- }
2180
- }
2181
- }
2182
- createClient() {
2183
- return createGitHubClient({ token: this.config.githubToken });
2184
- }
2185
- /**
2186
- * 获取仓库的默认分支。
2187
- * 失败时返回 `'unknown'`(不阻塞主流程)。
2188
- */
2189
- async fetchDefaultBranch(client, owner, repo) {
2190
- try {
2191
- const { data } = await client.rest.repos.get({
2192
- owner,
2193
- repo
2194
- });
2195
- return data.default_branch;
2196
- } catch {
2197
- return "unknown";
2198
- }
2199
- }
2200
- };
2201
-
2202
- //#endregion
2203
- //#region src/github/repo-selector.ts
2204
- /**
2205
- * 从文件读取仓库列表(每行一个 `owner/repo`)。
2206
- * 空行和 `#` 开头的注释行会被跳过。
2207
- */
2208
- function readReposFile(filePath) {
2209
- if (!(0, node_fs.existsSync)(filePath)) throw new _dependfix_core.AppError("REPO_FILE_NOT_FOUND", `Repositories file not found: ${filePath}`);
2210
- const lines = (0, node_fs.readFileSync)(filePath, "utf-8").split("\n");
2211
- const repos = [];
2212
- for (let i = 0; i < lines.length; i += 1) {
2213
- const line = lines[i].trim();
2214
- if (!line || line.startsWith("#")) continue;
2215
- if (!(0, _dependfix_core.isValidRepoIdentifier)(line)) throw new _dependfix_core.AppError("CONFIG_VALIDATION_ERROR", `Invalid repository identifier at ${filePath}:${i + 1}: "${line}". Expected format: owner/repo`);
2216
- repos.push(line);
2217
- }
2218
- if (repos.length === 0) throw new _dependfix_core.AppError("CONFIG_VALIDATION_ERROR", `No valid repository identifiers found in ${filePath}`);
2219
- return repos;
2220
- }
2221
- /**
2222
- * 合并 CLI 传入仓库列表与文件中的仓库列表,去重并校验。
2223
- */
2224
- function resolveRepoList(cliRepos, reposFilePath) {
2225
- const repos = [...cliRepos];
2226
- if (reposFilePath) {
2227
- const fileRepos = readReposFile(reposFilePath);
2228
- repos.push(...fileRepos);
2229
- }
2230
- const deduped = [...new Set(repos)];
2231
- for (const repo of deduped) if (!(0, _dependfix_core.isValidRepoIdentifier)(repo)) throw new _dependfix_core.AppError("CONFIG_VALIDATION_ERROR", `Invalid repository identifier: "${repo}". Expected format: owner/repo`);
2232
- return deduped;
2233
- }
2234
-
2235
- //#endregion
2236
- //#region src/config/index.ts
2237
- const RUNTIME_MODES = [
2238
- "report-only",
2239
- "fix",
2240
- "fix-and-pr",
2241
- "cleanup-branches"
2242
- ];
2243
- const SEVERITY_THRESHOLDS = [
2244
- "critical",
2245
- "high",
2246
- "medium",
2247
- "all"
2248
- ];
2249
- const DEFAULT_RUNTIME_CONFIG = {
2250
- mode: "report-only",
2251
- severityThreshold: "high",
2252
- maxAlertsPerRepository: 10
2253
- };
2254
- function isRuntimeMode$1(value) {
2255
- return RUNTIME_MODES.includes(value);
2256
- }
2257
- function isSeverityThreshold$1(value) {
2258
- return SEVERITY_THRESHOLDS.includes(value);
2259
- }
2260
- function normalizeBoolean(value, fieldName) {
2261
- if (value === void 0 || value.trim() === "") return;
2262
- const normalized = value.trim().toLowerCase();
2263
- if ([
2264
- "true",
2265
- "1",
2266
- "yes",
2267
- "on"
2268
- ].includes(normalized)) return true;
2269
- if ([
2270
- "false",
2271
- "0",
2272
- "no",
2273
- "off"
2274
- ].includes(normalized)) return false;
2275
- throw new _dependfix_core.AppError("CONFIG_VALIDATION_ERROR", `Invalid boolean value for ${fieldName}: ${value}`);
2276
- }
2277
- function normalizeInteger(value, fieldName) {
2278
- if (value === void 0 || value.trim() === "") return;
2279
- const parsed = Number.parseInt(value, 10);
2280
- if (!Number.isInteger(parsed) || parsed <= 0) throw new _dependfix_core.AppError("CONFIG_VALIDATION_ERROR", `${fieldName} must be a positive integer`);
2281
- return parsed;
2282
- }
2283
- function normalizeList(value) {
2284
- if (value === void 0 || value.trim() === "") return;
2285
- const items = value.split(",").map((item) => item.trim()).filter(Boolean);
2286
- return items.length > 0 ? items : void 0;
2287
- }
2288
- function readRuntimeMode(value, fieldName) {
2289
- if (value === void 0 || value.trim() === "") return;
2290
- if (!isRuntimeMode$1(value)) throw new _dependfix_core.AppError("CONFIG_VALIDATION_ERROR", `${fieldName} must be one of: ${RUNTIME_MODES.join(", ")}`);
2291
- return value;
2292
- }
2293
- function readSeverityThreshold(value, fieldName) {
2294
- if (value === void 0 || value.trim() === "") return;
2295
- if (!isSeverityThreshold$1(value)) throw new _dependfix_core.AppError("CONFIG_VALIDATION_ERROR", `${fieldName} must be one of: ${SEVERITY_THRESHOLDS.join(", ")}`);
2296
- return value;
2297
- }
2298
- function readEnvConfig(env = process.env) {
2299
- return {
2300
- mode: readRuntimeMode(env.AUTO_FIX_GITHUB_SECURITY_MODE, "AUTO_FIX_GITHUB_SECURITY_MODE"),
2301
- severityThreshold: readSeverityThreshold(env.AUTO_FIX_GITHUB_SECURITY_SEVERITY_THRESHOLD, "AUTO_FIX_GITHUB_SECURITY_SEVERITY_THRESHOLD"),
2302
- repositories: normalizeList(env.AUTO_FIX_GITHUB_SECURITY_REPOSITORIES),
2303
- dryRun: normalizeBoolean(env.AUTO_FIX_GITHUB_SECURITY_DRY_RUN, "AUTO_FIX_GITHUB_SECURITY_DRY_RUN"),
2304
- createPullRequest: normalizeBoolean(env.AUTO_FIX_GITHUB_SECURITY_CREATE_PR, "AUTO_FIX_GITHUB_SECURITY_CREATE_PR"),
2305
- commit: normalizeBoolean(env.AUTO_FIX_GITHUB_SECURITY_COMMIT, "AUTO_FIX_GITHUB_SECURITY_COMMIT"),
2306
- cleanupBranches: normalizeBoolean(env.AUTO_FIX_GITHUB_SECURITY_CLEANUP_BRANCHES, "AUTO_FIX_GITHUB_SECURITY_CLEANUP_BRANCHES"),
2307
- githubToken: env.AUTO_FIX_GITHUB_SECURITY_GITHUB_TOKEN?.trim() || env.GITHUB_TOKEN?.trim() || void 0,
2308
- maxAlertsPerRepository: normalizeInteger(env.AUTO_FIX_GITHUB_SECURITY_MAX_ALERTS_PER_REPOSITORY, "AUTO_FIX_GITHUB_SECURITY_MAX_ALERTS_PER_REPOSITORY")
2309
- };
2310
- }
2311
- function resolveDryRun(mode, cliOverrides, envConfig) {
2312
- if (cliOverrides.dryRun !== void 0) return cliOverrides.dryRun;
2313
- if (envConfig.dryRun !== void 0) return envConfig.dryRun;
2314
- return mode === "report-only";
2315
- }
2316
- function resolveCreatePullRequest(mode, cliOverrides, envConfig) {
2317
- if (cliOverrides.createPullRequest !== void 0) return cliOverrides.createPullRequest;
2318
- if (envConfig.createPullRequest !== void 0) return envConfig.createPullRequest;
2319
- return mode === "fix-and-pr";
2320
- }
2321
- function resolveCommit(cliOverrides, envConfig) {
2322
- if (cliOverrides.commit !== void 0) return cliOverrides.commit;
2323
- if (envConfig.commit !== void 0) return envConfig.commit;
2324
- return false;
2325
- }
2326
- function resolveCleanupBranches(cliOverrides, envConfig) {
2327
- if (cliOverrides.cleanupBranches !== void 0) return cliOverrides.cleanupBranches;
2328
- if (envConfig.cleanupBranches !== void 0) return envConfig.cleanupBranches;
2329
- return false;
2330
- }
2331
- function validateRuntimeConfig(config) {
2332
- if (!config.githubToken) throw new _dependfix_core.AppError("CONFIG_VALIDATION_ERROR", "Missing GitHub token. Provide GITHUB_TOKEN or AUTO_FIX_GITHUB_SECURITY_GITHUB_TOKEN.");
2333
- if (config.repositories.length === 0) throw new _dependfix_core.AppError("CONFIG_VALIDATION_ERROR", "Missing target repositories. Provide --repo, --repository, --repos-file or AUTO_FIX_GITHUB_SECURITY_REPOSITORIES.");
2334
- for (const repo of config.repositories) if (!(0, _dependfix_core.isValidRepoIdentifier)(repo)) throw new _dependfix_core.AppError("CONFIG_VALIDATION_ERROR", `Invalid repository identifier: "${repo}". Expected format: owner/repo`);
2335
- if (config.mode === "report-only" && config.createPullRequest) throw new _dependfix_core.AppError("CONFIG_VALIDATION_ERROR", "createPullRequest cannot be enabled when mode is report-only.");
2336
- if (config.dryRun && config.createPullRequest) throw new _dependfix_core.AppError("CONFIG_VALIDATION_ERROR", "createPullRequest cannot be enabled while dryRun is true.");
2337
- if (config.commit && config.mode !== "fix") throw new _dependfix_core.AppError("CONFIG_VALIDATION_ERROR", "commit is only supported in fix mode.");
2338
- if (config.commit && config.dryRun) throw new _dependfix_core.AppError("CONFIG_VALIDATION_ERROR", "commit cannot be enabled while dryRun is true.");
2339
- if (config.commit && config.createPullRequest) throw new _dependfix_core.AppError("CONFIG_VALIDATION_ERROR", "commit cannot be enabled together with createPullRequest. Use fix-and-pr mode instead.");
2340
- return config;
2341
- }
2342
- function resolveRuntimeConfig(options = {}) {
2343
- const envConfig = readEnvConfig(options.env);
2344
- const cliOverrides = options.cliOverrides ?? {};
2345
- const mode = cliOverrides.mode ?? envConfig.mode ?? DEFAULT_RUNTIME_CONFIG.mode;
2346
- let repositories = resolveRepoList([...envConfig.repositories ?? [], ...cliOverrides.repositories ?? []], cliOverrides.reposFilePath);
2347
- if (repositories.length === 0) {
2348
- const inferred = inferRepoFromGitRemote(options.workDir ?? process.cwd());
2349
- if (inferred) repositories = [inferred];
2350
- }
2351
- return validateRuntimeConfig({
2352
- mode,
2353
- severityThreshold: cliOverrides.severityThreshold ?? envConfig.severityThreshold ?? DEFAULT_RUNTIME_CONFIG.severityThreshold,
2354
- repositories,
2355
- dryRun: resolveDryRun(mode, cliOverrides, envConfig),
2356
- createPullRequest: resolveCreatePullRequest(mode, cliOverrides, envConfig),
2357
- commit: resolveCommit(cliOverrides, envConfig),
2358
- cleanupBranches: resolveCleanupBranches(cliOverrides, envConfig),
2359
- githubToken: cliOverrides.githubToken ?? envConfig.githubToken ?? "",
2360
- maxAlertsPerRepository: cliOverrides.maxAlertsPerRepository ?? envConfig.maxAlertsPerRepository ?? DEFAULT_RUNTIME_CONFIG.maxAlertsPerRepository
2361
- });
2362
- }
2363
- /** 匹配 GitHub remote URL 的正则(HTTPS / SSH / git@ 格式) */
2364
- const GITHUB_REMOTE_RE = /github\.com[/:]([^/]+)\/([^/\s.]+?)(?:\.git)?\s*$/i;
2365
- /**
2366
- * 从 git remote origin 推断 owner/repo。
2367
- *
2368
- * 支持格式:
2369
- * - `https://github.com/owner/repo.git`
2370
- * - `git@github.com:owner/repo.git`
2371
- * - `ssh://git@github.com/owner/repo.git`
2372
- *
2373
- * @returns `owner/repo` 或 `null`(非 GitHub / 无 origin)
2374
- */
2375
- function inferRepoFromGitRemote(workDir) {
2376
- try {
2377
- const url = (0, node_child_process.execSync)("git remote get-url origin", {
2378
- cwd: workDir,
2379
- encoding: "utf-8",
2380
- stdio: "pipe"
2381
- }).trim();
2382
- const match = GITHUB_REMOTE_RE.exec(url);
2383
- return match ? `${match[1]}/${match[2]}` : null;
2384
- } catch {
2385
- return null;
2386
- }
2387
- }
2388
-
2389
- //#endregion
2390
- //#region src/cli/index.ts
2391
- const argsDef = {
2392
- mode: {
2393
- type: "positional",
2394
- description: "运行模式:report-only, fix, fix-and-pr, cleanup-branches",
2395
- required: false,
2396
- default: "report-only"
2397
- },
2398
- repo: {
2399
- type: "string",
2400
- description: "目标仓库 (owner/repo),逗号分隔多个",
2401
- alias: [
2402
- "r",
2403
- "repository",
2404
- "repositories"
2405
- ]
2406
- },
2407
- "repos-file": {
2408
- type: "string",
2409
- description: "从文件读取仓库列表(每行一个 owner/repo)"
2410
- },
2411
- "severity-threshold": {
2412
- type: "string",
2413
- description: "严重级别阈值:critical, high, medium, all",
2414
- default: "high"
2415
- },
2416
- "dry-run": {
2417
- type: "boolean",
2418
- description: "试运行模式,不实际写入文件",
2419
- negativeDescription: "关闭试运行模式"
2420
- },
2421
- "create-pr": {
2422
- type: "boolean",
2423
- description: "创建 Pull Request",
2424
- negativeDescription: "不创建 Pull Request"
2425
- },
2426
- commit: {
2427
- type: "boolean",
2428
- description: "修复完成后在本地当前分支直接提交(不推送、不创建 PR)",
2429
- negativeDescription: "不自动提交(默认)"
2430
- },
2431
- "cleanup-branches": {
2432
- type: "boolean",
2433
- description: "(fix-and-pr 模式)结束后列出已合并的 dependfix 分支到报告,不自动删除",
2434
- negativeDescription: "不执行分支清理检查"
2435
- },
2436
- "github-token": {
2437
- type: "string",
2438
- description: "GitHub Personal Access Token"
2439
- },
2440
- "max-alerts-per-repository": {
2441
- type: "string",
2442
- description: "每个仓库最多处理的告警数",
2443
- default: "10"
2444
- },
2445
- commands: {
2446
- type: "string",
2447
- description: "自定义验证命令(逗号分隔),覆盖默认的 install/lint/build"
2448
- },
2449
- verbose: {
2450
- type: "boolean",
2451
- description: "输出详细日志"
2452
- }
2453
- };
2454
- function isRuntimeMode(value) {
2455
- return RUNTIME_MODES.includes(value);
2456
- }
2457
- function isSeverityThreshold(value) {
2458
- return SEVERITY_THRESHOLDS.includes(value);
2459
- }
2460
- function appendRepositories(target, value) {
2461
- for (const repository of value.split(",")) {
2462
- const trimmed = repository.trim();
2463
- if (!trimmed) continue;
2464
- if (!(0, _dependfix_core.isValidRepoIdentifier)(trimmed)) throw new _dependfix_core.AppError("ARGUMENT_PARSE_ERROR", `Invalid repository identifier: "${trimmed}". Expected format: owner/repo`);
2465
- target.push(trimmed);
2466
- }
2467
- }
2468
- function parseCommandsFlag(value) {
2469
- return value.split(",").map((cmd) => cmd.trim()).filter(Boolean);
2470
- }
2471
- function parsedArgsToCliOverrides(parsed) {
2472
- const overrides = {};
2473
- const rawMode = parsed.mode;
2474
- if (isRuntimeMode(rawMode)) overrides.mode = rawMode;
2475
- const repoValue = parsed.repo;
2476
- if (repoValue) {
2477
- const repos = [];
2478
- appendRepositories(repos, repoValue);
2479
- if (repos.length > 0) overrides.repositories = repos;
2480
- }
2481
- const reposFile = parsed["repos-file"];
2482
- if (reposFile) overrides.reposFilePath = reposFile;
2483
- const severityThreshold = parsed["severity-threshold"];
2484
- if (isSeverityThreshold(severityThreshold)) overrides.severityThreshold = severityThreshold;
2485
- if (parsed["dry-run"] !== void 0) overrides.dryRun = parsed["dry-run"];
2486
- if (parsed["create-pr"] !== void 0) overrides.createPullRequest = parsed["create-pr"];
2487
- if (parsed.commit !== void 0) overrides.commit = parsed.commit;
2488
- if (parsed["cleanup-branches"] !== void 0) overrides.cleanupBranches = parsed["cleanup-branches"];
2489
- const githubToken = parsed["github-token"];
2490
- if (githubToken) overrides.githubToken = githubToken;
2491
- const maxAlerts = parsed["max-alerts-per-repository"];
2492
- if (maxAlerts) {
2493
- const num = Number.parseInt(maxAlerts, 10);
2494
- if (Number.isNaN(num)) throw new _dependfix_core.AppError("ARGUMENT_PARSE_ERROR", `Invalid --max-alerts-per-repository value: "${maxAlerts}". Expected a positive integer.`);
2495
- overrides.maxAlertsPerRepository = num;
2496
- }
2497
- if (parsed.verbose !== void 0) overrides.verbose = parsed.verbose;
2498
- const commandsValue = parsed.commands;
2499
- if (commandsValue) overrides.commands = parseCommandsFlag(commandsValue);
2500
- return overrides;
2501
- }
2502
- function parseCliArgs(rawArgs) {
2503
- const configOverrides = parsedArgsToCliOverrides((0, citty.parseArgs)(rawArgs, argsDef));
2504
- return {
2505
- mode: configOverrides.mode,
2506
- configOverrides,
2507
- rawArgs: [...rawArgs]
2508
- };
2509
- }
2510
- function runCli(rawArgs) {
2511
- const invocation = parseCliArgs(rawArgs);
2512
- return {
2513
- ok: true,
2514
- invocation,
2515
- config: resolveRuntimeConfig({
2516
- env: process.env,
2517
- cliOverrides: invocation.configOverrides
2518
- })
2519
- };
2520
- }
2521
- async function runApp(rawArgs) {
2522
- const invocation = parseCliArgs(rawArgs);
2523
- const { exitCode } = await new DependfixApp({
2524
- config: resolveRuntimeConfig({
2525
- env: process.env,
2526
- cliOverrides: invocation.configOverrides
2527
- }),
2528
- verbose: invocation.configOverrides.verbose,
2529
- commands: invocation.configOverrides.commands
2530
- }).run();
2531
- return exitCode;
2532
- }
2533
- const dependfixCommand = (0, citty.defineCommand)({
2534
- meta: {
2535
- name: "dependfix",
2536
- version: "0.1.0",
2537
- description: "自动化处理 Dependabot / Code Scanning 安全告警的修复工具"
2538
- },
2539
- args: argsDef,
2540
- async run({ rawArgs }) {
2541
- let exitCode = 1;
2542
- try {
2543
- exitCode = await runApp(rawArgs);
2544
- } catch (error) {
2545
- const appError = (0, _dependfix_core.toAppError)(error, "CLI_EXECUTION_FAILED");
2546
- console.error(`Error: ${appError.message}`);
2547
- exitCode = 1;
2548
- }
2549
- process.exitCode = exitCode;
2550
- }
2551
- });
2552
-
2553
- //#endregion
2554
- Object.defineProperty(exports, 'DEFAULT_RUNTIME_CONFIG', {
2555
- enumerable: true,
2556
- get: function () {
2557
- return DEFAULT_RUNTIME_CONFIG;
2558
- }
2559
- });
2560
- Object.defineProperty(exports, 'DependfixApp', {
2561
- enumerable: true,
2562
- get: function () {
2563
- return DependfixApp;
2564
- }
2565
- });
2566
- Object.defineProperty(exports, 'RUNTIME_MODES', {
2567
- enumerable: true,
2568
- get: function () {
2569
- return RUNTIME_MODES;
2570
- }
2571
- });
2572
- Object.defineProperty(exports, 'SEVERITY_THRESHOLDS', {
2573
- enumerable: true,
2574
- get: function () {
2575
- return SEVERITY_THRESHOLDS;
2576
- }
2577
- });
2578
- Object.defineProperty(exports, '__toESM', {
2579
- enumerable: true,
2580
- get: function () {
2581
- return __toESM;
2582
- }
2583
- });
2584
- Object.defineProperty(exports, 'classifyLockfileFailure', {
2585
- enumerable: true,
2586
- get: function () {
2587
- return classifyLockfileFailure;
2588
- }
2589
- });
2590
- Object.defineProperty(exports, 'closePullRequest', {
2591
- enumerable: true,
2592
- get: function () {
2593
- return closePullRequest;
2594
- }
2595
- });
2596
- Object.defineProperty(exports, 'computeFixAndPrPlan', {
2597
- enumerable: true,
2598
- get: function () {
2599
- return computeFixAndPrPlan;
2600
- }
2601
- });
2602
- Object.defineProperty(exports, 'computeFixFingerprint', {
2603
- enumerable: true,
2604
- get: function () {
2605
- return computeFixFingerprint;
2606
- }
2607
- });
2608
- Object.defineProperty(exports, 'computeLockfileDiff', {
2609
- enumerable: true,
2610
- get: function () {
2611
- return computeLockfileDiff;
2612
- }
2613
- });
2614
- Object.defineProperty(exports, 'createFixBranch', {
2615
- enumerable: true,
2616
- get: function () {
2617
- return createFixBranch;
2618
- }
2619
- });
2620
- Object.defineProperty(exports, 'createGitHubClient', {
2621
- enumerable: true,
2622
- get: function () {
2623
- return createGitHubClient;
2624
- }
2625
- });
2626
- Object.defineProperty(exports, 'createPullRequest', {
2627
- enumerable: true,
2628
- get: function () {
2629
- return createPullRequest;
2630
- }
2631
- });
2632
- Object.defineProperty(exports, 'deleteRemoteBranch', {
2633
- enumerable: true,
2634
- get: function () {
2635
- return deleteRemoteBranch;
2636
- }
2637
- });
2638
- Object.defineProperty(exports, 'dependfixCommand', {
2639
- enumerable: true,
2640
- get: function () {
2641
- return dependfixCommand;
2642
- }
2643
- });
2644
- Object.defineProperty(exports, 'ensurePnpmOverrides', {
2645
- enumerable: true,
2646
- get: function () {
2647
- return ensurePnpmOverrides;
2648
- }
2649
- });
2650
- Object.defineProperty(exports, 'extractFingerprintFromBranch', {
2651
- enumerable: true,
2652
- get: function () {
2653
- return extractFingerprintFromBranch;
2654
- }
2655
- });
2656
- Object.defineProperty(exports, 'extractPrefix', {
2657
- enumerable: true,
2658
- get: function () {
2659
- return extractPrefix;
2660
- }
2661
- });
2662
- Object.defineProperty(exports, 'fetchDependabotAlerts', {
2663
- enumerable: true,
2664
- get: function () {
2665
- return fetchDependabotAlerts;
2666
- }
2667
- });
2668
- Object.defineProperty(exports, 'findDependencyVersion', {
2669
- enumerable: true,
2670
- get: function () {
2671
- return findDependencyVersion;
2672
- }
2673
- });
2674
- Object.defineProperty(exports, 'findDependfixOpenPR', {
2675
- enumerable: true,
2676
- get: function () {
2677
- return findDependfixOpenPR;
2678
- }
2679
- });
2680
- Object.defineProperty(exports, 'generatePRBody', {
2681
- enumerable: true,
2682
- get: function () {
2683
- return generatePRBody;
2684
- }
2685
- });
2686
- Object.defineProperty(exports, 'getBranchPrStatus', {
2687
- enumerable: true,
2688
- get: function () {
2689
- return getBranchPrStatus;
2690
- }
2691
- });
2692
- Object.defineProperty(exports, 'inferRepoFromGitRemote', {
2693
- enumerable: true,
2694
- get: function () {
2695
- return inferRepoFromGitRemote;
2696
- }
2697
- });
2698
- Object.defineProperty(exports, 'isConfirmAnswer', {
2699
- enumerable: true,
2700
- get: function () {
2701
- return isConfirmAnswer;
2702
- }
2703
- });
2704
- Object.defineProperty(exports, 'listDependfixBranches', {
2705
- enumerable: true,
2706
- get: function () {
2707
- return listDependfixBranches;
2708
- }
2709
- });
2710
- Object.defineProperty(exports, 'mapGitHubError', {
2711
- enumerable: true,
2712
- get: function () {
2713
- return mapGitHubError;
2714
- }
2715
- });
2716
- Object.defineProperty(exports, 'overrideTransitiveDependency', {
2717
- enumerable: true,
2718
- get: function () {
2719
- return overrideTransitiveDependency;
2720
- }
2721
- });
2722
- Object.defineProperty(exports, 'parseCliArgs', {
2723
- enumerable: true,
2724
- get: function () {
2725
- return parseCliArgs;
2726
- }
2727
- });
2728
- Object.defineProperty(exports, 'parseMajorVersion', {
2729
- enumerable: true,
2730
- get: function () {
2731
- return parseMajorVersion;
2732
- }
2733
- });
2734
- Object.defineProperty(exports, 'pushBranch', {
2735
- enumerable: true,
2736
- get: function () {
2737
- return pushBranch;
2738
- }
2739
- });
2740
- Object.defineProperty(exports, 'readEnvConfig', {
2741
- enumerable: true,
2742
- get: function () {
2743
- return readEnvConfig;
2744
- }
2745
- });
2746
- Object.defineProperty(exports, 'readLockfileVersion', {
2747
- enumerable: true,
2748
- get: function () {
2749
- return readLockfileVersion;
2750
- }
2751
- });
2752
- Object.defineProperty(exports, 'readReposFile', {
2753
- enumerable: true,
2754
- get: function () {
2755
- return readReposFile;
2756
- }
2757
- });
2758
- Object.defineProperty(exports, 'repairLockfile', {
2759
- enumerable: true,
2760
- get: function () {
2761
- return repairLockfile;
2762
- }
2763
- });
2764
- Object.defineProperty(exports, 'resolvePnpmVersion', {
2765
- enumerable: true,
2766
- get: function () {
2767
- return resolvePnpmVersion;
2768
- }
2769
- });
2770
- Object.defineProperty(exports, 'resolveRepoList', {
2771
- enumerable: true,
2772
- get: function () {
2773
- return resolveRepoList;
2774
- }
2775
- });
2776
- Object.defineProperty(exports, 'resolveRuntimeConfig', {
2777
- enumerable: true,
2778
- get: function () {
2779
- return resolveRuntimeConfig;
2780
- }
2781
- });
2782
- Object.defineProperty(exports, 'runCli', {
2783
- enumerable: true,
2784
- get: function () {
2785
- return runCli;
2786
- }
2787
- });
2788
- Object.defineProperty(exports, 'runVerification', {
2789
- enumerable: true,
2790
- get: function () {
2791
- return runVerification;
2792
- }
2793
- });
2794
- Object.defineProperty(exports, 'sanitizeOutput', {
2795
- enumerable: true,
2796
- get: function () {
2797
- return sanitizeOutput;
2798
- }
2799
- });
2800
- Object.defineProperty(exports, 'stageAndCommit', {
2801
- enumerable: true,
2802
- get: function () {
2803
- return stageAndCommit;
2804
- }
2805
- });
2806
- Object.defineProperty(exports, 'upgradeDependency', {
2807
- enumerable: true,
2808
- get: function () {
2809
- return upgradeDependency;
2810
- }
2811
- });
2812
- //# sourceMappingURL=cli.cjs.map