dep-brain 0.4.0 → 0.5.1

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
@@ -46,7 +46,9 @@ npx dep-brain analyze ./path-to-project
46
46
  npx dep-brain analyze --config depbrain.config.json
47
47
  npx dep-brain analyze --min-score 90 --fail-on-risks
48
48
  npx dep-brain analyze ./path-to-project --fail-on-unused --json
49
- npx dep-brain analyze --md > depbrain.md
49
+ npx dep-brain analyze --md > depbrain.md
50
+ npx dep-brain analyze --json --out depbrain.json
51
+ npx dep-brain report --from depbrain.json --md --out depbrain.md
50
52
 
51
53
  dep-brain config
52
54
  dep-brain config --config depbrain.config.json
@@ -102,6 +104,13 @@ Output includes `outputVersion` for schema stability and can be validated with:
102
104
  dep-brain analyze --md
103
105
  ```
104
106
 
107
+ ## Report From JSON
108
+
109
+ ```bash
110
+ dep-brain analyze --json --out depbrain.json
111
+ dep-brain report --from depbrain.json --md --out depbrain.md
112
+ ```
113
+
105
114
  ## Config File
106
115
 
107
116
  Create a `depbrain.config.json` file in the project root:
@@ -1,3 +1,5 @@
1
1
  import type { DuplicateDependency } from "../core/analyzer.js";
2
2
  import type { DependencyGraph } from "../core/graph-builder.js";
3
+ import type { CheckResult } from "../core/types.js";
3
4
  export declare function findDuplicateDependencies(graph: DependencyGraph): Promise<DuplicateDependency[]>;
5
+ export declare function runDuplicateCheck(graph: DependencyGraph): Promise<CheckResult>;
@@ -8,3 +8,20 @@ export async function findDuplicateDependencies(graph) {
8
8
  .filter((dependency) => dependency.versions.length > 1)
9
9
  .sort((left, right) => left.name.localeCompare(right.name));
10
10
  }
11
+ export async function runDuplicateCheck(graph) {
12
+ const duplicates = await findDuplicateDependencies(graph);
13
+ return {
14
+ name: "duplicate",
15
+ summary: `${duplicates.length} duplicate dependencies found`,
16
+ issues: duplicates.map((item) => ({
17
+ id: `duplicate:${item.name}`,
18
+ message: `${item.name} has ${item.versions.length} versions`,
19
+ severity: "warning",
20
+ meta: {
21
+ name: item.name,
22
+ versions: item.versions,
23
+ instances: item.instances
24
+ }
25
+ }))
26
+ };
27
+ }
@@ -1,6 +1,8 @@
1
1
  import type { OutdatedDependency } from "../core/analyzer.js";
2
2
  import type { DependencyGraph } from "../core/graph-builder.js";
3
+ import type { CheckResult } from "../core/types.js";
3
4
  export interface OutdatedOptions {
4
5
  resolveLatestVersion?: (name: string) => Promise<string | null>;
5
6
  }
6
7
  export declare function findOutdatedDependencies(graph: DependencyGraph, options?: OutdatedOptions): Promise<OutdatedDependency[]>;
8
+ export declare function runOutdatedCheck(graph: DependencyGraph): Promise<CheckResult>;
@@ -22,6 +22,24 @@ export async function findOutdatedDependencies(graph, options = {}) {
22
22
  .filter((item) => item !== null)
23
23
  .sort((left, right) => left.name.localeCompare(right.name));
24
24
  }
25
+ export async function runOutdatedCheck(graph) {
26
+ const outdated = await findOutdatedDependencies(graph);
27
+ return {
28
+ name: "outdated",
29
+ summary: `${outdated.length} outdated dependencies found`,
30
+ issues: outdated.map((item) => ({
31
+ id: `outdated:${item.name}`,
32
+ message: `${item.name} ${item.current} -> ${item.latest}`,
33
+ severity: item.updateType === "major" ? "critical" : "warning",
34
+ meta: {
35
+ name: item.name,
36
+ current: item.current,
37
+ latest: item.latest,
38
+ updateType: item.updateType
39
+ }
40
+ }))
41
+ };
42
+ }
25
43
  function normalizeVersion(versionRange) {
26
44
  return versionRange.trim().replace(/^[~^><=\s]+/, "");
27
45
  }
@@ -1,3 +1,5 @@
1
1
  import type { DependencyGraph } from "../core/graph-builder.js";
2
2
  import type { RiskDependency } from "../core/analyzer.js";
3
+ import type { CheckResult } from "../core/types.js";
3
4
  export declare function findRiskDependencies(graph: DependencyGraph): Promise<RiskDependency[]>;
5
+ export declare function runRiskCheck(graph: DependencyGraph): Promise<CheckResult>;
@@ -29,3 +29,19 @@ export async function findRiskDependencies(graph) {
29
29
  .filter((item) => item !== null)
30
30
  .sort((left, right) => left.name.localeCompare(right.name));
31
31
  }
32
+ export async function runRiskCheck(graph) {
33
+ const risks = await findRiskDependencies(graph);
34
+ return {
35
+ name: "risk",
36
+ summary: `${risks.length} risky dependencies found`,
37
+ issues: risks.map((item) => ({
38
+ id: `risk:${item.name}`,
39
+ message: `${item.name}: ${item.reasons.join("; ")}`,
40
+ severity: "warning",
41
+ meta: {
42
+ name: item.name,
43
+ reasons: item.reasons
44
+ }
45
+ }))
46
+ };
47
+ }
@@ -1,5 +1,10 @@
1
1
  import type { UnusedDependency } from "../core/analyzer.js";
2
2
  import type { DependencyGraph } from "../core/graph-builder.js";
3
- export declare function findUnusedDependencies(rootDir: string, graph: DependencyGraph, options?: {
4
- excludePaths?: string[];
3
+ import type { AnalysisContext, CheckResult } from "../core/types.js";
4
+ export declare function findUnusedDependencies(rootDir: string, graph: DependencyGraph, fileEntries: {
5
+ path: string;
6
+ content: string;
7
+ }[], options: {
8
+ hasTypeScriptConfig: boolean;
5
9
  }): Promise<UnusedDependency[]>;
10
+ export declare function runUnusedCheck(context: AnalysisContext): Promise<CheckResult>;
@@ -1,17 +1,20 @@
1
1
  import path from "node:path";
2
- import { collectProjectFiles, readTextFile } from "../utils/file-parser.js";
3
2
  const SOURCE_FILE_PATTERN = /\.(c|m)?(t|j)sx?$/;
4
3
  const CONFIG_FILE_PATTERN = /(^|[\\/])(vite|vitest|jest|eslint|prettier|rollup|webpack|babel|tsup|eslint\.config|commitlint|playwright|storybook|tailwind|postcss)\.config\.(c|m)?(t|j)s$/;
5
4
  const TEST_FILE_PATTERN = /(^|[\\/])(__tests__|test|tests|spec|specs)([\\/]|$)|\.(test|spec)\.(c|m)?(t|j)sx?$/;
6
5
  const RUNTIME_DIR_PATTERN = /(^|[\\/])(src|app|lib|server|client|pages|components)([\\/]|$)/;
7
- export async function findUnusedDependencies(rootDir, graph, options = {}) {
8
- const files = await collectProjectFiles(rootDir, SOURCE_FILE_PATTERN, options.excludePaths ?? []);
9
- const projectFiles = files.filter((filePath) => !filePath.includes(`${path.sep}node_modules${path.sep}`));
6
+ export async function findUnusedDependencies(rootDir, graph, fileEntries, options) {
7
+ const projectFiles = fileEntries
8
+ .map((entry) => entry.path)
9
+ .filter((filePath) => !filePath.includes(`${path.sep}node_modules${path.sep}`));
10
10
  const runtimeUsed = new Set();
11
11
  const devUsed = new Set();
12
- for (const filePath of projectFiles) {
13
- const content = await readTextFile(filePath);
14
- const imports = extractImportedPackages(content);
12
+ for (const entry of fileEntries) {
13
+ if (!SOURCE_FILE_PATTERN.test(entry.path)) {
14
+ continue;
15
+ }
16
+ const imports = extractImportedPackages(entry.content);
17
+ const filePath = entry.path;
15
18
  const isDevOnlyFile = isDevelopmentOnlyFile(rootDir, filePath);
16
19
  const target = isDevOnlyFile ? devUsed : runtimeUsed;
17
20
  for (const importedPackage of imports) {
@@ -25,8 +28,7 @@ export async function findUnusedDependencies(rootDir, graph, options = {}) {
25
28
  devUsed.add(referencedBinary);
26
29
  }
27
30
  const hasTypeScriptSources = projectFiles.some((filePath) => /\.(c|m)?tsx?$/.test(filePath));
28
- const hasTypeScriptConfig = await hasFile(rootDir, "tsconfig.json");
29
- if (hasTypeScriptConfig) {
31
+ if (options.hasTypeScriptConfig) {
30
32
  devUsed.add("typescript");
31
33
  }
32
34
  const unusedDependencies = Object.keys(graph.dependencies)
@@ -34,10 +36,26 @@ export async function findUnusedDependencies(rootDir, graph, options = {}) {
34
36
  .map((name) => ({ name, section: "dependencies" }));
35
37
  const unusedDevDependencies = Object.keys(graph.devDependencies)
36
38
  .filter((name) => !devUsed.has(name) && !runtimeUsed.has(name))
37
- .filter((name) => !isImplicitlyUsedDevDependency(name, hasTypeScriptSources, hasTypeScriptConfig))
39
+ .filter((name) => !isImplicitlyUsedDevDependency(name, hasTypeScriptSources, options.hasTypeScriptConfig))
38
40
  .map((name) => ({ name, section: "devDependencies" }));
39
41
  return [...unusedDependencies, ...unusedDevDependencies].sort((left, right) => left.name.localeCompare(right.name));
40
42
  }
43
+ export async function runUnusedCheck(context) {
44
+ const unused = await findUnusedDependencies(context.rootDir, context.graph, context.fileEntries, { hasTypeScriptConfig: context.hasTypeScriptConfig });
45
+ return {
46
+ name: "unused",
47
+ summary: `${unused.length} unused dependencies found`,
48
+ issues: unused.map((item) => ({
49
+ id: `unused:${item.section}:${item.name}`,
50
+ message: `${item.name} appears unused`,
51
+ severity: "warning",
52
+ meta: {
53
+ name: item.name,
54
+ section: item.section
55
+ }
56
+ }))
57
+ };
58
+ }
41
59
  function extractImportedPackages(content) {
42
60
  const imports = new Set();
43
61
  const patterns = [
@@ -109,12 +127,3 @@ function isImplicitlyUsedDevDependency(name, hasTypeScriptSources, hasTypeScript
109
127
  }
110
128
  return false;
111
129
  }
112
- async function hasFile(rootDir, fileName) {
113
- try {
114
- await readTextFile(path.join(rootDir, fileName));
115
- return true;
116
- }
117
- catch {
118
- return false;
119
- }
120
- }
package/dist/cli.js CHANGED
@@ -39,9 +39,33 @@ async function main() {
39
39
  return;
40
40
  }
41
41
  if (command !== "analyze") {
42
+ if (command === "report") {
43
+ const fromPath = optionValues.get("--from") ?? positionals[0];
44
+ if (!fromPath) {
45
+ console.error("Missing --from <file> for report");
46
+ printHelp();
47
+ process.exitCode = 1;
48
+ return;
49
+ }
50
+ try {
51
+ const raw = await fs.readFile(fromPath, "utf8");
52
+ const reportData = JSON.parse(raw);
53
+ const output = flags.has("--json")
54
+ ? JSON.stringify(reportData, null, 2)
55
+ : renderMarkdownReport(reportData);
56
+ await writeOutput(output, optionValues.get("--out"));
57
+ return;
58
+ }
59
+ catch (error) {
60
+ console.error("Failed to render report.");
61
+ console.error(error);
62
+ process.exitCode = 1;
63
+ return;
64
+ }
65
+ }
42
66
  if (command === "config") {
43
67
  if (!(await hasPackageJson(targetPath))) {
44
- console.error(`No package.json found at ${targetPath}`);
68
+ console.error(`No package.json found at ${sanitizeForLog(targetPath)}`);
45
69
  process.exitCode = 1;
46
70
  return;
47
71
  }
@@ -61,13 +85,13 @@ async function main() {
61
85
  return;
62
86
  }
63
87
  }
64
- console.error(`Unknown command: ${command}`);
88
+ console.error(`Unknown command: ${sanitizeForLog(command)}`);
65
89
  printHelp();
66
90
  process.exitCode = 1;
67
91
  return;
68
92
  }
69
93
  if (!(await hasPackageJson(targetPath))) {
70
- console.error(`No package.json found at ${targetPath}`);
94
+ console.error(`No package.json found at ${sanitizeForLog(targetPath)}`);
71
95
  process.exitCode = 1;
72
96
  return;
73
97
  }
@@ -78,21 +102,21 @@ async function main() {
78
102
  configPath: optionValues.get("--config"),
79
103
  config: cliConfig
80
104
  });
105
+ let output;
81
106
  if (flags.has("--json")) {
82
- process.stdout.write(`${renderJsonReport(result)}\n`);
107
+ output = renderJsonReport(result);
83
108
  }
84
109
  else if (flags.has("--md")) {
85
- process.stdout.write(`${renderMarkdownReport(result)}\n`);
110
+ output = renderMarkdownReport(result);
86
111
  }
87
112
  else {
88
- const output = renderConsoleReport(result);
89
- if (!output || output.trim().length === 0) {
90
- process.stdout.write(`${renderJsonReport(result)}\n`);
91
- }
92
- else {
93
- process.stdout.write(`${output}\n`);
94
- }
113
+ const consoleOutput = renderConsoleReport(result);
114
+ output =
115
+ !consoleOutput || consoleOutput.trim().length === 0
116
+ ? renderJsonReport(result)
117
+ : consoleOutput;
95
118
  }
119
+ await writeOutput(output, optionValues.get("--out"));
96
120
  if (!result.policy.passed) {
97
121
  process.exitCode = 1;
98
122
  }
@@ -148,7 +172,8 @@ function printHelp() {
148
172
  console.log("Dependency Brain");
149
173
  console.log("");
150
174
  console.log("Usage:");
151
- console.log(" dep-brain analyze [path] [--json] [--md] [--config path] [--min-score n] [--fail-on-risks] [--fail-on-outdated] [--fail-on-unused] [--fail-on-duplicates]");
175
+ console.log(" dep-brain analyze [path] [--json] [--md] [--out path] [--config path] [--min-score n] [--fail-on-risks] [--fail-on-outdated] [--fail-on-unused] [--fail-on-duplicates]");
176
+ console.log(" dep-brain report --from <file> [--md] [--json] [--out path]");
152
177
  console.log(" dep-brain config [path] [--config path]");
153
178
  console.log(" dep-brain help");
154
179
  console.log(" dep-brain --version");
@@ -157,6 +182,8 @@ function printHelp() {
157
182
  console.log(" --json Output JSON for analysis");
158
183
  console.log(" --md Output Markdown report");
159
184
  console.log(" --config <path> Path to depbrain.config.json");
185
+ console.log(" --from <file> Read analysis JSON from file");
186
+ console.log(" --out <path> Write output to a file");
160
187
  console.log(" --min-score <n> Minimum score required to pass");
161
188
  console.log(" --fail-on-risks Fail when risky dependencies exist");
162
189
  console.log(" --fail-on-outdated Fail when outdated dependencies exist");
@@ -176,3 +203,13 @@ async function loadPackageVersion() {
176
203
  return null;
177
204
  }
178
205
  }
206
+ async function writeOutput(output, outPath) {
207
+ if (outPath) {
208
+ await fs.writeFile(outPath, `${output}\n`, "utf8");
209
+ return;
210
+ }
211
+ process.stdout.write(`${output}\n`);
212
+ }
213
+ function sanitizeForLog(value) {
214
+ return value.replace(/[\r\n]+/g, " ").trim();
215
+ }
@@ -1,12 +1,13 @@
1
1
  import path from "node:path";
2
- import { findDuplicateDependencies } from "../checks/duplicate.js";
3
- import { findOutdatedDependencies } from "../checks/outdated.js";
4
- import { findRiskDependencies } from "../checks/risk.js";
5
- import { findUnusedDependencies } from "../checks/unused.js";
2
+ import { runDuplicateCheck } from "../checks/duplicate.js";
3
+ import { runOutdatedCheck } from "../checks/outdated.js";
4
+ import { runRiskCheck } from "../checks/risk.js";
5
+ 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
9
  import { calculateHealthScore } from "./scorer.js";
10
+ import { buildAnalysisContext } from "./context.js";
10
11
  export const OUTPUT_VERSION = "1.0";
11
12
  export async function analyzeProject(options = {}) {
12
13
  const rootDir = path.resolve(options.rootDir ?? process.cwd());
@@ -17,8 +18,9 @@ export async function analyzeProject(options = {}) {
17
18
  return analyzeSingleProject(rootDir, config);
18
19
  }
19
20
  const rootGraph = await buildDependencyGraph(rootDir);
20
- const rawDuplicates = await findDuplicateDependencies(rootGraph);
21
- const duplicates = rawDuplicates.filter((item) => !shouldIgnorePackage(item.name, "duplicates", config));
21
+ const duplicateCheck = await runDuplicateCheck(rootGraph);
22
+ const filteredDuplicateIssues = filterIssues(duplicateCheck.issues, "duplicates", config);
23
+ const duplicates = mapDuplicateIssues(filteredDuplicateIssues);
22
24
  const packages = [];
23
25
  for (const workspace of workspaces) {
24
26
  const result = await analyzeSingleProject(workspace.rootDir, config, {
@@ -129,20 +131,13 @@ function evaluatePolicy(summary, config) {
129
131
  };
130
132
  }
131
133
  async function analyzeSingleProject(rootDir, config, options = {}) {
132
- const graph = await buildDependencyGraph(rootDir);
133
- const [rawDuplicates, rawUnused, rawOutdated, rawRisks] = await Promise.all([
134
- findDuplicateDependencies(graph),
135
- findUnusedDependencies(rootDir, graph, {
136
- excludePaths: config.scan.excludePaths
137
- }),
138
- findOutdatedDependencies(graph),
139
- findRiskDependencies(graph)
140
- ]);
141
- const duplicates = rawDuplicates.filter((item) => !shouldIgnorePackage(item.name, "duplicates", config));
142
- const unused = rawUnused.filter((item) => !shouldIgnorePackage(item.name, "unused", config) &&
143
- !shouldIgnorePackage(item.name, item.section, config));
144
- const outdated = rawOutdated.filter((item) => !shouldIgnorePackage(item.name, "outdated", config));
145
- const risks = rawRisks.filter((item) => !shouldIgnorePackage(item.name, "risks", config));
134
+ const context = await buildAnalysisContext(rootDir, config);
135
+ const results = await runChecks(context);
136
+ const issueGroups = normalizeIssues(results, config);
137
+ const duplicates = mapDuplicateIssues(issueGroups.duplicates);
138
+ const unused = mapUnusedIssues(issueGroups.unused);
139
+ const outdated = mapOutdatedIssues(issueGroups.outdated);
140
+ const risks = mapRiskIssues(issueGroups.risks);
146
141
  const score = calculateHealthScore({
147
142
  duplicates: duplicates.length,
148
143
  unused: unused.length,
@@ -211,6 +206,89 @@ function shouldIgnorePackage(name, bucket, config) {
211
206
  }
212
207
  });
213
208
  }
209
+ async function runChecks(context) {
210
+ const checks = [
211
+ {
212
+ name: "duplicate",
213
+ run: () => runDuplicateCheck(context.graph)
214
+ },
215
+ {
216
+ name: "unused",
217
+ run: () => runUnusedCheck(context)
218
+ },
219
+ {
220
+ name: "outdated",
221
+ run: () => runOutdatedCheck(context.graph)
222
+ },
223
+ {
224
+ name: "risk",
225
+ run: () => runRiskCheck(context.graph)
226
+ }
227
+ ];
228
+ const results = [];
229
+ for (const check of checks) {
230
+ results.push(await check.run());
231
+ }
232
+ return results;
233
+ }
234
+ function normalizeIssues(results, config) {
235
+ const map = new Map();
236
+ for (const result of results) {
237
+ map.set(result.name, result.issues);
238
+ }
239
+ return {
240
+ duplicates: filterIssues(map.get("duplicate") ?? [], "duplicates", config),
241
+ unused: filterIssues(map.get("unused") ?? [], "unused", config),
242
+ outdated: filterIssues(map.get("outdated") ?? [], "outdated", config),
243
+ risks: filterIssues(map.get("risk") ?? [], "risks", config)
244
+ };
245
+ }
246
+ function filterIssues(issues, bucket, config) {
247
+ return issues.filter((issue) => {
248
+ const name = typeof issue.meta?.name === "string" ? issue.meta.name : issue.package ?? "";
249
+ if (!name) {
250
+ return true;
251
+ }
252
+ if (bucket === "unused") {
253
+ const section = issue.meta?.section === "devDependencies" ? "devDependencies" : "dependencies";
254
+ if (shouldIgnorePackage(name, section, config)) {
255
+ return false;
256
+ }
257
+ }
258
+ return !shouldIgnorePackage(name, bucket, config);
259
+ });
260
+ }
261
+ function mapDuplicateIssues(issues) {
262
+ return issues.map((issue) => ({
263
+ name: String(issue.meta?.name ?? issue.package ?? "unknown"),
264
+ versions: Array.isArray(issue.meta?.versions) ? issue.meta?.versions : [],
265
+ instances: Array.isArray(issue.meta?.instances)
266
+ ? issue.meta?.instances
267
+ : []
268
+ }));
269
+ }
270
+ function mapUnusedIssues(issues) {
271
+ return issues.map((issue) => ({
272
+ name: String(issue.meta?.name ?? issue.package ?? "unknown"),
273
+ section: issue.meta?.section === "devDependencies" ? "devDependencies" : "dependencies"
274
+ }));
275
+ }
276
+ function mapOutdatedIssues(issues) {
277
+ return issues.map((issue) => ({
278
+ name: String(issue.meta?.name ?? issue.package ?? "unknown"),
279
+ current: String(issue.meta?.current ?? ""),
280
+ latest: String(issue.meta?.latest ?? ""),
281
+ updateType: issue.meta?.updateType === "major" || issue.meta?.updateType === "minor" || issue.meta?.updateType === "patch"
282
+ ? issue.meta.updateType
283
+ : "unknown"
284
+ }));
285
+ }
286
+ function mapRiskIssues(issues) {
287
+ return issues.map((issue) => ({
288
+ name: String(issue.meta?.name ?? issue.package ?? "unknown"),
289
+ reasons: Array.isArray(issue.meta?.reasons) ? issue.meta?.reasons : []
290
+ }));
291
+ }
214
292
  function buildScoreBreakdown(counts, config) {
215
293
  return {
216
294
  baseScore: 100,
@@ -0,0 +1,3 @@
1
+ import type { AnalysisContext } from "./types.js";
2
+ import type { DepBrainConfig } from "../utils/config.js";
3
+ export declare function buildAnalysisContext(rootDir: string, config: DepBrainConfig): Promise<AnalysisContext>;
@@ -0,0 +1,34 @@
1
+ import path from "node:path";
2
+ import { buildDependencyGraph } from "./graph-builder.js";
3
+ import { collectProjectFiles, readTextFile } from "../utils/file-parser.js";
4
+ import { resolveWithinRoot } from "../utils/path.js";
5
+ const SOURCE_FILE_PATTERN = /\.(c|m)?(t|j)sx?$/;
6
+ export async function buildAnalysisContext(rootDir, config) {
7
+ const resolvedRoot = path.resolve(rootDir);
8
+ const graph = await buildDependencyGraph(resolvedRoot);
9
+ const projectFiles = await collectProjectFiles(resolvedRoot, SOURCE_FILE_PATTERN, config.scan.excludePaths);
10
+ const fileEntries = await Promise.all(projectFiles.map(async (filePath) => ({
11
+ path: filePath,
12
+ content: await readTextFile(filePath)
13
+ })));
14
+ const sourceText = fileEntries.map((entry) => entry.content).join("\n");
15
+ const hasTypeScriptConfig = await hasFile(resolvedRoot, "tsconfig.json");
16
+ return {
17
+ rootDir: resolvedRoot,
18
+ graph,
19
+ sourceText,
20
+ projectFiles,
21
+ fileEntries,
22
+ hasTypeScriptConfig
23
+ };
24
+ }
25
+ async function hasFile(rootDir, fileName) {
26
+ try {
27
+ const resolved = resolveWithinRoot(rootDir, fileName);
28
+ await readTextFile(resolved);
29
+ return true;
30
+ }
31
+ catch {
32
+ return false;
33
+ }
34
+ }
@@ -0,0 +1,28 @@
1
+ export type IssueSeverity = "info" | "warning" | "critical";
2
+ export type Issue = {
3
+ id: string;
4
+ message: string;
5
+ package?: string;
6
+ severity: IssueSeverity;
7
+ meta?: Record<string, unknown>;
8
+ };
9
+ export type CheckResult = {
10
+ name: string;
11
+ issues: Issue[];
12
+ summary: string;
13
+ };
14
+ export type AnalysisContext = {
15
+ rootDir: string;
16
+ graph: import("./graph-builder.js").DependencyGraph;
17
+ sourceText: string;
18
+ projectFiles: string[];
19
+ fileEntries: {
20
+ path: string;
21
+ content: string;
22
+ }[];
23
+ hasTypeScriptConfig: boolean;
24
+ };
25
+ export type CheckRunner = {
26
+ name: string;
27
+ run: (context: AnalysisContext) => Promise<CheckResult>;
28
+ };
@@ -0,0 +1 @@
1
+ export {};
package/dist/index.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  export { analyzeProject } from "./core/analyzer.js";
2
2
  export type { AnalysisOptions, AnalysisResult, DuplicateDependency, OutdatedDependency, PolicyResult, PackageAnalysisResult, ScoreBreakdown, RiskDependency, UnusedDependency } from "./core/analyzer.js";
3
3
  export { OUTPUT_VERSION } from "./core/analyzer.js";
4
+ export type { AnalysisContext, CheckResult, Issue } from "./core/types.js";
4
5
  export type { DepBrainConfig, DepBrainConfigOverrides } from "./utils/config.js";
5
6
  export type { WorkspacePackage } from "./utils/workspaces.js";
@@ -1,5 +1,5 @@
1
- import path from "node:path";
2
1
  import { readJsonFile } from "./file-parser.js";
2
+ import { resolveWithinRoot } from "./path.js";
3
3
  export const defaultConfig = {
4
4
  ignore: {
5
5
  dependencies: [],
@@ -32,7 +32,7 @@ export const defaultConfig = {
32
32
  }
33
33
  };
34
34
  export async function loadDepBrainConfig(rootDir, configPath) {
35
- const resolvedPath = path.resolve(rootDir, configPath ?? "depbrain.config.json");
35
+ const resolvedPath = resolveWithinRoot(rootDir, configPath ?? "depbrain.config.json");
36
36
  try {
37
37
  const loaded = await readJsonFile(resolvedPath);
38
38
  return normalizeConfig(loaded);
@@ -1,5 +1,6 @@
1
1
  import { promises as fs } from "node:fs";
2
2
  import path from "node:path";
3
+ import { isWithinRoot } from "./path.js";
3
4
  export async function readJsonFile(filePath) {
4
5
  const content = await fs.readFile(filePath, "utf8");
5
6
  return JSON.parse(content);
@@ -11,6 +12,9 @@ export async function collectProjectFiles(rootDir, pattern, excludePaths = []) {
11
12
  return collectProjectFilesInternal(rootDir, rootDir, pattern, excludePaths);
12
13
  }
13
14
  async function collectProjectFilesInternal(currentDir, baseDir, pattern, excludePaths) {
15
+ if (!isWithinRoot(baseDir, currentDir)) {
16
+ return [];
17
+ }
14
18
  const entries = await fs.readdir(currentDir, { withFileTypes: true });
15
19
  const files = [];
16
20
  for (const entry of entries) {
@@ -0,0 +1,2 @@
1
+ export declare function resolveWithinRoot(rootDir: string, targetPath: string): string;
2
+ export declare function isWithinRoot(rootDir: string, targetPath: string): boolean;
@@ -0,0 +1,16 @@
1
+ import path from "node:path";
2
+ export function resolveWithinRoot(rootDir, targetPath) {
3
+ const resolvedRoot = path.resolve(rootDir);
4
+ const resolvedTarget = path.resolve(resolvedRoot, targetPath);
5
+ if (!isWithinRoot(resolvedRoot, resolvedTarget)) {
6
+ throw new Error(`Path is خارج root: ${resolvedTarget}`);
7
+ }
8
+ return resolvedTarget;
9
+ }
10
+ export function isWithinRoot(rootDir, targetPath) {
11
+ const relative = path.relative(rootDir, targetPath);
12
+ if (!relative || relative === ".") {
13
+ return true;
14
+ }
15
+ return !relative.startsWith("..") && !path.isAbsolute(relative);
16
+ }
@@ -1,6 +1,7 @@
1
1
  import { promises as fs } from "node:fs";
2
2
  import path from "node:path";
3
3
  import { readJsonFile } from "./file-parser.js";
4
+ import { isWithinRoot, resolveWithinRoot } from "./path.js";
4
5
  export async function findWorkspacePackages(rootDir) {
5
6
  const rootPackageJsonPath = path.join(rootDir, "package.json");
6
7
  const rootPackage = await readJsonFile(rootPackageJsonPath).catch(() => null);
@@ -37,12 +38,21 @@ async function collectPackageJsonFiles(rootDir) {
37
38
  continue;
38
39
  }
39
40
  const fullPath = path.join(rootDir, entry.name);
41
+ if (!isWithinRoot(rootDir, fullPath)) {
42
+ continue;
43
+ }
40
44
  if (entry.isDirectory()) {
41
45
  files.push(...(await collectPackageJsonFiles(fullPath)));
42
46
  continue;
43
47
  }
44
48
  if (entry.isFile() && entry.name === "package.json") {
45
- files.push(fullPath);
49
+ try {
50
+ const resolved = resolveWithinRoot(rootDir, fullPath);
51
+ files.push(resolved);
52
+ }
53
+ catch {
54
+ continue;
55
+ }
46
56
  }
47
57
  }
48
58
  return files;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dep-brain",
3
- "version": "0.4.0",
3
+ "version": "0.5.1",
4
4
  "description": "CLI and library for dependency health analysis",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",