dep-brain 0.1.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/LICENSE +21 -0
- package/README.md +197 -0
- package/depbrain.config.json +20 -0
- package/depbrain.config.schema.json +38 -0
- package/dist/checks/duplicate.d.ts +3 -0
- package/dist/checks/duplicate.js +10 -0
- package/dist/checks/outdated.d.ts +6 -0
- package/dist/checks/outdated.js +51 -0
- package/dist/checks/risk.d.ts +3 -0
- package/dist/checks/risk.js +31 -0
- package/dist/checks/unused.d.ts +3 -0
- package/dist/checks/unused.js +120 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +118 -0
- package/dist/core/analyzer.d.ts +60 -0
- package/dist/core/analyzer.js +158 -0
- package/dist/core/graph-builder.d.ts +14 -0
- package/dist/core/graph-builder.js +63 -0
- package/dist/core/scorer.d.ts +7 -0
- package/dist/core/scorer.js +8 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +1 -0
- package/dist/reporters/console.d.ts +2 -0
- package/dist/reporters/console.js +51 -0
- package/dist/reporters/json.d.ts +2 -0
- package/dist/reporters/json.js +3 -0
- package/dist/utils/config.d.ts +27 -0
- package/dist/utils/config.js +66 -0
- package/dist/utils/file-parser.d.ts +3 -0
- package/dist/utils/file-parser.js +27 -0
- package/dist/utils/npm-api.d.ts +8 -0
- package/dist/utils/npm-api.js +52 -0
- package/dist/utils/workspaces.d.ts +6 -0
- package/dist/utils/workspaces.js +68 -0
- package/package.json +53 -0
package/CHANGELOG.md
ADDED
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Vijay prakash
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
# Dependency Brain
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/dep-brain)
|
|
4
|
+
[](https://www.npmjs.com/package/dep-brain)
|
|
5
|
+
[](LICENSE)
|
|
6
|
+
|
|
7
|
+
`dep-brain` is a CLI and library for analyzing dependency health in JavaScript and TypeScript projects.
|
|
8
|
+
|
|
9
|
+
## Vision
|
|
10
|
+
|
|
11
|
+
`npm audit + depcheck + dedupe + intelligence = one tool`
|
|
12
|
+
|
|
13
|
+
## What It Does
|
|
14
|
+
|
|
15
|
+
- Detect duplicate dependencies from `package-lock.json`
|
|
16
|
+
- Detect likely unused dependencies from source imports and scripts
|
|
17
|
+
- Detect outdated packages
|
|
18
|
+
- Highlight dependency risk signals
|
|
19
|
+
- Generate a simple project health score
|
|
20
|
+
- Output reports in human-readable or JSON format
|
|
21
|
+
|
|
22
|
+
## Current MVP Features
|
|
23
|
+
|
|
24
|
+
- Duplicate dependency detection with lockfile instance tracking
|
|
25
|
+
- Unused dependency detection with runtime vs dev-tool heuristics
|
|
26
|
+
- Outdated dependency reporting with `major`, `minor`, and `patch` classification
|
|
27
|
+
- Risk analysis based on npm package metadata
|
|
28
|
+
- Config loading from `depbrain.config.json`
|
|
29
|
+
- Ignore rules for noisy dependencies and checks
|
|
30
|
+
- CI-friendly policy evaluation with non-zero exit codes
|
|
31
|
+
- Workspace-aware analysis for npm workspaces
|
|
32
|
+
- Console reporting
|
|
33
|
+
- JSON output via `--json`
|
|
34
|
+
- Library entrypoint for programmatic use
|
|
35
|
+
|
|
36
|
+
## CLI Usage
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
npm install -g dep-brain
|
|
40
|
+
dep-brain analyze
|
|
41
|
+
|
|
42
|
+
npx dep-brain analyze
|
|
43
|
+
npx dep-brain analyze --json
|
|
44
|
+
npx dep-brain analyze ./path-to-project
|
|
45
|
+
npx dep-brain analyze --config depbrain.config.json
|
|
46
|
+
npx dep-brain analyze --min-score 90 --fail-on-risks
|
|
47
|
+
npx dep-brain analyze ./path-to-project --fail-on-unused --json
|
|
48
|
+
|
|
49
|
+
dep-brain config
|
|
50
|
+
dep-brain config --config depbrain.config.json
|
|
51
|
+
|
|
52
|
+
dep-brain help
|
|
53
|
+
dep-brain analyze --help
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Workspaces
|
|
57
|
+
|
|
58
|
+
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.
|
|
59
|
+
|
|
60
|
+
## Example Output
|
|
61
|
+
|
|
62
|
+
```text
|
|
63
|
+
Project Health: 78/100
|
|
64
|
+
Path: /your/project
|
|
65
|
+
Policy: FAIL
|
|
66
|
+
|
|
67
|
+
WARN Duplicates: 2
|
|
68
|
+
OK Unused: 0
|
|
69
|
+
WARN Outdated: 3
|
|
70
|
+
OK Risks: 0
|
|
71
|
+
|
|
72
|
+
Duplicate dependencies:
|
|
73
|
+
- ansi-regex: 5.0.1, 6.0.1
|
|
74
|
+
|
|
75
|
+
Outdated dependencies:
|
|
76
|
+
- chalk: ^4.1.2 -> 5.4.1 [major]
|
|
77
|
+
|
|
78
|
+
Policy reasons:
|
|
79
|
+
- Score 78 is below minimum 90
|
|
80
|
+
|
|
81
|
+
Suggestions:
|
|
82
|
+
- Consider consolidating ansi-regex to one version
|
|
83
|
+
- Review chalk: ^4.1.2 -> 5.4.1 (major)
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## JSON Output
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
dep-brain analyze --json
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## Config File
|
|
93
|
+
|
|
94
|
+
Create a `depbrain.config.json` file in the project root:
|
|
95
|
+
|
|
96
|
+
```json
|
|
97
|
+
{
|
|
98
|
+
"ignore": {
|
|
99
|
+
"unused": ["eslint"],
|
|
100
|
+
"outdated": ["typescript"]
|
|
101
|
+
},
|
|
102
|
+
"policy": {
|
|
103
|
+
"minScore": 90,
|
|
104
|
+
"failOnUnused": true,
|
|
105
|
+
"failOnRisks": true
|
|
106
|
+
},
|
|
107
|
+
"report": {
|
|
108
|
+
"maxSuggestions": 3
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Supported sections:
|
|
114
|
+
|
|
115
|
+
- `ignore.dependencies`
|
|
116
|
+
- `ignore.devDependencies`
|
|
117
|
+
- `ignore.unused`
|
|
118
|
+
- `ignore.duplicates`
|
|
119
|
+
- `ignore.outdated`
|
|
120
|
+
- `ignore.risks`
|
|
121
|
+
- `policy.minScore`
|
|
122
|
+
- `policy.failOnDuplicates`
|
|
123
|
+
- `policy.failOnUnused`
|
|
124
|
+
- `policy.failOnOutdated`
|
|
125
|
+
- `policy.failOnRisks`
|
|
126
|
+
- `report.maxSuggestions`
|
|
127
|
+
|
|
128
|
+
Sample config file:
|
|
129
|
+
|
|
130
|
+
- `depbrain.config.json`
|
|
131
|
+
- `depbrain.config.schema.json`
|
|
132
|
+
|
|
133
|
+
## CI Behavior
|
|
134
|
+
|
|
135
|
+
`dep-brain` now returns a non-zero exit code when configured policy checks fail.
|
|
136
|
+
|
|
137
|
+
Examples:
|
|
138
|
+
|
|
139
|
+
```bash
|
|
140
|
+
dep-brain analyze --fail-on-unused
|
|
141
|
+
dep-brain analyze --min-score 85 --fail-on-risks
|
|
142
|
+
dep-brain analyze --config depbrain.config.json
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
## Config Debugging
|
|
146
|
+
|
|
147
|
+
Print the resolved config (after defaults and CLI overrides):
|
|
148
|
+
|
|
149
|
+
```bash
|
|
150
|
+
dep-brain config
|
|
151
|
+
dep-brain config --config depbrain.config.json
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
## Development
|
|
155
|
+
|
|
156
|
+
```bash
|
|
157
|
+
npm install
|
|
158
|
+
npm run typecheck
|
|
159
|
+
npm run test
|
|
160
|
+
npm run build
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
## Project Structure
|
|
164
|
+
|
|
165
|
+
```text
|
|
166
|
+
src/
|
|
167
|
+
|-- cli.ts
|
|
168
|
+
|-- index.ts
|
|
169
|
+
|-- core/
|
|
170
|
+
| |-- analyzer.ts
|
|
171
|
+
| |-- graph-builder.ts
|
|
172
|
+
| `-- scorer.ts
|
|
173
|
+
|-- checks/
|
|
174
|
+
| |-- duplicate.ts
|
|
175
|
+
| |-- unused.ts
|
|
176
|
+
| |-- outdated.ts
|
|
177
|
+
| `-- risk.ts
|
|
178
|
+
|-- reporters/
|
|
179
|
+
| |-- console.ts
|
|
180
|
+
| `-- json.ts
|
|
181
|
+
`-- utils/
|
|
182
|
+
|-- file-parser.ts
|
|
183
|
+
|-- npm-api.ts
|
|
184
|
+
`-- config.ts
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
## Roadmap Direction
|
|
188
|
+
|
|
189
|
+
- Improve false-positive reduction for unused dependency detection
|
|
190
|
+
- Improve monorepo and workspace support
|
|
191
|
+
- Strengthen risk scoring and suggestions
|
|
192
|
+
- Add CI and GitHub Action support in later releases
|
|
193
|
+
|
|
194
|
+
## Repository Notes
|
|
195
|
+
|
|
196
|
+
- Project brief: [docs/project-brief.md](./docs/project-brief.md)
|
|
197
|
+
- Implementation history: [docs/implementation-log.md](./docs/implementation-log.md)
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"ignore": {
|
|
3
|
+
"unused": [],
|
|
4
|
+
"outdated": [],
|
|
5
|
+
"duplicates": [],
|
|
6
|
+
"risks": [],
|
|
7
|
+
"dependencies": [],
|
|
8
|
+
"devDependencies": []
|
|
9
|
+
},
|
|
10
|
+
"policy": {
|
|
11
|
+
"minScore": 85,
|
|
12
|
+
"failOnUnused": false,
|
|
13
|
+
"failOnOutdated": false,
|
|
14
|
+
"failOnDuplicates": false,
|
|
15
|
+
"failOnRisks": false
|
|
16
|
+
},
|
|
17
|
+
"report": {
|
|
18
|
+
"maxSuggestions": 5
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"title": "Dependency Brain Config",
|
|
4
|
+
"type": "object",
|
|
5
|
+
"additionalProperties": false,
|
|
6
|
+
"properties": {
|
|
7
|
+
"ignore": {
|
|
8
|
+
"type": "object",
|
|
9
|
+
"additionalProperties": false,
|
|
10
|
+
"properties": {
|
|
11
|
+
"dependencies": { "type": "array", "items": { "type": "string" } },
|
|
12
|
+
"devDependencies": { "type": "array", "items": { "type": "string" } },
|
|
13
|
+
"unused": { "type": "array", "items": { "type": "string" } },
|
|
14
|
+
"duplicates": { "type": "array", "items": { "type": "string" } },
|
|
15
|
+
"outdated": { "type": "array", "items": { "type": "string" } },
|
|
16
|
+
"risks": { "type": "array", "items": { "type": "string" } }
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
"policy": {
|
|
20
|
+
"type": "object",
|
|
21
|
+
"additionalProperties": false,
|
|
22
|
+
"properties": {
|
|
23
|
+
"minScore": { "type": "number" },
|
|
24
|
+
"failOnUnused": { "type": "boolean" },
|
|
25
|
+
"failOnOutdated": { "type": "boolean" },
|
|
26
|
+
"failOnDuplicates": { "type": "boolean" },
|
|
27
|
+
"failOnRisks": { "type": "boolean" }
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
"report": {
|
|
31
|
+
"type": "object",
|
|
32
|
+
"additionalProperties": false,
|
|
33
|
+
"properties": {
|
|
34
|
+
"maxSuggestions": { "type": "number" }
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export async function findDuplicateDependencies(graph) {
|
|
2
|
+
return Object.entries(graph.lockPackages)
|
|
3
|
+
.map(([name, instances]) => ({
|
|
4
|
+
name,
|
|
5
|
+
versions: Array.from(new Set(instances.map((instance) => instance.version))).sort(),
|
|
6
|
+
instances
|
|
7
|
+
}))
|
|
8
|
+
.filter((dependency) => dependency.versions.length > 1)
|
|
9
|
+
.sort((left, right) => left.name.localeCompare(right.name));
|
|
10
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { OutdatedDependency } from "../core/analyzer.js";
|
|
2
|
+
import type { DependencyGraph } from "../core/graph-builder.js";
|
|
3
|
+
export interface OutdatedOptions {
|
|
4
|
+
resolveLatestVersion?: (name: string) => Promise<string | null>;
|
|
5
|
+
}
|
|
6
|
+
export declare function findOutdatedDependencies(graph: DependencyGraph, options?: OutdatedOptions): Promise<OutdatedDependency[]>;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { getLatestVersion } from "../utils/npm-api.js";
|
|
2
|
+
export async function findOutdatedDependencies(graph, options = {}) {
|
|
3
|
+
const resolveLatestVersion = options.resolveLatestVersion ?? getLatestVersion;
|
|
4
|
+
const combined = {
|
|
5
|
+
...graph.dependencies,
|
|
6
|
+
...graph.devDependencies
|
|
7
|
+
};
|
|
8
|
+
const results = await Promise.all(Object.entries(combined).map(async ([name, current]) => {
|
|
9
|
+
const normalized = normalizeVersion(current);
|
|
10
|
+
const latest = await resolveLatestVersion(name);
|
|
11
|
+
if (!latest || latest === normalized) {
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
return {
|
|
15
|
+
name,
|
|
16
|
+
current,
|
|
17
|
+
latest,
|
|
18
|
+
updateType: classifyUpdateType(normalized, latest)
|
|
19
|
+
};
|
|
20
|
+
}));
|
|
21
|
+
return results
|
|
22
|
+
.filter((item) => item !== null)
|
|
23
|
+
.sort((left, right) => left.name.localeCompare(right.name));
|
|
24
|
+
}
|
|
25
|
+
function normalizeVersion(versionRange) {
|
|
26
|
+
return versionRange.trim().replace(/^[~^><=\s]+/, "");
|
|
27
|
+
}
|
|
28
|
+
function classifyUpdateType(currentVersion, latestVersion) {
|
|
29
|
+
const current = parseVersion(currentVersion);
|
|
30
|
+
const latest = parseVersion(latestVersion);
|
|
31
|
+
if (!current || !latest) {
|
|
32
|
+
return "unknown";
|
|
33
|
+
}
|
|
34
|
+
if (latest[0] !== current[0]) {
|
|
35
|
+
return "major";
|
|
36
|
+
}
|
|
37
|
+
if (latest[1] !== current[1]) {
|
|
38
|
+
return "minor";
|
|
39
|
+
}
|
|
40
|
+
if (latest[2] !== current[2]) {
|
|
41
|
+
return "patch";
|
|
42
|
+
}
|
|
43
|
+
return "unknown";
|
|
44
|
+
}
|
|
45
|
+
function parseVersion(version) {
|
|
46
|
+
const match = version.match(/(\d+)\.(\d+)\.(\d+)/);
|
|
47
|
+
if (!match) {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
return [Number(match[1]), Number(match[2]), Number(match[3])];
|
|
51
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { getPackageMetadata } from "../utils/npm-api.js";
|
|
2
|
+
const TWO_YEARS_IN_DAYS = 365 * 2;
|
|
3
|
+
export async function findRiskDependencies(graph) {
|
|
4
|
+
const names = Object.keys({
|
|
5
|
+
...graph.dependencies,
|
|
6
|
+
...graph.devDependencies
|
|
7
|
+
});
|
|
8
|
+
const results = await Promise.all(names.map(async (name) => {
|
|
9
|
+
const metadata = await getPackageMetadata(name);
|
|
10
|
+
const reasons = [];
|
|
11
|
+
if (!metadata) {
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
if (metadata.daysSincePublish !== null && metadata.daysSincePublish > TWO_YEARS_IN_DAYS) {
|
|
15
|
+
reasons.push("No release in over 2 years");
|
|
16
|
+
}
|
|
17
|
+
if (metadata.downloads !== null && metadata.downloads < 1000) {
|
|
18
|
+
reasons.push("Low weekly download volume");
|
|
19
|
+
}
|
|
20
|
+
if (!metadata.repository) {
|
|
21
|
+
reasons.push("Missing repository metadata");
|
|
22
|
+
}
|
|
23
|
+
if (reasons.length === 0) {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
return { name, reasons };
|
|
27
|
+
}));
|
|
28
|
+
return results
|
|
29
|
+
.filter((item) => item !== null)
|
|
30
|
+
.sort((left, right) => left.name.localeCompare(right.name));
|
|
31
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { collectProjectFiles, readTextFile } from "../utils/file-parser.js";
|
|
3
|
+
const SOURCE_FILE_PATTERN = /\.(c|m)?(t|j)sx?$/;
|
|
4
|
+
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
|
+
const TEST_FILE_PATTERN = /(^|[\\/])(__tests__|test|tests|spec|specs)([\\/]|$)|\.(test|spec)\.(c|m)?(t|j)sx?$/;
|
|
6
|
+
const RUNTIME_DIR_PATTERN = /(^|[\\/])(src|app|lib|server|client|pages|components)([\\/]|$)/;
|
|
7
|
+
export async function findUnusedDependencies(rootDir, graph) {
|
|
8
|
+
const files = await collectProjectFiles(rootDir, SOURCE_FILE_PATTERN);
|
|
9
|
+
const projectFiles = files.filter((filePath) => !filePath.includes(`${path.sep}node_modules${path.sep}`));
|
|
10
|
+
const runtimeUsed = new Set();
|
|
11
|
+
const devUsed = new Set();
|
|
12
|
+
for (const filePath of projectFiles) {
|
|
13
|
+
const content = await readTextFile(filePath);
|
|
14
|
+
const imports = extractImportedPackages(content);
|
|
15
|
+
const isDevOnlyFile = isDevelopmentOnlyFile(rootDir, filePath);
|
|
16
|
+
const target = isDevOnlyFile ? devUsed : runtimeUsed;
|
|
17
|
+
for (const importedPackage of imports) {
|
|
18
|
+
target.add(importedPackage);
|
|
19
|
+
if (!isDevOnlyFile) {
|
|
20
|
+
devUsed.add(importedPackage);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
for (const referencedBinary of extractScriptReferences(graph.scripts)) {
|
|
25
|
+
devUsed.add(referencedBinary);
|
|
26
|
+
}
|
|
27
|
+
const hasTypeScriptSources = projectFiles.some((filePath) => /\.(c|m)?tsx?$/.test(filePath));
|
|
28
|
+
const hasTypeScriptConfig = await hasFile(rootDir, "tsconfig.json");
|
|
29
|
+
if (hasTypeScriptConfig) {
|
|
30
|
+
devUsed.add("typescript");
|
|
31
|
+
}
|
|
32
|
+
const unusedDependencies = Object.keys(graph.dependencies)
|
|
33
|
+
.filter((name) => !runtimeUsed.has(name))
|
|
34
|
+
.map((name) => ({ name, section: "dependencies" }));
|
|
35
|
+
const unusedDevDependencies = Object.keys(graph.devDependencies)
|
|
36
|
+
.filter((name) => !devUsed.has(name) && !runtimeUsed.has(name))
|
|
37
|
+
.filter((name) => !isImplicitlyUsedDevDependency(name, hasTypeScriptSources, hasTypeScriptConfig))
|
|
38
|
+
.map((name) => ({ name, section: "devDependencies" }));
|
|
39
|
+
return [...unusedDependencies, ...unusedDevDependencies].sort((left, right) => left.name.localeCompare(right.name));
|
|
40
|
+
}
|
|
41
|
+
function extractImportedPackages(content) {
|
|
42
|
+
const imports = new Set();
|
|
43
|
+
const patterns = [
|
|
44
|
+
/\bimport\s+[^"'`]*?from\s+["'`]([^"'`]+)["'`]/g,
|
|
45
|
+
/\bexport\s+[^"'`]*?from\s+["'`]([^"'`]+)["'`]/g,
|
|
46
|
+
/\bimport\s*\(\s*["'`]([^"'`]+)["'`]\s*\)/g,
|
|
47
|
+
/\brequire\(\s*["'`]([^"'`]+)["'`]\s*\)/g,
|
|
48
|
+
/\bimport\s+["'`]([^"'`]+)["'`]/g
|
|
49
|
+
];
|
|
50
|
+
for (const pattern of patterns) {
|
|
51
|
+
for (const match of content.matchAll(pattern)) {
|
|
52
|
+
const dependencyName = normalizeModuleSpecifier(match[1]);
|
|
53
|
+
if (dependencyName) {
|
|
54
|
+
imports.add(dependencyName);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return imports;
|
|
59
|
+
}
|
|
60
|
+
function normalizeModuleSpecifier(specifier) {
|
|
61
|
+
if (!specifier ||
|
|
62
|
+
specifier.startsWith(".") ||
|
|
63
|
+
specifier.startsWith("/") ||
|
|
64
|
+
specifier.startsWith("#")) {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
if (specifier.startsWith("@")) {
|
|
68
|
+
const [scope, name] = specifier.split("/");
|
|
69
|
+
return scope && name ? `${scope}/${name}` : null;
|
|
70
|
+
}
|
|
71
|
+
return specifier.split("/")[0] ?? null;
|
|
72
|
+
}
|
|
73
|
+
function isDevelopmentOnlyFile(rootDir, filePath) {
|
|
74
|
+
const relativePath = path.relative(rootDir, filePath);
|
|
75
|
+
return (TEST_FILE_PATTERN.test(relativePath) ||
|
|
76
|
+
CONFIG_FILE_PATTERN.test(relativePath) ||
|
|
77
|
+
(!RUNTIME_DIR_PATTERN.test(relativePath) && relativePath.includes("scripts")));
|
|
78
|
+
}
|
|
79
|
+
function extractScriptReferences(scripts) {
|
|
80
|
+
const references = new Set();
|
|
81
|
+
for (const script of Object.values(scripts)) {
|
|
82
|
+
for (const token of script.split(/[^@\w./-]+/).filter(Boolean)) {
|
|
83
|
+
const normalized = normalizeScriptToken(token);
|
|
84
|
+
if (normalized) {
|
|
85
|
+
references.add(normalized);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return references;
|
|
90
|
+
}
|
|
91
|
+
function normalizeScriptToken(token) {
|
|
92
|
+
if (!token || token.startsWith(".") || token.includes("=") || token.startsWith("node")) {
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
if (token.startsWith("@")) {
|
|
96
|
+
const scopedMatch = token.match(/^(@[^/]+\/[^/]+)/);
|
|
97
|
+
if (scopedMatch) {
|
|
98
|
+
return scopedMatch[1];
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return token.replace(/\.cmd$/i, "");
|
|
102
|
+
}
|
|
103
|
+
function isImplicitlyUsedDevDependency(name, hasTypeScriptSources, hasTypeScriptConfig) {
|
|
104
|
+
if (name === "typescript" && (hasTypeScriptSources || hasTypeScriptConfig)) {
|
|
105
|
+
return true;
|
|
106
|
+
}
|
|
107
|
+
if (name.startsWith("@types/") && hasTypeScriptSources) {
|
|
108
|
+
return true;
|
|
109
|
+
}
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
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.d.ts
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { analyzeProject } from "./core/analyzer.js";
|
|
3
|
+
import { renderConsoleReport } from "./reporters/console.js";
|
|
4
|
+
import { renderJsonReport } from "./reporters/json.js";
|
|
5
|
+
import { promises as fs } from "node:fs";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
async function main() {
|
|
8
|
+
const args = process.argv.slice(2);
|
|
9
|
+
const command = args[0] ?? "analyze";
|
|
10
|
+
const optionValues = new Map();
|
|
11
|
+
const flags = new Set();
|
|
12
|
+
const positionals = [];
|
|
13
|
+
for (let index = 1; index < args.length; index += 1) {
|
|
14
|
+
const value = args[index];
|
|
15
|
+
if (!value?.startsWith("--")) {
|
|
16
|
+
positionals.push(value);
|
|
17
|
+
continue;
|
|
18
|
+
}
|
|
19
|
+
const nextValue = args[index + 1];
|
|
20
|
+
if (nextValue && !nextValue.startsWith("--")) {
|
|
21
|
+
optionValues.set(value, nextValue);
|
|
22
|
+
index += 1;
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
flags.add(value);
|
|
26
|
+
}
|
|
27
|
+
const targetPath = positionals[0] ?? process.cwd();
|
|
28
|
+
const showHelp = flags.has("--help") || command === "help";
|
|
29
|
+
if (showHelp) {
|
|
30
|
+
printHelp();
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
if (command !== "analyze") {
|
|
34
|
+
if (command === "config") {
|
|
35
|
+
if (!(await hasPackageJson(targetPath))) {
|
|
36
|
+
console.error(`No package.json found at ${targetPath}`);
|
|
37
|
+
process.exitCode = 1;
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
const config = await analyzeProject({
|
|
41
|
+
rootDir: targetPath,
|
|
42
|
+
configPath: optionValues.get("--config"),
|
|
43
|
+
config: buildCliConfig(flags, optionValues)
|
|
44
|
+
});
|
|
45
|
+
console.log(JSON.stringify(config.config, null, 2));
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
console.error(`Unknown command: ${command}`);
|
|
49
|
+
printHelp();
|
|
50
|
+
process.exitCode = 1;
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
if (!(await hasPackageJson(targetPath))) {
|
|
54
|
+
console.error(`No package.json found at ${targetPath}`);
|
|
55
|
+
process.exitCode = 1;
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
const cliConfig = buildCliConfig(flags, optionValues);
|
|
59
|
+
const result = await analyzeProject({
|
|
60
|
+
rootDir: targetPath,
|
|
61
|
+
configPath: optionValues.get("--config"),
|
|
62
|
+
config: cliConfig
|
|
63
|
+
});
|
|
64
|
+
console.log(flags.has("--json") ? renderJsonReport(result) : renderConsoleReport(result));
|
|
65
|
+
if (!result.policy.passed) {
|
|
66
|
+
process.exitCode = 1;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
void main();
|
|
70
|
+
function buildCliConfig(flags, optionValues) {
|
|
71
|
+
const minScore = optionValues.get("--min-score");
|
|
72
|
+
const policy = {};
|
|
73
|
+
if (minScore) {
|
|
74
|
+
policy.minScore = Number(minScore);
|
|
75
|
+
}
|
|
76
|
+
if (flags.has("--fail-on-duplicates")) {
|
|
77
|
+
policy.failOnDuplicates = true;
|
|
78
|
+
}
|
|
79
|
+
if (flags.has("--fail-on-outdated")) {
|
|
80
|
+
policy.failOnOutdated = true;
|
|
81
|
+
}
|
|
82
|
+
if (flags.has("--fail-on-risks")) {
|
|
83
|
+
policy.failOnRisks = true;
|
|
84
|
+
}
|
|
85
|
+
if (flags.has("--fail-on-unused")) {
|
|
86
|
+
policy.failOnUnused = true;
|
|
87
|
+
}
|
|
88
|
+
return {
|
|
89
|
+
policy
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
async function hasPackageJson(targetPath) {
|
|
93
|
+
try {
|
|
94
|
+
await fs.access(path.join(targetPath, "package.json"));
|
|
95
|
+
return true;
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
function printHelp() {
|
|
102
|
+
console.log("Dependency Brain");
|
|
103
|
+
console.log("");
|
|
104
|
+
console.log("Usage:");
|
|
105
|
+
console.log(" dep-brain analyze [path] [--json] [--config path] [--min-score n] [--fail-on-risks] [--fail-on-outdated] [--fail-on-unused] [--fail-on-duplicates]");
|
|
106
|
+
console.log(" dep-brain config [path] [--config path]");
|
|
107
|
+
console.log(" dep-brain help");
|
|
108
|
+
console.log("");
|
|
109
|
+
console.log("Options:");
|
|
110
|
+
console.log(" --json Output JSON for analysis");
|
|
111
|
+
console.log(" --config <path> Path to depbrain.config.json");
|
|
112
|
+
console.log(" --min-score <n> Minimum score required to pass");
|
|
113
|
+
console.log(" --fail-on-risks Fail when risky dependencies exist");
|
|
114
|
+
console.log(" --fail-on-outdated Fail when outdated dependencies exist");
|
|
115
|
+
console.log(" --fail-on-unused Fail when unused dependencies exist");
|
|
116
|
+
console.log(" --fail-on-duplicates Fail when duplicates exist");
|
|
117
|
+
console.log(" --help Show this help output");
|
|
118
|
+
}
|