reqscan 1.0.1 → 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
@@ -96,6 +96,17 @@ reqscan list
96
96
 
97
97
  ---
98
98
 
99
+ ## 🚩 Available Flags
100
+
101
+ | Flag | Description | Commands |
102
+ |------|-------------|----------|
103
+ | `--json` | Output machine-readable JSON | `check`, `audit`, `list` |
104
+ | `--save-dev` | Install missing packages as `devDependencies` | `install`, `fix` |
105
+ | `--dry-run` | Preview changes without executing | `install`, `clean`, `fix` |
106
+ | `--force` | Skip confirmation prompt | `clean`, `fix` |
107
+
108
+ ---
109
+
99
110
  ## 📤 Example Outputs
100
111
 
101
112
  ### `reqscan check`
@@ -175,12 +186,15 @@ Legend: ✓ present ✗ missing ~ declared but unused
175
186
  2. **Extracts** package names from all import styles:
176
187
  - `require('pkg')`
177
188
  - `import x from 'pkg'`
178
- - `import('pkg')`
189
+ - `import type { T } from 'pkg'`
190
+ - `import('pkg')` (dynamic)
179
191
  - `require.resolve('pkg')`
180
192
  - `export { x } from 'pkg'`
181
- 3. **Filters out** Node.js built-ins (`fs`, `path`, `http`, `node:*`, etc.)
182
- 4. **Compares** against `package.json` (all dependency types)
183
- 5. **Reports** and optionally acts on findings
193
+ - `import 'pkg'` (side-effect)
194
+ 3. **Strips comments** before scanning so commented-out imports are ignored
195
+ 4. **Filters out** Node.js built-ins (`fs`, `path`, `http`, `node:*`, etc.)
196
+ 5. **Compares** against `package.json` (all dependency types)
197
+ 6. **Reports** and optionally acts on findings
184
198
 
185
199
  ---
186
200
 
@@ -194,22 +208,25 @@ const result = await scanProject('/path/to/project');
194
208
  console.log(result.missing); // ['axios', 'uuid']
195
209
  console.log(result.present); // ['express', 'mongoose']
196
210
  console.log(result.unused); // ['jest']
197
- console.log(result.allImports); // all detected package names
211
+ console.log(result.allImports); // all detected package names (sorted)
212
+ console.log(result.declared); // all declared package names (sorted)
198
213
  console.log(result.scannedFiles); // list of files scanned
214
+ console.log(result.targetDir); // resolved project directory
199
215
  ```
200
216
 
201
217
  ### Return Value
202
218
 
203
219
  | Field | Type | Description |
204
220
  |-------|------|-------------|
205
- | `projectName` | `string` | Name from package.json |
206
- | `allImports` | `string[]` | All unique packages found in source |
207
- | `declared` | `string[]` | All packages in package.json |
221
+ | `projectName` | `string` | Name from `package.json` |
222
+ | `allImports` | `string[]` | All unique packages found in source (sorted) |
223
+ | `declared` | `string[]` | All packages in `package.json` (sorted) |
208
224
  | `missing` | `string[]` | Imported but not declared |
209
225
  | `present` | `string[]` | Imported AND declared |
210
226
  | `unused` | `string[]` | Declared but never imported |
211
227
  | `scannedFiles` | `string[]` | All files that were scanned |
212
- | `packageJson` | `object\|null` | Parsed package.json content |
228
+ | `packageJson` | `object\|null` | Parsed `package.json` content |
229
+ | `targetDir` | `string` | Resolved absolute path to the project root |
213
230
 
214
231
  ---
215
232
 
@@ -225,14 +242,12 @@ node test/test.js
225
242
 
226
243
  - [ ] `reqscan outdated` — Check for newer package versions
227
244
  - [ ] `reqscan upgrade` — Update all outdated packages
228
- - [ ] `--save-dev` flag for install command
229
- - [ ] `--dry-run` flag for previewing changes
230
245
  - [ ] Configuration file (`.reqscanrc`)
231
- - [ ] Monorepo/workspace support
232
- - [ ] JSON output mode (`--json` flag)
246
+ - [ ] Monorepo / workspace support
247
+ - [x] `--save-dev`, `--dry-run`, `--force`, `--json` flags *(shipped in v1.1.0)*
233
248
 
234
249
  ---
235
250
 
236
251
  ## 📄 License
237
252
 
238
- MIT
253
+ MIT
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "reqscan",
3
- "version": "1.0.1",
3
+ "version": "1.2.0",
4
4
  "description": "Dependency Manager for Node.js — scan, install, clean, and audit your project dependencies",
5
- "main": "src/scanner.js",
5
+ "main": "src/api.js",
6
6
  "bin": {
7
7
  "reqscan": "./src/index.js"
8
8
  },
@@ -10,6 +10,10 @@
10
10
  "start": "node src/index.js",
11
11
  "test": "node test/test.js"
12
12
  },
13
+ "files": [
14
+ "src",
15
+ "README.md"
16
+ ],
13
17
  "keywords": [
14
18
  "npm",
15
19
  "dependencies",
package/src/api.js ADDED
@@ -0,0 +1,13 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * reqscan — Public Programmatic API
5
+ *
6
+ * Usage:
7
+ * const { scanProject } = require('reqscan');
8
+ * const result = await scanProject('/path/to/project');
9
+ */
10
+
11
+ const { scanProject, readPackageJson } = require('./scanner');
12
+
13
+ module.exports = { scanProject, readPackageJson };
package/src/colors.js CHANGED
@@ -1,6 +1,9 @@
1
1
  'use strict';
2
2
 
3
- const c = {
3
+ // Respect NO_COLOR env (https://no-color.org) and non-TTY output
4
+ const ENABLED = !process.env.NO_COLOR && process.stdout.isTTY !== false;
5
+
6
+ const raw = {
4
7
  reset : '\x1b[0m',
5
8
  bold : '\x1b[1m',
6
9
  dim : '\x1b[2m',
@@ -11,20 +14,26 @@ const c = {
11
14
  white : '\x1b[37m',
12
15
  };
13
16
 
14
- const paint = (color, text) => `${color}${text}${c.reset}`;
17
+ const paint = (code, text) => ENABLED ? `${code}${text}${raw.reset}` : text;
15
18
 
16
19
  module.exports = {
17
- ...c,
18
- bold : (t) => paint(c.bold, t),
19
- dim : (t) => paint(c.dim, t),
20
- red : (t) => paint(c.red, t),
21
- green : (t) => paint(c.green, t),
22
- yellow : (t) => paint(c.yellow, t),
23
- cyan : (t) => paint(c.cyan, t),
24
- white : (t) => paint(c.white, t),
25
- boldCyan : (t) => `${c.bold}${c.cyan}${t}${c.reset}`,
26
- boldGreen : (t) => `${c.bold}${c.green}${t}${c.reset}`,
27
- boldRed : (t) => `${c.bold}${c.red}${t}${c.reset}`,
28
- boldYellow : (t) => `${c.bold}${c.yellow}${t}${c.reset}`,
20
+ // Raw codes (for constructing multi-codes manually)
21
+ ...raw,
22
+
23
+ // Single-color helpers
24
+ bold : (t) => paint(raw.bold, t),
25
+ dim : (t) => paint(raw.dim, t),
26
+ red : (t) => paint(raw.red, t),
27
+ green : (t) => paint(raw.green, t),
28
+ yellow : (t) => paint(raw.yellow, t),
29
+ cyan : (t) => paint(raw.cyan, t),
30
+ white : (t) => paint(raw.white, t),
31
+
32
+ // Combined helpers
33
+ boldCyan : (t) => ENABLED ? `${raw.bold}${raw.cyan}${t}${raw.reset}` : t,
34
+ boldGreen : (t) => ENABLED ? `${raw.bold}${raw.green}${t}${raw.reset}` : t,
35
+ boldRed : (t) => ENABLED ? `${raw.bold}${raw.red}${t}${raw.reset}` : t,
36
+ boldYellow : (t) => ENABLED ? `${raw.bold}${raw.yellow}${t}${raw.reset}` : t,
37
+
29
38
  LINE : '─'.repeat(50),
30
39
  };
package/src/commands.js CHANGED
@@ -1,9 +1,9 @@
1
1
  'use strict';
2
2
 
3
- const { execSync } = require('child_process');
4
- const readline = require('readline');
5
- const { scanProject} = require('./scanner');
6
- const col = require('./colors');
3
+ const { execSync } = require('child_process');
4
+ const readline = require('readline');
5
+ const { scanProject, readPackageJson } = require('./scanner');
6
+ const col = require('./colors');
7
7
 
8
8
  // ─── Shared helpers ───────────────────────────────────────────────────────────
9
9
 
@@ -29,10 +29,45 @@ function runNpm(args, cwd) {
29
29
  }
30
30
  }
31
31
 
32
+ function getVersion(pkgJson, pkg) {
33
+ return (
34
+ pkgJson?.dependencies?.[pkg] ||
35
+ pkgJson?.devDependencies?.[pkg] ||
36
+ pkgJson?.peerDependencies?.[pkg] ||
37
+ pkgJson?.optionalDependencies?.[pkg] || ''
38
+ );
39
+ }
40
+
41
+ function getDepType(pkgJson, pkg) {
42
+ if (pkgJson?.dependencies?.[pkg]) return 'dependencies';
43
+ if (pkgJson?.devDependencies?.[pkg]) return 'devDependencies';
44
+ if (pkgJson?.peerDependencies?.[pkg]) return 'peerDependencies';
45
+ if (pkgJson?.optionalDependencies?.[pkg]) return 'optionalDependencies';
46
+ return '';
47
+ }
48
+
32
49
  // ─── check ────────────────────────────────────────────────────────────────────
33
50
 
34
- async function cmdCheck(targetDir) {
51
+ async function cmdCheck(targetDir, { json = false } = {}) {
35
52
  const r = await scanProject(targetDir);
53
+
54
+ if (json) {
55
+ console.log(JSON.stringify({
56
+ project : r.projectName,
57
+ missing : r.missing,
58
+ present : r.present,
59
+ unused : r.unused,
60
+ summary : {
61
+ totalImports : r.allImports.length,
62
+ declared : r.declared.length,
63
+ present : r.present.length,
64
+ missing : r.missing.length,
65
+ unused : r.unused.length,
66
+ },
67
+ }, null, 2));
68
+ return r;
69
+ }
70
+
36
71
  header(r.projectName);
37
72
 
38
73
  // Summary
@@ -67,12 +102,10 @@ async function cmdCheck(targetDir) {
67
102
  console.log(col.boldGreen('✅ Already Declared Packages'));
68
103
  console.log(col.dim(col.LINE));
69
104
  r.present.forEach(p => {
70
- const ver =
71
- r.packageJson?.dependencies?.[p] ||
72
- r.packageJson?.devDependencies?.[p] ||
73
- r.packageJson?.peerDependencies?.[p] ||
74
- r.packageJson?.optionalDependencies?.[p] || '';
75
- console.log(` ${col.green('✓')} ${p} ${col.dim(ver)}`);
105
+ const ver = getVersion(r.packageJson, p);
106
+ const type = getDepType(r.packageJson, p);
107
+ const typeLabel = type === 'devDependencies' ? col.dim(' [dev]') : '';
108
+ console.log(` ${col.green('✓')} ${p} ${col.dim(ver)}${typeLabel}`);
76
109
  });
77
110
  console.log();
78
111
  }
@@ -81,9 +114,14 @@ async function cmdCheck(targetDir) {
81
114
  if (r.unused.length > 0) {
82
115
  console.log(col.boldYellow('⚠️ Declared but NOT imported in source (possibly unused)'));
83
116
  console.log(col.dim(col.LINE));
84
- r.unused.forEach(p => console.log(` ${col.yellow('~')} ${p}`));
117
+ r.unused.forEach(p => {
118
+ const type = getDepType(r.packageJson, p);
119
+ console.log(` ${col.yellow('~')} ${p} ${col.dim(`(${type})`)}`);
120
+ });
85
121
  console.log();
86
- console.log(col.dim('Tip: Run `deptrack clean` to remove unused packages.'));
122
+ console.log(col.dim('Note: Some packages may be used in config files, build scripts, or'));
123
+ console.log(col.dim(' CLI tools not directly imported in source. Review before removing.'));
124
+ console.log(col.dim('Tip: Run `reqscan clean` to interactively remove unused packages.'));
87
125
  console.log();
88
126
  }
89
127
 
@@ -96,7 +134,7 @@ async function cmdCheck(targetDir) {
96
134
 
97
135
  // ─── install ──────────────────────────────────────────────────────────────────
98
136
 
99
- async function cmdInstall(targetDir) {
137
+ async function cmdInstall(targetDir, { saveDev = false, dryRun = false } = {}) {
100
138
  console.log();
101
139
  console.log(col.boldCyan('📦 Installing missing dependencies...'));
102
140
  console.log();
@@ -112,17 +150,28 @@ async function cmdInstall(targetDir) {
112
150
  console.log(`Found ${col.boldRed(String(r.missing.length))} missing package(s):`);
113
151
  r.missing.forEach(p => console.log(` • ${p}`));
114
152
  console.log();
153
+
154
+ const flag = saveDev ? '--save-dev ' : '';
155
+ const cmd = `install ${flag}${r.missing.join(' ')}`;
156
+
157
+ if (dryRun) {
158
+ console.log(col.boldYellow('🔍 Dry run — would execute:'));
159
+ console.log(col.boldCyan(` npm ${cmd}`));
160
+ console.log();
161
+ return;
162
+ }
163
+
115
164
  console.log('Installing... ⚙️');
116
165
  console.log();
117
166
 
118
- const ok = runNpm(`install ${r.missing.join(' ')}`, targetDir);
167
+ const ok = runNpm(cmd, targetDir);
119
168
 
120
169
  if (ok) {
121
170
  console.log();
122
171
  console.log(col.boldGreen('✔ Successfully installed:'));
123
172
  r.missing.forEach(p => console.log(` • ${col.green(p)}`));
124
173
  console.log();
125
- console.log(col.dim('💡 Run `deptrack check` again to verify all packages are declared.'));
174
+ console.log(col.dim('💡 Run `reqscan check` again to verify all packages are declared.'));
126
175
  } else {
127
176
  console.log(col.boldRed('❌ Installation failed. Please check your npm setup and try again.'));
128
177
  }
@@ -131,7 +180,7 @@ async function cmdInstall(targetDir) {
131
180
 
132
181
  // ─── clean ────────────────────────────────────────────────────────────────────
133
182
 
134
- async function cmdClean(targetDir, { force = false } = {}) {
183
+ async function cmdClean(targetDir, { force = false, dryRun = false } = {}) {
135
184
  console.log();
136
185
  console.log(col.boldCyan('🧹 Scanning for unused dependencies...'));
137
186
  console.log();
@@ -146,11 +195,20 @@ async function cmdClean(targetDir, { force = false } = {}) {
146
195
 
147
196
  console.log(`Found ${col.boldYellow(String(r.unused.length))} unused package(s):`);
148
197
  r.unused.forEach(p => {
149
- const inDev = r.packageJson?.devDependencies?.[p];
150
- const kind = inDev ? 'devDependencies' : 'dependencies';
151
- console.log(` • ${p} ${col.dim(`(${kind})`)}`);
198
+ const type = getDepType(r.packageJson, p);
199
+ console.log(` ${p} ${col.dim(`(${type})`)}`);
152
200
  });
153
201
  console.log();
202
+ console.log(col.dim('Note: Verify these are truly unused before removing.'));
203
+ console.log(col.dim(' Config files and build tools may use packages without importing them.'));
204
+ console.log();
205
+
206
+ if (dryRun) {
207
+ console.log(col.boldYellow('🔍 Dry run — would execute:'));
208
+ console.log(col.boldCyan(` npm uninstall ${r.unused.join(' ')}`));
209
+ console.log();
210
+ return;
211
+ }
154
212
 
155
213
  let confirm = 'y';
156
214
  if (!force) {
@@ -158,6 +216,7 @@ async function cmdClean(targetDir, { force = false } = {}) {
158
216
  }
159
217
 
160
218
  if (confirm === '' || confirm.toLowerCase() === 'y') {
219
+ // Snapshot counts BEFORE running uninstall
161
220
  const depsBefore = Object.keys(r.packageJson?.dependencies || {}).length;
162
221
  const devDepsBefore = Object.keys(r.packageJson?.devDependencies || {}).length;
163
222
 
@@ -165,9 +224,8 @@ async function cmdClean(targetDir, { force = false } = {}) {
165
224
  const ok = runNpm(`uninstall ${r.unused.join(' ')}`, targetDir);
166
225
 
167
226
  if (ok) {
168
- // Re-read to get updated counts
169
- const { readPackageJson } = require('./scanner');
170
- const updated = readPackageJson(targetDir);
227
+ // Re-read package.json fresh after uninstall (not from require cache)
228
+ const updated = readPackageJson(targetDir);
171
229
  const depsAfter = Object.keys(updated?.dependencies || {}).length;
172
230
  const devDepsAfter = Object.keys(updated?.devDependencies || {}).length;
173
231
 
@@ -188,24 +246,58 @@ async function cmdClean(targetDir, { force = false } = {}) {
188
246
 
189
247
  // ─── fix ──────────────────────────────────────────────────────────────────────
190
248
 
191
- async function cmdFix(targetDir) {
249
+ async function cmdFix(targetDir, opts = {}) {
192
250
  console.log();
193
- console.log(col.boldCyan('🔧 Running deptrack fix (install missing + clean unused)...'));
251
+ console.log(col.boldCyan('🔧 Running reqscan fix (install missing + clean unused)...'));
194
252
  console.log(col.dim(col.LINE));
195
- await cmdInstall(targetDir);
253
+ await cmdInstall(targetDir, opts);
196
254
  console.log(col.dim(col.LINE));
197
- await cmdClean(targetDir, { force: false });
255
+ //await cmdClean(targetDir, { ...opts, force: false });
256
+
257
+ await cmdClean(targetDir, { ...opts, force: opts.dryRun || false });
258
+
259
+
260
+
198
261
  }
199
262
 
200
263
  // ─── audit ────────────────────────────────────────────────────────────────────
201
264
 
202
- async function cmdAudit(targetDir) {
265
+ async function cmdAudit(targetDir, { json = false } = {}) {
203
266
  const r = await scanProject(targetDir);
267
+
268
+ const total = r.allImports.length || 1;
269
+ const score = Math.round((r.present.length / total) * 100);
270
+
271
+ if (json) {
272
+ const deps = r.packageJson?.dependencies || {};
273
+ const devDeps = r.packageJson?.devDependencies || {};
274
+ const peer = r.packageJson?.peerDependencies || {};
275
+ const opt = r.packageJson?.optionalDependencies|| {};
276
+ console.log(JSON.stringify({
277
+ project : r.projectName,
278
+ healthScore : score,
279
+ filesScanned : r.scannedFiles.length,
280
+ summary: {
281
+ totalImports : r.allImports.length,
282
+ declared : r.declared.length,
283
+ present : r.present.length,
284
+ missing : r.missing.length,
285
+ unused : r.unused.length,
286
+ },
287
+ breakdown: {
288
+ dependencies : Object.keys(deps).length,
289
+ devDependencies : Object.keys(devDeps).length,
290
+ peerDependencies : Object.keys(peer).length,
291
+ optionalDependencies : Object.keys(opt).length,
292
+ },
293
+ missing : r.missing,
294
+ unused : r.unused,
295
+ }, null, 2));
296
+ return r;
297
+ }
298
+
204
299
  header(r.projectName);
205
300
 
206
- // Health score
207
- const total = r.allImports.length || 1;
208
- const score = Math.round((r.present.length / total) * 100);
209
301
  const scoreColor = score === 100 ? col.boldGreen : score >= 70 ? col.boldYellow : col.boldRed;
210
302
 
211
303
  console.log(col.bold('🏥 Dependency Health Report'));
@@ -219,7 +311,6 @@ async function cmdAudit(targetDir) {
219
311
  console.log(` ~ Unused declared : ${r.unused.length > 0 ? col.boldYellow(String(r.unused.length)) : col.boldGreen('0')}`);
220
312
  console.log();
221
313
 
222
- // Dependency breakdown
223
314
  const deps = r.packageJson?.dependencies || {};
224
315
  const devDeps = r.packageJson?.devDependencies || {};
225
316
  const peer = r.packageJson?.peerDependencies || {};
@@ -233,15 +324,14 @@ async function cmdAudit(targetDir) {
233
324
  console.log(` optionalDependencies : ${Object.keys(opt).length}`);
234
325
  console.log();
235
326
 
236
- // Recommendations
237
327
  const recs = [];
238
- if (r.missing.length > 0) recs.push(`Run ${col.cyan('deptrack install')} to install ${r.missing.length} missing package(s).`);
239
- if (r.unused.length > 0) recs.push(`Run ${col.cyan('deptrack clean')} to remove ${r.unused.length} unused package(s).`);
328
+ if (r.missing.length > 0) recs.push(`Run ${col.cyan('reqscan install')} to install ${r.missing.length} missing package(s).`);
329
+ if (r.unused.length > 0) recs.push(`Run ${col.cyan('reqscan clean')} to remove ${r.unused.length} potentially unused package(s).`);
240
330
  if (recs.length === 0) recs.push(col.green('Your project dependencies look great!'));
241
331
 
242
332
  console.log(col.bold('💡 Recommendations'));
243
333
  console.log(col.dim(col.LINE));
244
- recs.forEach(r => console.log(` → ${r}`));
334
+ recs.forEach(rec => console.log(` → ${rec}`));
245
335
  console.log();
246
336
  console.log(col.dim(col.LINE));
247
337
  console.log(col.dim('Audit complete.'));
@@ -252,35 +342,47 @@ async function cmdAudit(targetDir) {
252
342
 
253
343
  // ─── list ─────────────────────────────────────────────────────────────────────
254
344
 
255
- async function cmdList(targetDir) {
345
+ async function cmdList(targetDir, { json = false } = {}) {
256
346
  const r = await scanProject(targetDir);
347
+
348
+ if (json) {
349
+ const all = [...new Set([...r.allImports, ...r.declared])].sort();
350
+ const list = all.map(pkg => ({
351
+ name : pkg,
352
+ status : r.allImports.includes(pkg) && r.declared.includes(pkg) ? 'present'
353
+ : r.allImports.includes(pkg) ? 'missing' : 'unused',
354
+ version : getVersion(r.packageJson, pkg),
355
+ type : getDepType(r.packageJson, pkg),
356
+ }));
357
+ console.log(JSON.stringify({ project: r.projectName, packages: list }, null, 2));
358
+ return;
359
+ }
360
+
257
361
  header(r.projectName);
258
362
 
259
363
  console.log(col.bold('📋 All Dependencies with Status'));
260
364
  console.log(col.dim(col.LINE));
261
365
 
262
- const all = new Set([...r.allImports, ...r.declared]);
366
+ const all = new Set([...r.allImports, ...r.declared]);
263
367
  const sorted = [...all].sort();
264
368
 
265
369
  sorted.forEach(pkg => {
266
- const imported = r.allImports.includes(pkg);
267
- const declared = r.declared.includes(pkg);
268
- const ver =
269
- r.packageJson?.dependencies?.[pkg] ||
270
- r.packageJson?.devDependencies?.[pkg] ||
271
- r.packageJson?.peerDependencies?.[pkg] ||
272
- r.packageJson?.optionalDependencies?.[pkg] || '';
370
+ const imported = r.allImports.includes(pkg);
371
+ const declared = r.declared.includes(pkg);
372
+ const ver = getVersion(r.packageJson, pkg);
373
+ const type = getDepType(r.packageJson, pkg);
374
+ const typeTag = type === 'devDependencies' ? col.dim(' [dev]') : '';
273
375
 
274
376
  let status, icon;
275
377
  if (imported && declared) { icon = col.green('✓'); status = col.dim('present'); }
276
378
  else if (imported) { icon = col.red('✗'); status = col.red('missing'); }
277
379
  else { icon = col.yellow('~'); status = col.yellow('unused'); }
278
380
 
279
- console.log(` ${icon} ${pkg.padEnd(30)} ${col.dim(ver.padEnd(12))} ${status}`);
381
+ console.log(` ${icon} ${pkg.padEnd(30)} ${col.dim(ver.padEnd(12))} ${status}${typeTag}`);
280
382
  });
281
383
 
282
384
  console.log();
283
- console.log(col.dim(`Legend: ${col.green('✓')} present ${col.red('✗')} missing ${col.yellow('~')} declared but unused`));
385
+ console.log(col.dim(`Legend: ${col.green('✓')} present ${col.red('✗')} missing ${col.yellow('~')} declared but unused ${col.dim('[dev]')} devDependency`));
284
386
  console.log();
285
387
  console.log(col.dim(col.LINE));
286
388
  console.log(col.dim('List complete.'));
package/src/index.js CHANGED
@@ -7,9 +7,21 @@ const col = require('./colors');
7
7
  const { cmdCheck, cmdInstall, cmdClean, cmdFix, cmdAudit, cmdList } = require('./commands');
8
8
 
9
9
  // ─── Parse args ───────────────────────────────────────────────────────────────
10
- const args = process.argv.slice(2);
11
- const command = args[0];
12
- const dirArg = args[1];
10
+ const argv = process.argv.slice(2);
11
+
12
+
13
+ const nonFlags = argv.filter(arg => !arg.startsWith('-'));
14
+ const command = nonFlags[0] ?? null;
15
+ const dirArg = nonFlags[1] ?? null;
16
+
17
+
18
+
19
+ const flags = {
20
+ json : argv.includes('--json'),
21
+ saveDev : argv.includes('--save-dev'),
22
+ dryRun : argv.includes('--dry-run'),
23
+ force : argv.includes('--force'),
24
+ };
13
25
 
14
26
  // ─── Help text ────────────────────────────────────────────────────────────────
15
27
  function printHelp() {
@@ -17,7 +29,7 @@ function printHelp() {
17
29
  console.log(col.boldCyan('reqscan') + col.bold(' — Dependency Manager for Node.js'));
18
30
  console.log();
19
31
  console.log(col.bold('Usage:'));
20
- console.log(' reqscan <command> [project-dir]');
32
+ console.log(' reqscan <command> [project-dir] [flags]');
21
33
  console.log();
22
34
  console.log(col.bold('Commands:'));
23
35
  const cmds = [
@@ -32,20 +44,32 @@ function printHelp() {
32
44
  console.log(` ${col.cyan(cmd.padEnd(20))} ${desc}`);
33
45
  });
34
46
  console.log();
47
+ console.log(col.bold('Flags:'));
48
+ const flags_help = [
49
+ ['--json', 'Output results as JSON (check, audit, list)'],
50
+ ['--save-dev', 'Install missing packages as devDependencies'],
51
+ ['--dry-run', 'Preview what would be installed/removed'],
52
+ ['--force', 'Skip confirmation prompt (clean, fix)'],
53
+ ];
54
+ flags_help.forEach(([flag, desc]) => {
55
+ console.log(` ${col.cyan(flag.padEnd(20))} ${desc}`);
56
+ });
57
+ console.log();
35
58
  console.log(col.bold('Examples:'));
36
59
  console.log(` ${col.dim('reqscan check')} # scan current directory`);
37
60
  console.log(` ${col.dim('reqscan check ./my-app')} # scan specific folder`);
38
- console.log(` ${col.dim('reqscan install')} # install all missing`);
39
- console.log(` ${col.dim('reqscan clean')} # remove unused`);
40
- console.log(` ${col.dim('reqscan fix')} # install missing + clean unused`);
41
- console.log(` ${col.dim('reqscan audit')} # full health report`);
61
+ console.log(` ${col.dim('reqscan check --json')} # output as JSON`);
62
+ console.log(` ${col.dim('reqscan install --save-dev')} # install as devDependencies`);
63
+ console.log(` ${col.dim('reqscan install --dry-run')} # preview install`);
64
+ console.log(` ${col.dim('reqscan clean --force')} # remove unused without prompt`);
65
+ console.log(` ${col.dim('reqscan audit --json')} # full report as JSON`);
42
66
  console.log(` ${col.dim('reqscan list')} # show all with status`);
43
67
  console.log();
44
68
  }
45
69
 
46
70
  // ─── Resolve target directory ─────────────────────────────────────────────────
47
- function resolveDir(dirArg) {
48
- const target = dirArg ? path.resolve(dirArg) : process.cwd();
71
+ function resolveDir(arg) {
72
+ const target = arg ? path.resolve(arg) : process.cwd();
49
73
  if (!fs.existsSync(target)) {
50
74
  console.error(col.boldRed(`❌ Directory not found: ${target}`));
51
75
  process.exit(1);
@@ -74,16 +98,18 @@ function resolveDir(dirArg) {
74
98
 
75
99
  const targetDir = resolveDir(command === 'check' ? dirArg : (dirArg || undefined));
76
100
 
77
- console.log(col.dim(`\n🔍 Scanning project at: ${targetDir}`));
101
+ if (!flags.json) {
102
+ console.log(col.dim(`\n🔍 Scanning project at: ${targetDir}`));
103
+ }
78
104
 
79
105
  try {
80
106
  switch (command) {
81
- case 'check' : await cmdCheck(targetDir); break;
82
- case 'install': await cmdInstall(targetDir); break;
83
- case 'clean' : await cmdClean(targetDir); break;
84
- case 'fix' : await cmdFix(targetDir); break;
85
- case 'audit' : await cmdAudit(targetDir); break;
86
- case 'list' : await cmdList(targetDir); break;
107
+ case 'check' : await cmdCheck(targetDir, flags); break;
108
+ case 'install': await cmdInstall(targetDir, flags); break;
109
+ case 'clean' : await cmdClean(targetDir, flags); break;
110
+ case 'fix' : await cmdFix(targetDir, flags); break;
111
+ case 'audit' : await cmdAudit(targetDir, flags); break;
112
+ case 'list' : await cmdList(targetDir, flags); break;
87
113
  }
88
114
  } catch (err) {
89
115
  console.error(col.boldRed(`\n❌ Error: ${err.message}`));
package/src/scanner.js CHANGED
@@ -3,7 +3,7 @@
3
3
  const fs = require('fs');
4
4
  const path = require('path');
5
5
 
6
- // ─── Node.js built-in modules ───────────────────────────────────────────────
6
+ // ─── Node.js built-in modules ─────────────────────────────────────────────────
7
7
  const BUILTIN_MODULES = new Set([
8
8
  'assert','async_hooks','buffer','child_process','cluster','console',
9
9
  'constants','crypto','dgram','diagnostics_channel','dns','domain',
@@ -13,7 +13,6 @@ const BUILTIN_MODULES = new Set([
13
13
  'stream/consumers','stream/promises','stream/web','string_decoder',
14
14
  'sys','timers','timers/promises','tls','trace_events','tty','url',
15
15
  'util','util/types','v8','vm','wasi','worker_threads','zlib',
16
- // node: prefix variants
17
16
  'node:assert','node:buffer','node:child_process','node:cluster',
18
17
  'node:crypto','node:dgram','node:dns','node:domain','node:events',
19
18
  'node:fs','node:http','node:http2','node:https','node:inspector',
@@ -23,57 +22,74 @@ const BUILTIN_MODULES = new Set([
23
22
  'node:vm','node:worker_threads','node:zlib'
24
23
  ]);
25
24
 
26
- // ─── Directories to skip ─────────────────────────────────────────────────────
25
+ // ─── Directories to skip ──────────────────────────────────────────────────────
27
26
  const IGNORE_DIRS = new Set([
28
27
  'node_modules','.git','.next','.nuxt','dist','build',
29
28
  'coverage','.cache','.turbo','out','.svelte-kit','.vercel',
30
- '.output','public','static','assets'
29
+ '.output','public','static','assets','.idea','.vscode'
31
30
  ]);
32
31
 
33
- // ─── File extensions to scan ─────────────────────────────────────────────────
32
+ // ─── File extensions to scan ──────────────────────────────────────────────────
34
33
  const JS_EXTENSIONS = new Set(['.js','.jsx','.mjs','.cjs','.ts','.tsx','.mts','.cts']);
35
34
 
36
- // ─── Import/require patterns ─────────────────────────────────────────────────
35
+ // ─── Import/require patterns ──────────────────────────────────────────────────
37
36
  const IMPORT_PATTERNS = [
38
- /require\s*\(\s*['"`]([^'"`\s]+)['"`]\s*\)/g,
39
- /import\s+(?:[\w*{}\s,]+\s+from\s+)?['"`]([^'"`\s]+)['"`]/g,
40
- /import\s*\(\s*['"`]([^'"`\s]+)['"`]\s*\)/g,
41
- /require\.resolve\s*\(\s*['"`]([^'"`\s]+)['"`]\s*\)/g,
42
- /(?:export\s+(?:[\w*{}\s,]+\s+)?from\s+)['"`]([^'"`\s]+)['"`]/g,
37
+ // require('pkg')
38
+ /\brequire\s*\(\s*['"`]([^'"`\s]+)['"`]\s*\)/g,
39
+ // require.resolve('pkg')
40
+ /\brequire\.resolve\s*\(\s*['"`]([^'"`\s]+)['"`]\s*\)/g,
41
+ // import 'pkg' (side-effect)
42
+ /\bimport\s+['"`]([^'"`\s]+)['"`]/g,
43
+ // import [type] [...stuff...] from 'pkg'
44
+ /\bimport\s+(?:type\s+)?(?:[\w$*{},\s]+\s+from\s+)['"`]([^'"`\s]+)['"`]/g,
45
+ // import('pkg') dynamic
46
+ /\bimport\s*\(\s*['"`]([^'"`\s]+)['"`]\s*\)/g,
47
+ // export [type] [...stuff...] from 'pkg'
48
+ /\bexport\s+(?:type\s+)?(?:[\w$*{},\s]+\s+from\s+)['"`]([^'"`\s]+)['"`]/g,
43
49
  ];
44
50
 
45
51
  // ─── Helpers ──────────────────────────────────────────────────────────────────
46
52
 
47
53
  function extractPackageName(raw) {
48
54
  if (!raw) return null;
49
- // Skip relative, absolute, URL imports
50
- if (raw.startsWith('.') || raw.startsWith('/') || /^https?:\/\//.test(raw) || raw.startsWith('data:')) return null;
51
- // Scoped: @scope/pkg
55
+ if (
56
+ raw.startsWith('.') || raw.startsWith('/') ||
57
+ /^https?:\/\//.test(raw) ||
58
+ raw.startsWith('data:') || raw.startsWith('blob:')
59
+ ) return null;
52
60
  if (raw.startsWith('@')) {
53
61
  const parts = raw.split('/');
54
62
  return parts.length >= 2 ? `${parts[0]}/${parts[1]}` : null;
55
63
  }
56
- // Regular: pkg or pkg/subpath pkg
57
- return raw.split('/')[0];
64
+ return raw.split('/')[0] || null;
58
65
  }
59
66
 
60
67
  function extractImportsFromFile(filePath) {
61
68
  const found = new Set();
62
- try {
63
- const src = fs.readFileSync(filePath, 'utf8');
64
- for (const re of IMPORT_PATTERNS) {
65
- re.lastIndex = 0;
66
- let m;
67
- while ((m = re.exec(src)) !== null) {
68
- const pkg = extractPackageName(m[1]);
69
- if (pkg && !BUILTIN_MODULES.has(pkg)) found.add(pkg);
70
- }
69
+ let src;
70
+ try { src = fs.readFileSync(filePath, 'utf8'); }
71
+ catch (_) { return found; }
72
+
73
+ // Strip comments to avoid matching commented-out imports
74
+ const stripped = src
75
+ .replace(/\/\*[\s\S]*?\*\//g, ' ')
76
+ .replace(/\/\/[^\n]*/g, ' ');
77
+
78
+ for (const re of IMPORT_PATTERNS) {
79
+ re.lastIndex = 0;
80
+ let m;
81
+ while ((m = re.exec(stripped)) !== null) {
82
+ const pkg = extractPackageName(m[1]);
83
+ if (pkg && !BUILTIN_MODULES.has(pkg)) found.add(pkg);
71
84
  }
72
- } catch (_) { /* skip unreadable */ }
85
+ }
73
86
  return found;
74
87
  }
75
88
 
76
- function walkDir(dir, allImports = new Set(), scannedFiles = []) {
89
+ const MAX_DEPTH = 20;
90
+
91
+ function walkDir(dir, allImports = new Set(), scannedFiles = [], depth = 0) {
92
+ if (depth > MAX_DEPTH) return { allImports, scannedFiles };
77
93
  let entries;
78
94
  try { entries = fs.readdirSync(dir, { withFileTypes: true }); }
79
95
  catch (_) { return { allImports, scannedFiles }; }
@@ -82,7 +98,7 @@ function walkDir(dir, allImports = new Set(), scannedFiles = []) {
82
98
  if (IGNORE_DIRS.has(e.name)) continue;
83
99
  const full = path.join(dir, e.name);
84
100
  if (e.isDirectory()) {
85
- walkDir(full, allImports, scannedFiles);
101
+ walkDir(full, allImports, scannedFiles, depth + 1);
86
102
  } else if (e.isFile() && JS_EXTENSIONS.has(path.extname(e.name))) {
87
103
  scannedFiles.push(full);
88
104
  for (const pkg of extractImportsFromFile(full)) allImports.add(pkg);
@@ -98,8 +114,6 @@ function readPackageJson(dir) {
98
114
  catch (_) { return null; }
99
115
  }
100
116
 
101
- // ─── Public API ───────────────────────────────────────────────────────────────
102
-
103
117
  async function scanProject(targetDir) {
104
118
  const pkgJson = readPackageJson(targetDir);
105
119
  const { allImports, scannedFiles } = walkDir(targetDir);
@@ -111,24 +125,19 @@ async function scanProject(targetDir) {
111
125
  ...Object.keys(pkgJson?.optionalDependencies|| {}),
112
126
  ]);
113
127
 
114
- const missing = [];
115
- const present = [];
116
- const unused = [];
128
+ // Sort once, then derive all three arrays in a single pass each
129
+ const allImportsSorted = [...allImports].sort();
130
+ const declaredSorted = [...declared].sort();
117
131
 
118
- for (const pkg of [...allImports].sort()) {
119
- if (declared.has(pkg)) present.push(pkg);
120
- else missing.push(pkg);
121
- }
122
-
123
- for (const pkg of [...declared].sort()) {
124
- if (!allImports.has(pkg)) unused.push(pkg);
125
- }
132
+ const missing = allImportsSorted.filter(p => !declared.has(p));
133
+ const present = allImportsSorted.filter(p => declared.has(p));
134
+ const unused = declaredSorted.filter(p => !allImports.has(p));
126
135
 
127
136
  return {
128
- projectName : pkgJson?.name || path.basename(targetDir),
129
- packageJson : pkgJson,
130
- allImports : [...allImports].sort(),
131
- declared : [...declared].sort(),
137
+ projectName : pkgJson?.name || path.basename(targetDir),
138
+ packageJson : pkgJson,
139
+ allImports : allImportsSorted,
140
+ declared : declaredSorted,
132
141
  missing,
133
142
  present,
134
143
  unused,
@@ -137,4 +146,4 @@ async function scanProject(targetDir) {
137
146
  };
138
147
  }
139
148
 
140
- module.exports = { scanProject, readPackageJson };
149
+ module.exports = { scanProject, readPackageJson };
package/test/test.js DELETED
@@ -1,152 +0,0 @@
1
- 'use strict';
2
-
3
- const fs = require('fs');
4
- const path = require('path');
5
- const os = require('os');
6
- const { scanProject } = require('../src/scanner');
7
- const { cmdCheck, cmdAudit, cmdList } = require('../src/commands');
8
-
9
- // ─── Test helpers ─────────────────────────────────────────────────────────────
10
- let passed = 0;
11
- let failed = 0;
12
-
13
- function assert(condition, label) {
14
- if (condition) {
15
- console.log(` \x1b[32m✓\x1b[0m ${label}`);
16
- passed++;
17
- } else {
18
- console.error(` \x1b[31m✗\x1b[0m ${label}`);
19
- failed++;
20
- }
21
- }
22
-
23
- function section(title) {
24
- console.log(`\n\x1b[1m\x1b[36m${title}\x1b[0m`);
25
- console.log('─'.repeat(40));
26
- }
27
-
28
- // ─── Build mock project ───────────────────────────────────────────────────────
29
- function buildMockProject() {
30
- const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'depm-test-'));
31
-
32
- // package.json — express declared, jest declared (but never imported)
33
- fs.writeFileSync(path.join(tmpDir, 'package.json'), JSON.stringify({
34
- name: 'mock-project',
35
- version: '1.0.0',
36
- dependencies: {
37
- express: '^4.18.0',
38
- mongoose: '^7.0.0',
39
- },
40
- devDependencies: {
41
- jest: '^29.0.0',
42
- },
43
- }, null, 2));
44
-
45
- // index.js — requires express (declared), axios (missing), uuid (missing)
46
- fs.writeFileSync(path.join(tmpDir, 'index.js'), `
47
- const express = require('express');
48
- const axios = require('axios');
49
- const path = require('path'); // built-in
50
- const fs = require('fs'); // built-in
51
- import something from 'lodash';
52
- `);
53
-
54
- // utils/db.js — subdir, scoped pkg, subpath import
55
- fs.mkdirSync(path.join(tmpDir, 'utils'));
56
- fs.writeFileSync(path.join(tmpDir, 'utils', 'db.js'), `
57
- const mongoose = require('mongoose'); // declared
58
- const { v4 } = require('uuid'); // missing
59
- import { format } from 'date-fns'; // missing
60
- import core from '@babel/core'; // missing scoped
61
- `);
62
-
63
- // lib/helpers.ts — TypeScript file
64
- fs.mkdirSync(path.join(tmpDir, 'lib'));
65
- fs.writeFileSync(path.join(tmpDir, 'lib', 'helpers.ts'), `
66
- import { useState } from 'react'; // missing
67
- export { something } from 'lodash'; // missing (already found, de-duped)
68
- `);
69
-
70
- // node_modules — should be skipped entirely
71
- fs.mkdirSync(path.join(tmpDir, 'node_modules', 'express'), { recursive: true });
72
- fs.writeFileSync(path.join(tmpDir, 'node_modules', 'express', 'index.js'), `
73
- require('some-internal-dep'); // should NOT be counted
74
- `);
75
-
76
- return tmpDir;
77
- }
78
-
79
- // ─── Run tests ────────────────────────────────────────────────────────────────
80
- async function run() {
81
- console.log('\n\x1b[1mdepm — Test Suite\x1b[0m');
82
-
83
- const tmpDir = buildMockProject();
84
-
85
- // ── Scanner tests ────────────────────────────────────────────────────────
86
- section('Scanner: extracting imports');
87
- const r = await scanProject(tmpDir);
88
-
89
- assert(r.projectName === 'mock-project', 'project name read from package.json');
90
- assert(r.scannedFiles.length >= 3, 'scanned js/ts files (not node_modules)');
91
- assert(!r.allImports.includes('path'), 'built-in "path" excluded');
92
- assert(!r.allImports.includes('fs'), 'built-in "fs" excluded');
93
- assert(!r.allImports.includes('some-internal-dep'), 'node_modules skipped');
94
-
95
- section('Scanner: detected packages');
96
- assert(r.allImports.includes('express'), 'express detected');
97
- assert(r.allImports.includes('axios'), 'axios detected');
98
- assert(r.allImports.includes('lodash'), 'lodash detected (import default)');
99
- assert(r.allImports.includes('mongoose'), 'mongoose detected');
100
- assert(r.allImports.includes('uuid'), 'uuid detected');
101
- assert(r.allImports.includes('date-fns'), 'date-fns detected');
102
- assert(r.allImports.includes('@babel/core'),'scoped @babel/core detected');
103
- assert(r.allImports.includes('react'), 'react detected from .ts file');
104
-
105
- section('Scanner: missing vs present');
106
- assert(r.present.includes('express'), 'express in present (declared)');
107
- assert(r.present.includes('mongoose'), 'mongoose in present (declared)');
108
- assert(r.missing.includes('axios'), 'axios in missing');
109
- assert(r.missing.includes('uuid'), 'uuid in missing');
110
- assert(r.missing.includes('date-fns'), 'date-fns in missing');
111
- assert(r.missing.includes('@babel/core'), '@babel/core in missing');
112
- assert(r.missing.includes('react'), 'react in missing');
113
- assert(!r.missing.includes('express'), 'express NOT in missing');
114
- assert(!r.missing.includes('mongoose'), 'mongoose NOT in missing');
115
-
116
- section('Scanner: unused (declared but not imported)');
117
- assert(r.unused.includes('jest'), 'jest in unused (declared, never imported)');
118
- assert(!r.unused.includes('express'), 'express NOT in unused (it is imported)');
119
- assert(!r.unused.includes('mongoose'), 'mongoose NOT in unused (it is imported)');
120
-
121
- section('Commands: check (output only)');
122
- console.log('\n--- depm check output ---');
123
- await cmdCheck(tmpDir);
124
- assert(true, 'cmdCheck ran without error');
125
-
126
- section('Commands: audit (output only)');
127
- console.log('\n--- depm audit output ---');
128
- await cmdAudit(tmpDir);
129
- assert(true, 'cmdAudit ran without error');
130
-
131
- section('Commands: list (output only)');
132
- console.log('\n--- depm list output ---');
133
- await cmdList(tmpDir);
134
- assert(true, 'cmdList ran without error');
135
-
136
- // Cleanup
137
- fs.rmSync(tmpDir, { recursive: true, force: true });
138
-
139
- // ── Results ──────────────────────────────────────────────────────────────
140
- console.log('\n' + '─'.repeat(40));
141
- if (failed === 0) {
142
- console.log(`\x1b[32m\x1b[1m✅ All ${passed} tests passed.\x1b[0m\n`);
143
- } else {
144
- console.error(`\x1b[31m\x1b[1m❌ ${failed} test(s) failed. ${passed} passed.\x1b[0m\n`);
145
- process.exit(1);
146
- }
147
- }
148
-
149
- run().catch(err => {
150
- console.error('Unexpected error:', err);
151
- process.exit(1);
152
- });