dep-brain 1.1.0 → 1.3.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 +59 -25
- package/depbrain.config.json +20 -0
- package/depbrain.config.schema.json +36 -0
- package/depbrain.output.schema.json +5 -1
- package/dist/checks/risk.d.ts +3 -1
- package/dist/checks/risk.js +28 -12
- package/dist/cli.js +11 -3
- package/dist/core/analyzer.d.ts +2 -0
- package/dist/core/analyzer.js +32 -5
- package/dist/core/plugin-manager.d.ts +29 -0
- package/dist/core/plugin-manager.js +193 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/reporters/dashboard.d.ts +2 -0
- package/dist/reporters/dashboard.js +84 -0
- package/dist/utils/config.d.ts +24 -0
- package/dist/utils/config.js +43 -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.3.0
|
|
6
|
+
|
|
7
|
+
- Added plugin diagnostics under `extensions.depBrain.plugins` for failed plugin loads and hook errors.
|
|
8
|
+
- Added built-in `license` plugin through `plugins.enabled: ["license"]`.
|
|
9
|
+
- Added configurable risk thresholds for stale release days, aging release days, low downloads, and trust score weights.
|
|
10
|
+
- Added `--dashboard` and `--dashboard-out` for static HTML dashboard generation.
|
|
11
|
+
- Updated starter config, schemas, README, and tests for v1.3 behavior.
|
|
12
|
+
|
|
13
|
+
## 1.2.0
|
|
14
|
+
|
|
15
|
+
- Added `PluginManager` with `preScan`, `postScan`, and `reportHook` lifecycle support.
|
|
16
|
+
- Added disabled-by-default plugin config through `plugins.enabled` and `plugins.paths`.
|
|
17
|
+
- Added `extensions` to analysis output so plugins can enrich results without breaking schema.
|
|
18
|
+
- Added future config slots for risk thresholds, dashboard output, and notification webhook env names.
|
|
19
|
+
- Added regression coverage for plugin hooks enriching `extensions`.
|
|
20
|
+
|
|
5
21
|
## 1.1.0
|
|
6
22
|
|
|
7
23
|
- Added `--focus` modes for targeted duplicate, unused, outdated, risk, and health analysis.
|
package/README.md
CHANGED
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
- Highlight dependency risk signals
|
|
24
24
|
- Score package trust using supply-chain metadata
|
|
25
25
|
- Generate a simple project health score
|
|
26
|
-
- Output reports in console, JSON, Markdown, SARIF, and top-issues formats
|
|
26
|
+
- Output reports in console, JSON, Markdown, SARIF, dashboard, and top-issues formats
|
|
27
27
|
- Gate CI with score and finding policies
|
|
28
28
|
- Compare new findings against a baseline report
|
|
29
29
|
|
|
@@ -48,12 +48,14 @@ The long-term goal is not just to list problems, but to answer:
|
|
|
48
48
|
- JSON output via `--json`
|
|
49
49
|
- Markdown output via `--md`
|
|
50
50
|
- SARIF output via `--sarif`
|
|
51
|
+
- Static HTML dashboard via `--dashboard`
|
|
51
52
|
- Ranked top issues via `--top`
|
|
52
53
|
- Baseline mode via `--baseline`
|
|
53
54
|
- Focused analysis via `--focus`
|
|
54
55
|
- Low-noise CI defaults via `--ci`
|
|
55
56
|
- Starter config generation via `dep-brain init`
|
|
56
57
|
- Reusable GitHub Action via `action.yml`
|
|
58
|
+
- Built-in `license` plugin and plugin diagnostics
|
|
57
59
|
- Library entrypoint for programmatic use
|
|
58
60
|
|
|
59
61
|
## CLI Usage
|
|
@@ -73,6 +75,8 @@ npx dep-brain analyze ./path-to-project --fail-on-unused --json
|
|
|
73
75
|
npx dep-brain analyze --md > depbrain.md
|
|
74
76
|
npx dep-brain analyze --json --out depbrain.json
|
|
75
77
|
npx dep-brain analyze --sarif --out depbrain.sarif
|
|
78
|
+
npx dep-brain analyze --dashboard
|
|
79
|
+
npx dep-brain analyze --dashboard --dashboard-out reports/depbrain.html
|
|
76
80
|
npx dep-brain analyze --focus duplicates
|
|
77
81
|
npx dep-brain analyze --ci
|
|
78
82
|
npx dep-brain analyze --baseline depbrain-baseline.json
|
|
@@ -176,6 +180,28 @@ dep-brain analyze --top
|
|
|
176
180
|
|
|
177
181
|
Shows the highest-priority actionable findings first, including confidence and next-step guidance.
|
|
178
182
|
|
|
183
|
+
## Dashboard Output
|
|
184
|
+
|
|
185
|
+
```bash
|
|
186
|
+
dep-brain analyze --dashboard
|
|
187
|
+
dep-brain analyze --dashboard --dashboard-out reports/depbrain.html
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
Writes a static HTML dashboard. Default path comes from `dashboard.outputPath`.
|
|
191
|
+
|
|
192
|
+
## Plugins
|
|
193
|
+
|
|
194
|
+
```json
|
|
195
|
+
{
|
|
196
|
+
"plugins": {
|
|
197
|
+
"enabled": ["license"],
|
|
198
|
+
"paths": ["./depbrain-plugin.mjs"]
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
Built-in `license` plugin adds license counts under `extensions.license`. Failed plugin loads and hook errors are reported under `extensions.depBrain.plugins`.
|
|
204
|
+
|
|
179
205
|
## Report From JSON
|
|
180
206
|
|
|
181
207
|
```bash
|
|
@@ -194,30 +220,6 @@ dep-brain analyze --baseline depbrain-baseline.json --min-score 90 --fail-on-ris
|
|
|
194
220
|
|
|
195
221
|
The baseline file is a normal JSON analysis report. Matching entries in `duplicates`, `unused`, `outdated`, and `risks` are ignored before score, policy, suggestions, and top issues are calculated.
|
|
196
222
|
|
|
197
|
-
## GitHub Action
|
|
198
|
-
|
|
199
|
-
```yaml
|
|
200
|
-
name: Dependency Brain
|
|
201
|
-
|
|
202
|
-
on:
|
|
203
|
-
pull_request:
|
|
204
|
-
|
|
205
|
-
jobs:
|
|
206
|
-
dep-brain:
|
|
207
|
-
runs-on: ubuntu-latest
|
|
208
|
-
steps:
|
|
209
|
-
- uses: actions/checkout@v4
|
|
210
|
-
- uses: actions/setup-node@v4
|
|
211
|
-
with:
|
|
212
|
-
node-version: 22
|
|
213
|
-
- uses: prakashu51/dep-brain@v1
|
|
214
|
-
with:
|
|
215
|
-
format: sarif
|
|
216
|
-
out: depbrain.sarif
|
|
217
|
-
min-score: 85
|
|
218
|
-
fail-on-risks: "true"
|
|
219
|
-
```
|
|
220
|
-
|
|
221
223
|
## Config File
|
|
222
224
|
|
|
223
225
|
Create a `depbrain.config.json` file in the project root:
|
|
@@ -238,6 +240,26 @@ Create a `depbrain.config.json` file in the project root:
|
|
|
238
240
|
"report": {
|
|
239
241
|
"maxSuggestions": 3
|
|
240
242
|
},
|
|
243
|
+
"plugins": {
|
|
244
|
+
"enabled": ["license"],
|
|
245
|
+
"paths": []
|
|
246
|
+
},
|
|
247
|
+
"risk": {
|
|
248
|
+
"transitiveBloatThreshold": 50,
|
|
249
|
+
"typosquattingDistanceThreshold": 2,
|
|
250
|
+
"staleReleaseDays": 730,
|
|
251
|
+
"agingReleaseDays": 365,
|
|
252
|
+
"lowDownloadThreshold": 1000,
|
|
253
|
+
"lowTrustWeightThreshold": 6,
|
|
254
|
+
"mediumTrustWeightThreshold": 3
|
|
255
|
+
},
|
|
256
|
+
"dashboard": {
|
|
257
|
+
"outputPath": "depbrain-dashboard.html"
|
|
258
|
+
},
|
|
259
|
+
"notifications": {
|
|
260
|
+
"slackWebhookEnv": "DEPBRAIN_SLACK_WEBHOOK_URL",
|
|
261
|
+
"discordWebhookEnv": "DEPBRAIN_DISCORD_WEBHOOK_URL"
|
|
262
|
+
},
|
|
241
263
|
"scoring": {
|
|
242
264
|
"duplicateWeight": 5,
|
|
243
265
|
"outdatedWeight": 1,
|
|
@@ -264,6 +286,18 @@ Supported sections:
|
|
|
264
286
|
- `policy.failOnOutdated`
|
|
265
287
|
- `policy.failOnRisks`
|
|
266
288
|
- `report.maxSuggestions`
|
|
289
|
+
- `plugins.enabled`
|
|
290
|
+
- `plugins.paths`
|
|
291
|
+
- `risk.transitiveBloatThreshold`
|
|
292
|
+
- `risk.typosquattingDistanceThreshold`
|
|
293
|
+
- `risk.staleReleaseDays`
|
|
294
|
+
- `risk.agingReleaseDays`
|
|
295
|
+
- `risk.lowDownloadThreshold`
|
|
296
|
+
- `risk.lowTrustWeightThreshold`
|
|
297
|
+
- `risk.mediumTrustWeightThreshold`
|
|
298
|
+
- `dashboard.outputPath`
|
|
299
|
+
- `notifications.slackWebhookEnv`
|
|
300
|
+
- `notifications.discordWebhookEnv`
|
|
267
301
|
- `scoring.duplicateWeight`
|
|
268
302
|
- `scoring.outdatedWeight`
|
|
269
303
|
- `scoring.unusedWeight`
|
package/depbrain.config.json
CHANGED
|
@@ -19,6 +19,26 @@
|
|
|
19
19
|
"report": {
|
|
20
20
|
"maxSuggestions": 5
|
|
21
21
|
},
|
|
22
|
+
"plugins": {
|
|
23
|
+
"enabled": [],
|
|
24
|
+
"paths": []
|
|
25
|
+
},
|
|
26
|
+
"risk": {
|
|
27
|
+
"transitiveBloatThreshold": 50,
|
|
28
|
+
"typosquattingDistanceThreshold": 2,
|
|
29
|
+
"staleReleaseDays": 730,
|
|
30
|
+
"agingReleaseDays": 365,
|
|
31
|
+
"lowDownloadThreshold": 1000,
|
|
32
|
+
"lowTrustWeightThreshold": 6,
|
|
33
|
+
"mediumTrustWeightThreshold": 3
|
|
34
|
+
},
|
|
35
|
+
"dashboard": {
|
|
36
|
+
"outputPath": "depbrain-dashboard.html"
|
|
37
|
+
},
|
|
38
|
+
"notifications": {
|
|
39
|
+
"slackWebhookEnv": "DEPBRAIN_SLACK_WEBHOOK_URL",
|
|
40
|
+
"discordWebhookEnv": "DEPBRAIN_DISCORD_WEBHOOK_URL"
|
|
41
|
+
},
|
|
22
42
|
"scoring": {
|
|
23
43
|
"duplicateWeight": 5,
|
|
24
44
|
"outdatedWeight": 3,
|
|
@@ -36,6 +36,42 @@
|
|
|
36
36
|
"maxSuggestions": { "type": "number" }
|
|
37
37
|
}
|
|
38
38
|
},
|
|
39
|
+
"plugins": {
|
|
40
|
+
"type": "object",
|
|
41
|
+
"additionalProperties": false,
|
|
42
|
+
"properties": {
|
|
43
|
+
"enabled": { "type": "array", "items": { "type": "string" } },
|
|
44
|
+
"paths": { "type": "array", "items": { "type": "string" } }
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
"risk": {
|
|
48
|
+
"type": "object",
|
|
49
|
+
"additionalProperties": false,
|
|
50
|
+
"properties": {
|
|
51
|
+
"transitiveBloatThreshold": { "type": "number" },
|
|
52
|
+
"typosquattingDistanceThreshold": { "type": "number" },
|
|
53
|
+
"staleReleaseDays": { "type": "number" },
|
|
54
|
+
"agingReleaseDays": { "type": "number" },
|
|
55
|
+
"lowDownloadThreshold": { "type": "number" },
|
|
56
|
+
"lowTrustWeightThreshold": { "type": "number" },
|
|
57
|
+
"mediumTrustWeightThreshold": { "type": "number" }
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
"dashboard": {
|
|
61
|
+
"type": "object",
|
|
62
|
+
"additionalProperties": false,
|
|
63
|
+
"properties": {
|
|
64
|
+
"outputPath": { "type": "string" }
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
"notifications": {
|
|
68
|
+
"type": "object",
|
|
69
|
+
"additionalProperties": false,
|
|
70
|
+
"properties": {
|
|
71
|
+
"slackWebhookEnv": { "type": "string" },
|
|
72
|
+
"discordWebhookEnv": { "type": "string" }
|
|
73
|
+
}
|
|
74
|
+
},
|
|
39
75
|
"scoring": {
|
|
40
76
|
"type": "object",
|
|
41
77
|
"additionalProperties": false,
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
3
|
"title": "Dependency Brain Analysis Output",
|
|
4
4
|
"type": "object",
|
|
5
|
-
"required": ["outputVersion", "rootDir", "score", "scoreBreakdown", "policy", "ownershipSummary", "duplicates", "unused", "outdated", "risks", "suggestions", "topIssues", "config"],
|
|
5
|
+
"required": ["outputVersion", "rootDir", "score", "scoreBreakdown", "policy", "ownershipSummary", "duplicates", "unused", "outdated", "risks", "suggestions", "topIssues", "extensions", "config"],
|
|
6
6
|
"additionalProperties": false,
|
|
7
7
|
"properties": {
|
|
8
8
|
"recommendation": {
|
|
@@ -175,6 +175,10 @@
|
|
|
175
175
|
}
|
|
176
176
|
},
|
|
177
177
|
"suggestions": { "type": "array", "items": { "type": "string" } },
|
|
178
|
+
"extensions": {
|
|
179
|
+
"type": "object",
|
|
180
|
+
"additionalProperties": true
|
|
181
|
+
},
|
|
178
182
|
"topIssues": {
|
|
179
183
|
"type": "array",
|
|
180
184
|
"items": {
|
package/dist/checks/risk.d.ts
CHANGED
|
@@ -2,8 +2,10 @@ import type { DependencyGraph } from "../core/graph-builder.js";
|
|
|
2
2
|
import type { RiskDependency } from "../core/analyzer.js";
|
|
3
3
|
import type { CheckResult } from "../core/types.js";
|
|
4
4
|
import { type PackageMetadata } from "../utils/npm-api.js";
|
|
5
|
+
import type { DepBrainConfig } from "../utils/config.js";
|
|
5
6
|
export interface RiskCheckOptions {
|
|
6
7
|
resolvePackageMetadata?: (name: string) => Promise<PackageMetadata | null>;
|
|
8
|
+
thresholds?: DepBrainConfig["risk"];
|
|
7
9
|
}
|
|
8
10
|
export declare function findRiskDependencies(graph: DependencyGraph, options?: RiskCheckOptions): Promise<RiskDependency[]>;
|
|
9
|
-
export declare function runRiskCheck(graph: DependencyGraph): Promise<CheckResult>;
|
|
11
|
+
export declare function runRiskCheck(graph: DependencyGraph, options?: RiskCheckOptions): Promise<CheckResult>;
|
package/dist/checks/risk.js
CHANGED
|
@@ -1,6 +1,4 @@
|
|
|
1
1
|
import { getPackageMetadata } from "../utils/npm-api.js";
|
|
2
|
-
const TWO_YEARS_IN_DAYS = 365 * 2;
|
|
3
|
-
const ONE_YEAR_IN_DAYS = 365;
|
|
4
2
|
export async function findRiskDependencies(graph, options = {}) {
|
|
5
3
|
const resolvePackageMetadata = options.resolvePackageMetadata ?? getPackageMetadata;
|
|
6
4
|
const names = Object.keys({
|
|
@@ -17,7 +15,7 @@ export async function findRiskDependencies(graph, options = {}) {
|
|
|
17
15
|
: graph.devDependencies[name]
|
|
18
16
|
? "devDependencies"
|
|
19
17
|
: "unknown";
|
|
20
|
-
const assessment = assessRisk(metadata, dependencyType);
|
|
18
|
+
const assessment = assessRisk(metadata, dependencyType, options.thresholds);
|
|
21
19
|
if (!shouldReportRisk(assessment.trustScore, dependencyType)) {
|
|
22
20
|
return null;
|
|
23
21
|
}
|
|
@@ -50,8 +48,8 @@ async function mapWithConcurrency(items, limit, mapper) {
|
|
|
50
48
|
await Promise.all(workers);
|
|
51
49
|
return results;
|
|
52
50
|
}
|
|
53
|
-
export async function runRiskCheck(graph) {
|
|
54
|
-
const risks = await findRiskDependencies(graph);
|
|
51
|
+
export async function runRiskCheck(graph, options = {}) {
|
|
52
|
+
const risks = await findRiskDependencies(graph, options);
|
|
55
53
|
return {
|
|
56
54
|
name: "risk",
|
|
57
55
|
summary: `${risks.length} risky dependencies found`,
|
|
@@ -75,22 +73,27 @@ export async function runRiskCheck(graph) {
|
|
|
75
73
|
}))
|
|
76
74
|
};
|
|
77
75
|
}
|
|
78
|
-
function assessRisk(metadata, dependencyType) {
|
|
76
|
+
function assessRisk(metadata, dependencyType, thresholds) {
|
|
79
77
|
const reasons = [];
|
|
80
78
|
const reasonCodes = [];
|
|
81
79
|
let weight = 0;
|
|
82
|
-
|
|
83
|
-
|
|
80
|
+
const staleReleaseDays = thresholds?.staleReleaseDays ?? 730;
|
|
81
|
+
const agingReleaseDays = thresholds?.agingReleaseDays ?? 365;
|
|
82
|
+
const lowDownloadThreshold = thresholds?.lowDownloadThreshold ?? 1000;
|
|
83
|
+
const lowTrustWeightThreshold = thresholds?.lowTrustWeightThreshold ?? 6;
|
|
84
|
+
const mediumTrustWeightThreshold = thresholds?.mediumTrustWeightThreshold ?? 3;
|
|
85
|
+
if (metadata.daysSincePublish !== null && metadata.daysSincePublish > staleReleaseDays) {
|
|
86
|
+
reasons.push(`No release in over ${formatDays(staleReleaseDays)}`);
|
|
84
87
|
reasonCodes.push("stale_release");
|
|
85
88
|
weight += 3;
|
|
86
89
|
}
|
|
87
90
|
else if (metadata.daysSincePublish !== null &&
|
|
88
|
-
metadata.daysSincePublish >
|
|
89
|
-
reasons.push(
|
|
91
|
+
metadata.daysSincePublish > agingReleaseDays) {
|
|
92
|
+
reasons.push(`No release in over ${formatDays(agingReleaseDays)}`);
|
|
90
93
|
reasonCodes.push("aging_release");
|
|
91
94
|
weight += 2;
|
|
92
95
|
}
|
|
93
|
-
if (metadata.downloads !== null && metadata.downloads <
|
|
96
|
+
if (metadata.downloads !== null && metadata.downloads < lowDownloadThreshold) {
|
|
94
97
|
reasons.push("Low weekly download volume");
|
|
95
98
|
reasonCodes.push("low_download_volume");
|
|
96
99
|
weight += 2;
|
|
@@ -118,7 +121,11 @@ function assessRisk(metadata, dependencyType) {
|
|
|
118
121
|
weight += 1;
|
|
119
122
|
}
|
|
120
123
|
const confidence = reasons.length === 0 ? 0.5 : Math.min(0.99, 0.52 + weight * 0.07);
|
|
121
|
-
const trustScore = weight >=
|
|
124
|
+
const trustScore = weight >= lowTrustWeightThreshold
|
|
125
|
+
? "low"
|
|
126
|
+
: weight >= mediumTrustWeightThreshold
|
|
127
|
+
? "medium"
|
|
128
|
+
: "high";
|
|
122
129
|
return {
|
|
123
130
|
confidence,
|
|
124
131
|
trustScore,
|
|
@@ -135,6 +142,15 @@ function assessRisk(metadata, dependencyType) {
|
|
|
135
142
|
}
|
|
136
143
|
};
|
|
137
144
|
}
|
|
145
|
+
function formatDays(days) {
|
|
146
|
+
if (days === 730) {
|
|
147
|
+
return "2 years";
|
|
148
|
+
}
|
|
149
|
+
if (days === 365) {
|
|
150
|
+
return "12 months";
|
|
151
|
+
}
|
|
152
|
+
return `${days} days`;
|
|
153
|
+
}
|
|
138
154
|
function shouldReportRisk(trustScore, dependencyType) {
|
|
139
155
|
if (trustScore === "high") {
|
|
140
156
|
return false;
|
package/dist/cli.js
CHANGED
|
@@ -4,6 +4,7 @@ import { renderConsoleReport } from "./reporters/console.js";
|
|
|
4
4
|
import { renderJsonReport } from "./reporters/json.js";
|
|
5
5
|
import { renderMarkdownReport } from "./reporters/markdown.js";
|
|
6
6
|
import { renderSarifReport } from "./reporters/sarif.js";
|
|
7
|
+
import { renderDashboardReport } from "./reporters/dashboard.js";
|
|
7
8
|
import { defaultConfig } from "./utils/config.js";
|
|
8
9
|
import { promises as fs } from "node:fs";
|
|
9
10
|
import path from "node:path";
|
|
@@ -59,7 +60,9 @@ async function main() {
|
|
|
59
60
|
? JSON.stringify(reportData, null, 2)
|
|
60
61
|
: flags.has("--sarif")
|
|
61
62
|
? renderSarifReport(reportData)
|
|
62
|
-
:
|
|
63
|
+
: flags.has("--dashboard")
|
|
64
|
+
? renderDashboardReport(reportData)
|
|
65
|
+
: renderMarkdownReport(reportData);
|
|
63
66
|
await writeOutput(output, optionValues.get("--out"));
|
|
64
67
|
return;
|
|
65
68
|
}
|
|
@@ -149,6 +152,9 @@ async function main() {
|
|
|
149
152
|
: consoleOutput;
|
|
150
153
|
}
|
|
151
154
|
await writeOutput(output, optionValues.get("--out"));
|
|
155
|
+
if (flags.has("--dashboard")) {
|
|
156
|
+
await writeOutput(renderDashboardReport(result), optionValues.get("--dashboard-out") ?? result.config.dashboard.outputPath);
|
|
157
|
+
}
|
|
152
158
|
if (!result.policy.passed) {
|
|
153
159
|
process.exitCode = 1;
|
|
154
160
|
}
|
|
@@ -209,8 +215,8 @@ function printHelp() {
|
|
|
209
215
|
console.log("Dependency Brain");
|
|
210
216
|
console.log("");
|
|
211
217
|
console.log("Usage:");
|
|
212
|
-
console.log(" dep-brain analyze [path] [--json] [--md] [--sarif] [--top] [--focus kind] [--ci] [--out path] [--config path] [--baseline path] [--min-score n] [--fail-on-risks]");
|
|
213
|
-
console.log(" dep-brain report --from <file> [--md] [--json] [--sarif] [--top] [--out path]");
|
|
218
|
+
console.log(" dep-brain analyze [path] [--json] [--md] [--sarif] [--top] [--dashboard] [--focus kind] [--ci] [--out path] [--config path] [--baseline path] [--min-score n] [--fail-on-risks]");
|
|
219
|
+
console.log(" dep-brain report --from <file> [--md] [--json] [--sarif] [--top] [--dashboard] [--out path]");
|
|
214
220
|
console.log(" dep-brain config [path] [--config path]");
|
|
215
221
|
console.log(" dep-brain init [--out depbrain.config.json]");
|
|
216
222
|
console.log(" dep-brain help");
|
|
@@ -221,6 +227,8 @@ function printHelp() {
|
|
|
221
227
|
console.log(" --md Output Markdown report");
|
|
222
228
|
console.log(" --sarif Output SARIF format for Code Scanning");
|
|
223
229
|
console.log(" --top Output the ranked top issues only");
|
|
230
|
+
console.log(" --dashboard Write an HTML dashboard");
|
|
231
|
+
console.log(" --dashboard-out <path> Write dashboard HTML to a custom path");
|
|
224
232
|
console.log(" --focus <kind> Run all, health, duplicates, unused, outdated, or risks");
|
|
225
233
|
console.log(" --ci Apply low-noise CI defaults");
|
|
226
234
|
console.log(" --config <path> Path to depbrain.config.json");
|
package/dist/core/analyzer.d.ts
CHANGED
|
@@ -110,6 +110,7 @@ export interface AnalysisResult {
|
|
|
110
110
|
risks: RiskDependency[];
|
|
111
111
|
suggestions: string[];
|
|
112
112
|
topIssues: TopIssue[];
|
|
113
|
+
extensions: Record<string, unknown>;
|
|
113
114
|
config: DepBrainConfig;
|
|
114
115
|
packages?: PackageAnalysisResult[];
|
|
115
116
|
}
|
|
@@ -130,6 +131,7 @@ export interface PackageAnalysisResult {
|
|
|
130
131
|
risks: RiskDependency[];
|
|
131
132
|
suggestions: string[];
|
|
132
133
|
topIssues: TopIssue[];
|
|
134
|
+
extensions: Record<string, unknown>;
|
|
133
135
|
}
|
|
134
136
|
export declare const OUTPUT_VERSION = "1.4";
|
|
135
137
|
export interface ScoreBreakdown {
|
package/dist/core/analyzer.js
CHANGED
|
@@ -6,6 +6,7 @@ import { runUnusedCheck } from "../checks/unused.js";
|
|
|
6
6
|
import { loadDepBrainConfig } from "../utils/config.js";
|
|
7
7
|
import { findWorkspacePackages } from "../utils/workspaces.js";
|
|
8
8
|
import { buildDependencyGraph } from "./graph-builder.js";
|
|
9
|
+
import { PluginManager } from "./plugin-manager.js";
|
|
9
10
|
import { calculateHealthScore, calculateScoreDeductions } from "./scorer.js";
|
|
10
11
|
import { buildAnalysisContext } from "./context.js";
|
|
11
12
|
export const OUTPUT_VERSION = "1.4";
|
|
@@ -14,12 +15,15 @@ export async function analyzeProject(options = {}) {
|
|
|
14
15
|
const loadedConfig = await loadDepBrainConfig(rootDir, options.configPath);
|
|
15
16
|
const config = mergeConfig(loadedConfig, options.config);
|
|
16
17
|
const focus = options.focus ?? "all";
|
|
18
|
+
const plugins = await PluginManager.load(rootDir, config);
|
|
19
|
+
await plugins.runPreScan({ rootDir, config });
|
|
17
20
|
const workspaces = await findWorkspacePackages(rootDir);
|
|
18
21
|
if (workspaces.length === 0) {
|
|
19
|
-
|
|
22
|
+
const result = await analyzeSingleProject(rootDir, config, {
|
|
20
23
|
baseline: options.baseline,
|
|
21
24
|
focus
|
|
22
25
|
});
|
|
26
|
+
return plugins.runPostScan(result);
|
|
23
27
|
}
|
|
24
28
|
const rootGraph = await buildDependencyGraph(rootDir);
|
|
25
29
|
const duplicates = shouldRunCheck("duplicate", focus)
|
|
@@ -79,7 +83,7 @@ export async function analyzeProject(options = {}) {
|
|
|
79
83
|
outdated: outdated.length,
|
|
80
84
|
risks: risks.length
|
|
81
85
|
}, config);
|
|
82
|
-
|
|
86
|
+
const result = {
|
|
83
87
|
outputVersion: OUTPUT_VERSION,
|
|
84
88
|
rootDir,
|
|
85
89
|
score,
|
|
@@ -102,9 +106,11 @@ export async function analyzeProject(options = {}) {
|
|
|
102
106
|
outdated,
|
|
103
107
|
risks
|
|
104
108
|
}),
|
|
109
|
+
extensions: {},
|
|
105
110
|
config,
|
|
106
111
|
packages
|
|
107
112
|
};
|
|
113
|
+
return plugins.runPostScan(result);
|
|
108
114
|
}
|
|
109
115
|
function mergeConfig(base, overrides) {
|
|
110
116
|
if (!overrides) {
|
|
@@ -131,6 +137,26 @@ function mergeConfig(base, overrides) {
|
|
|
131
137
|
report: {
|
|
132
138
|
maxSuggestions: overrides.report?.maxSuggestions ?? base.report.maxSuggestions
|
|
133
139
|
},
|
|
140
|
+
plugins: {
|
|
141
|
+
enabled: overrides.plugins?.enabled ?? base.plugins.enabled,
|
|
142
|
+
paths: overrides.plugins?.paths ?? base.plugins.paths
|
|
143
|
+
},
|
|
144
|
+
risk: {
|
|
145
|
+
transitiveBloatThreshold: overrides.risk?.transitiveBloatThreshold ?? base.risk.transitiveBloatThreshold,
|
|
146
|
+
typosquattingDistanceThreshold: overrides.risk?.typosquattingDistanceThreshold ?? base.risk.typosquattingDistanceThreshold,
|
|
147
|
+
staleReleaseDays: overrides.risk?.staleReleaseDays ?? base.risk.staleReleaseDays,
|
|
148
|
+
agingReleaseDays: overrides.risk?.agingReleaseDays ?? base.risk.agingReleaseDays,
|
|
149
|
+
lowDownloadThreshold: overrides.risk?.lowDownloadThreshold ?? base.risk.lowDownloadThreshold,
|
|
150
|
+
lowTrustWeightThreshold: overrides.risk?.lowTrustWeightThreshold ?? base.risk.lowTrustWeightThreshold,
|
|
151
|
+
mediumTrustWeightThreshold: overrides.risk?.mediumTrustWeightThreshold ?? base.risk.mediumTrustWeightThreshold
|
|
152
|
+
},
|
|
153
|
+
dashboard: {
|
|
154
|
+
outputPath: overrides.dashboard?.outputPath ?? base.dashboard.outputPath
|
|
155
|
+
},
|
|
156
|
+
notifications: {
|
|
157
|
+
slackWebhookEnv: overrides.notifications?.slackWebhookEnv ?? base.notifications.slackWebhookEnv,
|
|
158
|
+
discordWebhookEnv: overrides.notifications?.discordWebhookEnv ?? base.notifications.discordWebhookEnv
|
|
159
|
+
},
|
|
134
160
|
scoring: {
|
|
135
161
|
duplicateWeight: overrides.scoring?.duplicateWeight ?? base.scoring.duplicateWeight,
|
|
136
162
|
outdatedWeight: overrides.scoring?.outdatedWeight ?? base.scoring.outdatedWeight,
|
|
@@ -166,7 +192,7 @@ function evaluatePolicy(summary, config) {
|
|
|
166
192
|
}
|
|
167
193
|
async function analyzeSingleProject(rootDir, config, options = {}) {
|
|
168
194
|
const context = await buildAnalysisContext(rootDir, config);
|
|
169
|
-
const results = await runChecks(context, options.focus ?? "all");
|
|
195
|
+
const results = await runChecks(context, options.focus ?? "all", config);
|
|
170
196
|
const issueGroups = normalizeIssues(results, config);
|
|
171
197
|
const duplicates = mapDuplicateIssues(issueGroups.duplicates);
|
|
172
198
|
const unused = mapUnusedIssues(issueGroups.unused);
|
|
@@ -238,6 +264,7 @@ async function analyzeSingleProject(rootDir, config, options = {}) {
|
|
|
238
264
|
outdated: baselineFiltered.outdated,
|
|
239
265
|
risks: baselineFiltered.risks
|
|
240
266
|
}),
|
|
267
|
+
extensions: {},
|
|
241
268
|
config
|
|
242
269
|
};
|
|
243
270
|
}
|
|
@@ -258,7 +285,7 @@ function shouldIgnorePackage(name, bucket, config) {
|
|
|
258
285
|
}
|
|
259
286
|
});
|
|
260
287
|
}
|
|
261
|
-
async function runChecks(context, focus) {
|
|
288
|
+
async function runChecks(context, focus, config) {
|
|
262
289
|
const checks = [
|
|
263
290
|
{
|
|
264
291
|
name: "duplicate",
|
|
@@ -274,7 +301,7 @@ async function runChecks(context, focus) {
|
|
|
274
301
|
},
|
|
275
302
|
{
|
|
276
303
|
name: "risk",
|
|
277
|
-
run: () => runRiskCheck(context.graph)
|
|
304
|
+
run: () => runRiskCheck(context.graph, { thresholds: config.risk })
|
|
278
305
|
}
|
|
279
306
|
];
|
|
280
307
|
const results = [];
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { AnalysisResult } from "./analyzer.js";
|
|
2
|
+
import type { DepBrainConfig } from "../utils/config.js";
|
|
3
|
+
export interface ProjectContext {
|
|
4
|
+
rootDir: string;
|
|
5
|
+
config: DepBrainConfig;
|
|
6
|
+
}
|
|
7
|
+
export interface DepBrainPlugin {
|
|
8
|
+
name: string;
|
|
9
|
+
preScan?: (context: ProjectContext) => Promise<void> | void;
|
|
10
|
+
postScan?: (result: AnalysisResult) => Promise<AnalysisResult | void> | AnalysisResult | void;
|
|
11
|
+
reportHook?: (result: AnalysisResult) => Promise<Record<string, unknown> | void> | Record<string, unknown> | void;
|
|
12
|
+
cliCommands?: (cli: unknown) => void;
|
|
13
|
+
}
|
|
14
|
+
export interface PluginDiagnostic {
|
|
15
|
+
spec: string;
|
|
16
|
+
code: "load_failed" | "invalid_plugin" | "hook_failed";
|
|
17
|
+
message: string;
|
|
18
|
+
plugin?: string;
|
|
19
|
+
hook?: "preScan" | "postScan" | "reportHook";
|
|
20
|
+
}
|
|
21
|
+
export declare class PluginManager {
|
|
22
|
+
private readonly plugins;
|
|
23
|
+
private readonly diagnostics;
|
|
24
|
+
private constructor();
|
|
25
|
+
static load(rootDir: string, config: DepBrainConfig): Promise<PluginManager>;
|
|
26
|
+
runPreScan(context: ProjectContext): Promise<void>;
|
|
27
|
+
runPostScan(result: AnalysisResult): Promise<AnalysisResult>;
|
|
28
|
+
private attachDiagnostics;
|
|
29
|
+
}
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import { promises as fs } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { pathToFileURL } from "node:url";
|
|
4
|
+
export class PluginManager {
|
|
5
|
+
plugins;
|
|
6
|
+
diagnostics;
|
|
7
|
+
constructor(plugins, diagnostics) {
|
|
8
|
+
this.plugins = plugins;
|
|
9
|
+
this.diagnostics = diagnostics;
|
|
10
|
+
}
|
|
11
|
+
static async load(rootDir, config) {
|
|
12
|
+
const specs = [
|
|
13
|
+
...config.plugins.enabled.map((name) => `dep-brain-plugin-${name}`),
|
|
14
|
+
...config.plugins.paths
|
|
15
|
+
];
|
|
16
|
+
const plugins = [];
|
|
17
|
+
const diagnostics = [];
|
|
18
|
+
for (const spec of specs) {
|
|
19
|
+
const result = await loadPlugin(rootDir, spec);
|
|
20
|
+
if (result.plugin) {
|
|
21
|
+
plugins.push(result.plugin);
|
|
22
|
+
}
|
|
23
|
+
if (result.diagnostic) {
|
|
24
|
+
diagnostics.push(result.diagnostic);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return new PluginManager(plugins, diagnostics);
|
|
28
|
+
}
|
|
29
|
+
async runPreScan(context) {
|
|
30
|
+
for (const plugin of this.plugins) {
|
|
31
|
+
try {
|
|
32
|
+
await plugin.preScan?.(context);
|
|
33
|
+
}
|
|
34
|
+
catch (error) {
|
|
35
|
+
this.diagnostics.push(buildHookDiagnostic(plugin.name, "preScan", error));
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
async runPostScan(result) {
|
|
40
|
+
let current = result;
|
|
41
|
+
for (const plugin of this.plugins) {
|
|
42
|
+
try {
|
|
43
|
+
const next = await plugin.postScan?.(current);
|
|
44
|
+
if (next) {
|
|
45
|
+
current = next;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
catch (error) {
|
|
49
|
+
this.diagnostics.push(buildHookDiagnostic(plugin.name, "postScan", error));
|
|
50
|
+
}
|
|
51
|
+
try {
|
|
52
|
+
const reportSection = await plugin.reportHook?.(current);
|
|
53
|
+
if (reportSection) {
|
|
54
|
+
current.extensions[plugin.name] = {
|
|
55
|
+
...(asRecord(current.extensions[plugin.name]) ?? {}),
|
|
56
|
+
...reportSection
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
this.diagnostics.push(buildHookDiagnostic(plugin.name, "reportHook", error));
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return this.attachDiagnostics(current);
|
|
65
|
+
}
|
|
66
|
+
attachDiagnostics(result) {
|
|
67
|
+
if (this.diagnostics.length === 0) {
|
|
68
|
+
return result;
|
|
69
|
+
}
|
|
70
|
+
result.extensions.depBrain = {
|
|
71
|
+
...(asRecord(result.extensions.depBrain) ?? {}),
|
|
72
|
+
plugins: this.diagnostics
|
|
73
|
+
};
|
|
74
|
+
return result;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
async function loadPlugin(rootDir, spec) {
|
|
78
|
+
const builtIn = getBuiltInPlugin(spec);
|
|
79
|
+
if (builtIn) {
|
|
80
|
+
return { plugin: builtIn };
|
|
81
|
+
}
|
|
82
|
+
try {
|
|
83
|
+
const resolved = spec.startsWith(".") || path.isAbsolute(spec)
|
|
84
|
+
? path.resolve(rootDir, spec)
|
|
85
|
+
: spec;
|
|
86
|
+
const moduleUrl = path.isAbsolute(resolved) ? pathToFileURL(resolved).href : resolved;
|
|
87
|
+
const mod = await import(moduleUrl);
|
|
88
|
+
const exported = mod.default ?? mod.plugin ?? mod;
|
|
89
|
+
const candidate = typeof exported === "function" ? new exported() : exported;
|
|
90
|
+
if (isPlugin(candidate)) {
|
|
91
|
+
return { plugin: candidate };
|
|
92
|
+
}
|
|
93
|
+
return {
|
|
94
|
+
plugin: null,
|
|
95
|
+
diagnostic: {
|
|
96
|
+
spec,
|
|
97
|
+
code: "invalid_plugin",
|
|
98
|
+
message: "Plugin must export an object with a string name."
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
catch (error) {
|
|
103
|
+
return {
|
|
104
|
+
plugin: null,
|
|
105
|
+
diagnostic: {
|
|
106
|
+
spec,
|
|
107
|
+
code: "load_failed",
|
|
108
|
+
message: error instanceof Error ? error.message : String(error)
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
function getBuiltInPlugin(spec) {
|
|
114
|
+
if (spec !== "license" && spec !== "dep-brain-plugin-license") {
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
return {
|
|
118
|
+
name: "license",
|
|
119
|
+
reportHook: async (result) => {
|
|
120
|
+
const packages = await collectLicensePackages(result.rootDir);
|
|
121
|
+
const licenses = packages.reduce((acc, item) => {
|
|
122
|
+
acc[item.license] = (acc[item.license] ?? 0) + 1;
|
|
123
|
+
return acc;
|
|
124
|
+
}, {});
|
|
125
|
+
return {
|
|
126
|
+
summary: {
|
|
127
|
+
total: packages.length,
|
|
128
|
+
unknown: packages.filter((item) => item.license === "UNKNOWN").length,
|
|
129
|
+
licenses
|
|
130
|
+
},
|
|
131
|
+
packages
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
async function collectLicensePackages(rootDir) {
|
|
137
|
+
const raw = await fs.readFile(path.join(rootDir, "package.json"), "utf8");
|
|
138
|
+
const pkg = JSON.parse(raw);
|
|
139
|
+
const names = Object.keys({
|
|
140
|
+
...(pkg.dependencies ?? {}),
|
|
141
|
+
...(pkg.devDependencies ?? {})
|
|
142
|
+
}).sort();
|
|
143
|
+
return Promise.all(names.map(async (name) => ({
|
|
144
|
+
name,
|
|
145
|
+
license: await readPackageLicense(rootDir, name)
|
|
146
|
+
})));
|
|
147
|
+
}
|
|
148
|
+
async function readPackageLicense(rootDir, name) {
|
|
149
|
+
try {
|
|
150
|
+
const raw = await fs.readFile(path.join(rootDir, "node_modules", name, "package.json"), "utf8");
|
|
151
|
+
const pkg = JSON.parse(raw);
|
|
152
|
+
if (typeof pkg.license === "string" && pkg.license.trim().length > 0) {
|
|
153
|
+
return pkg.license;
|
|
154
|
+
}
|
|
155
|
+
if (Array.isArray(pkg.licenses) && pkg.licenses.length > 0) {
|
|
156
|
+
const licenses = pkg.licenses
|
|
157
|
+
.map((item) => {
|
|
158
|
+
if (typeof item === "string") {
|
|
159
|
+
return item;
|
|
160
|
+
}
|
|
161
|
+
if (item && typeof item === "object" && typeof item.type === "string") {
|
|
162
|
+
return item.type;
|
|
163
|
+
}
|
|
164
|
+
return null;
|
|
165
|
+
})
|
|
166
|
+
.filter((item) => Boolean(item));
|
|
167
|
+
return licenses.length > 0 ? licenses.join(", ") : "UNKNOWN";
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
catch {
|
|
171
|
+
return "UNKNOWN";
|
|
172
|
+
}
|
|
173
|
+
return "UNKNOWN";
|
|
174
|
+
}
|
|
175
|
+
function buildHookDiagnostic(plugin, hook, error) {
|
|
176
|
+
return {
|
|
177
|
+
spec: plugin,
|
|
178
|
+
plugin,
|
|
179
|
+
hook,
|
|
180
|
+
code: "hook_failed",
|
|
181
|
+
message: error instanceof Error ? error.message : String(error)
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
function isPlugin(value) {
|
|
185
|
+
return Boolean(value &&
|
|
186
|
+
typeof value === "object" &&
|
|
187
|
+
typeof value.name === "string");
|
|
188
|
+
}
|
|
189
|
+
function asRecord(value) {
|
|
190
|
+
return value && typeof value === "object" && !Array.isArray(value)
|
|
191
|
+
? value
|
|
192
|
+
: null;
|
|
193
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
export { analyzeProject } from "./core/analyzer.js";
|
|
2
2
|
export type { AnalysisOptions, AnalysisFocus, AnalysisResult, DepBrainBaseline, DuplicateDependency, OutdatedDependency, PolicyResult, PackageAnalysisResult, Recommendation, RiskFactors, ScoreBreakdown, RiskDependency, TopIssue, TrustScore, UnusedDependency, WorkspaceDependencyUsage, WorkspaceOwnershipSummary } from "./core/analyzer.js";
|
|
3
3
|
export { OUTPUT_VERSION } from "./core/analyzer.js";
|
|
4
|
+
export { PluginManager } from "./core/plugin-manager.js";
|
|
5
|
+
export type { DepBrainPlugin, PluginDiagnostic, ProjectContext } from "./core/plugin-manager.js";
|
|
4
6
|
export type { AnalysisContext, CheckResult, Issue } from "./core/types.js";
|
|
5
7
|
export type { DepBrainConfig, DepBrainConfigOverrides } from "./utils/config.js";
|
|
6
8
|
export type { WorkspacePackage } from "./utils/workspaces.js";
|
package/dist/index.js
CHANGED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
export function renderDashboardReport(result) {
|
|
2
|
+
const topIssues = result.topIssues.map(renderTopIssue).join("");
|
|
3
|
+
const suggestions = result.suggestions.map((item) => `<li>${escapeHtml(item)}</li>`).join("");
|
|
4
|
+
return [
|
|
5
|
+
"<!doctype html>",
|
|
6
|
+
'<html lang="en">',
|
|
7
|
+
"<head>",
|
|
8
|
+
'<meta charset="utf-8">',
|
|
9
|
+
'<meta name="viewport" content="width=device-width, initial-scale=1">',
|
|
10
|
+
"<title>Dependency Brain Dashboard</title>",
|
|
11
|
+
"<style>",
|
|
12
|
+
"body{font-family:Arial,sans-serif;margin:0;color:#172033;background:#f6f8fb}",
|
|
13
|
+
"main{max-width:1120px;margin:0 auto;padding:32px}",
|
|
14
|
+
"header{display:flex;justify-content:space-between;gap:24px;align-items:flex-start;margin-bottom:24px}",
|
|
15
|
+
"h1{font-size:28px;margin:0 0 8px}",
|
|
16
|
+
"h2{font-size:18px;margin:0 0 12px}",
|
|
17
|
+
".muted{color:#637083;font-size:13px}",
|
|
18
|
+
".score{font-size:48px;font-weight:700}",
|
|
19
|
+
".grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:12px;margin-bottom:20px}",
|
|
20
|
+
".panel{background:#fff;border:1px solid #dce3ee;border-radius:8px;padding:16px}",
|
|
21
|
+
".metric{font-size:28px;font-weight:700;margin-top:4px}",
|
|
22
|
+
".pass{color:#167a43}.fail{color:#b42318}",
|
|
23
|
+
"ol,ul{margin:0;padding-left:20px}",
|
|
24
|
+
"li{margin:8px 0}",
|
|
25
|
+
".issue{margin-bottom:10px}",
|
|
26
|
+
".kind{font-size:12px;text-transform:uppercase;color:#637083}",
|
|
27
|
+
"@media(max-width:760px){main{padding:20px}.grid{grid-template-columns:repeat(2,minmax(0,1fr))}header{display:block}.score{font-size:40px}}",
|
|
28
|
+
"</style>",
|
|
29
|
+
"</head>",
|
|
30
|
+
"<body>",
|
|
31
|
+
"<main>",
|
|
32
|
+
"<header>",
|
|
33
|
+
"<div>",
|
|
34
|
+
"<h1>Dependency Brain Dashboard</h1>",
|
|
35
|
+
`<div class="muted">${escapeHtml(result.rootDir)}</div>`,
|
|
36
|
+
"</div>",
|
|
37
|
+
`<div class="${result.policy.passed ? "pass" : "fail"} score">${result.score}/100</div>`,
|
|
38
|
+
"</header>",
|
|
39
|
+
'<section class="grid">',
|
|
40
|
+
renderMetric("Duplicates", result.duplicates.length),
|
|
41
|
+
renderMetric("Unused", result.unused.length),
|
|
42
|
+
renderMetric("Outdated", result.outdated.length),
|
|
43
|
+
renderMetric("Risks", result.risks.length),
|
|
44
|
+
"</section>",
|
|
45
|
+
'<section class="panel">',
|
|
46
|
+
"<h2>Policy</h2>",
|
|
47
|
+
`<p class="${result.policy.passed ? "pass" : "fail"}">${result.policy.passed ? "Passed" : "Failed"}</p>`,
|
|
48
|
+
result.policy.reasons.length > 0
|
|
49
|
+
? `<ul>${result.policy.reasons.map((item) => `<li>${escapeHtml(item)}</li>`).join("")}</ul>`
|
|
50
|
+
: '<p class="muted">No policy failures.</p>',
|
|
51
|
+
"</section>",
|
|
52
|
+
'<section class="panel">',
|
|
53
|
+
"<h2>Top Issues</h2>",
|
|
54
|
+
topIssues.length > 0 ? `<ol>${topIssues}</ol>` : '<p class="muted">No actionable issues found.</p>',
|
|
55
|
+
"</section>",
|
|
56
|
+
'<section class="panel">',
|
|
57
|
+
"<h2>Suggestions</h2>",
|
|
58
|
+
suggestions.length > 0 ? `<ul>${suggestions}</ul>` : '<p class="muted">No suggestions.</p>',
|
|
59
|
+
"</section>",
|
|
60
|
+
"</main>",
|
|
61
|
+
"</body>",
|
|
62
|
+
"</html>"
|
|
63
|
+
].join("\n");
|
|
64
|
+
}
|
|
65
|
+
function renderMetric(label, value) {
|
|
66
|
+
return `<div class="panel"><div class="muted">${escapeHtml(label)}</div><div class="metric">${value}</div></div>`;
|
|
67
|
+
}
|
|
68
|
+
function renderTopIssue(item) {
|
|
69
|
+
return [
|
|
70
|
+
'<li class="issue">',
|
|
71
|
+
`<div class="kind">${escapeHtml(item.kind)} ${escapeHtml(item.priority)}</div>`,
|
|
72
|
+
`<strong>${escapeHtml(item.name)}</strong>`,
|
|
73
|
+
`<div>${escapeHtml(item.recommendation.summary)}</div>`,
|
|
74
|
+
"</li>"
|
|
75
|
+
].join("");
|
|
76
|
+
}
|
|
77
|
+
function escapeHtml(value) {
|
|
78
|
+
return value
|
|
79
|
+
.replace(/&/g, "&")
|
|
80
|
+
.replace(/</g, "<")
|
|
81
|
+
.replace(/>/g, ">")
|
|
82
|
+
.replace(/"/g, """)
|
|
83
|
+
.replace(/'/g, "'");
|
|
84
|
+
}
|
package/dist/utils/config.d.ts
CHANGED
|
@@ -19,6 +19,26 @@ export interface DepBrainConfig {
|
|
|
19
19
|
report: {
|
|
20
20
|
maxSuggestions: number;
|
|
21
21
|
};
|
|
22
|
+
plugins: {
|
|
23
|
+
enabled: string[];
|
|
24
|
+
paths: string[];
|
|
25
|
+
};
|
|
26
|
+
risk: {
|
|
27
|
+
transitiveBloatThreshold: number;
|
|
28
|
+
typosquattingDistanceThreshold: number;
|
|
29
|
+
staleReleaseDays: number;
|
|
30
|
+
agingReleaseDays: number;
|
|
31
|
+
lowDownloadThreshold: number;
|
|
32
|
+
lowTrustWeightThreshold: number;
|
|
33
|
+
mediumTrustWeightThreshold: number;
|
|
34
|
+
};
|
|
35
|
+
dashboard: {
|
|
36
|
+
outputPath: string;
|
|
37
|
+
};
|
|
38
|
+
notifications: {
|
|
39
|
+
slackWebhookEnv: string;
|
|
40
|
+
discordWebhookEnv: string;
|
|
41
|
+
};
|
|
22
42
|
scoring: {
|
|
23
43
|
duplicateWeight: number;
|
|
24
44
|
outdatedWeight: number;
|
|
@@ -33,6 +53,10 @@ export interface DepBrainConfigOverrides {
|
|
|
33
53
|
ignore?: Partial<DepBrainConfig["ignore"]>;
|
|
34
54
|
policy?: Partial<DepBrainConfig["policy"]>;
|
|
35
55
|
report?: Partial<DepBrainConfig["report"]>;
|
|
56
|
+
plugins?: Partial<DepBrainConfig["plugins"]>;
|
|
57
|
+
risk?: Partial<DepBrainConfig["risk"]>;
|
|
58
|
+
dashboard?: Partial<DepBrainConfig["dashboard"]>;
|
|
59
|
+
notifications?: Partial<DepBrainConfig["notifications"]>;
|
|
36
60
|
scoring?: Partial<DepBrainConfig["scoring"]>;
|
|
37
61
|
scan?: Partial<DepBrainConfig["scan"]>;
|
|
38
62
|
}
|
package/dist/utils/config.js
CHANGED
|
@@ -21,6 +21,26 @@ export const defaultConfig = {
|
|
|
21
21
|
report: {
|
|
22
22
|
maxSuggestions: 5
|
|
23
23
|
},
|
|
24
|
+
plugins: {
|
|
25
|
+
enabled: [],
|
|
26
|
+
paths: []
|
|
27
|
+
},
|
|
28
|
+
risk: {
|
|
29
|
+
transitiveBloatThreshold: 50,
|
|
30
|
+
typosquattingDistanceThreshold: 2,
|
|
31
|
+
staleReleaseDays: 730,
|
|
32
|
+
agingReleaseDays: 365,
|
|
33
|
+
lowDownloadThreshold: 1000,
|
|
34
|
+
lowTrustWeightThreshold: 6,
|
|
35
|
+
mediumTrustWeightThreshold: 3
|
|
36
|
+
},
|
|
37
|
+
dashboard: {
|
|
38
|
+
outputPath: "depbrain-dashboard.html"
|
|
39
|
+
},
|
|
40
|
+
notifications: {
|
|
41
|
+
slackWebhookEnv: "DEPBRAIN_SLACK_WEBHOOK_URL",
|
|
42
|
+
discordWebhookEnv: "DEPBRAIN_DISCORD_WEBHOOK_URL"
|
|
43
|
+
},
|
|
24
44
|
scoring: {
|
|
25
45
|
duplicateWeight: 5,
|
|
26
46
|
outdatedWeight: 3,
|
|
@@ -63,6 +83,26 @@ function normalizeConfig(loaded) {
|
|
|
63
83
|
report: {
|
|
64
84
|
maxSuggestions: normalizeNumber(loaded.report?.maxSuggestions, defaultConfig.report.maxSuggestions)
|
|
65
85
|
},
|
|
86
|
+
plugins: {
|
|
87
|
+
enabled: normalizeStringArray(loaded.plugins?.enabled, defaultConfig.plugins.enabled),
|
|
88
|
+
paths: normalizeStringArray(loaded.plugins?.paths, defaultConfig.plugins.paths)
|
|
89
|
+
},
|
|
90
|
+
risk: {
|
|
91
|
+
transitiveBloatThreshold: normalizeNumber(loaded.risk?.transitiveBloatThreshold, defaultConfig.risk.transitiveBloatThreshold),
|
|
92
|
+
typosquattingDistanceThreshold: normalizeNumber(loaded.risk?.typosquattingDistanceThreshold, defaultConfig.risk.typosquattingDistanceThreshold),
|
|
93
|
+
staleReleaseDays: normalizeNumber(loaded.risk?.staleReleaseDays, defaultConfig.risk.staleReleaseDays),
|
|
94
|
+
agingReleaseDays: normalizeNumber(loaded.risk?.agingReleaseDays, defaultConfig.risk.agingReleaseDays),
|
|
95
|
+
lowDownloadThreshold: normalizeNumber(loaded.risk?.lowDownloadThreshold, defaultConfig.risk.lowDownloadThreshold),
|
|
96
|
+
lowTrustWeightThreshold: normalizeNumber(loaded.risk?.lowTrustWeightThreshold, defaultConfig.risk.lowTrustWeightThreshold),
|
|
97
|
+
mediumTrustWeightThreshold: normalizeNumber(loaded.risk?.mediumTrustWeightThreshold, defaultConfig.risk.mediumTrustWeightThreshold)
|
|
98
|
+
},
|
|
99
|
+
dashboard: {
|
|
100
|
+
outputPath: normalizeString(loaded.dashboard?.outputPath, defaultConfig.dashboard.outputPath)
|
|
101
|
+
},
|
|
102
|
+
notifications: {
|
|
103
|
+
slackWebhookEnv: normalizeString(loaded.notifications?.slackWebhookEnv, defaultConfig.notifications.slackWebhookEnv),
|
|
104
|
+
discordWebhookEnv: normalizeString(loaded.notifications?.discordWebhookEnv, defaultConfig.notifications.discordWebhookEnv)
|
|
105
|
+
},
|
|
66
106
|
scoring: {
|
|
67
107
|
duplicateWeight: normalizeNumber(loaded.scoring?.duplicateWeight, defaultConfig.scoring.duplicateWeight),
|
|
68
108
|
outdatedWeight: normalizeNumber(loaded.scoring?.outdatedWeight, defaultConfig.scoring.outdatedWeight),
|
|
@@ -86,3 +126,6 @@ function normalizeBoolean(value, fallback) {
|
|
|
86
126
|
function normalizeNumber(value, fallback) {
|
|
87
127
|
return typeof value === "number" && Number.isFinite(value) ? value : fallback;
|
|
88
128
|
}
|
|
129
|
+
function normalizeString(value, fallback) {
|
|
130
|
+
return typeof value === "string" && value.trim().length > 0 ? value : fallback;
|
|
131
|
+
}
|