dep-brain 1.6.0 → 1.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +16 -0
- package/README.md +35 -4
- package/action.yml +19 -0
- package/dist/cli.js +82 -1
- package/dist/core/fix-plan.d.ts +32 -0
- package/dist/core/fix-plan.js +109 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +3 -0
- package/dist/reporters/pr-comment.d.ts +6 -0
- package/dist/reporters/pr-comment.js +46 -0
- package/dist/utils/github.d.ts +25 -0
- package/dist/utils/github.js +112 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,22 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## 1.8.0
|
|
6
|
+
|
|
7
|
+
- Added `dep-brain fix --unused --dry-run` for safe unused dependency removal previews.
|
|
8
|
+
- Added package-manager command rendering for npm, pnpm, and yarn.
|
|
9
|
+
- Added `--include-caution` to include caution-level unused dependency removals.
|
|
10
|
+
- Added JSON output for fix plans with commands, included items, and skipped items.
|
|
11
|
+
- Kept analysis output contract at `1.6` because no analysis JSON result fields changed.
|
|
12
|
+
|
|
13
|
+
## 1.7.0
|
|
14
|
+
|
|
15
|
+
- Added idempotent GitHub PR comments with `--pr-comment`.
|
|
16
|
+
- Added `--comment-on` trigger support for `always`, `failure`, and `new-findings`.
|
|
17
|
+
- Added PR comment markdown renderer with top issues, policy reasons, upgrade priorities, and baseline delta counts.
|
|
18
|
+
- Added GitHub Action inputs for PR comment runs.
|
|
19
|
+
- Kept analysis output contract at `1.6` because no JSON result fields changed.
|
|
20
|
+
|
|
5
21
|
## 1.6.0
|
|
6
22
|
|
|
7
23
|
- Added Slack and Discord webhook notification summaries.
|
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
`dep-brain` is a CLI and library for explainable dependency intelligence in JavaScript and TypeScript projects.
|
|
8
8
|
|
|
9
|
-
Current release `1.
|
|
9
|
+
Current release `1.8.0` adds unused dependency fix-plan dry runs while keeping analysis output contract `1.6`.
|
|
10
10
|
|
|
11
11
|
## Vision
|
|
12
12
|
|
|
@@ -29,6 +29,8 @@ Current release `1.6.0` adds Slack and Discord notification summaries while keep
|
|
|
29
29
|
- Output reports in console, JSON, Markdown, SARIF, dashboard, and top-issues formats
|
|
30
30
|
- Output upgrade-advice reports via `--advise`
|
|
31
31
|
- Send Slack and Discord webhook summaries for CI runs
|
|
32
|
+
- Create or update GitHub PR comments for pull request checks
|
|
33
|
+
- Preview safe unused dependency removal commands
|
|
32
34
|
- Gate CI with score and finding policies
|
|
33
35
|
- Compare new findings against a baseline report
|
|
34
36
|
|
|
@@ -38,7 +40,7 @@ The long-term goal is not just to list problems, but to answer:
|
|
|
38
40
|
- Can I remove it safely?
|
|
39
41
|
- What should I fix first?
|
|
40
42
|
|
|
41
|
-
## 1.
|
|
43
|
+
## 1.8 Highlights
|
|
42
44
|
|
|
43
45
|
- Duplicate dependency detection with lockfile instance tracking
|
|
44
46
|
- Unused dependency detection with runtime vs dev-tool heuristics
|
|
@@ -57,6 +59,8 @@ The long-term goal is not just to list problems, but to answer:
|
|
|
57
59
|
- Static HTML dashboard via `--dashboard`
|
|
58
60
|
- Upgrade advisor output via `--advise`
|
|
59
61
|
- Slack and Discord notification summaries via `--notify`
|
|
62
|
+
- GitHub PR comments via `--pr-comment`
|
|
63
|
+
- Safe unused dependency fix plans via `dep-brain fix --unused --dry-run`
|
|
60
64
|
- Ranked top issues via `--top`
|
|
61
65
|
- Baseline mode via `--baseline`
|
|
62
66
|
- Focused analysis via `--focus`
|
|
@@ -88,6 +92,11 @@ npx dep-brain analyze --dashboard
|
|
|
88
92
|
npx dep-brain analyze --dashboard --dashboard-out reports/depbrain.html
|
|
89
93
|
npx dep-brain analyze --notify
|
|
90
94
|
npx dep-brain analyze --notify --notify-on always
|
|
95
|
+
npx dep-brain analyze --pr-comment
|
|
96
|
+
npx dep-brain analyze --pr-comment --comment-on new-findings
|
|
97
|
+
npx dep-brain fix --unused --dry-run
|
|
98
|
+
npx dep-brain fix --unused --dry-run --include-caution
|
|
99
|
+
npx dep-brain fix --unused --dry-run --json
|
|
91
100
|
npx dep-brain analyze --focus duplicates
|
|
92
101
|
npx dep-brain analyze --ci
|
|
93
102
|
npx dep-brain analyze --baseline depbrain-baseline.json
|
|
@@ -173,7 +182,7 @@ Suggestions:
|
|
|
173
182
|
dep-brain analyze --json
|
|
174
183
|
```
|
|
175
184
|
|
|
176
|
-
Output includes `outputVersion` for schema stability. `dep-brain@1.
|
|
185
|
+
Output includes `outputVersion` for schema stability. `dep-brain@1.8.0` writes contract version `1.6`.
|
|
177
186
|
|
|
178
187
|
Validate against:
|
|
179
188
|
|
|
@@ -219,6 +228,25 @@ DEPBRAIN_DISCORD_WEBHOOK_URL=https://discord.com/api/webhooks/... dep-brain anal
|
|
|
219
228
|
|
|
220
229
|
`--notify` sends compact summaries to configured Slack and Discord webhook URLs. Default trigger is `failure`, so passing runs stay quiet unless `--notify-on always` is set.
|
|
221
230
|
|
|
231
|
+
## PR Comments
|
|
232
|
+
|
|
233
|
+
```bash
|
|
234
|
+
dep-brain analyze --ci --pr-comment
|
|
235
|
+
dep-brain analyze --ci --baseline depbrain-baseline.json --pr-comment --comment-on new-findings
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
`--pr-comment` creates or updates one GitHub pull request comment using `GITHUB_TOKEN`, `GITHUB_REPOSITORY`, and `GITHUB_EVENT_PATH`. The comment includes policy status, health score, top issues, upgrade priorities, and baseline delta counts when `--baseline` is used.
|
|
239
|
+
|
|
240
|
+
## Fix Plans
|
|
241
|
+
|
|
242
|
+
```bash
|
|
243
|
+
dep-brain fix --unused --dry-run
|
|
244
|
+
dep-brain fix --unused --dry-run --include-caution
|
|
245
|
+
dep-brain fix --unused --dry-run --json
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
Fix plans print package-manager-specific uninstall commands without changing files. `dep-brain` detects npm, pnpm, or yarn lockfiles and skips caution-level removals unless `--include-caution` is set.
|
|
249
|
+
|
|
222
250
|
## Plugins
|
|
223
251
|
|
|
224
252
|
```json
|
|
@@ -360,6 +388,7 @@ dep-brain analyze --min-score 85 --fail-on-risks
|
|
|
360
388
|
dep-brain analyze --config depbrain.config.json
|
|
361
389
|
dep-brain analyze --baseline depbrain-baseline.json --fail-on-unused
|
|
362
390
|
dep-brain analyze --ci --notify
|
|
391
|
+
dep-brain analyze --ci --pr-comment
|
|
363
392
|
```
|
|
364
393
|
|
|
365
394
|
## Config Debugging
|
|
@@ -406,7 +435,7 @@ src/
|
|
|
406
435
|
|
|
407
436
|
## Product Direction
|
|
408
437
|
|
|
409
|
-
`dep-brain` is in `v1.
|
|
438
|
+
`dep-brain` is in `v1.8.0` production CLI stage, with current focus on safe dependency removal previews and PR workflow integration.
|
|
410
439
|
|
|
411
440
|
Recent releases added:
|
|
412
441
|
|
|
@@ -415,6 +444,8 @@ Recent releases added:
|
|
|
415
444
|
- baseline, focus, and CI workflows
|
|
416
445
|
- structured upgrade advice with release-note links
|
|
417
446
|
- Slack and Discord notification summaries
|
|
447
|
+
- idempotent GitHub PR comments
|
|
448
|
+
- unused dependency fix-plan dry runs
|
|
418
449
|
|
|
419
450
|
Project should optimize for trust, clarity, and actionability over flashy UI, generic graphs, or simply adding more checks.
|
|
420
451
|
|
package/action.yml
CHANGED
|
@@ -59,6 +59,18 @@ inputs:
|
|
|
59
59
|
description: Notification trigger. Use always, failure, or never.
|
|
60
60
|
required: false
|
|
61
61
|
default: "failure"
|
|
62
|
+
pr-comment:
|
|
63
|
+
description: Create or update a GitHub PR comment.
|
|
64
|
+
required: false
|
|
65
|
+
default: "false"
|
|
66
|
+
comment-on:
|
|
67
|
+
description: PR comment trigger. Use always, failure, or new-findings.
|
|
68
|
+
required: false
|
|
69
|
+
default: "failure"
|
|
70
|
+
github-token:
|
|
71
|
+
description: GitHub token for PR comments.
|
|
72
|
+
required: false
|
|
73
|
+
default: ""
|
|
62
74
|
|
|
63
75
|
runs:
|
|
64
76
|
using: composite
|
|
@@ -80,6 +92,9 @@ runs:
|
|
|
80
92
|
INPUT_FAIL_ON_RISKS: ${{ inputs.fail-on-risks }}
|
|
81
93
|
INPUT_NOTIFY: ${{ inputs.notify }}
|
|
82
94
|
INPUT_NOTIFY_ON: ${{ inputs.notify-on }}
|
|
95
|
+
INPUT_PR_COMMENT: ${{ inputs.pr-comment }}
|
|
96
|
+
INPUT_COMMENT_ON: ${{ inputs.comment-on }}
|
|
97
|
+
GITHUB_TOKEN: ${{ inputs.github-token || github.token }}
|
|
83
98
|
run: |
|
|
84
99
|
set -euo pipefail
|
|
85
100
|
|
|
@@ -141,6 +156,10 @@ runs:
|
|
|
141
156
|
args+=("--notify" "--notify-on" "$INPUT_NOTIFY_ON")
|
|
142
157
|
fi
|
|
143
158
|
|
|
159
|
+
if [ "$INPUT_PR_COMMENT" = "true" ]; then
|
|
160
|
+
args+=("--pr-comment" "--comment-on" "$INPUT_COMMENT_ON")
|
|
161
|
+
fi
|
|
162
|
+
|
|
144
163
|
node "$GITHUB_ACTION_PATH/dist/cli.js" "${args[@]}"
|
|
145
164
|
|
|
146
165
|
branding:
|
package/dist/cli.js
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { analyzeProject } from "./core/analyzer.js";
|
|
3
|
+
import { buildUnusedFixPlan, renderFixPlan } from "./core/fix-plan.js";
|
|
3
4
|
import { renderConsoleReport } from "./reporters/console.js";
|
|
4
5
|
import { renderJsonReport } from "./reporters/json.js";
|
|
5
6
|
import { renderMarkdownReport } from "./reporters/markdown.js";
|
|
7
|
+
import { renderPrCommentReport } from "./reporters/pr-comment.js";
|
|
6
8
|
import { renderSarifReport } from "./reporters/sarif.js";
|
|
7
9
|
import { renderDashboardReport } from "./reporters/dashboard.js";
|
|
10
|
+
import { shouldPostPrComment, upsertGitHubPrComment } from "./utils/github.js";
|
|
8
11
|
import { sendConfiguredNotifications } from "./utils/notifications.js";
|
|
9
12
|
import { defaultConfig } from "./utils/config.js";
|
|
10
13
|
import { promises as fs } from "node:fs";
|
|
@@ -43,6 +46,44 @@ async function main() {
|
|
|
43
46
|
return;
|
|
44
47
|
}
|
|
45
48
|
if (command !== "analyze") {
|
|
49
|
+
if (command === "fix") {
|
|
50
|
+
if (!flags.has("--unused")) {
|
|
51
|
+
console.error("Missing --unused for fix");
|
|
52
|
+
printHelp();
|
|
53
|
+
process.exitCode = 1;
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
if (!flags.has("--dry-run")) {
|
|
57
|
+
console.error("Fix currently supports --dry-run only.");
|
|
58
|
+
process.exitCode = 1;
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
if (!(await hasPackageJson(targetPath))) {
|
|
62
|
+
console.error(`No package.json found at ${sanitizeForLog(targetPath)}`);
|
|
63
|
+
process.exitCode = 1;
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
try {
|
|
67
|
+
const cliConfig = buildCliConfig(flags, optionValues);
|
|
68
|
+
const result = await analyzeProject({
|
|
69
|
+
rootDir: targetPath,
|
|
70
|
+
configPath: optionValues.get("--config"),
|
|
71
|
+
config: cliConfig,
|
|
72
|
+
focus: "unused"
|
|
73
|
+
});
|
|
74
|
+
const plan = await buildUnusedFixPlan(result, {
|
|
75
|
+
includeCaution: flags.has("--include-caution")
|
|
76
|
+
});
|
|
77
|
+
await writeOutput(flags.has("--json") ? JSON.stringify(plan, null, 2) : renderFixPlan(plan), optionValues.get("--out"));
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
console.error("Failed to build fix plan.");
|
|
82
|
+
console.error(error);
|
|
83
|
+
process.exitCode = 1;
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
46
87
|
if (command === "report") {
|
|
47
88
|
const fromPath = optionValues.get("--from") ?? positionals[0];
|
|
48
89
|
if (!fromPath) {
|
|
@@ -175,6 +216,28 @@ async function main() {
|
|
|
175
216
|
}
|
|
176
217
|
}
|
|
177
218
|
}
|
|
219
|
+
if (flags.has("--pr-comment")) {
|
|
220
|
+
const trigger = parsePrCommentTrigger(optionValues.get("--comment-on"));
|
|
221
|
+
const newFindingsCount = countFindings(result);
|
|
222
|
+
if (shouldPostPrComment({
|
|
223
|
+
trigger,
|
|
224
|
+
policyPassed: result.policy.passed,
|
|
225
|
+
newFindingsCount
|
|
226
|
+
})) {
|
|
227
|
+
const commentResult = await upsertGitHubPrComment({
|
|
228
|
+
body: renderPrCommentReport(result, { hasBaseline: Boolean(baseline) })
|
|
229
|
+
});
|
|
230
|
+
if (commentResult.status === "created" || commentResult.status === "updated") {
|
|
231
|
+
console.error(`PR comment ${commentResult.status}.`);
|
|
232
|
+
}
|
|
233
|
+
else {
|
|
234
|
+
console.error(`PR comment skipped: ${commentResult.reason ?? "unknown reason"}`);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
else {
|
|
238
|
+
console.error(`PR comment skipped: ${trigger} trigger did not match.`);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
178
241
|
if (!result.policy.passed) {
|
|
179
242
|
process.exitCode = 1;
|
|
180
243
|
}
|
|
@@ -246,8 +309,9 @@ function printHelp() {
|
|
|
246
309
|
console.log("Dependency Brain");
|
|
247
310
|
console.log("");
|
|
248
311
|
console.log("Usage:");
|
|
249
|
-
console.log(" dep-brain analyze [path] [--json] [--md] [--sarif] [--top] [--dashboard] [--notify] [--
|
|
312
|
+
console.log(" dep-brain analyze [path] [--json] [--md] [--sarif] [--top] [--dashboard] [--notify] [--pr-comment] [--comment-on kind] [--focus kind] [--ci] [--out path] [--config path] [--baseline path] [--min-score n] [--fail-on-risks]");
|
|
250
313
|
console.log(" dep-brain report --from <file> [--md] [--json] [--sarif] [--top] [--advise] [--dashboard] [--out path]");
|
|
314
|
+
console.log(" dep-brain fix [path] --unused --dry-run [--include-caution] [--json] [--out path]");
|
|
251
315
|
console.log(" dep-brain config [path] [--config path]");
|
|
252
316
|
console.log(" dep-brain init [--out depbrain.config.json]");
|
|
253
317
|
console.log(" dep-brain help");
|
|
@@ -263,12 +327,17 @@ function printHelp() {
|
|
|
263
327
|
console.log(" --dashboard-out <path> Write dashboard HTML to a custom path");
|
|
264
328
|
console.log(" --notify Send Slack or Discord webhook summaries");
|
|
265
329
|
console.log(" --notify-on <kind> Send notifications on always, failure, or never");
|
|
330
|
+
console.log(" --pr-comment Create or update a GitHub PR comment");
|
|
331
|
+
console.log(" --comment-on <kind> Post PR comment on always, failure, or new-findings");
|
|
266
332
|
console.log(" --focus <kind> Run all, health, duplicates, unused, outdated, or risks");
|
|
267
333
|
console.log(" --ci Apply low-noise CI defaults");
|
|
268
334
|
console.log(" --config <path> Path to depbrain.config.json");
|
|
269
335
|
console.log(" --baseline <path> Ignore findings already present in a baseline JSON report");
|
|
270
336
|
console.log(" --from <file> Read analysis JSON from file");
|
|
271
337
|
console.log(" --out <path> Write output to a file");
|
|
338
|
+
console.log(" --unused Build an unused dependency fix plan");
|
|
339
|
+
console.log(" --dry-run Print fix commands without changing files");
|
|
340
|
+
console.log(" --include-caution Include caution-level unused dependency removals");
|
|
272
341
|
console.log(" --min-score <n> Minimum score required to pass");
|
|
273
342
|
console.log(" --fail-on-risks Fail when risky dependencies exist");
|
|
274
343
|
console.log(" --fail-on-outdated Fail when outdated dependencies exist");
|
|
@@ -396,3 +465,15 @@ function compareAdviceRisk(left, right) {
|
|
|
396
465
|
const rank = { high: 3, medium: 2, low: 1 };
|
|
397
466
|
return rank[left] - rank[right];
|
|
398
467
|
}
|
|
468
|
+
function parsePrCommentTrigger(value) {
|
|
469
|
+
if (value === "always" || value === "failure" || value === "new-findings") {
|
|
470
|
+
return value;
|
|
471
|
+
}
|
|
472
|
+
return "failure";
|
|
473
|
+
}
|
|
474
|
+
function countFindings(result) {
|
|
475
|
+
return (result.duplicates.length +
|
|
476
|
+
result.unused.length +
|
|
477
|
+
result.outdated.length +
|
|
478
|
+
result.risks.length);
|
|
479
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { AnalysisResult } from "./analyzer.js";
|
|
2
|
+
export type PackageManager = "npm" | "pnpm" | "yarn";
|
|
3
|
+
export interface FixPlanOptions {
|
|
4
|
+
includeCaution?: boolean;
|
|
5
|
+
}
|
|
6
|
+
export interface FixPlanItem {
|
|
7
|
+
name: string;
|
|
8
|
+
section: "dependencies" | "devDependencies";
|
|
9
|
+
package?: string;
|
|
10
|
+
confidence: number;
|
|
11
|
+
safety: "safe" | "caution" | "unknown";
|
|
12
|
+
command: string;
|
|
13
|
+
args: string[];
|
|
14
|
+
}
|
|
15
|
+
export interface SkippedFixItem {
|
|
16
|
+
name: string;
|
|
17
|
+
section: "dependencies" | "devDependencies";
|
|
18
|
+
package?: string;
|
|
19
|
+
confidence: number;
|
|
20
|
+
safety: "safe" | "caution" | "unknown";
|
|
21
|
+
reason: string;
|
|
22
|
+
}
|
|
23
|
+
export interface FixPlan {
|
|
24
|
+
packageManager: PackageManager;
|
|
25
|
+
dryRun: true;
|
|
26
|
+
commands: string[];
|
|
27
|
+
items: FixPlanItem[];
|
|
28
|
+
skipped: SkippedFixItem[];
|
|
29
|
+
}
|
|
30
|
+
export declare function buildUnusedFixPlan(result: AnalysisResult, options?: FixPlanOptions): Promise<FixPlan>;
|
|
31
|
+
export declare function detectPackageManager(rootDir: string): Promise<PackageManager>;
|
|
32
|
+
export declare function renderFixPlan(plan: FixPlan): string;
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { promises as fs } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
export async function buildUnusedFixPlan(result, options = {}) {
|
|
4
|
+
const packageManager = await detectPackageManager(result.rootDir);
|
|
5
|
+
const items = [];
|
|
6
|
+
const skipped = [];
|
|
7
|
+
for (const item of result.unused) {
|
|
8
|
+
const skipReason = getSkipReason(item, options);
|
|
9
|
+
if (skipReason) {
|
|
10
|
+
skipped.push({
|
|
11
|
+
name: item.name,
|
|
12
|
+
section: item.section,
|
|
13
|
+
package: item.package,
|
|
14
|
+
confidence: item.confidence,
|
|
15
|
+
safety: item.recommendation.safety,
|
|
16
|
+
reason: skipReason
|
|
17
|
+
});
|
|
18
|
+
continue;
|
|
19
|
+
}
|
|
20
|
+
const command = buildRemoveCommand(packageManager, item);
|
|
21
|
+
items.push({
|
|
22
|
+
name: item.name,
|
|
23
|
+
section: item.section,
|
|
24
|
+
package: item.package,
|
|
25
|
+
confidence: item.confidence,
|
|
26
|
+
safety: item.recommendation.safety,
|
|
27
|
+
command: command.join(" "),
|
|
28
|
+
args: command
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
return {
|
|
32
|
+
packageManager,
|
|
33
|
+
dryRun: true,
|
|
34
|
+
commands: items.map((item) => item.command),
|
|
35
|
+
items,
|
|
36
|
+
skipped
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
export async function detectPackageManager(rootDir) {
|
|
40
|
+
if (await fileExists(path.join(rootDir, "pnpm-lock.yaml"))) {
|
|
41
|
+
return "pnpm";
|
|
42
|
+
}
|
|
43
|
+
if (await fileExists(path.join(rootDir, "yarn.lock"))) {
|
|
44
|
+
return "yarn";
|
|
45
|
+
}
|
|
46
|
+
return "npm";
|
|
47
|
+
}
|
|
48
|
+
export function renderFixPlan(plan) {
|
|
49
|
+
const lines = [
|
|
50
|
+
"Dependency Brain Fix Plan",
|
|
51
|
+
"",
|
|
52
|
+
`Package manager: ${plan.packageManager}`,
|
|
53
|
+
"Mode: dry-run",
|
|
54
|
+
""
|
|
55
|
+
];
|
|
56
|
+
if (plan.commands.length === 0) {
|
|
57
|
+
lines.push("No safe unused dependency removals found.");
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
lines.push("Commands:");
|
|
61
|
+
for (const command of plan.commands) {
|
|
62
|
+
lines.push(`- ${command}`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
if (plan.skipped.length > 0) {
|
|
66
|
+
lines.push("");
|
|
67
|
+
lines.push("Skipped:");
|
|
68
|
+
for (const item of plan.skipped) {
|
|
69
|
+
lines.push(`- ${item.name}${item.package ? ` [${item.package}]` : ""}: ${item.reason}`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return lines.join("\n");
|
|
73
|
+
}
|
|
74
|
+
function getSkipReason(item, options) {
|
|
75
|
+
if (item.recommendation.safety !== "safe" &&
|
|
76
|
+
!(options.includeCaution && item.recommendation.safety === "caution")) {
|
|
77
|
+
return "requires --include-caution";
|
|
78
|
+
}
|
|
79
|
+
if (item.section === "dependencies" &&
|
|
80
|
+
item.confidence < 0.88 &&
|
|
81
|
+
!options.includeCaution) {
|
|
82
|
+
return "runtime dependency confidence below 88%";
|
|
83
|
+
}
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
function buildRemoveCommand(packageManager, item) {
|
|
87
|
+
if (packageManager === "pnpm") {
|
|
88
|
+
return item.package
|
|
89
|
+
? ["pnpm", "--filter", item.package, "remove", item.name]
|
|
90
|
+
: ["pnpm", "remove", item.name];
|
|
91
|
+
}
|
|
92
|
+
if (packageManager === "yarn") {
|
|
93
|
+
return item.package
|
|
94
|
+
? ["yarn", "workspace", item.package, "remove", item.name]
|
|
95
|
+
: ["yarn", "remove", item.name];
|
|
96
|
+
}
|
|
97
|
+
return item.package
|
|
98
|
+
? ["npm", "uninstall", item.name, "--workspace", item.package]
|
|
99
|
+
: ["npm", "uninstall", item.name];
|
|
100
|
+
}
|
|
101
|
+
async function fileExists(filePath) {
|
|
102
|
+
try {
|
|
103
|
+
await fs.access(filePath);
|
|
104
|
+
return true;
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
export { analyzeProject } from "./core/analyzer.js";
|
|
2
|
+
export { buildUnusedFixPlan, detectPackageManager, renderFixPlan } from "./core/fix-plan.js";
|
|
2
3
|
export type { AnalysisOptions, AnalysisFocus, AnalysisResult, DepBrainBaseline, DuplicateDependency, OutdatedDependency, PolicyResult, PackageAnalysisResult, Recommendation, RiskFactors, RiskTransitiveDependency, ScoreBreakdown, RiskDependency, TopIssue, TrustScore, UnusedDependency, WorkspaceDependencyUsage, WorkspaceOwnershipSummary } from "./core/analyzer.js";
|
|
3
4
|
export { OUTPUT_VERSION } from "./core/analyzer.js";
|
|
4
5
|
export { PluginManager } from "./core/plugin-manager.js";
|
|
5
6
|
export type { DepBrainPlugin, PluginDiagnostic, ProjectContext } from "./core/plugin-manager.js";
|
|
6
7
|
export type { AnalysisContext, CheckResult, Issue } from "./core/types.js";
|
|
8
|
+
export type { FixPlan, FixPlanItem, FixPlanOptions, PackageManager, SkippedFixItem } from "./core/fix-plan.js";
|
|
7
9
|
export type { DepBrainConfig, DepBrainConfigOverrides } from "./utils/config.js";
|
|
8
10
|
export { renderNotificationMessage, sendConfiguredNotifications, shouldSendNotification } from "./utils/notifications.js";
|
|
11
|
+
export { shouldPostPrComment, upsertGitHubPrComment } from "./utils/github.js";
|
|
12
|
+
export { PR_COMMENT_MARKER, renderPrCommentReport } from "./reporters/pr-comment.js";
|
|
9
13
|
export type { NotificationChannel, NotificationResult, NotificationSendInput, NotificationSender } from "./utils/notifications.js";
|
|
14
|
+
export type { GitHubPrCommentInput, GitHubPrCommentResult, GitHubRequest, PrCommentTrigger } from "./utils/github.js";
|
|
10
15
|
export type { WorkspacePackage } from "./utils/workspaces.js";
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
export { analyzeProject } from "./core/analyzer.js";
|
|
2
|
+
export { buildUnusedFixPlan, detectPackageManager, renderFixPlan } from "./core/fix-plan.js";
|
|
2
3
|
export { OUTPUT_VERSION } from "./core/analyzer.js";
|
|
3
4
|
export { PluginManager } from "./core/plugin-manager.js";
|
|
4
5
|
export { renderNotificationMessage, sendConfiguredNotifications, shouldSendNotification } from "./utils/notifications.js";
|
|
6
|
+
export { shouldPostPrComment, upsertGitHubPrComment } from "./utils/github.js";
|
|
7
|
+
export { PR_COMMENT_MARKER, renderPrCommentReport } from "./reporters/pr-comment.js";
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { AnalysisResult } from "../core/analyzer.js";
|
|
2
|
+
export declare const PR_COMMENT_MARKER = "<!-- dep-brain-report -->";
|
|
3
|
+
export interface PrCommentOptions {
|
|
4
|
+
hasBaseline?: boolean;
|
|
5
|
+
}
|
|
6
|
+
export declare function renderPrCommentReport(result: AnalysisResult, options?: PrCommentOptions): string;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
export const PR_COMMENT_MARKER = "<!-- dep-brain-report -->";
|
|
2
|
+
export function renderPrCommentReport(result, options = {}) {
|
|
3
|
+
const lines = [
|
|
4
|
+
PR_COMMENT_MARKER,
|
|
5
|
+
"## Dependency Brain",
|
|
6
|
+
"",
|
|
7
|
+
`**Policy:** ${result.policy.passed ? "PASS" : "FAIL"}`,
|
|
8
|
+
`**Project Health:** ${result.score}/100`,
|
|
9
|
+
`**Findings:** duplicates ${result.duplicates.length}, unused ${result.unused.length}, outdated ${result.outdated.length}, risks ${result.risks.length}`
|
|
10
|
+
];
|
|
11
|
+
if (options.hasBaseline) {
|
|
12
|
+
lines.push(`**New since baseline:** duplicates ${result.duplicates.length}, unused ${result.unused.length}, outdated ${result.outdated.length}, risks ${result.risks.length}`);
|
|
13
|
+
}
|
|
14
|
+
if (result.policy.reasons.length > 0) {
|
|
15
|
+
lines.push("");
|
|
16
|
+
lines.push("### Policy Reasons");
|
|
17
|
+
for (const reason of result.policy.reasons) {
|
|
18
|
+
lines.push(`- ${reason}`);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
if (result.topIssues.length > 0) {
|
|
22
|
+
lines.push("");
|
|
23
|
+
lines.push("### Top Issues");
|
|
24
|
+
for (const item of result.topIssues.slice(0, 5)) {
|
|
25
|
+
lines.push(`- **${item.priority.toUpperCase()}** ${item.kind} \`${item.name}\`${item.package ? ` [${item.package}]` : ""}: ${item.summary}`);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
const upgradePriorities = summarizeUpgradePriorities(result);
|
|
29
|
+
if (upgradePriorities) {
|
|
30
|
+
lines.push("");
|
|
31
|
+
lines.push("### Upgrade Priorities");
|
|
32
|
+
lines.push(upgradePriorities);
|
|
33
|
+
}
|
|
34
|
+
lines.push("");
|
|
35
|
+
lines.push("_Generated by dep-brain._");
|
|
36
|
+
return lines.join("\n");
|
|
37
|
+
}
|
|
38
|
+
function summarizeUpgradePriorities(result) {
|
|
39
|
+
const counts = result.outdated.reduce((acc, item) => {
|
|
40
|
+
acc[item.advice.risk] += 1;
|
|
41
|
+
return acc;
|
|
42
|
+
}, { high: 0, medium: 0, low: 0 });
|
|
43
|
+
return [`high ${counts.high}`, `medium ${counts.medium}`, `low ${counts.low}`]
|
|
44
|
+
.filter((entry) => !entry.endsWith(" 0"))
|
|
45
|
+
.join(", ");
|
|
46
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export type PrCommentTrigger = "always" | "failure" | "new-findings";
|
|
2
|
+
export interface GitHubPrCommentInput {
|
|
3
|
+
body: string;
|
|
4
|
+
env?: Record<string, string | undefined>;
|
|
5
|
+
request?: GitHubRequest;
|
|
6
|
+
}
|
|
7
|
+
export interface GitHubPrCommentResult {
|
|
8
|
+
status: "created" | "updated" | "skipped";
|
|
9
|
+
reason?: string;
|
|
10
|
+
}
|
|
11
|
+
export type GitHubRequest = (url: string, init: {
|
|
12
|
+
method: "GET" | "POST" | "PATCH";
|
|
13
|
+
headers: Record<string, string>;
|
|
14
|
+
body?: string;
|
|
15
|
+
}) => Promise<{
|
|
16
|
+
ok: boolean;
|
|
17
|
+
status: number;
|
|
18
|
+
json: () => Promise<unknown>;
|
|
19
|
+
}>;
|
|
20
|
+
export declare function upsertGitHubPrComment(input: GitHubPrCommentInput): Promise<GitHubPrCommentResult>;
|
|
21
|
+
export declare function shouldPostPrComment(input: {
|
|
22
|
+
trigger: PrCommentTrigger;
|
|
23
|
+
policyPassed: boolean;
|
|
24
|
+
newFindingsCount: number;
|
|
25
|
+
}): boolean;
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { promises as fs } from "node:fs";
|
|
2
|
+
import { PR_COMMENT_MARKER } from "../reporters/pr-comment.js";
|
|
3
|
+
export async function upsertGitHubPrComment(input) {
|
|
4
|
+
const env = input.env ?? process.env;
|
|
5
|
+
const token = env.GITHUB_TOKEN;
|
|
6
|
+
const repository = env.GITHUB_REPOSITORY;
|
|
7
|
+
const eventPath = env.GITHUB_EVENT_PATH;
|
|
8
|
+
if (!token) {
|
|
9
|
+
return { status: "skipped", reason: "GITHUB_TOKEN is not set" };
|
|
10
|
+
}
|
|
11
|
+
if (!repository) {
|
|
12
|
+
return { status: "skipped", reason: "GITHUB_REPOSITORY is not set" };
|
|
13
|
+
}
|
|
14
|
+
if (!eventPath) {
|
|
15
|
+
return { status: "skipped", reason: "GITHUB_EVENT_PATH is not set" };
|
|
16
|
+
}
|
|
17
|
+
const pullNumber = await readPullNumber(eventPath);
|
|
18
|
+
if (!pullNumber) {
|
|
19
|
+
return { status: "skipped", reason: "pull request event not found" };
|
|
20
|
+
}
|
|
21
|
+
const request = input.request ?? defaultGitHubRequest;
|
|
22
|
+
const commentsUrl = `https://api.github.com/repos/${repository}/issues/${pullNumber}/comments`;
|
|
23
|
+
const headers = buildHeaders(token);
|
|
24
|
+
const existingResponse = await request(commentsUrl, {
|
|
25
|
+
method: "GET",
|
|
26
|
+
headers
|
|
27
|
+
});
|
|
28
|
+
if (!existingResponse.ok) {
|
|
29
|
+
return {
|
|
30
|
+
status: "skipped",
|
|
31
|
+
reason: `failed to list comments: HTTP ${existingResponse.status}`
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
const comments = normalizeComments(await existingResponse.json());
|
|
35
|
+
const existing = comments.find((comment) => typeof comment.body === "string" && comment.body.includes(PR_COMMENT_MARKER));
|
|
36
|
+
if (existing) {
|
|
37
|
+
const updateResponse = await request(`https://api.github.com/repos/${repository}/issues/comments/${existing.id}`, {
|
|
38
|
+
method: "PATCH",
|
|
39
|
+
headers,
|
|
40
|
+
body: JSON.stringify({ body: input.body })
|
|
41
|
+
});
|
|
42
|
+
return updateResponse.ok
|
|
43
|
+
? { status: "updated" }
|
|
44
|
+
: {
|
|
45
|
+
status: "skipped",
|
|
46
|
+
reason: `failed to update comment: HTTP ${updateResponse.status}`
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
const createResponse = await request(commentsUrl, {
|
|
50
|
+
method: "POST",
|
|
51
|
+
headers,
|
|
52
|
+
body: JSON.stringify({ body: input.body })
|
|
53
|
+
});
|
|
54
|
+
return createResponse.ok
|
|
55
|
+
? { status: "created" }
|
|
56
|
+
: {
|
|
57
|
+
status: "skipped",
|
|
58
|
+
reason: `failed to create comment: HTTP ${createResponse.status}`
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
export function shouldPostPrComment(input) {
|
|
62
|
+
if (input.trigger === "always") {
|
|
63
|
+
return true;
|
|
64
|
+
}
|
|
65
|
+
if (input.trigger === "failure") {
|
|
66
|
+
return !input.policyPassed;
|
|
67
|
+
}
|
|
68
|
+
return input.newFindingsCount > 0;
|
|
69
|
+
}
|
|
70
|
+
async function readPullNumber(eventPath) {
|
|
71
|
+
try {
|
|
72
|
+
const raw = await fs.readFile(eventPath, "utf8");
|
|
73
|
+
const event = JSON.parse(raw);
|
|
74
|
+
const value = event.pull_request?.number ?? event.number;
|
|
75
|
+
return typeof value === "number" ? value : null;
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
async function defaultGitHubRequest(url, init) {
|
|
82
|
+
const response = await fetch(url, init);
|
|
83
|
+
return {
|
|
84
|
+
ok: response.ok,
|
|
85
|
+
status: response.status,
|
|
86
|
+
json: () => response.json()
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
function buildHeaders(token) {
|
|
90
|
+
return {
|
|
91
|
+
accept: "application/vnd.github+json",
|
|
92
|
+
authorization: `Bearer ${token}`,
|
|
93
|
+
"content-type": "application/json",
|
|
94
|
+
"x-github-api-version": "2022-11-28"
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
function normalizeComments(value) {
|
|
98
|
+
if (!Array.isArray(value)) {
|
|
99
|
+
return [];
|
|
100
|
+
}
|
|
101
|
+
return value
|
|
102
|
+
.map((item) => {
|
|
103
|
+
if (!item || typeof item !== "object") {
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
const comment = item;
|
|
107
|
+
return typeof comment.id === "number"
|
|
108
|
+
? { id: comment.id, body: comment.body }
|
|
109
|
+
: null;
|
|
110
|
+
})
|
|
111
|
+
.filter((item) => item !== null);
|
|
112
|
+
}
|