@macrulez/devtoolz 0.2.0 → 0.3.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.
Files changed (80) hide show
  1. package/README.md +131 -8
  2. package/dist/cli.js +265 -8
  3. package/dist/cli.js.map +1 -1
  4. package/dist/commands/case-check/report.js +8 -6
  5. package/dist/commands/case-check/report.js.map +1 -1
  6. package/dist/commands/circular-imports/report.js +11 -12
  7. package/dist/commands/circular-imports/report.js.map +1 -1
  8. package/dist/commands/console-strip/report.js +9 -7
  9. package/dist/commands/console-strip/report.js.map +1 -1
  10. package/dist/commands/dead-exports/report.js +6 -4
  11. package/dist/commands/dead-exports/report.js.map +1 -1
  12. package/dist/commands/empty-catch/report.js +5 -3
  13. package/dist/commands/empty-catch/report.js.map +1 -1
  14. package/dist/commands/exports-doctor/report.js +7 -5
  15. package/dist/commands/exports-doctor/report.js.map +1 -1
  16. package/dist/commands/full-check/report.d.ts +3 -0
  17. package/dist/commands/full-check/report.js +55 -0
  18. package/dist/commands/full-check/report.js.map +1 -0
  19. package/dist/commands/full-check/run.d.ts +32 -0
  20. package/dist/commands/full-check/run.js +251 -0
  21. package/dist/commands/full-check/run.js.map +1 -0
  22. package/dist/commands/image-hash/cache.d.ts +14 -0
  23. package/dist/commands/image-hash/cache.js +40 -0
  24. package/dist/commands/image-hash/cache.js.map +1 -0
  25. package/dist/commands/image-hash/core.d.ts +56 -0
  26. package/dist/commands/image-hash/core.js +293 -0
  27. package/dist/commands/image-hash/core.js.map +1 -0
  28. package/dist/commands/image-hash/hash.d.ts +19 -0
  29. package/dist/commands/image-hash/hash.js +97 -0
  30. package/dist/commands/image-hash/hash.js.map +1 -0
  31. package/dist/commands/image-hash/report.d.ts +3 -0
  32. package/dist/commands/image-hash/report.js +94 -0
  33. package/dist/commands/image-hash/report.js.map +1 -0
  34. package/dist/commands/image-hash/run.d.ts +60 -0
  35. package/dist/commands/image-hash/run.js +315 -0
  36. package/dist/commands/image-hash/run.js.map +1 -0
  37. package/dist/commands/image-hash/sharp-loader.d.ts +59 -0
  38. package/dist/commands/image-hash/sharp-loader.js +79 -0
  39. package/dist/commands/image-hash/sharp-loader.js.map +1 -0
  40. package/dist/commands/orphan-tests/report.js +7 -5
  41. package/dist/commands/orphan-tests/report.js.map +1 -1
  42. package/dist/commands/readme-check/report.js +7 -5
  43. package/dist/commands/readme-check/report.js.map +1 -1
  44. package/dist/commands/scripts-check/report.js +7 -5
  45. package/dist/commands/scripts-check/report.js.map +1 -1
  46. package/dist/commands/stale-ts-ignore/report.js +7 -5
  47. package/dist/commands/stale-ts-ignore/report.js.map +1 -1
  48. package/dist/commands/strip-comments/report.js +6 -4
  49. package/dist/commands/strip-comments/report.js.map +1 -1
  50. package/dist/commands/todo-report/report.js +7 -5
  51. package/dist/commands/todo-report/report.js.map +1 -1
  52. package/dist/commands/unused-deps/report.js +7 -5
  53. package/dist/commands/unused-deps/report.js.map +1 -1
  54. package/dist/format/colors.d.ts +4 -0
  55. package/dist/format/colors.js +4 -3
  56. package/dist/format/colors.js.map +1 -1
  57. package/dist/format/diff-render.js +2 -2
  58. package/dist/format/diff-render.js.map +1 -1
  59. package/dist/format/file-list.d.ts +1 -9
  60. package/dist/format/file-list.js +5 -16
  61. package/dist/format/file-list.js.map +1 -1
  62. package/dist/format/findings-list.d.ts +1 -8
  63. package/dist/format/findings-list.js +5 -14
  64. package/dist/format/findings-list.js.map +1 -1
  65. package/dist/format/style.d.ts +20 -0
  66. package/dist/format/style.js +36 -0
  67. package/dist/format/style.js.map +1 -0
  68. package/dist/format/table.d.ts +8 -0
  69. package/dist/format/table.js +44 -0
  70. package/dist/format/table.js.map +1 -0
  71. package/dist/format/vibes.d.ts +4 -1
  72. package/dist/format/vibes.js +25 -10
  73. package/dist/format/vibes.js.map +1 -1
  74. package/dist/index.d.ts +7 -0
  75. package/dist/index.js +4 -0
  76. package/dist/index.js.map +1 -1
  77. package/dist/utils/walk.d.ts +2 -0
  78. package/dist/utils/walk.js +4 -3
  79. package/dist/utils/walk.js.map +1 -1
  80. package/package.json +4 -1
package/README.md CHANGED
@@ -127,30 +127,77 @@ looking for is never touched.
127
127
  `.vue` files get an isolated per-file check (same technique as
128
128
  `readme-check`'s own virtual-file typechecking) — a plain
129
129
  `ts.Program` can't include `.vue` in a whole-project run at all.
130
+ - **`image-hash`** — generates [blurhash](https://blurha.sh) and/or
131
+ [thumbhash](https://evanw.github.io/thumbhash/) placeholders for raster
132
+ images (jpg, png, webp, gif, avif, tiff). Takes files and/or
133
+ directories — several of each, comma-separated too — and `-r` walks
134
+ subdirectories. Prints to stdout by default; `-o <file>` collects
135
+ everything into one file, `--per-file` / `--out-dir <dir>` writes a
136
+ file per image (`photo.jpg` → `photo.jpg.blurhash.txt`, with
137
+ `--suffix` and `--out-ext` to rename). Formats: `json`, `plain` (just
138
+ the hash text, no newline), `csv`, and `ts`/`js` modules you can import
139
+ straight into the app. Besides the two hashes it can produce the
140
+ dominant color and a tiny PNG preview (`-t all`), accepts `http(s)`
141
+ URLs and path lists from a file or stdin, and can keep an output file in
142
+ sync: `--cache` skips unchanged images, `--update --prune` merges into
143
+ the existing file, `--check` fails with exit code 1 when it is stale
144
+ (made for CI). `--dry-run` writes nothing and shows the result as a
145
+ table instead. Uses the native `sharp` library, which devtoolz
146
+ offers to install on first use — see Requirements.
147
+ - **`full-check`** — runs all thirteen other commands in one sweep, each in
148
+ its own safe read-only mode — the three that can write to disk
149
+ (`strip-comments`/`console-strip`/`case-check`) are always called as a
150
+ preview, `-y`/`--fix` are never passed. One summary table: which
151
+ commands are clean, which found something, which errored. Since
152
+ `stale-ts-ignore` is the one command that can make a bare `full-check`
153
+ noticeably slower, running it with no flags in a real terminal asks
154
+ first — `--skip stale-ts-ignore` skips both the question and the
155
+ command, `--no-prompt` runs everything without asking (the default
156
+ answer either way).
130
157
 
131
158
  `strip-comments`, `console-strip`, and `case-check` share the same safety
132
159
  model: `--dry-run` (or just running with neither `--dry-run` nor `-y`)
133
160
  only previews, `-y`/`--yes` is required to actually write anything,
134
161
  `--diff` shows a real unified diff per file. `dead-exports`,
135
162
  `unused-deps`, `circular-imports`, `exports-doctor`, `readme-check`,
136
- `empty-catch`, `todo-report`, `scripts-check`, `orphan-tests`, and
137
- `stale-ts-ignore` are all read-only — none of them ever write anything,
138
- there's nothing to preview or apply. Every command supports `--json` for
163
+ `empty-catch`, `todo-report`, `scripts-check`, `orphan-tests`,
164
+ `stale-ts-ignore`, and `full-check` are all read-only — none of them ever
165
+ write anything, there's nothing to preview or apply. `image-hash` writes
166
+ files only when you name an output (`-o`, `--per-file`, `--out-dir`) and
167
+ prints to stdout otherwise. Every command supports `--json` for
139
168
  machine-readable output.
140
169
 
141
170
  ## Tone
142
171
 
143
172
  Not dead silent, not relentlessly jokey either — a small banner and some
144
173
  personality when everything comes back clean, nothing cute mixed into the
145
- actual findings list, which stays in aligned, colored columns (file in
146
- white, count/tag in dim grey) for quick scanning. Auto-disables (banner,
147
- color, celebration copy) the moment output isn't a real interactive
148
- terminal — piped, `CI` set, `NO_COLOR` set — on top of the explicit
149
- `--quiet`/`--plain` flags.
174
+ actual findings list, which stays in aligned columns for quick scanning.
175
+
176
+ Output is colored with one palette across every command: file paths in
177
+ cyan with their `:line:column` dimmed, names and kinds in yellow, the
178
+ "N problems found" heading in bold red, what a command would do in bold
179
+ yellow and what it did in bold green, hints dimmed with the flags they
180
+ mention (`-y`, `--dry-run`) picked out in cyan, `✔`/`✖` in the
181
+ `full-check` table in green/red, and the diffs red/green as before.
182
+
183
+ Color turns itself off the moment output isn't a real interactive
184
+ terminal — piped, `CI` set, `NO_COLOR` set, `TERM=dumb`. `--color` (or
185
+ `FORCE_COLOR=1`) turns it back on anyway, which is what you want for
186
+ `devtoolz … --color | less -R` or a CI log that renders ANSI; `--plain`
187
+ turns color, the banner and the celebration copy all off, and `--quiet`
188
+ only drops the banner and clean-run output. The text is identical with
189
+ and without color — only escape codes are added.
150
190
 
151
191
  ## Requirements
152
192
 
153
193
  - Node.js 20+
194
+ - `image-hash` only: the [`sharp`](https://sharp.pixelplumbing.com)
195
+ image library. It ships prebuilt binaries for the common platforms and
196
+ is not installed with devtoolz — the first `image-hash` run offers to
197
+ install it into `~/.devtoolz/deps` (`-y` / `--yes` agrees up front, which
198
+ is also what you want in CI; without a terminal and without `--yes` the
199
+ command stops and says so). A `sharp` you already have installed
200
+ alongside devtoolz is used as-is.
154
201
 
155
202
  ## Installation
156
203
 
@@ -199,6 +246,17 @@ devtoolz scripts-check # package.json scripts vs README/
199
246
  devtoolz orphan-tests src # test files whose source disappeared
200
247
 
201
248
  devtoolz stale-ts-ignore # find @ts-ignore comments suppressing nothing
249
+
250
+ devtoolz image-hash public/img # blurhash + thumbhash of every image, as JSON on stdout
251
+ devtoolz image-hash a.jpg,b.png,photos -r -t blurhash -f csv -o hashes.csv
252
+ devtoolz image-hash public/img -r -f plain --per-file # photo.jpg.blurhash.txt + photo.jpg.thumbhash.txt
253
+ devtoolz image-hash public/img -r -f ts -t thumbhash -o src/placeholders.ts --name placeholders
254
+ devtoolz image-hash public/img -r --dry-run # write nothing, show a table of the hashes
255
+ devtoolz image-hash public/img -r -t all --components auto -o hashes.json --cache --update --prune
256
+ devtoolz image-hash public/img -r -o hashes.json --check # CI: exit 1 if hashes.json is stale
257
+
258
+ devtoolz full-check # run every command, one summary table
259
+ devtoolz full-check --skip stale-ts-ignore # same, minus the expensive one
202
260
  ```
203
261
 
204
262
  Every command has built-in `--help` — `devtoolz --help` lists every
@@ -382,6 +440,71 @@ Checked 1 @ts-ignore directive.
382
440
  src/example.ts:2 @ts-ignore doesn't suppress anything — the line below it typechecks cleanly without it
383
441
  ```
384
442
 
443
+ `devtoolz full-check` against a small package with a few real, unrelated
444
+ problems scattered across it:
445
+
446
+ ```
447
+ ✖ strip-comments 1 found
448
+ ✔ console-strip clean
449
+ ✖ dead-exports 1 found
450
+ ✔ case-check clean
451
+ ✔ exports-doctor clean
452
+ ✔ readme-check clean
453
+ ✔ unused-deps clean
454
+ ✔ circular-imports clean
455
+ ✖ empty-catch 1 found
456
+ ✔ todo-report clean
457
+ ✖ scripts-check 3 found
458
+ ✔ orphan-tests clean
459
+ ✔ stale-ts-ignore clean
460
+
461
+ 9 clean, 4 found something — see `devtoolz <command> --help` for full detail on any of them.
462
+ ```
463
+
464
+ `devtoolz image-hash public/img -r -f plain --per-file` writing a text file
465
+ per hash next to each image:
466
+
467
+ ```
468
+ 🧰 devtoolz
469
+
470
+ Hashed 2 images.
471
+
472
+ Wrote 4 files:
473
+ public/img/hero.jpg.blurhash.txt
474
+ public/img/hero.jpg.thumbhash.txt
475
+ public/img/logo.png.blurhash.txt
476
+ public/img/logo.png.thumbhash.txt
477
+ ```
478
+
479
+ `devtoolz image-hash hero.jpg -t blurhash` printing JSON to stdout, ready to
480
+ pipe or paste:
481
+
482
+ ```json
483
+ {
484
+ "hero.jpg": {
485
+ "width": 1600,
486
+ "height": 900,
487
+ "blurhash": "LEHV6nWB2yk8pyo0adR*.7kCMdnj"
488
+ }
489
+ }
490
+ ```
491
+
492
+ `devtoolz image-hash public/img -r -t blurhash --dry-run` shows what would be
493
+ generated as a table and writes nothing:
494
+
495
+ ```
496
+ ┌─────────────────────┬─────────┬──────────────────────────────┐
497
+ │ File │ Size │ BlurHash │
498
+ ├─────────────────────┼─────────┼──────────────────────────────┤
499
+ │ public/img/blue.png │ 120×300 │ L704c9gSfQgSf:fRfQfRfQfQfQfQ │
500
+ │ public/img/red.jpg │ 200×100 │ L6T9R{,YfQ,Y|cjtfQjtfQfQfQfQ │
501
+ └─────────────────────┴─────────┴──────────────────────────────┘
502
+
503
+ Hashed 2 images.
504
+
505
+ (dry run — nothing written)
506
+ ```
507
+
385
508
  ## Development
386
509
 
387
510
  ```bash
package/dist/cli.js CHANGED
@@ -1,6 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  import { Command } from 'commander';
3
+ import { readFileSync } from 'node:fs';
3
4
  import { resolve } from 'node:path';
5
+ import { createInterface } from 'node:readline/promises';
4
6
  import { runStripComments } from './commands/strip-comments/run.js';
5
7
  import { renderStripCommentsReport } from './commands/strip-comments/report.js';
6
8
  import { runConsoleStrip } from './commands/console-strip/run.js';
@@ -28,6 +30,12 @@ import { runOrphanTests } from './commands/orphan-tests/run.js';
28
30
  import { renderOrphanTestsReport } from './commands/orphan-tests/report.js';
29
31
  import { runStaleTsIgnore } from './commands/stale-ts-ignore/run.js';
30
32
  import { renderStaleTsIgnoreReport } from './commands/stale-ts-ignore/report.js';
33
+ import { runFullCheck, FULL_CHECK_COMMAND_NAMES, SLOW_FULL_CHECK_COMMAND, } from './commands/full-check/run.js';
34
+ import { renderFullCheckReport } from './commands/full-check/report.js';
35
+ import { runImageHash } from './commands/image-hash/run.js';
36
+ import { renderImageHashReport } from './commands/image-hash/report.js';
37
+ import { DEFAULT_CACHE_FILE, DEFAULT_IMAGE_EXTENSIONS, ImageHashUsageError, OUTPUT_FORMATS, parseComponents, parseExtensions, parseFileList, parseFormat, parseMaxPixels, parseSampleSize, parseTypes, } from './commands/image-hash/core.js';
38
+ import { createStyle } from './format/style.js';
31
39
  function wrapText(text, width) {
32
40
  const words = text.split(' ');
33
41
  const lines = [];
@@ -153,6 +161,7 @@ program
153
161
  .option('--json', 'machine-readable output', false)
154
162
  .option('--quiet', 'suppress output when there is nothing to report', false)
155
163
  .option('--plain', 'disable color/banner/celebration copy, even in a real terminal', false)
164
+ .option('--color', 'force colored output even when piped or in CI', false)
156
165
  .action((paths, options) => {
157
166
  const report = runStripComments({
158
167
  paths,
@@ -172,6 +181,7 @@ program
172
181
  const text = renderStripCommentsReport(report, {
173
182
  quiet: options.quiet,
174
183
  plain: options.plain,
184
+ color: options.color,
175
185
  dryRun: options.dryRun,
176
186
  });
177
187
  if (text)
@@ -195,6 +205,7 @@ program
195
205
  .option('--json', 'machine-readable output', false)
196
206
  .option('--quiet', 'suppress output when there is nothing to report', false)
197
207
  .option('--plain', 'disable color/banner/celebration copy, even in a real terminal', false)
208
+ .option('--color', 'force colored output even when piped or in CI', false)
198
209
  .action((paths, options) => {
199
210
  const report = runConsoleStrip({
200
211
  paths,
@@ -215,6 +226,7 @@ program
215
226
  const text = renderConsoleStripReport(report, {
216
227
  quiet: options.quiet,
217
228
  plain: options.plain,
229
+ color: options.color,
218
230
  });
219
231
  if (text)
220
232
  console.log(text);
@@ -235,6 +247,7 @@ program
235
247
  .option('--json', 'machine-readable output', false)
236
248
  .option('--quiet', 'suppress output when there is nothing to report', false)
237
249
  .option('--plain', 'disable color/banner/celebration copy, even in a real terminal', false)
250
+ .option('--color', 'force colored output even when piped or in CI', false)
238
251
  .action((paths, options) => {
239
252
  const report = runDeadExports({
240
253
  paths,
@@ -250,7 +263,11 @@ program
250
263
  console.log(JSON.stringify(report, null, 2));
251
264
  }
252
265
  else {
253
- const text = renderDeadExportsReport(report, { quiet: options.quiet, plain: options.plain });
266
+ const text = renderDeadExportsReport(report, {
267
+ quiet: options.quiet,
268
+ plain: options.plain,
269
+ color: options.color,
270
+ });
254
271
  if (text)
255
272
  console.log(text);
256
273
  }
@@ -268,6 +285,7 @@ program
268
285
  .option('--json', 'machine-readable output', false)
269
286
  .option('--quiet', 'suppress output when there is nothing to report', false)
270
287
  .option('--plain', 'disable color/banner/celebration copy, even in a real terminal', false)
288
+ .option('--color', 'force colored output even when piped or in CI', false)
271
289
  .action((paths, options) => {
272
290
  const report = runCircularImports({
273
291
  paths,
@@ -284,6 +302,7 @@ program
284
302
  const text = renderCircularImportsReport(report, {
285
303
  quiet: options.quiet,
286
304
  plain: options.plain,
305
+ color: options.color,
287
306
  });
288
307
  if (text)
289
308
  console.log(text);
@@ -302,6 +321,7 @@ program
302
321
  .option('--json', 'machine-readable output', false)
303
322
  .option('--quiet', 'suppress output when there is nothing to report', false)
304
323
  .option('--plain', 'disable color/banner/celebration copy, even in a real terminal', false)
324
+ .option('--color', 'force colored output even when piped or in CI', false)
305
325
  .action((dir, options) => {
306
326
  const report = runUnusedDeps({
307
327
  dir: resolve(dir),
@@ -315,7 +335,11 @@ program
315
335
  console.log(JSON.stringify(report, null, 2));
316
336
  }
317
337
  else {
318
- const text = renderUnusedDepsReport(report, { quiet: options.quiet, plain: options.plain });
338
+ const text = renderUnusedDepsReport(report, {
339
+ quiet: options.quiet,
340
+ plain: options.plain,
341
+ color: options.color,
342
+ });
319
343
  if (text)
320
344
  console.log(text);
321
345
  }
@@ -337,6 +361,7 @@ program
337
361
  .option('--json', 'machine-readable output', false)
338
362
  .option('--quiet', 'suppress output when there is nothing to report', false)
339
363
  .option('--plain', 'disable color/banner/celebration copy, even in a real terminal', false)
364
+ .option('--color', 'force colored output even when piped or in CI', false)
340
365
  .action((paths, options) => {
341
366
  const report = runCaseCheck({
342
367
  paths,
@@ -354,7 +379,11 @@ program
354
379
  console.log(JSON.stringify(report, null, 2));
355
380
  }
356
381
  else {
357
- const text = renderCaseCheckReport(report, { quiet: options.quiet, plain: options.plain });
382
+ const text = renderCaseCheckReport(report, {
383
+ quiet: options.quiet,
384
+ plain: options.plain,
385
+ color: options.color,
386
+ });
358
387
  if (text)
359
388
  console.log(text);
360
389
  }
@@ -367,13 +396,18 @@ program
367
396
  .option('--json', 'machine-readable output', false)
368
397
  .option('--quiet', 'suppress output when there is nothing to report', false)
369
398
  .option('--plain', 'disable color/banner/celebration copy, even in a real terminal', false)
399
+ .option('--color', 'force colored output even when piped or in CI', false)
370
400
  .action((dir, options) => {
371
401
  const report = runExportsDoctor({ dir: resolve(dir) });
372
402
  if (options.json) {
373
403
  console.log(JSON.stringify(report, null, 2));
374
404
  }
375
405
  else {
376
- const text = renderExportsDoctorReport(report, { quiet: options.quiet, plain: options.plain });
406
+ const text = renderExportsDoctorReport(report, {
407
+ quiet: options.quiet,
408
+ plain: options.plain,
409
+ color: options.color,
410
+ });
377
411
  if (text)
378
412
  console.log(text);
379
413
  }
@@ -389,6 +423,7 @@ program
389
423
  .option('--json', 'machine-readable output', false)
390
424
  .option('--quiet', 'suppress output when there is nothing to report', false)
391
425
  .option('--plain', 'disable color/banner/celebration copy, even in a real terminal', false)
426
+ .option('--color', 'force colored output even when piped or in CI', false)
392
427
  .action((dir, options) => {
393
428
  const report = runReadmeCheck({
394
429
  dir: resolve(dir),
@@ -400,7 +435,11 @@ program
400
435
  console.log(JSON.stringify(report, null, 2));
401
436
  }
402
437
  else {
403
- const text = renderReadmeCheckReport(report, { quiet: options.quiet, plain: options.plain });
438
+ const text = renderReadmeCheckReport(report, {
439
+ quiet: options.quiet,
440
+ plain: options.plain,
441
+ color: options.color,
442
+ });
404
443
  if (text)
405
444
  console.log(text);
406
445
  }
@@ -417,6 +456,7 @@ program
417
456
  .option('--json', 'machine-readable output', false)
418
457
  .option('--quiet', 'suppress output when there is nothing to report', false)
419
458
  .option('--plain', 'disable color/banner/celebration copy, even in a real terminal', false)
459
+ .option('--color', 'force colored output even when piped or in CI', false)
420
460
  .action((paths, options) => {
421
461
  const report = runEmptyCatch({
422
462
  paths,
@@ -429,7 +469,11 @@ program
429
469
  console.log(JSON.stringify(report, null, 2));
430
470
  }
431
471
  else {
432
- const text = renderEmptyCatchReport(report, { quiet: options.quiet, plain: options.plain });
472
+ const text = renderEmptyCatchReport(report, {
473
+ quiet: options.quiet,
474
+ plain: options.plain,
475
+ color: options.color,
476
+ });
433
477
  if (text)
434
478
  console.log(text);
435
479
  }
@@ -448,6 +492,7 @@ program
448
492
  .option('--json', 'machine-readable output', false)
449
493
  .option('--quiet', 'suppress output when there is nothing to report', false)
450
494
  .option('--plain', 'disable color/banner/celebration copy, even in a real terminal', false)
495
+ .option('--color', 'force colored output even when piped or in CI', false)
451
496
  .action((paths, options) => {
452
497
  const report = runTodoReport({
453
498
  paths,
@@ -462,7 +507,11 @@ program
462
507
  console.log(JSON.stringify(report, null, 2));
463
508
  }
464
509
  else {
465
- const text = renderTodoReportReport(report, { quiet: options.quiet, plain: options.plain });
510
+ const text = renderTodoReportReport(report, {
511
+ quiet: options.quiet,
512
+ plain: options.plain,
513
+ color: options.color,
514
+ });
466
515
  if (text)
467
516
  console.log(text);
468
517
  }
@@ -476,6 +525,7 @@ program
476
525
  .option('--json', 'machine-readable output', false)
477
526
  .option('--quiet', 'suppress output when there is nothing to report', false)
478
527
  .option('--plain', 'disable color/banner/celebration copy, even in a real terminal', false)
528
+ .option('--color', 'force colored output even when piped or in CI', false)
479
529
  .action((dir, options) => {
480
530
  const report = runScriptsCheck({ dir: resolve(dir), files: options.file });
481
531
  if (options.json) {
@@ -485,6 +535,7 @@ program
485
535
  const text = renderScriptsCheckReport(report, {
486
536
  quiet: options.quiet,
487
537
  plain: options.plain,
538
+ color: options.color,
488
539
  });
489
540
  if (text)
490
541
  console.log(text);
@@ -506,6 +557,7 @@ program
506
557
  .option('--json', 'machine-readable output', false)
507
558
  .option('--quiet', 'suppress output when there is nothing to report', false)
508
559
  .option('--plain', 'disable color/banner/celebration copy, even in a real terminal', false)
560
+ .option('--color', 'force colored output even when piped or in CI', false)
509
561
  .action((paths, options) => {
510
562
  const report = runOrphanTests({
511
563
  paths,
@@ -522,7 +574,11 @@ program
522
574
  console.log(JSON.stringify(report, null, 2));
523
575
  }
524
576
  else {
525
- const text = renderOrphanTestsReport(report, { quiet: options.quiet, plain: options.plain });
577
+ const text = renderOrphanTestsReport(report, {
578
+ quiet: options.quiet,
579
+ plain: options.plain,
580
+ color: options.color,
581
+ });
526
582
  if (text)
527
583
  console.log(text);
528
584
  }
@@ -538,6 +594,7 @@ program
538
594
  .option('--json', 'machine-readable output', false)
539
595
  .option('--quiet', 'suppress output when there is nothing to report', false)
540
596
  .option('--plain', 'disable color/banner/celebration copy, even in a real terminal', false)
597
+ .option('--color', 'force colored output even when piped or in CI', false)
541
598
  .action((dir, options) => {
542
599
  const report = runStaleTsIgnore({
543
600
  dir: resolve(dir),
@@ -553,6 +610,206 @@ program
553
610
  const text = renderStaleTsIgnoreReport(report, {
554
611
  quiet: options.quiet,
555
612
  plain: options.plain,
613
+ color: options.color,
614
+ });
615
+ if (text)
616
+ console.log(text);
617
+ }
618
+ process.exitCode = report.exitCode;
619
+ });
620
+ program
621
+ .command('image-hash')
622
+ .description('Generate blurhash/thumbhash placeholders (and dominant color, tiny preview) for raster images — to stdout, one file, or a file per image')
623
+ .argument('[paths...]', 'image files, directories and http(s) URLs (several allowed, comma-separated too)', [])
624
+ .option('--cwd <path>', 'root paths are resolved against', process.cwd())
625
+ .option('-r, --recursive', 'also walk subdirectories of every given directory', false)
626
+ .option('--files-from <file>', 'read more paths/URLs from a file, one per line (- = stdin, # starts a comment)')
627
+ .option('--ext <list>', 'comma-separated image extensions to pick up from directories', DEFAULT_IMAGE_EXTENSIONS.join(','))
628
+ .option('--ignore <glob>', 'extra ignore pattern (repeatable), on top of the built-in defaults', (val, prev) => [...prev, val], [])
629
+ .option('--no-respect-gitignore', "don't also honor the project's .gitignore")
630
+ .option('-t, --type <list>', 'what to generate, comma-separated: blurhash, thumbhash, color (dominant), preview (tiny PNG data URI); both = blurhash+thumbhash, all = everything', 'both')
631
+ .option('--components <XxY|auto>', 'blurhash components, each side 1-9, or auto to pick them by aspect ratio', '4x3')
632
+ .option('--size <px>', 'longest side the image is scaled down to before hashing (1-100)', '100')
633
+ .option('--max-pixels <n>', 'refuse images with more pixels than this (0 = no limit; default: sharp limit, ~268 million)')
634
+ .option('-f, --format <format>', `format of the generated hashes: ${OUTPUT_FORMATS.join(', ')} (plain = just the hash text); --json prints the full report instead`, 'json')
635
+ .option('--name <identifier>', 'exported constant name for --format ts/js', 'imageHashes')
636
+ .option('--key-base <dir>', 'make the file keys in the output relative to this directory')
637
+ .option('--key-prefix <text>', 'text put in front of every file key, e.g. / for URL-style keys')
638
+ .option('-o, --out <file>', 'write everything into one file instead of stdout')
639
+ .option('--update', 'merge into the existing -o file instead of replacing it', false)
640
+ .option('--prune', 'with --update, drop entries whose image no longer exists', false)
641
+ .option('--per-file', 'write one file per image, next to the image', false)
642
+ .option('--out-dir <dir>', 'write the per-image files into this directory (implies --per-file)')
643
+ .option('--suffix <text>', 'per-image file name suffix after the image file name, {type} = blurhash/thumbhash/hash (default: .{type})')
644
+ .option('--out-ext <ext>', 'per-image file extension (default: by --format, plain = .txt)')
645
+ .option('--check', 'write nothing; exit 1 if the output files are missing or differ from the images', false)
646
+ .option('--cache [file]', `skip images that did not change since the last run, remembered in this file (default: ${DEFAULT_CACHE_FILE})`)
647
+ .option('--concurrency <n>', 'images processed in parallel', '4')
648
+ .option('--dry-run', 'write nothing — show the result as a table instead', false)
649
+ .option('-y, --yes', 'install the missing "sharp" image library without asking', false)
650
+ .option('--json', 'machine-readable output', false)
651
+ .option('--quiet', 'suppress output when there is nothing to report', false)
652
+ .option('--plain', 'disable color/banner/celebration copy, even in a real terminal', false)
653
+ .option('--color', 'force colored output even when piped or in CI', false)
654
+ .action(async (paths, options) => {
655
+ const style = createStyle({ plain: options.plain, color: options.color });
656
+ const showProgress = !options.json && !options.quiet && Boolean(process.stderr.isTTY) && !options.plain;
657
+ let progressShown = false;
658
+ try {
659
+ const listed = options.filesFrom !== undefined
660
+ ? parseFileList(readFileSync(options.filesFrom === '-' ? 0 : options.filesFrom, 'utf8'))
661
+ : [];
662
+ const report = await runImageHash({
663
+ paths: [...paths, ...listed],
664
+ cwd: resolve(options.cwd),
665
+ types: parseTypes(options.type),
666
+ recursive: options.recursive,
667
+ extensions: parseExtensions(options.ext),
668
+ ignoreGlobs: options.ignore,
669
+ respectGitignore: options.respectGitignore,
670
+ components: parseComponents(options.components),
671
+ size: parseSampleSize(options.size),
672
+ ...(options.maxPixels !== undefined
673
+ ? { maxPixels: parseMaxPixels(options.maxPixels) }
674
+ : {}),
675
+ format: parseFormat(options.format),
676
+ exportName: options.name,
677
+ ...(options.keyBase !== undefined ? { keyBase: options.keyBase } : {}),
678
+ ...(options.keyPrefix !== undefined ? { keyPrefix: options.keyPrefix } : {}),
679
+ ...(options.out !== undefined ? { out: options.out } : {}),
680
+ update: options.update,
681
+ prune: options.prune,
682
+ perFile: options.perFile,
683
+ ...(options.outDir !== undefined ? { outDir: options.outDir } : {}),
684
+ ...(options.suffix !== undefined ? { suffix: options.suffix } : {}),
685
+ ...(options.outExt !== undefined ? { outExtension: options.outExt } : {}),
686
+ check: options.check,
687
+ ...(options.cache !== undefined
688
+ ? { cache: options.cache === true ? DEFAULT_CACHE_FILE : String(options.cache) }
689
+ : {}),
690
+ concurrency: Math.max(1, Number.parseInt(options.concurrency, 10) || 4),
691
+ assumeYes: options.yes,
692
+ dryRun: options.dryRun,
693
+ ...(showProgress
694
+ ? {
695
+ onProgress: ({ done, total }) => {
696
+ if (total < 2)
697
+ return;
698
+ progressShown = true;
699
+ process.stderr.write(`\r${style.info(`Hashing ${done}/${total}`)}`);
700
+ },
701
+ }
702
+ : {}),
703
+ });
704
+ if (progressShown)
705
+ process.stderr.write('\r\x1b[K');
706
+ if (options.json) {
707
+ console.log(JSON.stringify(report, null, 2));
708
+ }
709
+ else if (report.stdout !== null) {
710
+ process.stdout.write(report.stdout);
711
+ for (const error of report.errors)
712
+ console.error(`${error.file} — ${error.message}`);
713
+ }
714
+ else {
715
+ const text = renderImageHashReport(report, {
716
+ quiet: options.quiet,
717
+ plain: options.plain,
718
+ color: options.color,
719
+ });
720
+ if (text)
721
+ console.log(text);
722
+ }
723
+ process.exitCode = report.exitCode;
724
+ }
725
+ catch (error) {
726
+ if (progressShown)
727
+ process.stderr.write('\r\x1b[K');
728
+ if (error instanceof ImageHashUsageError) {
729
+ console.error(`error: ${error.message}`);
730
+ process.exitCode = 2;
731
+ return;
732
+ }
733
+ throw error;
734
+ }
735
+ });
736
+ // Only `full-check` ever prompts — every other command here is a single,
737
+ // fast, non-interactive check. `stale-ts-ignore` is the one command a
738
+ // bare `full-check` can turn into a genuinely slow run, so this asks
739
+ // about that ONE command specifically, not a general "run everything?"
740
+ // confirmation. Defaults to running it (Enter alone picks option 1) —
741
+ // matches what happens when there's no TTY to ask at all (see
742
+ // `shouldPrompt` below), so a script and an interactive "just press
743
+ // enter" run behave the same way.
744
+ // Deliberately a SINGLE `question()` call, no retry-on-invalid-input loop
745
+ // — found via real testing that a second `question()` on the same (or
746
+ // even a fresh) `readline/promises` interface can silently lose the
747
+ // answer and hang forever once more than one line was already available
748
+ // on stdin when the first question resolved (a real, reproducible
749
+ // readline quirk with piped/non-interactive input, not something specific
750
+ // to this code). One question, with an explicit default on anything else
751
+ // typed, sidesteps the whole bug class instead of risking it.
752
+ async function promptStaleTsIgnoreChoice() {
753
+ 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' +
754
+ ' 1) Run it anyway (full sweep)\n' +
755
+ ' 2) Skip it for this run\n\n');
756
+ const rl = createInterface({ input: process.stdin, output: process.stderr });
757
+ let answer;
758
+ try {
759
+ answer = (await rl.question('Choice [1/2, Enter = 1]: ')).trim();
760
+ }
761
+ finally {
762
+ rl.close();
763
+ }
764
+ if (answer === '2')
765
+ return false;
766
+ if (answer !== '' && answer !== '1') {
767
+ process.stderr.write(`(didn't recognize "${answer}" — running it, same as Enter)\n`);
768
+ }
769
+ return true;
770
+ }
771
+ program
772
+ .command('full-check')
773
+ .description('Runs every other command in its own safe/read-only mode — a full diagnostic sweep, never applies a fix')
774
+ .argument('[dir]', 'directory every command scans/reads from', '.')
775
+ .option('--skip <command>', `skip a command by name (repeatable) — valid names: ${FULL_CHECK_COMMAND_NAMES.join(', ')}`, (val, prev) => [...prev, val], [])
776
+ .option('--no-prompt', "don't ask about stale-ts-ignore even in an interactive terminal — runs it unless --skip already excludes it")
777
+ .option('--json', 'machine-readable output', false)
778
+ .option('--quiet', 'suppress output when there is nothing to report', false)
779
+ .option('--plain', 'disable color/banner/celebration copy, even in a real terminal', false)
780
+ .option('--color', 'force colored output even when piped or in CI', false)
781
+ .action(async (dir, options) => {
782
+ const skip = new Set(options.skip);
783
+ const shouldPrompt = options.prompt &&
784
+ !options.json &&
785
+ !skip.has(SLOW_FULL_CHECK_COMMAND) &&
786
+ Boolean(process.stdin.isTTY) &&
787
+ Boolean(process.stdout.isTTY);
788
+ if (shouldPrompt) {
789
+ const runIt = await promptStaleTsIgnoreChoice();
790
+ if (!runIt)
791
+ skip.add(SLOW_FULL_CHECK_COMMAND);
792
+ process.stderr.write('\n');
793
+ }
794
+ const showProgress = !options.json && !options.quiet;
795
+ const report = runFullCheck({
796
+ dir: resolve(dir),
797
+ skip: [...skip],
798
+ ...(showProgress
799
+ ? {
800
+ onCommandStart: (command) => process.stderr.write(`Running ${command}...\n`),
801
+ onProgress: (message) => process.stderr.write(`${message}\n`),
802
+ }
803
+ : {}),
804
+ });
805
+ if (options.json) {
806
+ console.log(JSON.stringify(report, null, 2));
807
+ }
808
+ else {
809
+ const text = renderFullCheckReport(report, {
810
+ quiet: options.quiet,
811
+ plain: options.plain,
812
+ color: options.color,
556
813
  });
557
814
  if (text)
558
815
  console.log(text);