dep-brain 1.1.0 → 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/README.md CHANGED
@@ -238,6 +238,21 @@ Create a `depbrain.config.json` file in the project root:
238
238
  "report": {
239
239
  "maxSuggestions": 3
240
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
+ },
241
256
  "scoring": {
242
257
  "duplicateWeight": 5,
243
258
  "outdatedWeight": 1,
@@ -264,6 +279,13 @@ Supported sections:
264
279
  - `policy.failOnOutdated`
265
280
  - `policy.failOnRisks`
266
281
  - `report.maxSuggestions`
282
+ - `plugins.enabled`
283
+ - `plugins.paths`
284
+ - `risk.transitiveBloatThreshold`
285
+ - `risk.typosquattingDistanceThreshold`
286
+ - `dashboard.outputPath`
287
+ - `notifications.slackWebhookEnv`
288
+ - `notifications.discordWebhookEnv`
267
289
  - `scoring.duplicateWeight`
268
290
  - `scoring.outdatedWeight`
269
291
  - `scoring.unusedWeight`
@@ -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": {
@@ -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 {
@@ -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
- return analyzeSingleProject(rootDir, config, {
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
- return {
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,21 @@ 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
+ },
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
+ },
134
155
  scoring: {
135
156
  duplicateWeight: overrides.scoring?.duplicateWeight ?? base.scoring.duplicateWeight,
136
157
  outdatedWeight: overrides.scoring?.outdatedWeight ?? base.scoring.outdatedWeight,
@@ -238,6 +259,7 @@ async function analyzeSingleProject(rootDir, config, options = {}) {
238
259
  outdated: baselineFiltered.outdated,
239
260
  risks: baselineFiltered.risks
240
261
  }),
262
+ extensions: {},
241
263
  config
242
264
  };
243
265
  }
@@ -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/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, 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
@@ -1,2 +1,3 @@
1
1
  export { analyzeProject } from "./core/analyzer.js";
2
2
  export { OUTPUT_VERSION } from "./core/analyzer.js";
3
+ export { PluginManager } from "./core/plugin-manager.js";
@@ -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
  }
@@ -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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dep-brain",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "description": "CLI and library for explainable dependency intelligence",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",