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.
@@ -0,0 +1,792 @@
1
+ import { createRequire } from "node:module";
2
+ import { AppError, isValidRepoIdentifier, toAppError } from "@dependfix/core";
3
+ import { defineCommand, parseArgs, renderUsage, runCommand, runMain, showUsage } from "citty";
4
+ import { ALERT_SOURCES, DependfixApp, RUNTIME_MODES, SEVERITY_THRESHOLDS, formatHistory, queryRepoHistory, resolveRuntimeConfig } from "@dependfix/engine";
5
+ import { homedir } from "node:os";
6
+ import { cpSync, existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync } from "node:fs";
7
+ import { dirname, join } from "node:path";
8
+ import { createHash } from "node:crypto";
9
+
10
+ //#region src/cli/index.ts
11
+ const argsDef = {
12
+ mode: {
13
+ type: "positional",
14
+ description: "运行模式:report-only, fix, fix-and-pr, cleanup-branches",
15
+ required: false,
16
+ default: "report-only"
17
+ },
18
+ repo: {
19
+ type: "string",
20
+ description: "目标仓库 (owner/repo),逗号分隔多个",
21
+ alias: [
22
+ "r",
23
+ "repository",
24
+ "repositories"
25
+ ]
26
+ },
27
+ "repos-file": {
28
+ type: "string",
29
+ description: "从文件读取仓库列表(每行一个 owner/repo)"
30
+ },
31
+ owner: {
32
+ type: "string",
33
+ description: "owner / org 自动发现(逗号分隔多个或多次传入),与 --repo 合并去重(显式优先)"
34
+ },
35
+ "repo-topics": {
36
+ type: "string",
37
+ description: "发现结果 topic 白名单(逗号分隔,AND 语义;仅影响 --owner 发现结果)"
38
+ },
39
+ "repo-include": {
40
+ type: "string",
41
+ description: "仓库白名单 glob(逗号分隔多个或多次传入,如 owner/*、owner/pkg-*;仅作用于发现结果)"
42
+ },
43
+ "repo-exclude": {
44
+ type: "string",
45
+ description: "仓库黑名单 glob(逗号分隔多个或多次传入;显式列表与发现结果均受约束,与 include 冲突时胜出)"
46
+ },
47
+ "repo-topics-exclude": {
48
+ type: "string",
49
+ description: "发现结果 topic 黑名单(逗号分隔,排除含任一指定 topic 的仓库)"
50
+ },
51
+ "severity-threshold": {
52
+ type: "string",
53
+ description: "严重级别阈值:critical, high, medium, all",
54
+ default: "high"
55
+ },
56
+ "dry-run": {
57
+ type: "boolean",
58
+ description: "试运行模式,不实际写入文件",
59
+ negativeDescription: "关闭试运行模式"
60
+ },
61
+ "create-pr": {
62
+ type: "boolean",
63
+ description: "创建 Pull Request",
64
+ negativeDescription: "不创建 Pull Request"
65
+ },
66
+ commit: {
67
+ type: "boolean",
68
+ description: "修复完成后在本地当前分支直接提交(不推送、不创建 PR)",
69
+ negativeDescription: "不自动提交(默认)"
70
+ },
71
+ "cleanup-branches": {
72
+ type: "boolean",
73
+ description: "(fix-and-pr 模式)结束后列出已合并的 dependfix 分支到报告,不自动删除",
74
+ negativeDescription: "不执行分支清理检查"
75
+ },
76
+ "cleanup-branches-auto": {
77
+ type: "boolean",
78
+ description: "(fix-and-pr 模式)结束后自动删除已合并/已关闭的 dependfix 分支(非交互,不删有 open PR 的分支)",
79
+ negativeDescription: "不自动删除分支"
80
+ },
81
+ "github-token": {
82
+ type: "string",
83
+ description: "GitHub Personal Access Token"
84
+ },
85
+ "alerts-token": {
86
+ type: "string",
87
+ description: "Dependabot alerts 专用 token(可选,最小权限 PAT,仅 Dependabot alerts: read;缺省回退 --github-token。GITHUB_TOKEN 无法读取 Dependabot alerts)"
88
+ },
89
+ "alerts-source": {
90
+ type: "string",
91
+ description: "告警数据源:github-dependabot(默认,GitHub Dependabot alerts API)或 pnpm-audit(本地无 token 回退,扫描当前工作区 lockfile;repository 优先 --repo → git remote → local 兜底)"
92
+ },
93
+ "code-scanning": {
94
+ type: "boolean",
95
+ description: "同时拉取 Code Scanning alerts(与 Dependabot 并行,默认关闭;需要 token 具备 security-events: read,GITHUB_TOKEN 默认具备)",
96
+ negativeDescription: "不拉取 Code Scanning alerts(默认)"
97
+ },
98
+ "allow-major-upgrade": {
99
+ type: "boolean",
100
+ description: "跨线告警(推荐版本跨大版本,当前线内无修复版本)显式授权自动升级:仅根 package.json 直接依赖(workspace 成员独占声明维持人工)且 lockfile 单版本的告警自动跨线升级,升级后复核脆弱实例、强制完整验证(install+lint+build),失败自动回滚;间接依赖 / 多版本共存跨线告警维持人工处理。仅 CLI 可用,Action 不支持",
101
+ negativeDescription: "不自动升级跨线告警(默认,维持人工处理)"
102
+ },
103
+ "max-alerts-per-repository": {
104
+ type: "string",
105
+ description: "每个仓库最多处理的告警数",
106
+ default: "20"
107
+ },
108
+ "max-concurrency": {
109
+ type: "string",
110
+ description: "多仓库并发窗口(1-16,默认 1 保守串行;>1 可能触发 GitHub 限流)",
111
+ default: "1"
112
+ },
113
+ "max-retries": {
114
+ type: "string",
115
+ description: "GitHub API 限流重试次数(0-10,默认 3;429/rate limit 指数退避重试)",
116
+ default: "3"
117
+ },
118
+ "max-backoff-ms": {
119
+ type: "string",
120
+ description: "限流退避单次等待上限毫秒(100-120000,默认 30000;Retry-After / reset / 指数退避均受此约束)",
121
+ default: "30000"
122
+ },
123
+ history: {
124
+ type: "string",
125
+ description: "查询仓库历史运行摘要(读 dependfix-reports/index.json,倒序时间;不执行扫描)"
126
+ },
127
+ "upgrade-groups": {
128
+ type: "string",
129
+ description: "用户显式分组(覆盖自动分组),格式 \"name1:pkg1,pkg2;name2:pkg3\""
130
+ },
131
+ "toolchain-pnpm-version": {
132
+ type: "string",
133
+ description: "lockfile 修复用的 pnpm 版本(工具链固定;缺省从 package.json packageManager 解析;PIN_TOOLCHAIN 策略用 corepack pnpm@<version> 执行)"
134
+ },
135
+ commands: {
136
+ type: "string",
137
+ description: "自定义验证命令(逗号分隔),覆盖默认的 install/lint/build"
138
+ },
139
+ verbose: {
140
+ type: "boolean",
141
+ description: "输出详细日志"
142
+ },
143
+ ai: {
144
+ type: "boolean",
145
+ description: "开启 AI breaking change 研判(默认关闭;需配置 AI API Key。触发范围见 --ai-trigger;dry-run 不触发、不产生费用)",
146
+ negativeDescription: "关闭 AI 研判(默认)"
147
+ },
148
+ "ai-provider": {
149
+ type: "string",
150
+ description: "AI 提供商:openai-compatible(默认,DeepSeek 等指定 --ai-base-url 兼容)或 anthropic"
151
+ },
152
+ "ai-model": {
153
+ type: "string",
154
+ description: "AI 模型名(默认 deepseek-v4-flash)"
155
+ },
156
+ "ai-base-url": {
157
+ type: "string",
158
+ description: "OpenAI 兼容端点基地址(默认 https://api.deepseek.com,与默认模型 deepseek-v4-flash 配套;使用 OpenAI 官方模型时指定 https://api.openai.com/v1 + --ai-model)"
159
+ },
160
+ "ai-api-url": {
161
+ type: "string",
162
+ description: "Anthropic 兼容端点(仅 --ai-provider anthropic 生效;默认 https://api.anthropic.com/v1/messages,自托管/网关可显式指定)"
163
+ },
164
+ "ai-api-key": {
165
+ type: "string",
166
+ description: "AI API Key(优先 DEPENDFIX_AI_API_KEY env;注意命令行参数会出现在进程列表/shell history,敏感环境请用 env)"
167
+ },
168
+ "ai-trigger": {
169
+ type: "string",
170
+ description: "AI 研判触发范围:failure(升级验证失败)/ major(major 升级)/ both(默认)"
171
+ }
172
+ };
173
+ function isRuntimeMode(value) {
174
+ return RUNTIME_MODES.includes(value);
175
+ }
176
+ function isSeverityThreshold(value) {
177
+ return SEVERITY_THRESHOLDS.includes(value);
178
+ }
179
+ function isAlertSource(value) {
180
+ return ALERT_SOURCES.includes(value);
181
+ }
182
+ function appendRepositories(target, value) {
183
+ for (const repository of value.split(",")) {
184
+ const trimmed = repository.trim();
185
+ if (!trimmed) continue;
186
+ if (!isValidRepoIdentifier(trimmed)) throw new AppError("ARGUMENT_PARSE_ERROR", `Invalid repository identifier: "${trimmed}". Expected format: owner/repo`);
187
+ target.push(trimmed);
188
+ }
189
+ }
190
+ function parseCommandsFlag(value) {
191
+ return value.split(",").map((cmd) => cmd.trim()).filter(Boolean);
192
+ }
193
+ /**
194
+ * 严格整数字面量解析(修复:拒绝 `2.5` 被 parseInt 静默截断为 2)。
195
+ * 仅接受 `^\d+$`;范围语义由调用方在 expected 描述中声明(config 校验兜底)。
196
+ */
197
+ function parseIntegerFlag(value, flagName, expected) {
198
+ const trimmed = value.trim();
199
+ if (!/^\d+$/.test(trimmed)) throw new AppError("ARGUMENT_PARSE_ERROR", `Invalid ${flagName} value: "${value}". ${expected}`);
200
+ return Number.parseInt(trimmed, 10);
201
+ }
202
+ /**
203
+ * 归一化可重复 flag 值(citty/mri 对重复传入返回数组,单次传入返回字符串):
204
+ * 按逗号拆分 + 去空白 + 去空项。`--owner a,b --owner c` → ['a', 'b', 'c']。
205
+ */
206
+ function normalizeFlagList(value) {
207
+ if (value === void 0) return [];
208
+ return (Array.isArray(value) ? value : [value]).flatMap((v) => v.split(",")).map((s) => s.trim()).filter(Boolean);
209
+ }
210
+ function parsedArgsToCliOverrides(parsed) {
211
+ const overrides = {};
212
+ const rawMode = parsed.mode;
213
+ if (isRuntimeMode(rawMode)) overrides.mode = rawMode;
214
+ const repoValue = parsed.repo;
215
+ if (repoValue) {
216
+ const repos = [];
217
+ appendRepositories(repos, repoValue);
218
+ if (repos.length > 0) overrides.repositories = repos;
219
+ }
220
+ const reposFile = parsed["repos-file"];
221
+ if (reposFile) overrides.reposFilePath = reposFile;
222
+ const ownerValue = normalizeFlagList(parsed.owner);
223
+ if (ownerValue.length > 0) overrides.owner = ownerValue;
224
+ const repoTopicsValue = normalizeFlagList(parsed["repo-topics"]);
225
+ if (repoTopicsValue.length > 0) overrides.repoTopics = repoTopicsValue;
226
+ const repoIncludeValue = normalizeFlagList(parsed["repo-include"]);
227
+ if (repoIncludeValue.length > 0) overrides.repoInclude = repoIncludeValue;
228
+ const repoExcludeValue = normalizeFlagList(parsed["repo-exclude"]);
229
+ if (repoExcludeValue.length > 0) overrides.repoExclude = repoExcludeValue;
230
+ const repoTopicsExcludeValue = normalizeFlagList(parsed["repo-topics-exclude"]);
231
+ if (repoTopicsExcludeValue.length > 0) overrides.repoTopicsExclude = repoTopicsExcludeValue;
232
+ const severityThreshold = parsed["severity-threshold"];
233
+ if (isSeverityThreshold(severityThreshold)) overrides.severityThreshold = severityThreshold;
234
+ if (parsed["dry-run"] !== void 0) overrides.dryRun = parsed["dry-run"];
235
+ if (parsed["create-pr"] !== void 0) overrides.createPullRequest = parsed["create-pr"];
236
+ if (parsed.commit !== void 0) overrides.commit = parsed.commit;
237
+ if (parsed["cleanup-branches"] !== void 0) overrides.cleanupBranches = parsed["cleanup-branches"];
238
+ if (parsed["cleanup-branches-auto"] !== void 0) overrides.cleanupBranchesAuto = parsed["cleanup-branches-auto"];
239
+ const githubToken = parsed["github-token"];
240
+ if (githubToken) overrides.githubToken = githubToken;
241
+ const alertsToken = parsed["alerts-token"];
242
+ if (alertsToken) overrides.alertsToken = alertsToken;
243
+ const alertsSource = parsed["alerts-source"];
244
+ if (alertsSource) {
245
+ if (!isAlertSource(alertsSource)) throw new AppError("ARGUMENT_PARSE_ERROR", `Invalid --alerts-source value: "${alertsSource}". Expected one of: ${ALERT_SOURCES.join(", ")}.`);
246
+ overrides.alertSource = alertsSource;
247
+ }
248
+ if (parsed["code-scanning"] !== void 0) overrides.codeScanningEnabled = parsed["code-scanning"];
249
+ if (parsed["allow-major-upgrade"] !== void 0) overrides.allowMajorUpgrade = parsed["allow-major-upgrade"];
250
+ const maxAlerts = parsed["max-alerts-per-repository"];
251
+ if (maxAlerts) overrides.maxAlertsPerRepository = parseIntegerFlag(maxAlerts, "--max-alerts-per-repository", "Expected a positive integer.");
252
+ const maxConcurrency = parsed["max-concurrency"];
253
+ if (maxConcurrency) overrides.maxConcurrency = parseIntegerFlag(maxConcurrency, "--max-concurrency", "Expected an integer between 1 and 16.");
254
+ const maxRetries = parsed["max-retries"];
255
+ if (maxRetries) overrides.maxRetries = parseIntegerFlag(maxRetries, "--max-retries", "Expected an integer between 0 and 10.");
256
+ const maxBackoffMs = parsed["max-backoff-ms"];
257
+ if (maxBackoffMs) overrides.maxBackoffMs = parseIntegerFlag(maxBackoffMs, "--max-backoff-ms", "Expected an integer between 100 and 120000.");
258
+ const history = parsed.history;
259
+ if (history) overrides.history = history;
260
+ if (parsed.verbose !== void 0) overrides.verbose = parsed.verbose;
261
+ const commandsValue = parsed.commands;
262
+ if (commandsValue) overrides.commands = parseCommandsFlag(commandsValue);
263
+ const upgradeGroups = parsed["upgrade-groups"];
264
+ if (upgradeGroups) overrides.upgradeGroups = parseUpgradeGroupsFlag(upgradeGroups);
265
+ const toolchainPnpmVersion = parsed["toolchain-pnpm-version"];
266
+ if (toolchainPnpmVersion) overrides.toolchainPnpmVersion = toolchainPnpmVersion;
267
+ if (parsed.ai !== void 0) overrides.aiEnabled = parsed.ai;
268
+ const aiProvider = parsed["ai-provider"];
269
+ if (aiProvider) {
270
+ if (aiProvider !== "openai-compatible" && aiProvider !== "anthropic") throw new AppError("ARGUMENT_PARSE_ERROR", `Invalid --ai-provider value: "${aiProvider}". Expected "openai-compatible" or "anthropic".`);
271
+ overrides.aiProvider = aiProvider;
272
+ }
273
+ const aiModel = parsed["ai-model"];
274
+ if (aiModel) overrides.aiModel = aiModel;
275
+ const aiBaseUrl = parsed["ai-base-url"];
276
+ if (aiBaseUrl) overrides.aiBaseUrl = aiBaseUrl;
277
+ const aiApiUrl = parsed["ai-api-url"];
278
+ if (aiApiUrl) overrides.aiApiUrl = aiApiUrl;
279
+ const aiApiKey = parsed["ai-api-key"];
280
+ if (aiApiKey) overrides.aiApiKey = aiApiKey;
281
+ const aiTrigger = parsed["ai-trigger"];
282
+ if (aiTrigger) {
283
+ if (aiTrigger !== "failure" && aiTrigger !== "major" && aiTrigger !== "both") throw new AppError("ARGUMENT_PARSE_ERROR", `Invalid --ai-trigger value: "${aiTrigger}". Expected "failure", "major" or "both".`);
284
+ overrides.aiTrigger = aiTrigger;
285
+ }
286
+ return overrides;
287
+ }
288
+ /**
289
+ * 解析 `--upgrade-groups "name1:pkg1,pkg2;name2:pkg3"`。
290
+ * 与 config 的 normalizeUpgradeGroups 保持一致(CLI 与 env 同格式):
291
+ * 空 entry 忽略;非空但缺冒号/组名或包列表为空 → 抛 ARGUMENT_PARSE_ERROR;
292
+ * 原型链风险键名(__proto__ / constructor / prototype)忽略。
293
+ */
294
+ function parseUpgradeGroupsFlag(value) {
295
+ const result = {};
296
+ for (const entry of value.split(";")) {
297
+ if (!entry.trim()) continue;
298
+ const idx = entry.indexOf(":");
299
+ if (idx <= 0) throw new AppError("ARGUMENT_PARSE_ERROR", `Invalid --upgrade-groups entry: "${entry}". Expected format: "name:pkg1,pkg2"`);
300
+ const name = entry.slice(0, idx).trim();
301
+ const pkgs = entry.slice(idx + 1).split(",").map((p) => p.trim()).filter(Boolean);
302
+ if (name === "__proto__" || name === "constructor" || name === "prototype") continue;
303
+ if (!name || pkgs.length === 0) throw new AppError("ARGUMENT_PARSE_ERROR", `Invalid --upgrade-groups entry: "${entry}". Expected format: "name:pkg1,pkg2"`);
304
+ result[name] = pkgs;
305
+ }
306
+ return result;
307
+ }
308
+ function parseCliArgs(rawArgs) {
309
+ const configOverrides = parsedArgsToCliOverrides(parseArgs(rawArgs, argsDef));
310
+ return {
311
+ mode: configOverrides.mode,
312
+ configOverrides,
313
+ rawArgs: [...rawArgs]
314
+ };
315
+ }
316
+
317
+ //#endregion
318
+ //#region src/app/pipeline.ts
319
+ function createPipeline(deps = {}) {
320
+ const env = deps.env ?? process.env;
321
+ const logger = deps.logger ?? console;
322
+ const resolveConfig = deps.resolveConfig ?? ((opts) => resolveRuntimeConfig(opts));
323
+ const exit = deps.exit ?? ((code) => {
324
+ process.exitCode = code;
325
+ });
326
+ return {
327
+ parse(rawArgs) {
328
+ const invocation = parseCliArgs(rawArgs);
329
+ return {
330
+ ok: true,
331
+ invocation,
332
+ config: resolveConfig({
333
+ env,
334
+ cliOverrides: invocation.configOverrides
335
+ })
336
+ };
337
+ },
338
+ async run(rawArgs) {
339
+ const invocation = parseCliArgs(rawArgs);
340
+ if (invocation.configOverrides.history) {
341
+ const entries = queryRepoHistory(invocation.configOverrides.history);
342
+ logger.info(formatHistory(entries, invocation.configOverrides.history));
343
+ return 0;
344
+ }
345
+ const config = resolveConfig({
346
+ env,
347
+ cliOverrides: invocation.configOverrides
348
+ });
349
+ const { exitCode } = await new DependfixApp({
350
+ config,
351
+ verbose: invocation.configOverrides.verbose,
352
+ commands: invocation.configOverrides.commands
353
+ }).run();
354
+ exit(exitCode);
355
+ return exitCode;
356
+ }
357
+ };
358
+ }
359
+
360
+ //#endregion
361
+ //#region src/skills/agents.ts
362
+ const AGENTS = [
363
+ {
364
+ id: "claude-code",
365
+ displayName: "Claude Code",
366
+ projectSkillDir: (root) => join(root, ".claude", "skills"),
367
+ globalSkillDir: (home) => join(home, ".claude", "skills"),
368
+ detectInstalled: (home) => existsSync(join(home, ".claude"))
369
+ },
370
+ {
371
+ id: "opencode",
372
+ displayName: "OpenCode",
373
+ projectSkillDir: (root) => join(root, ".agents", "skills"),
374
+ globalSkillDir: (home) => join(home, ".config", "opencode", "skills"),
375
+ detectInstalled: (home) => existsSync(join(home, ".config", "opencode"))
376
+ },
377
+ {
378
+ id: "cursor",
379
+ displayName: "Cursor",
380
+ projectSkillDir: (root) => join(root, ".cursor", "skills"),
381
+ globalSkillDir: (home) => join(home, ".cursor", "skills"),
382
+ detectInstalled: (home) => existsSync(join(home, ".cursor"))
383
+ },
384
+ {
385
+ id: "copilot",
386
+ displayName: "GitHub Copilot",
387
+ projectSkillDir: () => void 0,
388
+ globalSkillDir: (home) => join(home, ".copilot", "skills"),
389
+ detectInstalled: (home) => existsSync(join(home, ".copilot"))
390
+ }
391
+ ];
392
+
393
+ //#endregion
394
+ //#region src/skills/source.ts
395
+ const PRODUCT_SKILL_NAME = "dependfix-remediator";
396
+ /**
397
+ * 解析产品 skill 内容目录(含 SKILL.md 的目录)。
398
+ * 找不到包或内容目录时抛错(install / doctor 的必要前置)。
399
+ */
400
+ function resolveProductSkillSourceDir() {
401
+ const pkgJsonPath = createRequire(import.meta.url).resolve("@dependfix/skills/package.json");
402
+ return join(dirname(pkgJsonPath), PRODUCT_SKILL_NAME);
403
+ }
404
+
405
+ //#endregion
406
+ //#region src/skills/installer.ts
407
+ /** 收集目录内文件相对路径 -> sha256(递归) */
408
+ function collectFileHashes(dir) {
409
+ const result = /* @__PURE__ */ new Map();
410
+ if (!existsSync(dir)) return result;
411
+ const walk = (current, relPrefix) => {
412
+ for (const entry of readdirSync(current, { withFileTypes: true })) {
413
+ const full = join(current, entry.name);
414
+ const rel = relPrefix ? `${relPrefix}/${entry.name}` : entry.name;
415
+ if (entry.isDirectory()) walk(full, rel);
416
+ else if (entry.isFile()) result.set(rel, hashFile(full));
417
+ }
418
+ };
419
+ walk(dir, "");
420
+ return result;
421
+ }
422
+ function hashFile(filePath) {
423
+ return createHash("sha256").update(readFileSync(filePath)).digest("hex");
424
+ }
425
+ /** 两个目录内容是否完全一致(文件集合 + 内容 hash) */
426
+ function isContentSame(dirA, dirB) {
427
+ const mapA = collectFileHashes(dirA);
428
+ const mapB = collectFileHashes(dirB);
429
+ if (mapA.size !== mapB.size) return false;
430
+ for (const [rel, hash] of mapA) if (mapB.get(rel) !== hash) return false;
431
+ return true;
432
+ }
433
+ /** 复制目录(镜像语义:先清空目标内同名子目录再复制) */
434
+ function mirrorCopy(sourceDir, targetSkillDir) {
435
+ rmSync(targetSkillDir, {
436
+ recursive: true,
437
+ force: true
438
+ });
439
+ mkdirSync(targetSkillDir, { recursive: true });
440
+ for (const entry of readdirSync(sourceDir)) {
441
+ const src = join(sourceDir, entry);
442
+ const dst = join(targetSkillDir, entry);
443
+ if (statSync(src).isDirectory()) cpSync(src, dst, { recursive: true });
444
+ else cpSync(src, dst);
445
+ }
446
+ }
447
+ /**
448
+ * 安装产品 skill 到单个 agent skills 目录。
449
+ * 返回 InstallResult;失败时 status='failed' 且 detail 含错误信息(不抛错,便于批量收尾)。
450
+ */
451
+ async function installSkillToDir(options) {
452
+ const { sourceDir, targetDir, force, dryRun, confirmOverwrite } = options;
453
+ const skillTarget = join(targetDir, PRODUCT_SKILL_NAME);
454
+ if (!existsSync(sourceDir)) return {
455
+ status: "failed",
456
+ targetDir,
457
+ detail: `权威源不存在: ${sourceDir}`
458
+ };
459
+ if (dryRun) try {
460
+ return existsSync(skillTarget) && isContentSame(sourceDir, skillTarget) ? {
461
+ status: "up-to-date",
462
+ targetDir,
463
+ detail: "[dry-run] 已一致"
464
+ } : {
465
+ status: "installed",
466
+ targetDir,
467
+ detail: "[dry-run] 将安装"
468
+ };
469
+ } catch (error) {
470
+ return {
471
+ status: "failed",
472
+ targetDir,
473
+ detail: errorMessage(error)
474
+ };
475
+ }
476
+ if (!existsSync(skillTarget)) try {
477
+ mirrorCopy(sourceDir, skillTarget);
478
+ return {
479
+ status: "installed",
480
+ targetDir
481
+ };
482
+ } catch (error) {
483
+ cleanupPartial(skillTarget);
484
+ return {
485
+ status: "failed",
486
+ targetDir,
487
+ detail: errorMessage(error)
488
+ };
489
+ }
490
+ let same;
491
+ try {
492
+ same = isContentSame(sourceDir, skillTarget);
493
+ } catch (error) {
494
+ return {
495
+ status: "failed",
496
+ targetDir,
497
+ detail: errorMessage(error)
498
+ };
499
+ }
500
+ if (same) return {
501
+ status: "up-to-date",
502
+ targetDir
503
+ };
504
+ if (!(force ?? (confirmOverwrite ? await confirmOverwrite(skillTarget) : false))) return {
505
+ status: "skipped-conflict",
506
+ targetDir,
507
+ detail: "目标存在内容不一致的同名 skill,已跳过(使用 --force 或交互确认覆盖)"
508
+ };
509
+ try {
510
+ mirrorCopy(sourceDir, skillTarget);
511
+ return {
512
+ status: "installed",
513
+ targetDir,
514
+ detail: "已覆盖旧版本"
515
+ };
516
+ } catch (error) {
517
+ cleanupPartial(skillTarget);
518
+ return {
519
+ status: "failed",
520
+ targetDir,
521
+ detail: errorMessage(error)
522
+ };
523
+ }
524
+ }
525
+ /** mirrorCopy 失败后清理半写目录(避免下次安装误判为内容不一致需 force) */
526
+ function cleanupPartial(skillTarget) {
527
+ try {
528
+ rmSync(skillTarget, {
529
+ recursive: true,
530
+ force: true
531
+ });
532
+ } catch {}
533
+ }
534
+ function errorMessage(error) {
535
+ return error instanceof Error ? error.message : String(error);
536
+ }
537
+
538
+ //#endregion
539
+ //#region src/skills/doctor.ts
540
+ function runDoctor(options) {
541
+ const { homeDir, projectRoot = process.cwd() } = options;
542
+ const findings = [];
543
+ for (const agent of AGENTS) {
544
+ if (!agent.detectInstalled(homeDir)) continue;
545
+ const globalDir = agent.globalSkillDir(homeDir);
546
+ if (!existsSync(globalDir)) findings.push({
547
+ level: "warn",
548
+ message: `${agent.displayName} 已检测到,但全局 skills 目录缺失(${globalDir})——可能官方目录约定已变更,或尚未初始化`
549
+ });
550
+ const projectDir = agent.projectSkillDir(projectRoot);
551
+ if (projectDir && !existsSync(projectDir)) findings.push({
552
+ level: "ok",
553
+ message: `${agent.displayName} 项目级 skills 目录不存在(${projectDir})——非问题,按需创建`
554
+ });
555
+ }
556
+ const sourceDir = options.productSourceDir ?? safeResolveSourceDir();
557
+ if (sourceDir) for (const agent of AGENTS) {
558
+ if (!agent.detectInstalled(homeDir)) continue;
559
+ const installedDir = join(agent.globalSkillDir(homeDir), PRODUCT_SKILL_NAME);
560
+ if (!existsSync(installedDir)) {
561
+ findings.push({
562
+ level: "ok",
563
+ message: `${agent.displayName}: ${PRODUCT_SKILL_NAME} 未安装(运行 dependfix skills install)`
564
+ });
565
+ continue;
566
+ }
567
+ if (isContentSame(sourceDir, installedDir)) findings.push({
568
+ level: "ok",
569
+ message: `${agent.displayName}: ${PRODUCT_SKILL_NAME} 已安装且与当前版本一致`
570
+ });
571
+ else findings.push({
572
+ level: "warn",
573
+ message: `${agent.displayName}: ${PRODUCT_SKILL_NAME} 已安装但内容与当前版本不一致(建议重新 install)`
574
+ });
575
+ }
576
+ const internalSkillsRoot = join(projectRoot, ".github", "skills");
577
+ if (existsSync(internalSkillsRoot)) {
578
+ const missing = [];
579
+ for (const entry of readdirSync(internalSkillsRoot, { withFileTypes: true })) {
580
+ if (!entry.isDirectory()) continue;
581
+ const skillFile = join(internalSkillsRoot, entry.name, "SKILL.md");
582
+ if (!existsSync(skillFile)) {
583
+ missing.push(`${entry.name}(缺 SKILL.md)`);
584
+ continue;
585
+ }
586
+ const content = readFileSync(skillFile, "utf8");
587
+ if (!/metadata:\s*\r?\n\s*internal:\s*true/.test(content)) missing.push(entry.name);
588
+ }
589
+ if (missing.length > 0) findings.push({
590
+ level: "error",
591
+ message: `内部 skill internal 标记缺失(生态可见泄漏风险): ${missing.join(", ")}`
592
+ });
593
+ else findings.push({
594
+ level: "ok",
595
+ message: `内部 skill internal 标记完整性检查通过(.github/skills)`
596
+ });
597
+ }
598
+ return findings;
599
+ }
600
+ /** 解析产品 skill 源目录;失败返回 undefined(如 CLI 运行在无 @dependfix/skills 的调试环境) */
601
+ function safeResolveSourceDir() {
602
+ try {
603
+ return resolveProductSkillSourceDir();
604
+ } catch {
605
+ return;
606
+ }
607
+ }
608
+
609
+ //#endregion
610
+ //#region src/skills/index.ts
611
+ async function runInstall(args) {
612
+ const home = homedir();
613
+ const projectRoot = process.cwd();
614
+ let sourceDir;
615
+ try {
616
+ sourceDir = resolveProductSkillSourceDir();
617
+ } catch (error) {
618
+ console.error(`无法定位产品 skill 内容(@dependfix/skills 未安装?): ${error instanceof Error ? error.message : String(error)}`);
619
+ process.exitCode = 1;
620
+ return;
621
+ }
622
+ const detected = AGENTS.filter((agent) => agent.detectInstalled(home));
623
+ if (detected.length === 0) {
624
+ console.info("未检测到已安装的 agent 工具(Claude Code / OpenCode / Cursor / Copilot)。");
625
+ console.info("主通道安装(推荐): npx skills add dependfix/dependfix -s dependfix-remediator -g");
626
+ return;
627
+ }
628
+ console.info(`检测到 ${detected.length} 个 agent 工具,目标模式: ${args.project ? "项目级" : "全局"}\n`);
629
+ const results = [];
630
+ for (const agent of detected) {
631
+ const agentDir = args.project ? agent.projectSkillDir(projectRoot) : agent.globalSkillDir(home);
632
+ if (!agentDir) {
633
+ console.info(`- ${agent.displayName}: 不支持 ${args.project ? "项目级" : ""}安装,跳过`);
634
+ continue;
635
+ }
636
+ const result = await installSkillToDir({
637
+ sourceDir,
638
+ targetDir: agentDir,
639
+ force: args.force,
640
+ dryRun: args["dry-run"],
641
+ confirmOverwrite: (existingDir) => confirmOverwritePrompt(agent.displayName, existingDir)
642
+ });
643
+ results.push({
644
+ agent: agent.displayName,
645
+ result
646
+ });
647
+ }
648
+ console.info("\n安装清单:");
649
+ const installIcons = {
650
+ installed: "✓",
651
+ "up-to-date": "=",
652
+ "skipped-conflict": "!",
653
+ failed: "✗"
654
+ };
655
+ for (const { agent, result } of results) {
656
+ const icon = installIcons[result.status];
657
+ console.info(` ${icon} ${agent}: ${result.status}${result.detail ? ` — ${result.detail}` : ""}`);
658
+ }
659
+ if (results.filter((r) => r.result.status === "failed").length > 0) process.exitCode = 1;
660
+ }
661
+ /** 交互式覆盖确认;非 TTY 返回 false(不静默覆盖) */
662
+ async function confirmOverwritePrompt(agentName, existingDir) {
663
+ if (!process.stdin.isTTY) {
664
+ console.warn(` ${agentName}: 目标已存在内容不一致的 dependfix-remediator,非交互环境跳过(使用 --force 覆盖)`);
665
+ return false;
666
+ }
667
+ const readline = (await import("node:readline")).createInterface({
668
+ input: process.stdin,
669
+ output: process.stdout
670
+ });
671
+ return new Promise((resolve) => {
672
+ readline.question(` 覆盖 ${existingDir} 中的旧版本?[y/N] `, (answer) => {
673
+ readline.close();
674
+ resolve(answer.trim().toLowerCase() === "y" || answer.trim().toLowerCase() === "yes");
675
+ });
676
+ });
677
+ }
678
+ async function runDoctorCli() {
679
+ const findings = runDoctor({
680
+ homeDir: homedir(),
681
+ projectRoot: process.cwd()
682
+ });
683
+ const doctorIcons = {
684
+ ok: "✓",
685
+ warn: "!",
686
+ error: "✗"
687
+ };
688
+ let warnCount = 0;
689
+ let errorCount = 0;
690
+ for (const finding of findings) {
691
+ const icon = doctorIcons[finding.level];
692
+ console.info(` ${icon} [${finding.level}] ${finding.message}`);
693
+ if (finding.level === "warn") warnCount++;
694
+ else if (finding.level === "error") errorCount++;
695
+ }
696
+ console.info(`\ndoctor 完成: ${findings.length} 项检查,${warnCount} warning, ${errorCount} error`);
697
+ if (errorCount > 0) process.exitCode = 1;
698
+ }
699
+ const skillsCommand = defineCommand({
700
+ meta: {
701
+ name: "skills",
702
+ description: "产品 skill 管理(npx skills 生态的离线兜底安装器)"
703
+ },
704
+ subCommands: {
705
+ install: defineCommand({
706
+ meta: {
707
+ name: "install",
708
+ description: "检测本机已装 agent 工具并安装 dependfix-remediator 产品 skill"
709
+ },
710
+ args: {
711
+ project: {
712
+ type: "boolean",
713
+ description: "安装到当前项目目录(默认用户全局)",
714
+ negativeDescription: "安装到用户全局(默认)"
715
+ },
716
+ force: {
717
+ type: "boolean",
718
+ description: "覆盖已存在的内容不一致同名 skill(跳过确认)",
719
+ negativeDescription: "内容不一致时需确认(默认)"
720
+ },
721
+ "dry-run": {
722
+ type: "boolean",
723
+ description: "只输出将要执行的操作,不写文件",
724
+ negativeDescription: "实际执行安装"
725
+ }
726
+ },
727
+ async run({ args }) {
728
+ await runInstall(args);
729
+ }
730
+ }),
731
+ doctor: defineCommand({
732
+ meta: {
733
+ name: "doctor",
734
+ description: "检查 agent 目录约定、产品 skill 安装状态与内部 skill internal 标记完整性"
735
+ },
736
+ async run() {
737
+ await runDoctorCli();
738
+ }
739
+ })
740
+ },
741
+ async run({ cmd, rawArgs }) {
742
+ const sub = rawArgs[0];
743
+ if (sub === "install" || sub === "doctor") return;
744
+ console.info(await renderUsage(cmd));
745
+ }
746
+ });
747
+
748
+ //#endregion
749
+ //#region src/cli/runner.ts
750
+ /** 默认 CLI 日志(console);platform 场景经 createPipeline 注入自定义 logger */
751
+ const cliLogger = console;
752
+ function runCli(rawArgs) {
753
+ return createPipeline({ logger: cliLogger }).parse(rawArgs);
754
+ }
755
+ async function runApp(rawArgs) {
756
+ return createPipeline({ logger: cliLogger }).run(rawArgs);
757
+ }
758
+ const dependfixCommand = defineCommand({
759
+ meta: {
760
+ name: "dependfix",
761
+ version: "0.1.0",
762
+ description: "自动化处理 Dependabot / Code Scanning 安全告警的修复工具"
763
+ },
764
+ args: argsDef,
765
+ async run({ rawArgs }) {
766
+ if (rawArgs[0] === "skills") {
767
+ await runCommand(skillsCommand, { rawArgs: rawArgs.slice(1) });
768
+ return;
769
+ }
770
+ let exitCode = 1;
771
+ try {
772
+ exitCode = await runApp(rawArgs);
773
+ } catch (error) {
774
+ const appError = toAppError(error, "CLI_EXECUTION_FAILED");
775
+ console.error(`Error: ${appError.message}`);
776
+ exitCode = 1;
777
+ }
778
+ process.exitCode = exitCode;
779
+ }
780
+ });
781
+ async function runDependfixMain(rawArgs = process.argv.slice(2)) {
782
+ if (rawArgs[0] === "skills" && (rawArgs.includes("--help") || rawArgs.includes("-h"))) {
783
+ console.info(await renderUsage(skillsCommand));
784
+ process.exitCode = 0;
785
+ return;
786
+ }
787
+ await runMain(dependfixCommand, { showUsage });
788
+ }
789
+
790
+ //#endregion
791
+ export { argsDef as a, createPipeline as i, runCli as n, parseCliArgs as o, runDependfixMain as r, dependfixCommand as t };
792
+ //# sourceMappingURL=runner.mjs.map