helpforbegs 0.0.0-stage → 1.0.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 helpforbegs contributors
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 CHANGED
@@ -1,3 +1,147 @@
1
- # Temporary Holding Version
1
+ # helpforbegs
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ > **Dependency help for beginners. No jargon, just plain English.**
4
+
5
+ A gentle, accurate command-line assistant that scans your Node.js project and tells you:
6
+ - Which packages you installed but never use.
7
+ - Which packages you use in your code but forgot to install.
8
+ - How much lighter and cleaner your project will be after cleaning up.
9
+
10
+ Zero build steps. Zero TypeScript compilation needed. Safe by default.
11
+
12
+ ---
13
+
14
+ ## 📸 Demo Output
15
+
16
+ ```text
17
+ 🔍 Scanning your project... (42 files read)
18
+
19
+ ❌ Installed but never used (2)
20
+ • lodash (1.4 MB) You installed lodash but never used it.
21
+ • moment (4.2 MB) You installed moment but never used it.
22
+
23
+ 🤔 Probably unused (1)
24
+ • cors Known development tool, but no active script or config reference was detected
25
+
26
+ ⚠️ Used but not installed (1)
27
+ • axios Used in src/api.js:3 but missing from package.json.
28
+
29
+ ✅ 9 packages are used correctly
30
+
31
+ 📊 Health score: 78/100 (B) "Pretty tidy, a few dusty corners."
32
+ 💾 You could free about 5.6 MB.
33
+
34
+ 💡 Run `npx helpforbegs remove` to clean up safely.
35
+ ```
36
+
37
+ ---
38
+
39
+ ## ⚡ Quick Start
40
+
41
+ You don't even need to install it permanently! Just run `npx` inside any Node.js project:
42
+
43
+ ```bash
44
+ npx helpforbegs
45
+ ```
46
+
47
+ Or install it globally if you want to use it anytime:
48
+
49
+ ```bash
50
+ npm install -g helpforbegs
51
+ helpforbegs
52
+ ```
53
+
54
+ ---
55
+
56
+ ## 🛠️ Commands
57
+
58
+ | Command | What it does |
59
+ |---|---|
60
+ | `helpforbegs` or `helpforbegs scan` | Runs a complete project scan and prints the friendly health report. |
61
+ | `helpforbegs remove` | Displays unused packages, prompts for confirmation (`y/N`), creates a timestamped backup of `package.json`, and safely uninstalls them. |
62
+ | `helpforbegs why <package>` | Explains in plain English with exact file paths and line numbers where a package is used, or why it is considered unused. |
63
+ | `helpforbegs score` | Prints only the project health score, letter grade, and summary comment (great for CI or quick checks). |
64
+
65
+ ---
66
+
67
+ ## 🚩 Flags & Options
68
+
69
+ - `--path <dir>`: Scan another folder instead of the current directory (e.g. `npx helpforbegs --path ../my-express-app`).
70
+ - `--dry-run`: Test `helpforbegs remove` safely without touching or deleting any files.
71
+ - `--yes` or `-y`: Automatically confirm uninstallation in `remove` mode (required in CI or non-interactive terminals).
72
+ - `--roast`: Enable humorous, playfully sarcastic feedback on your dependency habits.
73
+ - `--json`: Output raw JSON data for CI pipelines, scripts, or custom dashboards.
74
+ - `--no-color`: Disable all ANSI terminal colors (automatically respected if `NO_COLOR` is set or when output is redirected to a file).
75
+ - `--help` (`-h`): Display the CLI help screen.
76
+ - `--version` (`-v`): Show the installed version.
77
+ - `--debug`: Display full technical stack traces on unexpected errors.
78
+
79
+ ---
80
+
81
+ ## 🧠 How Detection Works (In Plain English)
82
+
83
+ Many tools use simple text searches or regular expressions to find package names, which often leads to mistakes. `helpforbegs` works differently:
84
+
85
+ 1. **Real AST Code Parsing**: We parse your JavaScript and TypeScript files using `@babel/parser`. This means we inspect the actual syntax tree of your code. We recognize:
86
+ - CommonJS `require('pkg')` and `require.resolve('pkg')`
87
+ - ES Module `import ... from 'pkg'`, `import 'pkg'`, and `export ... from 'pkg'`
88
+ - Dynamic `import('pkg')`
89
+ - TypeScript `import type { ... } from 'pkg'`
90
+ 2. **Package Name Normalization**: We strip subpaths (`lodash/fp` $\rightarrow$ `lodash`, `@babel/parser/lib` $\rightarrow$ `@babel/parser`), ignore relative paths (`./utils`), and filter out Node.js built-ins (`fs`, `path`, `node:test`, `crypto`).
91
+ 3. **Path Alias Intelligence**: We read path aliases in `tsconfig.json`, `jsconfig.json` (`compilerOptions.paths`), and `package.json` (`imports`). Imports like `@/components/Button` or `~/helpers` are recognized as internal aliases and are never flagged as missing npm packages.
92
+ 4. **Script Execution (`bin` commands)**: When you install CLI packages like `nodemon` or `rimraf`, you don't `require` them in code—you run them in `package.json` scripts. We read the `bin` field of packages in `node_modules` and match them against your script commands.
93
+ 5. **Config Files**: We check ESLint configs, Prettier plugins, Babel presets, Tailwind, PostCSS, and `tsconfig.json` compiler types.
94
+ 6. **Peer Dependencies**: If another installed package requires a dependency as a `peerDependency`, we will never mark it as unused.
95
+ 7. **Safe-List for Dev Tools**: Common development tools (`typescript`, `eslint`, `prettier`, `jest`, `nodemon`, `husky`, etc.) are recognized so you aren't pressured to delete your build pipeline.
96
+ 8. **Special Dotenv Rule**: `dotenv` is only flagged as unused if it is neither imported in code nor passed as a `-r dotenv/config` flag in scripts.
97
+
98
+ ---
99
+
100
+ ## 🛡️ Why You Can Trust the Results
101
+
102
+ False positives are dangerous: telling a beginner to uninstall a required package could break their project.
103
+
104
+ `helpforbegs` follows strict safety rules:
105
+ 1. **Never guess when unsure**: When confidence is not 100%, packages are marked `"Probably unused"` with an explanation to double-check, and `helpforbegs remove` will never delete them automatically.
106
+ 2. **Timestamped backups**: Before modifying `package.json`, a backup named `package.json.helpforbegs.<timestamp>.bak` is created.
107
+ 3. **Respects your package manager**: Automatically detects whether you use `npm`, `yarn`, `pnpm`, or `bun` by checking lockfiles, so your lockfile stays consistent.
108
+ 4. **Large files protection**: Skips files larger than 1 MB to prevent memory bloat, and informs you in the report.
109
+ 5. **Graceful syntax recovery**: If a file contains syntax errors that fail AST parsing, it falls back to a regex scanner and notes: `"Could not fully read src/x.js"`.
110
+
111
+ ---
112
+
113
+ ## 🙈 Ignoring Packages (`.helpforbegsignore`)
114
+
115
+ If you have a package that is used in a way automated tools cannot see, create a `.helpforbegsignore` file in your project root:
116
+
117
+ ```text
118
+ # .helpforbegsignore
119
+ # Packages listed here will never be flagged as unused
120
+
121
+ cors
122
+ dotenv
123
+ ```
124
+
125
+ ---
126
+
127
+ ## ⚠️ Known Limitations
128
+
129
+ 1. **Dynamic require variables**: Expressions like `require(myVariableName)` cannot be statically analyzed because their value is only known while your app is running.
130
+ 2. **Shell scripts outside package.json**: Tools invoked exclusively inside custom `.sh` or `.bat` files outside `package.json` scripts won't be seen unless listed in `.helpforbegsignore`.
131
+ 3. **Code generated at runtime**: Dynamically evaluated code (e.g. `eval()` or code fetched over a network) cannot be scanned statically.
132
+
133
+ ---
134
+
135
+ ## 🤝 Contributing
136
+
137
+ Contributions are welcome!
138
+ 1. Fork the repository.
139
+ 2. Clone your fork and run `npm install`.
140
+ 3. Run tests with `npm test`.
141
+ 4. Submit a Pull Request.
142
+
143
+ ---
144
+
145
+ ## 📄 License
146
+
147
+ MIT © helpforbegs contributors
package/bin/cli.js ADDED
@@ -0,0 +1,108 @@
1
+ #!/usr/bin/env node
2
+
3
+ // bin/cli.js
4
+ // WHY: This is the executable entry point when someone runs `helpforbegs`
5
+ // or `npx helpforbegs` in their terminal.
6
+ // It coordinates argument parsing, routing to commands, and clean exit codes.
7
+
8
+ const path = require('path');
9
+ const { parseArgs } = require('../src/args');
10
+ const { analyzeProject } = require('../src/analyzer');
11
+ const { calculateScoreAndSavings } = require('../src/scorer');
12
+ const { reportResults } = require('../src/reporter');
13
+ const { explainPackage } = require('../src/why');
14
+ const { removeUnused } = require('../src/remover');
15
+ const pkgInfo = require('../package.json');
16
+
17
+ /**
18
+ * Prints user-friendly help documentation.
19
+ */
20
+ function printHelp() {
21
+ console.log(`
22
+ helpforbegs - ${pkgInfo.description}
23
+
24
+ USAGE:
25
+ $ npx helpforbegs [command] [options]
26
+
27
+ COMMANDS:
28
+ scan (default) Scans your project and prints the health report
29
+ remove Safely uninstalls unused packages after confirmation
30
+ why <package> Explains exactly where a package is used, or why it is unused
31
+ score Prints only the health score and letter grade
32
+
33
+ FLAGS:
34
+ --path <dir> Scan another directory (default: current directory)
35
+ --dry-run Preview what \`remove\` would uninstall without changing files
36
+ --yes, -y Confirm \`remove\` automatically (required in CI / non-TTY)
37
+ --roast Funny, gently sarcastic commentary
38
+ --json Output raw JSON data for scripts and CI
39
+ --no-color Disable ANSI terminal colors
40
+ --help, -h Show this help screen
41
+ --version, -v Show the version of helpforbegs
42
+ --debug Show technical stack traces on unexpected errors
43
+
44
+ EXAMPLES:
45
+ $ npx helpforbegs
46
+ $ npx helpforbegs --roast
47
+ $ npx helpforbegs why lodash
48
+ $ npx helpforbegs remove --dry-run
49
+ $ npx helpforbegs --path ./my-app
50
+ `);
51
+ }
52
+
53
+ async function main() {
54
+ const { command, targetPkg, flags } = parseArgs(process.argv.slice(2));
55
+
56
+ // 1. --help / -h
57
+ if (flags.help) {
58
+ printHelp();
59
+ process.exit(0);
60
+ }
61
+
62
+ // 2. --version / -v
63
+ if (flags.version) {
64
+ console.log(pkgInfo.version);
65
+ process.exit(0);
66
+ }
67
+
68
+ const targetDir = flags.targetDir;
69
+
70
+ try {
71
+ if (command === 'why') {
72
+ const exitCode = explainPackage(targetDir, targetPkg, flags);
73
+ process.exit(exitCode);
74
+ }
75
+
76
+ if (command === 'remove') {
77
+ const exitCode = await removeUnused(targetDir, flags);
78
+ process.exit(exitCode);
79
+ }
80
+
81
+ // Default: 'scan' or 'score'
82
+ const analysis = analyzeProject(targetDir);
83
+ const scoreData = calculateScoreAndSavings(analysis, { roast: flags.roast });
84
+
85
+ const exitCode = reportResults(analysis, scoreData, {
86
+ ...flags,
87
+ scoreOnly: command === 'score',
88
+ });
89
+
90
+ process.exit(exitCode);
91
+ } catch (err) {
92
+ // Exit code 2 for internal/input errors (no package.json, invalid JSON, etc.)
93
+ if (err.code === 'NO_PKG_JSON' || err.code === 'INVALID_PKG_JSON') {
94
+ console.error(`\n❌ ${err.message}\n`);
95
+ process.exit(2);
96
+ }
97
+
98
+ console.error(`\n❌ An unexpected error occurred: ${err.message}`);
99
+ if (flags.debug) {
100
+ console.error(err.stack);
101
+ } else {
102
+ console.error(' Run with `--debug` to view the full technical stack trace.\n');
103
+ }
104
+ process.exit(2);
105
+ }
106
+ }
107
+
108
+ main();
package/package.json CHANGED
@@ -1,6 +1,45 @@
1
1
  {
2
2
  "name": "helpforbegs",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "1.0.0",
4
+ "description": "Dependency help for beginners. No jargon, just plain English.",
5
+ "main": "src/analyzer.js",
6
+ "bin": {
7
+ "helpforbegs": "bin/cli.js"
8
+ },
9
+ "files": [
10
+ "bin",
11
+ "src"
12
+ ],
13
+ "scripts": {
14
+ "test": "jest"
15
+ },
16
+ "repository": {
17
+ "type": "git",
18
+ "url": "git+https://github.com/your-username/helpforbegs.git"
19
+ },
20
+ "bugs": {
21
+ "url": "https://github.com/your-username/helpforbegs/issues"
22
+ },
23
+ "homepage": "https://github.com/your-username/helpforbegs#readme",
24
+ "keywords": [
25
+ "dependencies",
26
+ "unused",
27
+ "cleanup",
28
+ "depcheck",
29
+ "beginner",
30
+ "cli",
31
+ "package-json"
32
+ ],
33
+ "engines": {
34
+ "node": ">=18"
35
+ },
36
+ "author": "Shriyan Kogta",
37
+ "license": "MIT",
38
+ "dependencies": {
39
+ "@babel/parser": "^7.24.0",
40
+ "chalk": "^4.1.2"
41
+ },
42
+ "devDependencies": {
43
+ "jest": "^29.7.0"
44
+ }
45
+ }
@@ -0,0 +1,348 @@
1
+ // src/analyzer.js
2
+ // WHY: This is the brain of helpforbegs. It collects findings from all our scanners
3
+ // (AST parser, config reader, scripts, bin mapper, peer dependencies, and safe-list)
4
+ // and sorts every package into one of four clear verdict buckets:
5
+ // 1. Used
6
+ // 2. Unused (high confidence)
7
+ // 3. Probably unused (tooling with uncertainty)
8
+ // 4. Missing (imported in code but forgotten in package.json)
9
+
10
+ const fs = require('fs');
11
+ const path = require('path');
12
+ const { findFiles } = require('./fileFinder');
13
+ const { parseImports } = require('./astParser');
14
+ const {
15
+ extractPathAliases,
16
+ readIgnoreFile,
17
+ collectPeerDependencies,
18
+ scanConfigFiles,
19
+ checkDotenvInScripts,
20
+ } = require('./configReader');
21
+ const { mapInstalledBins, findBinUsageInScripts } = require('./binMapper');
22
+ const { isSafeListed } = require('./safeList');
23
+
24
+ /**
25
+ * Resolves monorepo workspace directories if "workspaces" is defined in package.json.
26
+ *
27
+ * @param {string} projectDir
28
+ * @param {string[]|object} workspacesConfig
29
+ * @returns {string[]} List of absolute paths to workspace packages
30
+ */
31
+ function resolveWorkspaces(projectDir, workspacesConfig) {
32
+ const patterns = Array.isArray(workspacesConfig)
33
+ ? workspacesConfig
34
+ : (workspacesConfig && Array.isArray(workspacesConfig.packages) ? workspacesConfig.packages : []);
35
+
36
+ const workspaceDirs = [];
37
+
38
+ for (const pattern of patterns) {
39
+ if (pattern.endsWith('/*')) {
40
+ // e.g. "packages/*"
41
+ const parentDir = path.join(projectDir, pattern.slice(0, -2));
42
+ if (fs.existsSync(parentDir)) {
43
+ try {
44
+ const entries = fs.readdirSync(parentDir, { withFileTypes: true });
45
+ for (const entry of entries) {
46
+ if (entry.isDirectory()) {
47
+ const subDir = path.join(parentDir, entry.name);
48
+ if (fs.existsSync(path.join(subDir, 'package.json'))) {
49
+ workspaceDirs.push(subDir);
50
+ }
51
+ }
52
+ }
53
+ } catch {
54
+ // Ignore directory read errors
55
+ }
56
+ }
57
+ } else {
58
+ // Direct directory path, e.g. "apps/web"
59
+ const directDir = path.join(projectDir, pattern);
60
+ if (fs.existsSync(path.join(directDir, 'package.json'))) {
61
+ workspaceDirs.push(directDir);
62
+ }
63
+ }
64
+ }
65
+
66
+ return workspaceDirs;
67
+ }
68
+
69
+ /**
70
+ * Main analysis function that scans a project folder and categorizes all dependencies.
71
+ *
72
+ * @param {string} projectDir - Absolute path to the project root
73
+ * @param {object} [options]
74
+ * @returns {object} Full analysis results
75
+ */
76
+ function analyzeProject(projectDir, options = {}) {
77
+ const pkgJsonPath = path.join(projectDir, 'package.json');
78
+
79
+ // Verify that package.json exists
80
+ if (!fs.existsSync(pkgJsonPath)) {
81
+ const error = new Error(`No package.json found in ${projectDir}. Make sure you are in a Node.js project folder.`);
82
+ error.code = 'NO_PKG_JSON';
83
+ throw error;
84
+ }
85
+
86
+ // Parse package.json
87
+ let rootPkgJson;
88
+ try {
89
+ const content = fs.readFileSync(pkgJsonPath, 'utf8');
90
+ rootPkgJson = JSON.parse(content);
91
+ } catch (err) {
92
+ const error = new Error(`Could not read package.json in ${projectDir}. It contains invalid JSON syntax.`);
93
+ error.code = 'INVALID_PKG_JSON';
94
+ throw error;
95
+ }
96
+
97
+ // Check for monorepo workspaces
98
+ const workspaceDirs = rootPkgJson.workspaces ? resolveWorkspaces(projectDir, rootPkgJson.workspaces) : [];
99
+ const isMonorepo = workspaceDirs.length > 0;
100
+
101
+ // Aggregate all declared dependencies across root and workspaces
102
+ const declaredDeps = new Map(); // pkgName -> { isDev: boolean, definedIn: string }
103
+ const workspacePkgNames = new Set();
104
+
105
+ function registerDeps(pkgData, sourcePath) {
106
+ if (pkgData.name) {
107
+ workspacePkgNames.add(pkgData.name);
108
+ }
109
+ const regularDeps = pkgData.dependencies || {};
110
+ const devDeps = pkgData.devDependencies || {};
111
+
112
+ for (const dep of Object.keys(regularDeps)) {
113
+ if (!declaredDeps.has(dep)) {
114
+ declaredDeps.set(dep, { isDev: false, definedIn: sourcePath });
115
+ }
116
+ }
117
+ for (const dep of Object.keys(devDeps)) {
118
+ // If already in dependencies, regular dependency takes precedence
119
+ if (!declaredDeps.has(dep)) {
120
+ declaredDeps.set(dep, { isDev: true, definedIn: sourcePath });
121
+ }
122
+ }
123
+ }
124
+
125
+ registerDeps(rootPkgJson, 'package.json');
126
+
127
+ for (const wsDir of workspaceDirs) {
128
+ try {
129
+ const wsPkgJson = JSON.parse(fs.readFileSync(path.join(wsDir, 'package.json'), 'utf8'));
130
+ registerDeps(wsPkgJson, path.relative(projectDir, wsDir));
131
+ } catch {
132
+ // Ignore unparseable workspace package.json
133
+ }
134
+ }
135
+
136
+ // Discover files to scan
137
+ const { files, skippedLargeFiles } = findFiles(projectDir);
138
+ const knownAliases = extractPathAliases(projectDir, rootPkgJson);
139
+ const userIgnored = readIgnoreFile(projectDir);
140
+ const peerDeps = collectPeerDependencies(projectDir);
141
+ const { usedPackages: configPackages, hasTsConfig } = scanConfigFiles(projectDir);
142
+ const { binToPkg, missingNodeModules } = mapInstalledBins(projectDir);
143
+
144
+ // Collect scripts from root and workspaces
145
+ const allScripts = { ...(rootPkgJson.scripts || {}) };
146
+ for (const wsDir of workspaceDirs) {
147
+ try {
148
+ const wsPkg = JSON.parse(fs.readFileSync(path.join(wsDir, 'package.json'), 'utf8'));
149
+ Object.assign(allScripts, wsPkg.scripts || {});
150
+ } catch {
151
+ // Ignore
152
+ }
153
+ }
154
+
155
+ const binUsageInScripts = findBinUsageInScripts(allScripts, binToPkg);
156
+ const dotenvInScripts = checkDotenvInScripts(allScripts);
157
+
158
+ // Track code imports: pkgName -> Array<{ file: string, line: number, isTypeOnly: boolean }>
159
+ const codeImports = new Map();
160
+ const parseWarnings = [];
161
+ let hasTypeScriptFiles = false;
162
+
163
+ for (const fileObj of files) {
164
+ if (fileObj.ext === '.ts' || fileObj.ext === '.tsx') {
165
+ hasTypeScriptFiles = true;
166
+ }
167
+
168
+ try {
169
+ const content = fs.readFileSync(fileObj.fullPath, 'utf8');
170
+ const { imports, warning } = parseImports(content, fileObj.fullPath, { knownAliases });
171
+
172
+ if (warning) {
173
+ parseWarnings.push(warning);
174
+ }
175
+
176
+ for (const item of imports) {
177
+ if (!codeImports.has(item.packageName)) {
178
+ codeImports.set(item.packageName, []);
179
+ }
180
+ codeImports.get(item.packageName).push({
181
+ file: fileObj.relativePath,
182
+ line: item.line,
183
+ isTypeOnly: item.isTypeOnly,
184
+ });
185
+ }
186
+ } catch {
187
+ parseWarnings.push(`Could not read file: ${fileObj.relativePath}`);
188
+ }
189
+ }
190
+
191
+ // Build verdicts:
192
+ const used = new Map(); // pkgName -> { locations: [], reasons: [] }
193
+ const unused = [];
194
+ const probablyUnused = [];
195
+ const missing = [];
196
+
197
+ // Helper to mark a package as used with a specific reason
198
+ function markUsed(pkgName, reason, locations = []) {
199
+ if (!used.has(pkgName)) {
200
+ used.set(pkgName, { locations: [], reasons: [] });
201
+ }
202
+ const entry = used.get(pkgName);
203
+ if (reason && !entry.reasons.includes(reason)) {
204
+ entry.reasons.push(reason);
205
+ }
206
+ if (locations && locations.length > 0) {
207
+ entry.locations.push(...locations);
208
+ }
209
+ }
210
+
211
+ // 1. Evaluate all declared packages
212
+ for (const [pkgName, meta] of declaredDeps.entries()) {
213
+ let isFound = false;
214
+
215
+ // a) Found in user .helpforbegsignore
216
+ if (userIgnored.has(pkgName)) {
217
+ markUsed(pkgName, 'Listed in .helpforbegsignore');
218
+ continue;
219
+ }
220
+
221
+ // b) Found in source code imports
222
+ if (codeImports.has(pkgName)) {
223
+ markUsed(pkgName, 'Imported in source code', codeImports.get(pkgName));
224
+ isFound = true;
225
+ }
226
+
227
+ // c) Found in package.json scripts (directly or via bin name)
228
+ if (binUsageInScripts.has(pkgName)) {
229
+ const uses = binUsageInScripts.get(pkgName);
230
+ const scriptsList = uses.map((u) => `"${u.scriptName}"`).join(', ');
231
+ markUsed(pkgName, `Executed in package.json scripts: ${scriptsList}`);
232
+ isFound = true;
233
+ } else {
234
+ // Also check if the raw package name itself appears as a command in scripts
235
+ const escaped = pkgName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
236
+ const scriptRegex = new RegExp(`(^|[;&|\\s])${escaped}(\\s|$|[;&|])`);
237
+ for (const [sName, sCmd] of Object.entries(allScripts)) {
238
+ if (typeof sCmd === 'string' && scriptRegex.test(sCmd)) {
239
+ markUsed(pkgName, `Referenced in script "${sName}"`);
240
+ isFound = true;
241
+ break;
242
+ }
243
+ }
244
+ }
245
+
246
+ // d) Found in config files (ESLint, Prettier, Babel, tsconfig)
247
+ if (configPackages.has(pkgName)) {
248
+ markUsed(pkgName, 'Referenced in project configuration files');
249
+ isFound = true;
250
+ }
251
+
252
+ // e) Declared as a peerDependency by an installed package
253
+ if (peerDeps.has(pkgName)) {
254
+ markUsed(pkgName, 'Required as a peer dependency by another installed package');
255
+ isFound = true;
256
+ }
257
+
258
+ // f) Handling @types/xyz packages
259
+ if (pkgName.startsWith('@types/')) {
260
+ const basePkg = pkgName.slice('@types/'.length);
261
+
262
+ // If the base package is used in code or declared
263
+ if (codeImports.has(basePkg) || declaredDeps.has(basePkg)) {
264
+ markUsed(pkgName, `TypeScript definitions for used package "${basePkg}"`);
265
+ isFound = true;
266
+ }
267
+ // Special rule: @types/node is used if project has any TS files or tsconfig.json
268
+ else if (basePkg === 'node' && (hasTypeScriptFiles || hasTsConfig)) {
269
+ markUsed(pkgName, 'TypeScript type definitions for Node.js runtime');
270
+ isFound = true;
271
+ }
272
+ }
273
+
274
+ // g) Special dotenv check:
275
+ if (pkgName === 'dotenv') {
276
+ if (isFound || dotenvInScripts) {
277
+ markUsed(pkgName, dotenvInScripts ? 'Loaded via -r dotenv/config in scripts' : 'Imported in source code');
278
+ continue;
279
+ } else {
280
+ unused.push({
281
+ name: pkgName,
282
+ isDev: meta.isDev,
283
+ reason: 'Neither imported in code nor referenced with -r dotenv/config in scripts',
284
+ });
285
+ continue;
286
+ }
287
+ }
288
+
289
+ // If already confirmed as used, we're done with this package
290
+ if (isFound) {
291
+ continue;
292
+ }
293
+
294
+ // h) Built-in safe-list check:
295
+ // If a package is in the safe-list but wasn't found in scripts or configs,
296
+ // we mark it as "probably unused" so the user is encouraged to double check
297
+ // rather than accidentally breaking their tooling.
298
+ if (isSafeListed(pkgName)) {
299
+ probablyUnused.push({
300
+ name: pkgName,
301
+ isDev: meta.isDev,
302
+ reason: 'Known development tool, but no active script or config reference was detected',
303
+ });
304
+ continue;
305
+ }
306
+
307
+ // Otherwise, high confidence UNUSED
308
+ unused.push({
309
+ name: pkgName,
310
+ isDev: meta.isDev,
311
+ reason: `Installed in ${meta.isDev ? 'devDependencies' : 'dependencies'} but never imported or referenced`,
312
+ });
313
+ }
314
+
315
+ // 2. Detect missing packages (imported in code but not in package.json)
316
+ for (const [pkgName, locations] of codeImports.entries()) {
317
+ if (
318
+ !declaredDeps.has(pkgName) &&
319
+ !workspacePkgNames.has(pkgName) &&
320
+ !userIgnored.has(pkgName)
321
+ ) {
322
+ missing.push({
323
+ name: pkgName,
324
+ locations,
325
+ });
326
+ }
327
+ }
328
+
329
+ return {
330
+ projectDir,
331
+ rootPkgJson,
332
+ isMonorepo,
333
+ workspacesCount: workspaceDirs.length,
334
+ missingNodeModules,
335
+ filesReadCount: files.length,
336
+ skippedLargeFiles,
337
+ parseWarnings,
338
+ used,
339
+ unused,
340
+ probablyUnused,
341
+ missing,
342
+ };
343
+ }
344
+
345
+ module.exports = {
346
+ analyzeProject,
347
+ resolveWorkspaces,
348
+ };