dep-brain 1.0.2 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +9 -0
- package/README.md +56 -0
- package/action.yml +18 -0
- package/depbrain.config.json +15 -0
- package/depbrain.config.schema.json +31 -0
- package/depbrain.output.schema.json +5 -1
- package/dist/cli.js +67 -2
- package/dist/core/analyzer.d.ts +4 -0
- package/dist/core/analyzer.js +62 -16
- package/dist/core/plugin-manager.d.ts +20 -0
- package/dist/core/plugin-manager.js +69 -0
- package/dist/core/scorer.d.ts +7 -0
- package/dist/core/scorer.js +15 -6
- package/dist/index.d.ts +3 -1
- package/dist/index.js +1 -0
- package/dist/utils/config.d.ts +19 -0
- package/dist/utils/config.js +33 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## 1.1.0
|
|
6
|
+
|
|
7
|
+
- Added `--focus` modes for targeted duplicate, unused, outdated, risk, and health analysis.
|
|
8
|
+
- Added `--ci` for low-noise CI defaults focused on duplicate and runtime risk enforcement.
|
|
9
|
+
- Added `dep-brain init` to generate a starter `depbrain.config.json`.
|
|
10
|
+
- Introduced capped health score deductions so large outdated/risk counts do not automatically collapse project health to `0/100`.
|
|
11
|
+
- Added GitHub Action inputs for `focus` and `ci`.
|
|
12
|
+
- Added regression coverage for focused analysis and capped scoring.
|
|
13
|
+
|
|
5
14
|
## 1.0.2
|
|
6
15
|
|
|
7
16
|
- Treated npm `overrides` entries as intentional version pins so direct override packages are not flagged as unused.
|
package/README.md
CHANGED
|
@@ -50,6 +50,9 @@ The long-term goal is not just to list problems, but to answer:
|
|
|
50
50
|
- SARIF output via `--sarif`
|
|
51
51
|
- Ranked top issues via `--top`
|
|
52
52
|
- Baseline mode via `--baseline`
|
|
53
|
+
- Focused analysis via `--focus`
|
|
54
|
+
- Low-noise CI defaults via `--ci`
|
|
55
|
+
- Starter config generation via `dep-brain init`
|
|
53
56
|
- Reusable GitHub Action via `action.yml`
|
|
54
57
|
- Library entrypoint for programmatic use
|
|
55
58
|
|
|
@@ -70,10 +73,13 @@ npx dep-brain analyze ./path-to-project --fail-on-unused --json
|
|
|
70
73
|
npx dep-brain analyze --md > depbrain.md
|
|
71
74
|
npx dep-brain analyze --json --out depbrain.json
|
|
72
75
|
npx dep-brain analyze --sarif --out depbrain.sarif
|
|
76
|
+
npx dep-brain analyze --focus duplicates
|
|
77
|
+
npx dep-brain analyze --ci
|
|
73
78
|
npx dep-brain analyze --baseline depbrain-baseline.json
|
|
74
79
|
npx dep-brain analyze --baseline depbrain-baseline.json --min-score 90 --fail-on-risks
|
|
75
80
|
npx dep-brain report --from depbrain.json --md --out depbrain.md
|
|
76
81
|
|
|
82
|
+
dep-brain init
|
|
77
83
|
dep-brain config
|
|
78
84
|
dep-brain config --config depbrain.config.json
|
|
79
85
|
|
|
@@ -82,6 +88,34 @@ dep-brain analyze --help
|
|
|
82
88
|
dep-brain --version
|
|
83
89
|
```
|
|
84
90
|
|
|
91
|
+
## Focus Modes
|
|
92
|
+
|
|
93
|
+
Use `--focus` when you want a targeted signal instead of a full report:
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
dep-brain analyze --focus duplicates
|
|
97
|
+
dep-brain analyze --focus health
|
|
98
|
+
dep-brain analyze --focus risks
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Supported values are `all`, `health`, `duplicates`, `unused`, `outdated`, and `risks`.
|
|
102
|
+
|
|
103
|
+
## CI Preset
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
dep-brain analyze --ci
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
The CI preset applies low-noise defaults: a minimum score of `70`, failure on duplicates, and failure on risky dependencies.
|
|
110
|
+
|
|
111
|
+
## Config Init
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
dep-brain init
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Creates a starter `depbrain.config.json` with practical defaults for CI and common TypeScript/NestJS tooling.
|
|
118
|
+
|
|
85
119
|
## Workspaces
|
|
86
120
|
|
|
87
121
|
If the root `package.json` defines `workspaces`, `dep-brain` analyzes each workspace package and reports per-package results. Aggregated counts are still shown at the top-level summary.
|
|
@@ -204,6 +238,21 @@ Create a `depbrain.config.json` file in the project root:
|
|
|
204
238
|
"report": {
|
|
205
239
|
"maxSuggestions": 3
|
|
206
240
|
},
|
|
241
|
+
"plugins": {
|
|
242
|
+
"enabled": [],
|
|
243
|
+
"paths": []
|
|
244
|
+
},
|
|
245
|
+
"risk": {
|
|
246
|
+
"transitiveBloatThreshold": 50,
|
|
247
|
+
"typosquattingDistanceThreshold": 2
|
|
248
|
+
},
|
|
249
|
+
"dashboard": {
|
|
250
|
+
"outputPath": "depbrain-dashboard.html"
|
|
251
|
+
},
|
|
252
|
+
"notifications": {
|
|
253
|
+
"slackWebhookEnv": "DEPBRAIN_SLACK_WEBHOOK_URL",
|
|
254
|
+
"discordWebhookEnv": "DEPBRAIN_DISCORD_WEBHOOK_URL"
|
|
255
|
+
},
|
|
207
256
|
"scoring": {
|
|
208
257
|
"duplicateWeight": 5,
|
|
209
258
|
"outdatedWeight": 1,
|
|
@@ -230,6 +279,13 @@ Supported sections:
|
|
|
230
279
|
- `policy.failOnOutdated`
|
|
231
280
|
- `policy.failOnRisks`
|
|
232
281
|
- `report.maxSuggestions`
|
|
282
|
+
- `plugins.enabled`
|
|
283
|
+
- `plugins.paths`
|
|
284
|
+
- `risk.transitiveBloatThreshold`
|
|
285
|
+
- `risk.typosquattingDistanceThreshold`
|
|
286
|
+
- `dashboard.outputPath`
|
|
287
|
+
- `notifications.slackWebhookEnv`
|
|
288
|
+
- `notifications.discordWebhookEnv`
|
|
233
289
|
- `scoring.duplicateWeight`
|
|
234
290
|
- `scoring.outdatedWeight`
|
|
235
291
|
- `scoring.unusedWeight`
|
package/action.yml
CHANGED
|
@@ -23,6 +23,14 @@ inputs:
|
|
|
23
23
|
description: Optional baseline JSON report used to ignore existing findings.
|
|
24
24
|
required: false
|
|
25
25
|
default: ""
|
|
26
|
+
focus:
|
|
27
|
+
description: Analysis focus. Use all, health, duplicates, unused, outdated, or risks.
|
|
28
|
+
required: false
|
|
29
|
+
default: "all"
|
|
30
|
+
ci:
|
|
31
|
+
description: Apply low-noise CI defaults.
|
|
32
|
+
required: false
|
|
33
|
+
default: "false"
|
|
26
34
|
min-score:
|
|
27
35
|
description: Minimum project health score required to pass.
|
|
28
36
|
required: false
|
|
@@ -55,6 +63,8 @@ runs:
|
|
|
55
63
|
INPUT_OUT: ${{ inputs.out }}
|
|
56
64
|
INPUT_CONFIG: ${{ inputs.config }}
|
|
57
65
|
INPUT_BASELINE: ${{ inputs.baseline }}
|
|
66
|
+
INPUT_FOCUS: ${{ inputs.focus }}
|
|
67
|
+
INPUT_CI: ${{ inputs.ci }}
|
|
58
68
|
INPUT_MIN_SCORE: ${{ inputs.min-score }}
|
|
59
69
|
INPUT_FAIL_ON_UNUSED: ${{ inputs.fail-on-unused }}
|
|
60
70
|
INPUT_FAIL_ON_OUTDATED: ${{ inputs.fail-on-outdated }}
|
|
@@ -89,6 +99,14 @@ runs:
|
|
|
89
99
|
args+=("--baseline" "$INPUT_BASELINE")
|
|
90
100
|
fi
|
|
91
101
|
|
|
102
|
+
if [ "$INPUT_FOCUS" != "all" ]; then
|
|
103
|
+
args+=("--focus" "$INPUT_FOCUS")
|
|
104
|
+
fi
|
|
105
|
+
|
|
106
|
+
if [ "$INPUT_CI" = "true" ]; then
|
|
107
|
+
args+=("--ci")
|
|
108
|
+
fi
|
|
109
|
+
|
|
92
110
|
if [ -n "$INPUT_MIN_SCORE" ]; then
|
|
93
111
|
args+=("--min-score" "$INPUT_MIN_SCORE")
|
|
94
112
|
fi
|
package/depbrain.config.json
CHANGED
|
@@ -19,6 +19,21 @@
|
|
|
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
|
+
},
|
|
30
|
+
"dashboard": {
|
|
31
|
+
"outputPath": "depbrain-dashboard.html"
|
|
32
|
+
},
|
|
33
|
+
"notifications": {
|
|
34
|
+
"slackWebhookEnv": "DEPBRAIN_SLACK_WEBHOOK_URL",
|
|
35
|
+
"discordWebhookEnv": "DEPBRAIN_DISCORD_WEBHOOK_URL"
|
|
36
|
+
},
|
|
22
37
|
"scoring": {
|
|
23
38
|
"duplicateWeight": 5,
|
|
24
39
|
"outdatedWeight": 3,
|
|
@@ -36,6 +36,37 @@
|
|
|
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
|
+
}
|
|
54
|
+
},
|
|
55
|
+
"dashboard": {
|
|
56
|
+
"type": "object",
|
|
57
|
+
"additionalProperties": false,
|
|
58
|
+
"properties": {
|
|
59
|
+
"outputPath": { "type": "string" }
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
"notifications": {
|
|
63
|
+
"type": "object",
|
|
64
|
+
"additionalProperties": false,
|
|
65
|
+
"properties": {
|
|
66
|
+
"slackWebhookEnv": { "type": "string" },
|
|
67
|
+
"discordWebhookEnv": { "type": "string" }
|
|
68
|
+
}
|
|
69
|
+
},
|
|
39
70
|
"scoring": {
|
|
40
71
|
"type": "object",
|
|
41
72
|
"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/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 { defaultConfig } from "./utils/config.js";
|
|
7
8
|
import { promises as fs } from "node:fs";
|
|
8
9
|
import path from "node:path";
|
|
9
10
|
async function main() {
|
|
@@ -91,6 +92,22 @@ async function main() {
|
|
|
91
92
|
return;
|
|
92
93
|
}
|
|
93
94
|
}
|
|
95
|
+
if (command === "init") {
|
|
96
|
+
try {
|
|
97
|
+
const outputPath = optionValues.get("--out") ?? "depbrain.config.json";
|
|
98
|
+
const resolvedOut = resolveUserPath(outputPath);
|
|
99
|
+
const config = buildStarterConfig();
|
|
100
|
+
await fs.writeFile(resolvedOut, `${JSON.stringify(config, null, 2)}\n`, "utf8");
|
|
101
|
+
console.log(`Created ${path.relative(process.cwd(), resolvedOut) || outputPath}`);
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
catch (error) {
|
|
105
|
+
console.error("Failed to create config.");
|
|
106
|
+
console.error(error);
|
|
107
|
+
process.exitCode = 1;
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
94
111
|
console.error(`Unknown command: ${sanitizeForLog(command)}`);
|
|
95
112
|
printHelp();
|
|
96
113
|
process.exitCode = 1;
|
|
@@ -108,7 +125,8 @@ async function main() {
|
|
|
108
125
|
rootDir: targetPath,
|
|
109
126
|
configPath: optionValues.get("--config"),
|
|
110
127
|
config: cliConfig,
|
|
111
|
-
baseline
|
|
128
|
+
baseline,
|
|
129
|
+
focus: parseFocus(optionValues.get("--focus"))
|
|
112
130
|
});
|
|
113
131
|
let output;
|
|
114
132
|
if (flags.has("--json")) {
|
|
@@ -169,6 +187,11 @@ function buildCliConfig(flags, optionValues) {
|
|
|
169
187
|
if (flags.has("--fail-on-unused")) {
|
|
170
188
|
policy.failOnUnused = true;
|
|
171
189
|
}
|
|
190
|
+
if (flags.has("--ci")) {
|
|
191
|
+
policy.minScore = policy.minScore ?? 70;
|
|
192
|
+
policy.failOnDuplicates = true;
|
|
193
|
+
policy.failOnRisks = true;
|
|
194
|
+
}
|
|
172
195
|
return {
|
|
173
196
|
policy
|
|
174
197
|
};
|
|
@@ -186,9 +209,10 @@ function printHelp() {
|
|
|
186
209
|
console.log("Dependency Brain");
|
|
187
210
|
console.log("");
|
|
188
211
|
console.log("Usage:");
|
|
189
|
-
console.log(" dep-brain analyze [path] [--json] [--md] [--sarif] [--top] [--out path] [--config path] [--baseline path] [--min-score n] [--fail-on-risks]");
|
|
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]");
|
|
190
213
|
console.log(" dep-brain report --from <file> [--md] [--json] [--sarif] [--top] [--out path]");
|
|
191
214
|
console.log(" dep-brain config [path] [--config path]");
|
|
215
|
+
console.log(" dep-brain init [--out depbrain.config.json]");
|
|
192
216
|
console.log(" dep-brain help");
|
|
193
217
|
console.log(" dep-brain --version");
|
|
194
218
|
console.log("");
|
|
@@ -197,6 +221,8 @@ function printHelp() {
|
|
|
197
221
|
console.log(" --md Output Markdown report");
|
|
198
222
|
console.log(" --sarif Output SARIF format for Code Scanning");
|
|
199
223
|
console.log(" --top Output the ranked top issues only");
|
|
224
|
+
console.log(" --focus <kind> Run all, health, duplicates, unused, outdated, or risks");
|
|
225
|
+
console.log(" --ci Apply low-noise CI defaults");
|
|
200
226
|
console.log(" --config <path> Path to depbrain.config.json");
|
|
201
227
|
console.log(" --baseline <path> Ignore findings already present in a baseline JSON report");
|
|
202
228
|
console.log(" --from <file> Read analysis JSON from file");
|
|
@@ -209,6 +235,45 @@ function printHelp() {
|
|
|
209
235
|
console.log(" --help Show this help output");
|
|
210
236
|
console.log(" --version Show CLI version");
|
|
211
237
|
}
|
|
238
|
+
function parseFocus(value) {
|
|
239
|
+
if (value === "duplicates" ||
|
|
240
|
+
value === "unused" ||
|
|
241
|
+
value === "outdated" ||
|
|
242
|
+
value === "risks" ||
|
|
243
|
+
value === "health" ||
|
|
244
|
+
value === "all") {
|
|
245
|
+
return value;
|
|
246
|
+
}
|
|
247
|
+
return "all";
|
|
248
|
+
}
|
|
249
|
+
function buildStarterConfig() {
|
|
250
|
+
return {
|
|
251
|
+
...defaultConfig,
|
|
252
|
+
ignore: {
|
|
253
|
+
...defaultConfig.ignore,
|
|
254
|
+
unused: [
|
|
255
|
+
"@nestjs/platform-express",
|
|
256
|
+
"reflect-metadata",
|
|
257
|
+
"source-map-support",
|
|
258
|
+
"ts-loader",
|
|
259
|
+
"ts-node",
|
|
260
|
+
"tsconfig-paths"
|
|
261
|
+
]
|
|
262
|
+
},
|
|
263
|
+
policy: {
|
|
264
|
+
...defaultConfig.policy,
|
|
265
|
+
minScore: 70,
|
|
266
|
+
failOnDuplicates: true,
|
|
267
|
+
failOnRisks: true
|
|
268
|
+
},
|
|
269
|
+
scoring: {
|
|
270
|
+
duplicateWeight: 8,
|
|
271
|
+
outdatedWeight: 1,
|
|
272
|
+
unusedWeight: 2,
|
|
273
|
+
riskWeight: 4
|
|
274
|
+
}
|
|
275
|
+
};
|
|
276
|
+
}
|
|
212
277
|
async function loadPackageVersion() {
|
|
213
278
|
try {
|
|
214
279
|
const pkgPath = new URL("../package.json", import.meta.url);
|
package/dist/core/analyzer.d.ts
CHANGED
|
@@ -4,7 +4,9 @@ export interface AnalysisOptions {
|
|
|
4
4
|
configPath?: string;
|
|
5
5
|
config?: DepBrainConfigOverrides;
|
|
6
6
|
baseline?: DepBrainBaseline;
|
|
7
|
+
focus?: AnalysisFocus;
|
|
7
8
|
}
|
|
9
|
+
export type AnalysisFocus = "all" | "duplicates" | "unused" | "outdated" | "risks" | "health";
|
|
8
10
|
export interface DepBrainBaseline {
|
|
9
11
|
duplicates?: Array<Partial<Pick<DuplicateDependency, "name">>>;
|
|
10
12
|
unused?: Array<Partial<Pick<UnusedDependency, "name" | "section" | "package">>>;
|
|
@@ -108,6 +110,7 @@ export interface AnalysisResult {
|
|
|
108
110
|
risks: RiskDependency[];
|
|
109
111
|
suggestions: string[];
|
|
110
112
|
topIssues: TopIssue[];
|
|
113
|
+
extensions: Record<string, unknown>;
|
|
111
114
|
config: DepBrainConfig;
|
|
112
115
|
packages?: PackageAnalysisResult[];
|
|
113
116
|
}
|
|
@@ -128,6 +131,7 @@ export interface PackageAnalysisResult {
|
|
|
128
131
|
risks: RiskDependency[];
|
|
129
132
|
suggestions: string[];
|
|
130
133
|
topIssues: TopIssue[];
|
|
134
|
+
extensions: Record<string, unknown>;
|
|
131
135
|
}
|
|
132
136
|
export declare const OUTPUT_VERSION = "1.4";
|
|
133
137
|
export interface ScoreBreakdown {
|
package/dist/core/analyzer.js
CHANGED
|
@@ -6,27 +6,34 @@ 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 {
|
|
9
|
+
import { PluginManager } from "./plugin-manager.js";
|
|
10
|
+
import { calculateHealthScore, calculateScoreDeductions } from "./scorer.js";
|
|
10
11
|
import { buildAnalysisContext } from "./context.js";
|
|
11
12
|
export const OUTPUT_VERSION = "1.4";
|
|
12
13
|
export async function analyzeProject(options = {}) {
|
|
13
14
|
const rootDir = path.resolve(options.rootDir ?? process.cwd());
|
|
14
15
|
const loadedConfig = await loadDepBrainConfig(rootDir, options.configPath);
|
|
15
16
|
const config = mergeConfig(loadedConfig, options.config);
|
|
17
|
+
const focus = options.focus ?? "all";
|
|
18
|
+
const plugins = await PluginManager.load(rootDir, config);
|
|
19
|
+
await plugins.runPreScan({ rootDir, config });
|
|
16
20
|
const workspaces = await findWorkspacePackages(rootDir);
|
|
17
21
|
if (workspaces.length === 0) {
|
|
18
|
-
|
|
19
|
-
baseline: options.baseline
|
|
22
|
+
const result = await analyzeSingleProject(rootDir, config, {
|
|
23
|
+
baseline: options.baseline,
|
|
24
|
+
focus
|
|
20
25
|
});
|
|
26
|
+
return plugins.runPostScan(result);
|
|
21
27
|
}
|
|
22
28
|
const rootGraph = await buildDependencyGraph(rootDir);
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
|
|
29
|
+
const duplicates = shouldRunCheck("duplicate", focus)
|
|
30
|
+
? mapDuplicateIssues(filterIssues((await runDuplicateCheck(rootGraph)).issues, "duplicates", config))
|
|
31
|
+
: [];
|
|
26
32
|
const packages = [];
|
|
27
33
|
for (const workspace of workspaces) {
|
|
28
34
|
const result = await analyzeSingleProject(workspace.rootDir, config, {
|
|
29
|
-
packageName: workspace.name
|
|
35
|
+
packageName: workspace.name,
|
|
36
|
+
focus
|
|
30
37
|
});
|
|
31
38
|
packages.push({ ...result, name: workspace.name });
|
|
32
39
|
}
|
|
@@ -76,7 +83,7 @@ export async function analyzeProject(options = {}) {
|
|
|
76
83
|
outdated: outdated.length,
|
|
77
84
|
risks: risks.length
|
|
78
85
|
}, config);
|
|
79
|
-
|
|
86
|
+
const result = {
|
|
80
87
|
outputVersion: OUTPUT_VERSION,
|
|
81
88
|
rootDir,
|
|
82
89
|
score,
|
|
@@ -99,9 +106,11 @@ export async function analyzeProject(options = {}) {
|
|
|
99
106
|
outdated,
|
|
100
107
|
risks
|
|
101
108
|
}),
|
|
109
|
+
extensions: {},
|
|
102
110
|
config,
|
|
103
111
|
packages
|
|
104
112
|
};
|
|
113
|
+
return plugins.runPostScan(result);
|
|
105
114
|
}
|
|
106
115
|
function mergeConfig(base, overrides) {
|
|
107
116
|
if (!overrides) {
|
|
@@ -128,6 +137,21 @@ function mergeConfig(base, overrides) {
|
|
|
128
137
|
report: {
|
|
129
138
|
maxSuggestions: overrides.report?.maxSuggestions ?? base.report.maxSuggestions
|
|
130
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
|
+
},
|
|
148
|
+
dashboard: {
|
|
149
|
+
outputPath: overrides.dashboard?.outputPath ?? base.dashboard.outputPath
|
|
150
|
+
},
|
|
151
|
+
notifications: {
|
|
152
|
+
slackWebhookEnv: overrides.notifications?.slackWebhookEnv ?? base.notifications.slackWebhookEnv,
|
|
153
|
+
discordWebhookEnv: overrides.notifications?.discordWebhookEnv ?? base.notifications.discordWebhookEnv
|
|
154
|
+
},
|
|
131
155
|
scoring: {
|
|
132
156
|
duplicateWeight: overrides.scoring?.duplicateWeight ?? base.scoring.duplicateWeight,
|
|
133
157
|
outdatedWeight: overrides.scoring?.outdatedWeight ?? base.scoring.outdatedWeight,
|
|
@@ -163,7 +187,7 @@ function evaluatePolicy(summary, config) {
|
|
|
163
187
|
}
|
|
164
188
|
async function analyzeSingleProject(rootDir, config, options = {}) {
|
|
165
189
|
const context = await buildAnalysisContext(rootDir, config);
|
|
166
|
-
const results = await runChecks(context);
|
|
190
|
+
const results = await runChecks(context, options.focus ?? "all");
|
|
167
191
|
const issueGroups = normalizeIssues(results, config);
|
|
168
192
|
const duplicates = mapDuplicateIssues(issueGroups.duplicates);
|
|
169
193
|
const unused = mapUnusedIssues(issueGroups.unused);
|
|
@@ -235,6 +259,7 @@ async function analyzeSingleProject(rootDir, config, options = {}) {
|
|
|
235
259
|
outdated: baselineFiltered.outdated,
|
|
236
260
|
risks: baselineFiltered.risks
|
|
237
261
|
}),
|
|
262
|
+
extensions: {},
|
|
238
263
|
config
|
|
239
264
|
};
|
|
240
265
|
}
|
|
@@ -255,7 +280,7 @@ function shouldIgnorePackage(name, bucket, config) {
|
|
|
255
280
|
}
|
|
256
281
|
});
|
|
257
282
|
}
|
|
258
|
-
async function runChecks(context) {
|
|
283
|
+
async function runChecks(context, focus) {
|
|
259
284
|
const checks = [
|
|
260
285
|
{
|
|
261
286
|
name: "duplicate",
|
|
@@ -276,10 +301,21 @@ async function runChecks(context) {
|
|
|
276
301
|
];
|
|
277
302
|
const results = [];
|
|
278
303
|
for (const check of checks) {
|
|
279
|
-
|
|
304
|
+
if (shouldRunCheck(check.name, focus)) {
|
|
305
|
+
results.push(await check.run());
|
|
306
|
+
}
|
|
280
307
|
}
|
|
281
308
|
return results;
|
|
282
309
|
}
|
|
310
|
+
function shouldRunCheck(checkName, focus) {
|
|
311
|
+
if (focus === "all" || focus === "health") {
|
|
312
|
+
return true;
|
|
313
|
+
}
|
|
314
|
+
return ((focus === "duplicates" && checkName === "duplicate") ||
|
|
315
|
+
(focus === "unused" && checkName === "unused") ||
|
|
316
|
+
(focus === "outdated" && checkName === "outdated") ||
|
|
317
|
+
(focus === "risks" && checkName === "risk"));
|
|
318
|
+
}
|
|
283
319
|
function normalizeIssues(results, config) {
|
|
284
320
|
const map = new Map();
|
|
285
321
|
for (const result of results) {
|
|
@@ -603,12 +639,22 @@ function normalizeWorkspaceUsage(value) {
|
|
|
603
639
|
.filter((entry) => entry !== null);
|
|
604
640
|
}
|
|
605
641
|
function buildScoreBreakdown(counts, config) {
|
|
642
|
+
const deductions = calculateScoreDeductions({
|
|
643
|
+
duplicates: counts.duplicates,
|
|
644
|
+
outdated: counts.outdated,
|
|
645
|
+
unused: counts.unused,
|
|
646
|
+
risks: counts.risks,
|
|
647
|
+
duplicateWeight: config.scoring.duplicateWeight,
|
|
648
|
+
outdatedWeight: config.scoring.outdatedWeight,
|
|
649
|
+
unusedWeight: config.scoring.unusedWeight,
|
|
650
|
+
riskWeight: config.scoring.riskWeight
|
|
651
|
+
});
|
|
606
652
|
return {
|
|
607
|
-
baseScore:
|
|
608
|
-
duplicates:
|
|
609
|
-
outdated:
|
|
610
|
-
unused:
|
|
611
|
-
risks:
|
|
653
|
+
baseScore: deductions.baseScore,
|
|
654
|
+
duplicates: deductions.duplicates,
|
|
655
|
+
outdated: deductions.outdated,
|
|
656
|
+
unused: deductions.unused,
|
|
657
|
+
risks: deductions.risks,
|
|
612
658
|
weights: {
|
|
613
659
|
duplicateWeight: config.scoring.duplicateWeight,
|
|
614
660
|
outdatedWeight: config.scoring.outdatedWeight,
|
|
@@ -0,0 +1,20 @@
|
|
|
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 declare class PluginManager {
|
|
15
|
+
private readonly plugins;
|
|
16
|
+
private constructor();
|
|
17
|
+
static load(rootDir: string, config: DepBrainConfig): Promise<PluginManager>;
|
|
18
|
+
runPreScan(context: ProjectContext): Promise<void>;
|
|
19
|
+
runPostScan(result: AnalysisResult): Promise<AnalysisResult>;
|
|
20
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { pathToFileURL } from "node:url";
|
|
3
|
+
export class PluginManager {
|
|
4
|
+
plugins;
|
|
5
|
+
constructor(plugins) {
|
|
6
|
+
this.plugins = plugins;
|
|
7
|
+
}
|
|
8
|
+
static async load(rootDir, config) {
|
|
9
|
+
const specs = [
|
|
10
|
+
...config.plugins.enabled.map((name) => `dep-brain-plugin-${name}`),
|
|
11
|
+
...config.plugins.paths
|
|
12
|
+
];
|
|
13
|
+
const plugins = [];
|
|
14
|
+
for (const spec of specs) {
|
|
15
|
+
const plugin = await loadPlugin(rootDir, spec);
|
|
16
|
+
if (plugin) {
|
|
17
|
+
plugins.push(plugin);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return new PluginManager(plugins);
|
|
21
|
+
}
|
|
22
|
+
async runPreScan(context) {
|
|
23
|
+
for (const plugin of this.plugins) {
|
|
24
|
+
await plugin.preScan?.(context);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
async runPostScan(result) {
|
|
28
|
+
let current = result;
|
|
29
|
+
for (const plugin of this.plugins) {
|
|
30
|
+
const next = await plugin.postScan?.(current);
|
|
31
|
+
if (next) {
|
|
32
|
+
current = next;
|
|
33
|
+
}
|
|
34
|
+
const reportSection = await plugin.reportHook?.(current);
|
|
35
|
+
if (reportSection) {
|
|
36
|
+
current.extensions[plugin.name] = {
|
|
37
|
+
...(asRecord(current.extensions[plugin.name]) ?? {}),
|
|
38
|
+
...reportSection
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return current;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
async function loadPlugin(rootDir, spec) {
|
|
46
|
+
try {
|
|
47
|
+
const resolved = spec.startsWith(".") || path.isAbsolute(spec)
|
|
48
|
+
? path.resolve(rootDir, spec)
|
|
49
|
+
: spec;
|
|
50
|
+
const moduleUrl = path.isAbsolute(resolved) ? pathToFileURL(resolved).href : resolved;
|
|
51
|
+
const mod = await import(moduleUrl);
|
|
52
|
+
const exported = mod.default ?? mod.plugin ?? mod;
|
|
53
|
+
const candidate = typeof exported === "function" ? new exported() : exported;
|
|
54
|
+
return isPlugin(candidate) ? candidate : null;
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
function isPlugin(value) {
|
|
61
|
+
return Boolean(value &&
|
|
62
|
+
typeof value === "object" &&
|
|
63
|
+
typeof value.name === "string");
|
|
64
|
+
}
|
|
65
|
+
function asRecord(value) {
|
|
66
|
+
return value && typeof value === "object" && !Array.isArray(value)
|
|
67
|
+
? value
|
|
68
|
+
: null;
|
|
69
|
+
}
|
package/dist/core/scorer.d.ts
CHANGED
|
@@ -9,3 +9,10 @@ export interface ScoreInputs {
|
|
|
9
9
|
riskWeight?: number;
|
|
10
10
|
}
|
|
11
11
|
export declare function calculateHealthScore(inputs: ScoreInputs): number;
|
|
12
|
+
export declare function calculateScoreDeductions(inputs: ScoreInputs): {
|
|
13
|
+
baseScore: number;
|
|
14
|
+
duplicates: number;
|
|
15
|
+
outdated: number;
|
|
16
|
+
unused: number;
|
|
17
|
+
risks: number;
|
|
18
|
+
};
|
package/dist/core/scorer.js
CHANGED
|
@@ -1,12 +1,21 @@
|
|
|
1
1
|
export function calculateHealthScore(inputs) {
|
|
2
|
+
const breakdown = calculateScoreDeductions(inputs);
|
|
3
|
+
return Math.max(0, breakdown.baseScore -
|
|
4
|
+
breakdown.duplicates -
|
|
5
|
+
breakdown.outdated -
|
|
6
|
+
breakdown.unused -
|
|
7
|
+
breakdown.risks);
|
|
8
|
+
}
|
|
9
|
+
export function calculateScoreDeductions(inputs) {
|
|
2
10
|
const duplicateWeight = inputs.duplicateWeight ?? 5;
|
|
3
11
|
const outdatedWeight = inputs.outdatedWeight ?? 3;
|
|
4
12
|
const unusedWeight = inputs.unusedWeight ?? 4;
|
|
5
13
|
const riskWeight = inputs.riskWeight ?? 10;
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
inputs.
|
|
9
|
-
inputs.
|
|
10
|
-
inputs.
|
|
11
|
-
|
|
14
|
+
return {
|
|
15
|
+
baseScore: 100,
|
|
16
|
+
duplicates: Math.min(35, inputs.duplicates * duplicateWeight),
|
|
17
|
+
outdated: Math.min(25, inputs.outdated * outdatedWeight),
|
|
18
|
+
unused: Math.min(20, inputs.unused * unusedWeight),
|
|
19
|
+
risks: Math.min(30, inputs.risks * riskWeight)
|
|
20
|
+
};
|
|
12
21
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
export { analyzeProject } from "./core/analyzer.js";
|
|
2
|
-
export type { AnalysisOptions, AnalysisResult, DepBrainBaseline, DuplicateDependency, OutdatedDependency, PolicyResult, PackageAnalysisResult, Recommendation, RiskFactors, ScoreBreakdown, RiskDependency, TopIssue, TrustScore, UnusedDependency, WorkspaceDependencyUsage, WorkspaceOwnershipSummary } from "./core/analyzer.js";
|
|
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, 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
package/dist/utils/config.d.ts
CHANGED
|
@@ -19,6 +19,21 @@ 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
|
+
};
|
|
30
|
+
dashboard: {
|
|
31
|
+
outputPath: string;
|
|
32
|
+
};
|
|
33
|
+
notifications: {
|
|
34
|
+
slackWebhookEnv: string;
|
|
35
|
+
discordWebhookEnv: string;
|
|
36
|
+
};
|
|
22
37
|
scoring: {
|
|
23
38
|
duplicateWeight: number;
|
|
24
39
|
outdatedWeight: number;
|
|
@@ -33,6 +48,10 @@ export interface DepBrainConfigOverrides {
|
|
|
33
48
|
ignore?: Partial<DepBrainConfig["ignore"]>;
|
|
34
49
|
policy?: Partial<DepBrainConfig["policy"]>;
|
|
35
50
|
report?: Partial<DepBrainConfig["report"]>;
|
|
51
|
+
plugins?: Partial<DepBrainConfig["plugins"]>;
|
|
52
|
+
risk?: Partial<DepBrainConfig["risk"]>;
|
|
53
|
+
dashboard?: Partial<DepBrainConfig["dashboard"]>;
|
|
54
|
+
notifications?: Partial<DepBrainConfig["notifications"]>;
|
|
36
55
|
scoring?: Partial<DepBrainConfig["scoring"]>;
|
|
37
56
|
scan?: Partial<DepBrainConfig["scan"]>;
|
|
38
57
|
}
|
package/dist/utils/config.js
CHANGED
|
@@ -21,6 +21,21 @@ 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
|
+
},
|
|
32
|
+
dashboard: {
|
|
33
|
+
outputPath: "depbrain-dashboard.html"
|
|
34
|
+
},
|
|
35
|
+
notifications: {
|
|
36
|
+
slackWebhookEnv: "DEPBRAIN_SLACK_WEBHOOK_URL",
|
|
37
|
+
discordWebhookEnv: "DEPBRAIN_DISCORD_WEBHOOK_URL"
|
|
38
|
+
},
|
|
24
39
|
scoring: {
|
|
25
40
|
duplicateWeight: 5,
|
|
26
41
|
outdatedWeight: 3,
|
|
@@ -63,6 +78,21 @@ function normalizeConfig(loaded) {
|
|
|
63
78
|
report: {
|
|
64
79
|
maxSuggestions: normalizeNumber(loaded.report?.maxSuggestions, defaultConfig.report.maxSuggestions)
|
|
65
80
|
},
|
|
81
|
+
plugins: {
|
|
82
|
+
enabled: normalizeStringArray(loaded.plugins?.enabled, defaultConfig.plugins.enabled),
|
|
83
|
+
paths: normalizeStringArray(loaded.plugins?.paths, defaultConfig.plugins.paths)
|
|
84
|
+
},
|
|
85
|
+
risk: {
|
|
86
|
+
transitiveBloatThreshold: normalizeNumber(loaded.risk?.transitiveBloatThreshold, defaultConfig.risk.transitiveBloatThreshold),
|
|
87
|
+
typosquattingDistanceThreshold: normalizeNumber(loaded.risk?.typosquattingDistanceThreshold, defaultConfig.risk.typosquattingDistanceThreshold)
|
|
88
|
+
},
|
|
89
|
+
dashboard: {
|
|
90
|
+
outputPath: normalizeString(loaded.dashboard?.outputPath, defaultConfig.dashboard.outputPath)
|
|
91
|
+
},
|
|
92
|
+
notifications: {
|
|
93
|
+
slackWebhookEnv: normalizeString(loaded.notifications?.slackWebhookEnv, defaultConfig.notifications.slackWebhookEnv),
|
|
94
|
+
discordWebhookEnv: normalizeString(loaded.notifications?.discordWebhookEnv, defaultConfig.notifications.discordWebhookEnv)
|
|
95
|
+
},
|
|
66
96
|
scoring: {
|
|
67
97
|
duplicateWeight: normalizeNumber(loaded.scoring?.duplicateWeight, defaultConfig.scoring.duplicateWeight),
|
|
68
98
|
outdatedWeight: normalizeNumber(loaded.scoring?.outdatedWeight, defaultConfig.scoring.outdatedWeight),
|
|
@@ -86,3 +116,6 @@ function normalizeBoolean(value, fallback) {
|
|
|
86
116
|
function normalizeNumber(value, fallback) {
|
|
87
117
|
return typeof value === "number" && Number.isFinite(value) ? value : fallback;
|
|
88
118
|
}
|
|
119
|
+
function normalizeString(value, fallback) {
|
|
120
|
+
return typeof value === "string" && value.trim().length > 0 ? value : fallback;
|
|
121
|
+
}
|