@macrulez/devtoolz 0.1.0 → 0.2.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.
Files changed (135) hide show
  1. package/README.md +197 -2
  2. package/dist/cli.js +314 -0
  3. package/dist/cli.js.map +1 -1
  4. package/dist/commands/circular-imports/core.d.ts +15 -0
  5. package/dist/commands/circular-imports/core.js +95 -0
  6. package/dist/commands/circular-imports/core.js.map +1 -0
  7. package/dist/commands/circular-imports/report.d.ts +3 -0
  8. package/dist/commands/circular-imports/report.js +41 -0
  9. package/dist/commands/circular-imports/report.js.map +1 -0
  10. package/dist/commands/circular-imports/run.d.ts +19 -0
  11. package/dist/commands/circular-imports/run.js +29 -0
  12. package/dist/commands/circular-imports/run.js.map +1 -0
  13. package/dist/commands/dead-exports/core.d.ts +1 -1
  14. package/dist/commands/dead-exports/core.js +2 -2
  15. package/dist/commands/dead-exports/core.js.map +1 -1
  16. package/dist/commands/dead-exports/entry.d.ts +0 -1
  17. package/dist/commands/dead-exports/entry.js +1 -14
  18. package/dist/commands/dead-exports/entry.js.map +1 -1
  19. package/dist/commands/dead-exports/run.js +3 -2
  20. package/dist/commands/dead-exports/run.js.map +1 -1
  21. package/dist/commands/empty-catch/core.d.ts +8 -0
  22. package/dist/commands/empty-catch/core.js +61 -0
  23. package/dist/commands/empty-catch/core.js.map +1 -0
  24. package/dist/commands/empty-catch/report.d.ts +3 -0
  25. package/dist/commands/empty-catch/report.js +27 -0
  26. package/dist/commands/empty-catch/report.js.map +1 -0
  27. package/dist/commands/empty-catch/run.d.ts +17 -0
  28. package/dist/commands/empty-catch/run.js +34 -0
  29. package/dist/commands/empty-catch/run.js.map +1 -0
  30. package/dist/commands/empty-catch/vue.d.ts +2 -0
  31. package/dist/commands/empty-catch/vue.js +16 -0
  32. package/dist/commands/empty-catch/vue.js.map +1 -0
  33. package/dist/commands/full-check/report.d.ts +3 -0
  34. package/dist/commands/full-check/report.js +61 -0
  35. package/dist/commands/full-check/report.js.map +1 -0
  36. package/dist/commands/full-check/run.d.ts +32 -0
  37. package/dist/commands/full-check/run.js +251 -0
  38. package/dist/commands/full-check/run.js.map +1 -0
  39. package/dist/commands/orphan-tests/core.d.ts +9 -0
  40. package/dist/commands/orphan-tests/core.js +42 -0
  41. package/dist/commands/orphan-tests/core.js.map +1 -0
  42. package/dist/commands/orphan-tests/report.d.ts +3 -0
  43. package/dist/commands/orphan-tests/report.js +30 -0
  44. package/dist/commands/orphan-tests/report.js.map +1 -0
  45. package/dist/commands/orphan-tests/run.d.ts +25 -0
  46. package/dist/commands/orphan-tests/run.js +55 -0
  47. package/dist/commands/orphan-tests/run.js.map +1 -0
  48. package/dist/commands/readme-check/run.js +8 -3
  49. package/dist/commands/readme-check/run.js.map +1 -1
  50. package/dist/commands/scripts-check/core.d.ts +18 -0
  51. package/dist/commands/scripts-check/core.js +121 -0
  52. package/dist/commands/scripts-check/core.js.map +1 -0
  53. package/dist/commands/scripts-check/report.d.ts +3 -0
  54. package/dist/commands/scripts-check/report.js +34 -0
  55. package/dist/commands/scripts-check/report.js.map +1 -0
  56. package/dist/commands/scripts-check/run.d.ts +15 -0
  57. package/dist/commands/scripts-check/run.js +81 -0
  58. package/dist/commands/scripts-check/run.js.map +1 -0
  59. package/dist/commands/stale-ts-ignore/check.d.ts +23 -0
  60. package/dist/commands/stale-ts-ignore/check.js +104 -0
  61. package/dist/commands/stale-ts-ignore/check.js.map +1 -0
  62. package/dist/commands/stale-ts-ignore/core.d.ts +11 -0
  63. package/dist/commands/stale-ts-ignore/core.js +42 -0
  64. package/dist/commands/stale-ts-ignore/core.js.map +1 -0
  65. package/dist/commands/stale-ts-ignore/report.d.ts +3 -0
  66. package/dist/commands/stale-ts-ignore/report.js +30 -0
  67. package/dist/commands/stale-ts-ignore/report.js.map +1 -0
  68. package/dist/commands/stale-ts-ignore/run.d.ts +23 -0
  69. package/dist/commands/stale-ts-ignore/run.js +101 -0
  70. package/dist/commands/stale-ts-ignore/run.js.map +1 -0
  71. package/dist/commands/stale-ts-ignore/vue.d.ts +9 -0
  72. package/dist/commands/stale-ts-ignore/vue.js +16 -0
  73. package/dist/commands/stale-ts-ignore/vue.js.map +1 -0
  74. package/dist/commands/strip-comments/core.js +2 -56
  75. package/dist/commands/strip-comments/core.js.map +1 -1
  76. package/dist/commands/todo-report/core.d.ts +16 -0
  77. package/dist/commands/todo-report/core.js +112 -0
  78. package/dist/commands/todo-report/core.js.map +1 -0
  79. package/dist/commands/todo-report/report.d.ts +3 -0
  80. package/dist/commands/todo-report/report.js +31 -0
  81. package/dist/commands/todo-report/report.js.map +1 -0
  82. package/dist/commands/todo-report/run.d.ts +22 -0
  83. package/dist/commands/todo-report/run.js +41 -0
  84. package/dist/commands/todo-report/run.js.map +1 -0
  85. package/dist/commands/todo-report/vue.d.ts +2 -0
  86. package/dist/commands/todo-report/vue.js +71 -0
  87. package/dist/commands/todo-report/vue.js.map +1 -0
  88. package/dist/commands/unused-deps/config-files.d.ts +9 -0
  89. package/dist/commands/unused-deps/config-files.js +64 -0
  90. package/dist/commands/unused-deps/config-files.js.map +1 -0
  91. package/dist/commands/unused-deps/core.d.ts +25 -0
  92. package/dist/commands/unused-deps/core.js +121 -0
  93. package/dist/commands/unused-deps/core.js.map +1 -0
  94. package/dist/commands/unused-deps/disk-resolve.d.ts +11 -0
  95. package/dist/commands/unused-deps/disk-resolve.js +40 -0
  96. package/dist/commands/unused-deps/disk-resolve.js.map +1 -0
  97. package/dist/commands/unused-deps/package-name.d.ts +8 -0
  98. package/dist/commands/unused-deps/package-name.js +20 -0
  99. package/dist/commands/unused-deps/package-name.js.map +1 -0
  100. package/dist/commands/unused-deps/report.d.ts +3 -0
  101. package/dist/commands/unused-deps/report.js +32 -0
  102. package/dist/commands/unused-deps/report.js.map +1 -0
  103. package/dist/commands/unused-deps/run.d.ts +18 -0
  104. package/dist/commands/unused-deps/run.js +68 -0
  105. package/dist/commands/unused-deps/run.js.map +1 -0
  106. package/dist/index.d.ts +54 -5
  107. package/dist/index.js +35 -3
  108. package/dist/index.js.map +1 -1
  109. package/dist/utils/escape-regexp.d.ts +1 -0
  110. package/dist/utils/escape-regexp.js +4 -0
  111. package/dist/utils/escape-regexp.js.map +1 -0
  112. package/dist/utils/find-comments.d.ts +10 -0
  113. package/dist/utils/find-comments.js +62 -0
  114. package/dist/utils/find-comments.js.map +1 -0
  115. package/dist/utils/find-package-dir.d.ts +2 -0
  116. package/dist/utils/find-package-dir.js +15 -0
  117. package/dist/utils/find-package-dir.js.map +1 -0
  118. package/dist/utils/line-of.d.ts +2 -0
  119. package/dist/utils/line-of.js +12 -0
  120. package/dist/utils/line-of.js.map +1 -0
  121. package/dist/utils/load-tsconfig.d.ts +20 -0
  122. package/dist/utils/load-tsconfig.js +33 -0
  123. package/dist/utils/load-tsconfig.js.map +1 -0
  124. package/dist/utils/parse-module.d.ts +32 -0
  125. package/dist/utils/parse-module.js +178 -0
  126. package/dist/utils/parse-module.js.map +1 -0
  127. package/dist/utils/resolve-specifier.d.ts +15 -0
  128. package/dist/utils/resolve-specifier.js +63 -0
  129. package/dist/utils/resolve-specifier.js.map +1 -0
  130. package/dist/utils/walk.js +20 -1
  131. package/dist/utils/walk.js.map +1 -1
  132. package/dist/utils/workspace.d.ts +14 -0
  133. package/dist/utils/workspace.js +101 -0
  134. package/dist/utils/workspace.js.map +1 -0
  135. package/package.json +1 -1
package/README.md CHANGED
@@ -37,6 +37,23 @@ looking for is never touched.
37
37
  symbol is really declared, and auto-detects a pnpm/npm/yarn workspace
38
38
  so a sibling package importing your export doesn't read as dead either.
39
39
  `--strict` checks the entry points too, for when you actually want that.
40
+ - **`unused-deps`** — finds `package.json` dependencies nothing imports,
41
+ and the reverse: a package genuinely imported but never declared (a
42
+ "phantom" dependency, working only because something else hoisted it
43
+ into `node_modules`). Knows about the usual ways a dependency is used
44
+ without ever being imported — invoked from `scripts`/`lint-staged` by
45
+ its bin name, referenced as a string or bare key in a config file
46
+ (`vitest.config.ts`'s `environment: 'happy-dom'`,
47
+ `postcss.config.js`'s `plugins: { autoprefixer: {} }`) — plus a small,
48
+ explicit list of packages no static analysis could ever catch
49
+ (`@types/*`, `typescript`, `@vitest/coverage-*`, `postcss`/`sass`).
50
+ `--strict` checks that last list too, for a manual audit.
51
+ - **`circular-imports`** — finds import cycles (`A → B → … → A`) in your
52
+ own code — the kind that can silently produce `undefined` at module
53
+ load time in ESM. Reports every cycle found, not just the first, as
54
+ its full chain. A cycle made entirely of `import type` is harmless at
55
+ runtime (types are erased) and hidden by default — `--include-types`
56
+ shows those too, clearly marked.
40
57
  - **`case-check`** — finds imports whose case doesn't match the real
41
58
  file on disk. Windows and macOS are case-insensitive by default, so
42
59
  `import './foo'` against a real `Foo.ts` works fine right up until it
@@ -64,12 +81,71 @@ looking for is never touched.
64
81
  dropped rather than reported as noise (see `--help`/features.md for
65
82
  exactly which). `--lang` opts into `tsx`/`js`/`jsx` (default: `ts`
66
83
  only — `tsx` needs the package to actually configure JSX itself).
84
+ - **`empty-catch`** — finds `catch` blocks that do nothing with the
85
+ error, or do so little it's effectively swallowed: fully empty, or a
86
+ body that's nothing but `console.*` calls with no `throw`, no write to
87
+ an outer-scope variable, no meaningful `return`. Syntactically valid
88
+ code that ordinary lint rules can't catch — needs a semantic check, not
89
+ a syntactic one. A comment inside the block (found via a real token
90
+ scan, not text matching) exempts the finding, same principle as
91
+ ESLint's `no-empty` for documented empty blocks. Handles `.vue` files.
92
+ - **`todo-report`** — summarizes `TODO`/`FIXME`/`HACK` comments across
93
+ the project — file:line plus the note's actual text, including one
94
+ wrapped across several `//` lines or a multi-line `/* */` block, joined
95
+ back into one readable line. `--tags` configures the list (default
96
+ `TODO,FIXME,HACK`); `--max <n>` turns the default "fail on any finding"
97
+ into a ratchet for a project that's knowingly living with existing
98
+ debt. Handles `.vue` files, including `<!-- -->` template comments.
99
+ - **`scripts-check`** — cross-checks `package.json`'s `scripts` against
100
+ README/docs and `.github/workflows/*.yml`: a mentioned-but-undeclared
101
+ script (`npm run X`, `npm test`/`start`, `yarn`/`pnpm run X`) is a
102
+ real broken link — renamed a script, forgot to update the docs or CI —
103
+ and, as a weaker signal, a declared script nothing documents. npm's
104
+ own reserved lifecycle names (`prepublishOnly`,
105
+ `preinstall`, `postinstall`, `prepare`, any `pre*`/`post*` for another
106
+ declared script) are exempt from the second direction — npm calls
107
+ those itself. A CI step under its own `working-directory:` (a `demo/`
108
+ subproject, say) is understood to belong to a different `package.json`
109
+ entirely, not flagged against this one's.
110
+ - **`orphan-tests`** — finds test files whose source disappeared —
111
+ renamed or deleted, the test still green, testing nothing real
112
+ anymore. Reliable only under a simple naming convention: co-located
113
+ (`Foo.test.ts` next to `Foo.ts`, including a sibling `__tests__`/`tests`
114
+ directory one level down), or an explicit `--source-dir`/`--test-dir`
115
+ mirror. A scenario/integration test with no single corresponding source
116
+ file is a known false positive under that strict rule — `--ignore`
117
+ is the documented way to exclude it, not something this command tries
118
+ to guess.
119
+ - **`stale-ts-ignore`** — finds a `// @ts-ignore` that no longer
120
+ suppresses anything: the code below it was fixed, the comment wasn't
121
+ removed. There's no compiler API for "was this specific directive
122
+ needed" — this runs a real project-wide typecheck twice (once as-is,
123
+ once with every directive masked out) and compares the delta at each
124
+ directive's own line, so it's the most expensive command here, and
125
+ says so up front when there's real work to do. Skips the expensive
126
+ part entirely when there's not a single `@ts-ignore` in the project.
127
+ `.vue` files get an isolated per-file check (same technique as
128
+ `readme-check`'s own virtual-file typechecking) — a plain
129
+ `ts.Program` can't include `.vue` in a whole-project run at all.
130
+ - **`full-check`** — runs all twelve other commands in one sweep, each in
131
+ its own safe read-only mode — the three that can write to disk
132
+ (`strip-comments`/`console-strip`/`case-check`) are always called as a
133
+ preview, `-y`/`--fix` are never passed. One summary table: which
134
+ commands are clean, which found something, which errored. Since
135
+ `stale-ts-ignore` is the one command that can make a bare `full-check`
136
+ noticeably slower, running it with no flags in a real terminal asks
137
+ first — `--skip stale-ts-ignore` skips both the question and the
138
+ command, `--no-prompt` runs everything without asking (the default
139
+ answer either way).
67
140
 
68
141
  `strip-comments`, `console-strip`, and `case-check` share the same safety
69
142
  model: `--dry-run` (or just running with neither `--dry-run` nor `-y`)
70
143
  only previews, `-y`/`--yes` is required to actually write anything,
71
- `--diff` shows a real unified diff per file. `dead-exports` is read-only,
72
- it never writes anything — there's nothing to preview. Every command
144
+ `--diff` shows a real unified diff per file. `dead-exports`,
145
+ `unused-deps`, `circular-imports`, `exports-doctor`, `readme-check`,
146
+ `empty-catch`, `todo-report`, `scripts-check`, `orphan-tests`,
147
+ `stale-ts-ignore`, and `full-check` are all read-only — none of them ever
148
+ write anything, there's nothing to preview or apply. Every command
73
149
  supports `--json` for machine-readable output.
74
150
 
75
151
  ## Tone
@@ -109,6 +185,11 @@ devtoolz console-strip src -y
109
185
 
110
186
  devtoolz dead-exports src # report-only, nothing to apply
111
187
 
188
+ devtoolz unused-deps # package.json deps vs what's actually imported
189
+ devtoolz unused-deps path/to/package --strict # also check @types/*, typescript, etc.
190
+
191
+ devtoolz circular-imports src # find import cycles (A -> B -> ... -> A)
192
+
112
193
  devtoolz case-check src --fix --diff --dry-run # preview a case fix
113
194
  devtoolz case-check src --fix -y # apply it
114
195
 
@@ -117,6 +198,20 @@ devtoolz exports-doctor path/to/package # or a specific package director
117
198
 
118
199
  devtoolz readme-check # typecheck ts blocks in ./README.md
119
200
  devtoolz readme-check --file docs/guide.md # check another doc instead/as well
201
+
202
+ devtoolz empty-catch src # find catch blocks that swallow the error
203
+
204
+ devtoolz todo-report src # summarize TODO/FIXME/HACK comments
205
+ devtoolz todo-report src --max 20 # ratchet: fail only once findings exceed 20
206
+
207
+ devtoolz scripts-check # package.json scripts vs README/CI mentions
208
+
209
+ devtoolz orphan-tests src # test files whose source disappeared
210
+
211
+ devtoolz stale-ts-ignore # find @ts-ignore comments suppressing nothing
212
+
213
+ devtoolz full-check # run every command, one summary table
214
+ devtoolz full-check --skip stale-ts-ignore # same, minus the expensive one
120
215
  ```
121
216
 
122
217
  Every command has built-in `--help` — `devtoolz --help` lists every
@@ -175,6 +270,33 @@ Scanned 3 files.
175
270
  src/types.ts:1 Options (type)
176
271
  ```
177
272
 
273
+ `devtoolz unused-deps` against a package with one truly unused dependency
274
+ and one phantom (imported but never declared):
275
+
276
+ ```
277
+ 🧰 devtoolz, reporting for duty
278
+
279
+ Scanned 1 file.
280
+
281
+ 2 problems found:
282
+ phantom dotenv resolves from C:\tmp\demo-pkg\node_modules\dotenv — not declared in package.json
283
+ unused left-pad (dependencies, not imported anywhere)
284
+ ```
285
+
286
+ `devtoolz circular-imports src` against two files that import each other:
287
+
288
+ ```
289
+ 🧰 devtoolz — chores, automated
290
+
291
+ Scanned 2 files.
292
+
293
+ 1 circular import found:
294
+
295
+ src/order.ts
296
+ → src/user.ts
297
+ → src/order.ts
298
+ ```
299
+
178
300
  `devtoolz case-check src --fix --diff --dry-run` after a file got renamed
179
301
  `Helper.ts` → `helper.ts` on someone's Mac, with the import never updated:
180
302
 
@@ -221,6 +343,79 @@ Typechecked 1 code block across 1 file.
221
343
  README.md:6:7 ts Type 'string' is not assignable to type 'number'.
222
344
  ```
223
345
 
346
+ `devtoolz empty-catch src` against a `catch` block that only logs the
347
+ error and never rethrows or handles it:
348
+
349
+ ```
350
+ Scanned 1 file.
351
+
352
+ 1 problem found:
353
+ src/example.ts:4:5 console-only only logged, never handled — silently swallowed either way
354
+ ```
355
+
356
+ `devtoolz todo-report src` against a `TODO` wrapped across several `//`
357
+ lines — joined back into one readable note, not cut off mid-sentence:
358
+
359
+ ```
360
+ Scanned 1 file.
361
+
362
+ 1 comment found (TODO: 1):
363
+ src/example.ts:9:4 TODO TODO: replace the hardcoded timeout below with a value read from config once loadConfig() above actually works end to end
364
+ ```
365
+
366
+ `devtoolz scripts-check` against a package whose README has a typo'd
367
+ script name, and two real scripts nothing documents:
368
+
369
+ ```
370
+ Checked 1 source against package.json's scripts.
371
+
372
+ 3 problems found:
373
+ README.md:4 buld mentioned here, but not in package.json scripts
374
+ package.json:4 build in package.json scripts, but not mentioned anywhere checked
375
+ package.json:6 deploy in package.json scripts, but not mentioned anywhere checked
376
+ ```
377
+
378
+ `devtoolz orphan-tests src` after `parseQuery.ts` was renamed/removed but
379
+ its test survived:
380
+
381
+ ```
382
+ Scanned 3 files.
383
+
384
+ 1 orphan test found:
385
+ src/parseQuery.test.ts orphan no matching source found (tried .ts, .tsx, .js, .jsx, .mjs, .cjs, .vue)
386
+ ```
387
+
388
+ `devtoolz stale-ts-ignore` against a `@ts-ignore` above a line that
389
+ typechecks cleanly on its own — the code was fixed, the comment wasn't:
390
+
391
+ ```
392
+ Checked 1 @ts-ignore directive.
393
+
394
+ 1 stale @ts-ignore found:
395
+ src/example.ts:2 @ts-ignore doesn't suppress anything — the line below it typechecks cleanly without it
396
+ ```
397
+
398
+ `devtoolz full-check` against a small package with a few real, unrelated
399
+ problems scattered across it:
400
+
401
+ ```
402
+ ✖ strip-comments 1 found
403
+ ✔ console-strip clean
404
+ ✖ dead-exports 1 found
405
+ ✔ case-check clean
406
+ ✔ exports-doctor clean
407
+ ✔ readme-check clean
408
+ ✔ unused-deps clean
409
+ ✔ circular-imports clean
410
+ ✖ empty-catch 1 found
411
+ ✔ todo-report clean
412
+ ✖ scripts-check 3 found
413
+ ✔ orphan-tests clean
414
+ ✔ stale-ts-ignore clean
415
+
416
+ 9 clean, 4 found something — see `devtoolz <command> --help` for full detail on any of them.
417
+ ```
418
+
224
419
  ## Development
225
420
 
226
421
  ```bash
package/dist/cli.js CHANGED
@@ -1,18 +1,36 @@
1
1
  #!/usr/bin/env node
2
2
  import { Command } from 'commander';
3
3
  import { resolve } from 'node:path';
4
+ import { createInterface } from 'node:readline/promises';
4
5
  import { runStripComments } from './commands/strip-comments/run.js';
5
6
  import { renderStripCommentsReport } from './commands/strip-comments/report.js';
6
7
  import { runConsoleStrip } from './commands/console-strip/run.js';
7
8
  import { renderConsoleStripReport } from './commands/console-strip/report.js';
8
9
  import { runDeadExports } from './commands/dead-exports/run.js';
9
10
  import { renderDeadExportsReport } from './commands/dead-exports/report.js';
11
+ import { runCircularImports } from './commands/circular-imports/run.js';
12
+ import { renderCircularImportsReport } from './commands/circular-imports/report.js';
13
+ import { runUnusedDeps } from './commands/unused-deps/run.js';
14
+ import { renderUnusedDepsReport } from './commands/unused-deps/report.js';
10
15
  import { runCaseCheck } from './commands/case-check/run.js';
11
16
  import { renderCaseCheckReport } from './commands/case-check/report.js';
12
17
  import { runExportsDoctor } from './commands/exports-doctor/run.js';
13
18
  import { renderExportsDoctorReport } from './commands/exports-doctor/report.js';
14
19
  import { runReadmeCheck } from './commands/readme-check/run.js';
15
20
  import { renderReadmeCheckReport } from './commands/readme-check/report.js';
21
+ import { runEmptyCatch } from './commands/empty-catch/run.js';
22
+ import { renderEmptyCatchReport } from './commands/empty-catch/report.js';
23
+ import { runTodoReport } from './commands/todo-report/run.js';
24
+ import { renderTodoReportReport } from './commands/todo-report/report.js';
25
+ import { DEFAULT_TAGS } from './commands/todo-report/core.js';
26
+ import { runScriptsCheck } from './commands/scripts-check/run.js';
27
+ import { renderScriptsCheckReport } from './commands/scripts-check/report.js';
28
+ import { runOrphanTests } from './commands/orphan-tests/run.js';
29
+ import { renderOrphanTestsReport } from './commands/orphan-tests/report.js';
30
+ import { runStaleTsIgnore } from './commands/stale-ts-ignore/run.js';
31
+ import { renderStaleTsIgnoreReport } from './commands/stale-ts-ignore/report.js';
32
+ import { runFullCheck, FULL_CHECK_COMMAND_NAMES, SLOW_FULL_CHECK_COMMAND, } from './commands/full-check/run.js';
33
+ import { renderFullCheckReport } from './commands/full-check/report.js';
16
34
  function wrapText(text, width) {
17
35
  const words = text.split(' ');
18
36
  const lines = [];
@@ -241,6 +259,71 @@ program
241
259
  }
242
260
  process.exitCode = report.exitCode;
243
261
  });
262
+ program
263
+ .command('circular-imports')
264
+ .description('Find import cycles in the local module graph (A -> B -> ... -> A)')
265
+ .argument('[paths...]', 'files/directories to process', [])
266
+ .option('--cwd <path>', 'root paths are resolved against', process.cwd())
267
+ .option('--ext <list>', 'comma-separated extensions to include', '.ts,.tsx,.js,.jsx,.cjs,.mjs')
268
+ .option('--ignore <glob>', 'extra ignore pattern (repeatable), on top of the built-in defaults', (val, prev) => [...prev, val], [])
269
+ .option('--no-respect-gitignore', "don't also honor the project's .gitignore")
270
+ .option('--include-types', 'also report cycles made entirely of `import type` edges (harmless at runtime by default, so hidden)', false)
271
+ .option('--json', 'machine-readable output', false)
272
+ .option('--quiet', 'suppress output when there is nothing to report', false)
273
+ .option('--plain', 'disable color/banner/celebration copy, even in a real terminal', false)
274
+ .action((paths, options) => {
275
+ const report = runCircularImports({
276
+ paths,
277
+ cwd: resolve(options.cwd),
278
+ extensions: options.ext.split(',').map((e) => e.trim()),
279
+ ignoreGlobs: options.ignore,
280
+ respectGitignore: options.respectGitignore,
281
+ includeTypes: options.includeTypes,
282
+ });
283
+ if (options.json) {
284
+ console.log(JSON.stringify(report, null, 2));
285
+ }
286
+ else {
287
+ const text = renderCircularImportsReport(report, {
288
+ quiet: options.quiet,
289
+ plain: options.plain,
290
+ });
291
+ if (text)
292
+ console.log(text);
293
+ }
294
+ process.exitCode = report.exitCode;
295
+ });
296
+ program
297
+ .command('unused-deps')
298
+ .description('Find package.json dependencies nothing imports, and imports of packages package.json never declared')
299
+ .argument('[dir]', 'package directory to check — also the scan root', '.')
300
+ .option('--ext <list>', 'comma-separated extensions to include', '.ts,.tsx,.js,.jsx,.cjs,.mjs')
301
+ .option('--ignore <glob>', 'extra ignore pattern (repeatable), on top of the built-in defaults', (val, prev) => [...prev, val], [])
302
+ .option('--no-respect-gitignore', "don't also honor the project's .gitignore")
303
+ .option('--strict', 'also check packages that are structurally undetectable as "used" (currently: @types/*)', false)
304
+ .option('--ignore-package <name>', 'exempt a specific dependency from the unused/phantom check (repeatable)', (val, prev) => [...prev, val], [])
305
+ .option('--json', 'machine-readable output', false)
306
+ .option('--quiet', 'suppress output when there is nothing to report', false)
307
+ .option('--plain', 'disable color/banner/celebration copy, even in a real terminal', false)
308
+ .action((dir, options) => {
309
+ const report = runUnusedDeps({
310
+ dir: resolve(dir),
311
+ extensions: options.ext.split(',').map((e) => e.trim()),
312
+ ignoreGlobs: options.ignore,
313
+ respectGitignore: options.respectGitignore,
314
+ strict: options.strict,
315
+ ignorePackages: options.ignorePackage,
316
+ });
317
+ if (options.json) {
318
+ console.log(JSON.stringify(report, null, 2));
319
+ }
320
+ else {
321
+ const text = renderUnusedDepsReport(report, { quiet: options.quiet, plain: options.plain });
322
+ if (text)
323
+ console.log(text);
324
+ }
325
+ process.exitCode = report.exitCode;
326
+ });
244
327
  program
245
328
  .command('case-check')
246
329
  .description("Find imports whose case doesn't match the real file on disk — works on Windows/Mac, breaks on Linux CI")
@@ -326,5 +409,236 @@ program
326
409
  }
327
410
  process.exitCode = report.exitCode;
328
411
  });
412
+ program
413
+ .command('empty-catch')
414
+ .description('Find catch blocks that do nothing with the error, or only log it — both silently swallow it')
415
+ .argument('[paths...]', 'files/directories to process', [])
416
+ .option('--cwd <path>', 'root paths are resolved against', process.cwd())
417
+ .option('--ext <list>', 'comma-separated extensions to include', '.ts,.tsx,.js,.jsx,.cjs,.mjs,.vue')
418
+ .option('--ignore <glob>', 'extra ignore pattern (repeatable), on top of the built-in defaults', (val, prev) => [...prev, val], [])
419
+ .option('--no-respect-gitignore', "don't also honor the project's .gitignore")
420
+ .option('--json', 'machine-readable output', false)
421
+ .option('--quiet', 'suppress output when there is nothing to report', false)
422
+ .option('--plain', 'disable color/banner/celebration copy, even in a real terminal', false)
423
+ .action((paths, options) => {
424
+ const report = runEmptyCatch({
425
+ paths,
426
+ cwd: resolve(options.cwd),
427
+ extensions: options.ext.split(',').map((e) => e.trim()),
428
+ ignoreGlobs: options.ignore,
429
+ respectGitignore: options.respectGitignore,
430
+ });
431
+ if (options.json) {
432
+ console.log(JSON.stringify(report, null, 2));
433
+ }
434
+ else {
435
+ const text = renderEmptyCatchReport(report, { quiet: options.quiet, plain: options.plain });
436
+ if (text)
437
+ console.log(text);
438
+ }
439
+ process.exitCode = report.exitCode;
440
+ });
441
+ program
442
+ .command('todo-report')
443
+ .description('Summarize TODO/FIXME/HACK comments across the project — file:line + the text itself')
444
+ .argument('[paths...]', 'files/directories to process', [])
445
+ .option('--cwd <path>', 'root paths are resolved against', process.cwd())
446
+ .option('--ext <list>', 'comma-separated extensions to include', '.ts,.tsx,.js,.jsx,.cjs,.mjs,.vue')
447
+ .option('--ignore <glob>', 'extra ignore pattern (repeatable), on top of the built-in defaults', (val, prev) => [...prev, val], [])
448
+ .option('--no-respect-gitignore', "don't also honor the project's .gitignore")
449
+ .option('--tags <list>', 'comma-separated tags to look for', DEFAULT_TAGS.join(','))
450
+ .option('--max <n>', "don't fail unless findings exceed this count (a ratchet, not a hard zero)", '0')
451
+ .option('--json', 'machine-readable output', false)
452
+ .option('--quiet', 'suppress output when there is nothing to report', false)
453
+ .option('--plain', 'disable color/banner/celebration copy, even in a real terminal', false)
454
+ .action((paths, options) => {
455
+ const report = runTodoReport({
456
+ paths,
457
+ cwd: resolve(options.cwd),
458
+ extensions: options.ext.split(',').map((e) => e.trim()),
459
+ ignoreGlobs: options.ignore,
460
+ respectGitignore: options.respectGitignore,
461
+ tags: options.tags.split(',').map((t) => t.trim()),
462
+ max: Number(options.max),
463
+ });
464
+ if (options.json) {
465
+ console.log(JSON.stringify(report, null, 2));
466
+ }
467
+ else {
468
+ const text = renderTodoReportReport(report, { quiet: options.quiet, plain: options.plain });
469
+ if (text)
470
+ console.log(text);
471
+ }
472
+ process.exitCode = report.exitCode;
473
+ });
474
+ program
475
+ .command('scripts-check')
476
+ .description("Cross-checks package.json's scripts against README/docs and .github/workflows — a mention of a script that doesn't exist, and (weaker signal) a script nothing documents")
477
+ .argument('[dir]', 'package directory to check', '.')
478
+ .option('--file <path>', 'markdown/doc file to check, relative to [dir] (repeatable) — default: README.md', (val, prev) => [...prev, val], [])
479
+ .option('--json', 'machine-readable output', false)
480
+ .option('--quiet', 'suppress output when there is nothing to report', false)
481
+ .option('--plain', 'disable color/banner/celebration copy, even in a real terminal', false)
482
+ .action((dir, options) => {
483
+ const report = runScriptsCheck({ dir: resolve(dir), files: options.file });
484
+ if (options.json) {
485
+ console.log(JSON.stringify(report, null, 2));
486
+ }
487
+ else {
488
+ const text = renderScriptsCheckReport(report, {
489
+ quiet: options.quiet,
490
+ plain: options.plain,
491
+ });
492
+ if (text)
493
+ console.log(text);
494
+ }
495
+ process.exitCode = report.exitCode;
496
+ });
497
+ program
498
+ .command('orphan-tests')
499
+ .description('Finds test files whose source disappeared — renamed or deleted, test still green, testing nothing real anymore. Reliable only under a co-located (X.test.ts next to X.ts) or --source-dir/--test-dir mirrored naming convention')
500
+ .argument('[paths...]', 'files/directories to process (co-located mode only)', [])
501
+ .option('--cwd <path>', 'root paths are resolved against', process.cwd())
502
+ .option('--ext <list>', 'comma-separated extensions to consider as possible test files', '.ts,.tsx,.js,.jsx,.mjs,.cjs')
503
+ .option('--ignore <glob>', 'extra ignore pattern (repeatable), on top of the built-in defaults', (val, prev) => [...prev, val], [])
504
+ .option('--no-respect-gitignore', "don't also honor the project's .gitignore")
505
+ .option('--test-suffix <list>', 'comma-separated suffixes that mark a test file', '.test,.spec')
506
+ .option('--source-ext <list>', 'comma-separated extensions tried for a matching source file', '.ts,.tsx,.js,.jsx,.mjs,.cjs,.vue')
507
+ .option('--source-dir <path>', 'mirrored layout: source root, relative to --cwd (must be given together with --test-dir)')
508
+ .option('--test-dir <path>', 'mirrored layout: test root, relative to --cwd (must be given together with --source-dir)')
509
+ .option('--json', 'machine-readable output', false)
510
+ .option('--quiet', 'suppress output when there is nothing to report', false)
511
+ .option('--plain', 'disable color/banner/celebration copy, even in a real terminal', false)
512
+ .action((paths, options) => {
513
+ const report = runOrphanTests({
514
+ paths,
515
+ cwd: resolve(options.cwd),
516
+ extensions: options.ext.split(',').map((e) => e.trim()),
517
+ ignoreGlobs: options.ignore,
518
+ respectGitignore: options.respectGitignore,
519
+ testSuffixes: options.testSuffix.split(',').map((s) => s.trim()),
520
+ sourceExtensions: options.sourceExt.split(',').map((e) => e.trim()),
521
+ ...(options.sourceDir ? { sourceDir: options.sourceDir } : {}),
522
+ ...(options.testDir ? { testDir: options.testDir } : {}),
523
+ });
524
+ if (options.json) {
525
+ console.log(JSON.stringify(report, null, 2));
526
+ }
527
+ else {
528
+ const text = renderOrphanTestsReport(report, { quiet: options.quiet, plain: options.plain });
529
+ if (text)
530
+ console.log(text);
531
+ }
532
+ process.exitCode = report.exitCode;
533
+ });
534
+ program
535
+ .command('stale-ts-ignore')
536
+ .description("Finds a `// @ts-ignore` that no longer suppresses anything — the code below it was fixed, the comment wasn't removed. Runs a full project typecheck twice, so it's the most expensive command here")
537
+ .argument('[dir]', 'package directory to check', '.')
538
+ .option('--tsconfig <path>', 'tsconfig.json to read compiler options from (auto-detected by default)')
539
+ .option('--ignore <glob>', 'extra ignore pattern (repeatable), on top of the built-in defaults — applies to .vue file discovery only', (val, prev) => [...prev, val], [])
540
+ .option('--no-respect-gitignore', "don't also honor the project's .gitignore for .vue discovery")
541
+ .option('--json', 'machine-readable output', false)
542
+ .option('--quiet', 'suppress output when there is nothing to report', false)
543
+ .option('--plain', 'disable color/banner/celebration copy, even in a real terminal', false)
544
+ .action((dir, options) => {
545
+ const report = runStaleTsIgnore({
546
+ dir: resolve(dir),
547
+ ...(options.tsconfig ? { tsconfig: options.tsconfig } : {}),
548
+ ignoreGlobs: options.ignore,
549
+ respectGitignore: options.respectGitignore,
550
+ ...(options.quiet ? {} : { onProgress: (message) => console.error(message) }),
551
+ });
552
+ if (options.json) {
553
+ console.log(JSON.stringify(report, null, 2));
554
+ }
555
+ else {
556
+ const text = renderStaleTsIgnoreReport(report, {
557
+ quiet: options.quiet,
558
+ plain: options.plain,
559
+ });
560
+ if (text)
561
+ console.log(text);
562
+ }
563
+ process.exitCode = report.exitCode;
564
+ });
565
+ // Only `full-check` ever prompts — every other command here is a single,
566
+ // fast, non-interactive check. `stale-ts-ignore` is the one command a
567
+ // bare `full-check` can turn into a genuinely slow run, so this asks
568
+ // about that ONE command specifically, not a general "run everything?"
569
+ // confirmation. Defaults to running it (Enter alone picks option 1) —
570
+ // matches what happens when there's no TTY to ask at all (see
571
+ // `shouldPrompt` below), so a script and an interactive "just press
572
+ // enter" run behave the same way.
573
+ // Deliberately a SINGLE `question()` call, no retry-on-invalid-input loop
574
+ // — found via real testing that a second `question()` on the same (or
575
+ // even a fresh) `readline/promises` interface can silently lose the
576
+ // answer and hang forever once more than one line was already available
577
+ // on stdin when the first question resolved (a real, reproducible
578
+ // readline quirk with piped/non-interactive input, not something specific
579
+ // to this code). One question, with an explicit default on anything else
580
+ // typed, sidesteps the whole bug class instead of risking it.
581
+ async function promptStaleTsIgnoreChoice() {
582
+ process.stderr.write('stale-ts-ignore runs a full project typecheck twice — this can take noticeably longer than the other commands on a large project.\n\n' +
583
+ ' 1) Run it anyway (full sweep)\n' +
584
+ ' 2) Skip it for this run\n\n');
585
+ const rl = createInterface({ input: process.stdin, output: process.stderr });
586
+ let answer;
587
+ try {
588
+ answer = (await rl.question('Choice [1/2, Enter = 1]: ')).trim();
589
+ }
590
+ finally {
591
+ rl.close();
592
+ }
593
+ if (answer === '2')
594
+ return false;
595
+ if (answer !== '' && answer !== '1') {
596
+ process.stderr.write(`(didn't recognize "${answer}" — running it, same as Enter)\n`);
597
+ }
598
+ return true;
599
+ }
600
+ program
601
+ .command('full-check')
602
+ .description('Runs every other command in its own safe/read-only mode — a full diagnostic sweep, never applies a fix')
603
+ .argument('[dir]', 'directory every command scans/reads from', '.')
604
+ .option('--skip <command>', `skip a command by name (repeatable) — valid names: ${FULL_CHECK_COMMAND_NAMES.join(', ')}`, (val, prev) => [...prev, val], [])
605
+ .option('--no-prompt', "don't ask about stale-ts-ignore even in an interactive terminal — runs it unless --skip already excludes it")
606
+ .option('--json', 'machine-readable output', false)
607
+ .option('--quiet', 'suppress output when there is nothing to report', false)
608
+ .option('--plain', 'disable color/banner/celebration copy, even in a real terminal', false)
609
+ .action(async (dir, options) => {
610
+ const skip = new Set(options.skip);
611
+ const shouldPrompt = options.prompt &&
612
+ !options.json &&
613
+ !skip.has(SLOW_FULL_CHECK_COMMAND) &&
614
+ Boolean(process.stdin.isTTY) &&
615
+ Boolean(process.stdout.isTTY);
616
+ if (shouldPrompt) {
617
+ const runIt = await promptStaleTsIgnoreChoice();
618
+ if (!runIt)
619
+ skip.add(SLOW_FULL_CHECK_COMMAND);
620
+ process.stderr.write('\n');
621
+ }
622
+ const showProgress = !options.json && !options.quiet;
623
+ const report = runFullCheck({
624
+ dir: resolve(dir),
625
+ skip: [...skip],
626
+ ...(showProgress
627
+ ? {
628
+ onCommandStart: (command) => process.stderr.write(`Running ${command}...\n`),
629
+ onProgress: (message) => process.stderr.write(`${message}\n`),
630
+ }
631
+ : {}),
632
+ });
633
+ if (options.json) {
634
+ console.log(JSON.stringify(report, null, 2));
635
+ }
636
+ else {
637
+ const text = renderFullCheckReport(report, { quiet: options.quiet, plain: options.plain });
638
+ if (text)
639
+ console.log(text);
640
+ }
641
+ process.exitCode = report.exitCode;
642
+ });
329
643
  program.parse(process.argv);
330
644
  //# sourceMappingURL=cli.js.map