@macrulez/devtoolz 0.2.1 → 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 (76) hide show
  1. package/README.md +97 -8
  2. package/dist/cli.js +185 -9
  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.js +24 -30
  17. package/dist/commands/full-check/report.js.map +1 -1
  18. package/dist/commands/image-hash/cache.d.ts +14 -0
  19. package/dist/commands/image-hash/cache.js +40 -0
  20. package/dist/commands/image-hash/cache.js.map +1 -0
  21. package/dist/commands/image-hash/core.d.ts +56 -0
  22. package/dist/commands/image-hash/core.js +293 -0
  23. package/dist/commands/image-hash/core.js.map +1 -0
  24. package/dist/commands/image-hash/hash.d.ts +19 -0
  25. package/dist/commands/image-hash/hash.js +97 -0
  26. package/dist/commands/image-hash/hash.js.map +1 -0
  27. package/dist/commands/image-hash/report.d.ts +3 -0
  28. package/dist/commands/image-hash/report.js +94 -0
  29. package/dist/commands/image-hash/report.js.map +1 -0
  30. package/dist/commands/image-hash/run.d.ts +60 -0
  31. package/dist/commands/image-hash/run.js +315 -0
  32. package/dist/commands/image-hash/run.js.map +1 -0
  33. package/dist/commands/image-hash/sharp-loader.d.ts +59 -0
  34. package/dist/commands/image-hash/sharp-loader.js +79 -0
  35. package/dist/commands/image-hash/sharp-loader.js.map +1 -0
  36. package/dist/commands/orphan-tests/report.js +7 -5
  37. package/dist/commands/orphan-tests/report.js.map +1 -1
  38. package/dist/commands/readme-check/report.js +7 -5
  39. package/dist/commands/readme-check/report.js.map +1 -1
  40. package/dist/commands/scripts-check/report.js +7 -5
  41. package/dist/commands/scripts-check/report.js.map +1 -1
  42. package/dist/commands/stale-ts-ignore/report.js +7 -5
  43. package/dist/commands/stale-ts-ignore/report.js.map +1 -1
  44. package/dist/commands/strip-comments/report.js +6 -4
  45. package/dist/commands/strip-comments/report.js.map +1 -1
  46. package/dist/commands/todo-report/report.js +7 -5
  47. package/dist/commands/todo-report/report.js.map +1 -1
  48. package/dist/commands/unused-deps/report.js +7 -5
  49. package/dist/commands/unused-deps/report.js.map +1 -1
  50. package/dist/format/colors.d.ts +4 -0
  51. package/dist/format/colors.js +4 -3
  52. package/dist/format/colors.js.map +1 -1
  53. package/dist/format/diff-render.js +2 -2
  54. package/dist/format/diff-render.js.map +1 -1
  55. package/dist/format/file-list.d.ts +1 -9
  56. package/dist/format/file-list.js +5 -16
  57. package/dist/format/file-list.js.map +1 -1
  58. package/dist/format/findings-list.d.ts +1 -8
  59. package/dist/format/findings-list.js +5 -14
  60. package/dist/format/findings-list.js.map +1 -1
  61. package/dist/format/style.d.ts +20 -0
  62. package/dist/format/style.js +36 -0
  63. package/dist/format/style.js.map +1 -0
  64. package/dist/format/table.d.ts +8 -0
  65. package/dist/format/table.js +44 -0
  66. package/dist/format/table.js.map +1 -0
  67. package/dist/format/vibes.d.ts +4 -1
  68. package/dist/format/vibes.js +25 -10
  69. package/dist/format/vibes.js.map +1 -1
  70. package/dist/index.d.ts +4 -0
  71. package/dist/index.js +2 -0
  72. package/dist/index.js.map +1 -1
  73. package/dist/utils/walk.d.ts +2 -0
  74. package/dist/utils/walk.js +4 -3
  75. package/dist/utils/walk.js.map +1 -1
  76. package/package.json +4 -1
package/README.md CHANGED
@@ -127,7 +127,24 @@ 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
- - **`full-check`** — runs all twelve other commands in one sweep, each in
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
131
148
  its own safe read-only mode — the three that can write to disk
132
149
  (`strip-comments`/`console-strip`/`case-check`) are always called as a
133
150
  preview, `-y`/`--fix` are never passed. One summary table: which
@@ -145,22 +162,42 @@ only previews, `-y`/`--yes` is required to actually write anything,
145
162
  `unused-deps`, `circular-imports`, `exports-doctor`, `readme-check`,
146
163
  `empty-catch`, `todo-report`, `scripts-check`, `orphan-tests`,
147
164
  `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
149
- supports `--json` for machine-readable output.
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
168
+ machine-readable output.
150
169
 
151
170
  ## Tone
152
171
 
153
172
  Not dead silent, not relentlessly jokey either — a small banner and some
154
173
  personality when everything comes back clean, nothing cute mixed into the
155
- actual findings list, which stays in aligned, colored columns (file in
156
- white, count/tag in dim grey) for quick scanning. Auto-disables (banner,
157
- color, celebration copy) the moment output isn't a real interactive
158
- terminal — piped, `CI` set, `NO_COLOR` set — on top of the explicit
159
- `--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.
160
190
 
161
191
  ## Requirements
162
192
 
163
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.
164
201
 
165
202
  ## Installation
166
203
 
@@ -210,6 +247,14 @@ devtoolz orphan-tests src # test files whose source disappe
210
247
 
211
248
  devtoolz stale-ts-ignore # find @ts-ignore comments suppressing nothing
212
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
+
213
258
  devtoolz full-check # run every command, one summary table
214
259
  devtoolz full-check --skip stale-ts-ignore # same, minus the expensive one
215
260
  ```
@@ -416,6 +461,50 @@ problems scattered across it:
416
461
  9 clean, 4 found something — see `devtoolz <command> --help` for full detail on any of them.
417
462
  ```
418
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
+
419
508
  ## Development
420
509
 
421
510
  ```bash
package/dist/cli.js CHANGED
@@ -1,5 +1,6 @@
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';
4
5
  import { createInterface } from 'node:readline/promises';
5
6
  import { runStripComments } from './commands/strip-comments/run.js';
@@ -31,6 +32,10 @@ import { runStaleTsIgnore } from './commands/stale-ts-ignore/run.js';
31
32
  import { renderStaleTsIgnoreReport } from './commands/stale-ts-ignore/report.js';
32
33
  import { runFullCheck, FULL_CHECK_COMMAND_NAMES, SLOW_FULL_CHECK_COMMAND, } from './commands/full-check/run.js';
33
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';
34
39
  function wrapText(text, width) {
35
40
  const words = text.split(' ');
36
41
  const lines = [];
@@ -156,6 +161,7 @@ program
156
161
  .option('--json', 'machine-readable output', false)
157
162
  .option('--quiet', 'suppress output when there is nothing to report', false)
158
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)
159
165
  .action((paths, options) => {
160
166
  const report = runStripComments({
161
167
  paths,
@@ -175,6 +181,7 @@ program
175
181
  const text = renderStripCommentsReport(report, {
176
182
  quiet: options.quiet,
177
183
  plain: options.plain,
184
+ color: options.color,
178
185
  dryRun: options.dryRun,
179
186
  });
180
187
  if (text)
@@ -198,6 +205,7 @@ program
198
205
  .option('--json', 'machine-readable output', false)
199
206
  .option('--quiet', 'suppress output when there is nothing to report', false)
200
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)
201
209
  .action((paths, options) => {
202
210
  const report = runConsoleStrip({
203
211
  paths,
@@ -218,6 +226,7 @@ program
218
226
  const text = renderConsoleStripReport(report, {
219
227
  quiet: options.quiet,
220
228
  plain: options.plain,
229
+ color: options.color,
221
230
  });
222
231
  if (text)
223
232
  console.log(text);
@@ -238,6 +247,7 @@ program
238
247
  .option('--json', 'machine-readable output', false)
239
248
  .option('--quiet', 'suppress output when there is nothing to report', false)
240
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)
241
251
  .action((paths, options) => {
242
252
  const report = runDeadExports({
243
253
  paths,
@@ -253,7 +263,11 @@ program
253
263
  console.log(JSON.stringify(report, null, 2));
254
264
  }
255
265
  else {
256
- 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
+ });
257
271
  if (text)
258
272
  console.log(text);
259
273
  }
@@ -271,6 +285,7 @@ program
271
285
  .option('--json', 'machine-readable output', false)
272
286
  .option('--quiet', 'suppress output when there is nothing to report', false)
273
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)
274
289
  .action((paths, options) => {
275
290
  const report = runCircularImports({
276
291
  paths,
@@ -287,6 +302,7 @@ program
287
302
  const text = renderCircularImportsReport(report, {
288
303
  quiet: options.quiet,
289
304
  plain: options.plain,
305
+ color: options.color,
290
306
  });
291
307
  if (text)
292
308
  console.log(text);
@@ -305,6 +321,7 @@ program
305
321
  .option('--json', 'machine-readable output', false)
306
322
  .option('--quiet', 'suppress output when there is nothing to report', false)
307
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)
308
325
  .action((dir, options) => {
309
326
  const report = runUnusedDeps({
310
327
  dir: resolve(dir),
@@ -318,7 +335,11 @@ program
318
335
  console.log(JSON.stringify(report, null, 2));
319
336
  }
320
337
  else {
321
- 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
+ });
322
343
  if (text)
323
344
  console.log(text);
324
345
  }
@@ -340,6 +361,7 @@ program
340
361
  .option('--json', 'machine-readable output', false)
341
362
  .option('--quiet', 'suppress output when there is nothing to report', false)
342
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)
343
365
  .action((paths, options) => {
344
366
  const report = runCaseCheck({
345
367
  paths,
@@ -357,7 +379,11 @@ program
357
379
  console.log(JSON.stringify(report, null, 2));
358
380
  }
359
381
  else {
360
- 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
+ });
361
387
  if (text)
362
388
  console.log(text);
363
389
  }
@@ -370,13 +396,18 @@ program
370
396
  .option('--json', 'machine-readable output', false)
371
397
  .option('--quiet', 'suppress output when there is nothing to report', false)
372
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)
373
400
  .action((dir, options) => {
374
401
  const report = runExportsDoctor({ dir: resolve(dir) });
375
402
  if (options.json) {
376
403
  console.log(JSON.stringify(report, null, 2));
377
404
  }
378
405
  else {
379
- 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
+ });
380
411
  if (text)
381
412
  console.log(text);
382
413
  }
@@ -392,6 +423,7 @@ program
392
423
  .option('--json', 'machine-readable output', false)
393
424
  .option('--quiet', 'suppress output when there is nothing to report', false)
394
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)
395
427
  .action((dir, options) => {
396
428
  const report = runReadmeCheck({
397
429
  dir: resolve(dir),
@@ -403,7 +435,11 @@ program
403
435
  console.log(JSON.stringify(report, null, 2));
404
436
  }
405
437
  else {
406
- 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
+ });
407
443
  if (text)
408
444
  console.log(text);
409
445
  }
@@ -420,6 +456,7 @@ program
420
456
  .option('--json', 'machine-readable output', false)
421
457
  .option('--quiet', 'suppress output when there is nothing to report', false)
422
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)
423
460
  .action((paths, options) => {
424
461
  const report = runEmptyCatch({
425
462
  paths,
@@ -432,7 +469,11 @@ program
432
469
  console.log(JSON.stringify(report, null, 2));
433
470
  }
434
471
  else {
435
- 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
+ });
436
477
  if (text)
437
478
  console.log(text);
438
479
  }
@@ -451,6 +492,7 @@ program
451
492
  .option('--json', 'machine-readable output', false)
452
493
  .option('--quiet', 'suppress output when there is nothing to report', false)
453
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)
454
496
  .action((paths, options) => {
455
497
  const report = runTodoReport({
456
498
  paths,
@@ -465,7 +507,11 @@ program
465
507
  console.log(JSON.stringify(report, null, 2));
466
508
  }
467
509
  else {
468
- 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
+ });
469
515
  if (text)
470
516
  console.log(text);
471
517
  }
@@ -479,6 +525,7 @@ program
479
525
  .option('--json', 'machine-readable output', false)
480
526
  .option('--quiet', 'suppress output when there is nothing to report', false)
481
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)
482
529
  .action((dir, options) => {
483
530
  const report = runScriptsCheck({ dir: resolve(dir), files: options.file });
484
531
  if (options.json) {
@@ -488,6 +535,7 @@ program
488
535
  const text = renderScriptsCheckReport(report, {
489
536
  quiet: options.quiet,
490
537
  plain: options.plain,
538
+ color: options.color,
491
539
  });
492
540
  if (text)
493
541
  console.log(text);
@@ -509,6 +557,7 @@ program
509
557
  .option('--json', 'machine-readable output', false)
510
558
  .option('--quiet', 'suppress output when there is nothing to report', false)
511
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)
512
561
  .action((paths, options) => {
513
562
  const report = runOrphanTests({
514
563
  paths,
@@ -525,7 +574,11 @@ program
525
574
  console.log(JSON.stringify(report, null, 2));
526
575
  }
527
576
  else {
528
- 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
+ });
529
582
  if (text)
530
583
  console.log(text);
531
584
  }
@@ -541,6 +594,7 @@ program
541
594
  .option('--json', 'machine-readable output', false)
542
595
  .option('--quiet', 'suppress output when there is nothing to report', false)
543
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)
544
598
  .action((dir, options) => {
545
599
  const report = runStaleTsIgnore({
546
600
  dir: resolve(dir),
@@ -556,12 +610,129 @@ program
556
610
  const text = renderStaleTsIgnoreReport(report, {
557
611
  quiet: options.quiet,
558
612
  plain: options.plain,
613
+ color: options.color,
559
614
  });
560
615
  if (text)
561
616
  console.log(text);
562
617
  }
563
618
  process.exitCode = report.exitCode;
564
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
+ });
565
736
  // Only `full-check` ever prompts — every other command here is a single,
566
737
  // fast, non-interactive check. `stale-ts-ignore` is the one command a
567
738
  // bare `full-check` can turn into a genuinely slow run, so this asks
@@ -606,6 +777,7 @@ program
606
777
  .option('--json', 'machine-readable output', false)
607
778
  .option('--quiet', 'suppress output when there is nothing to report', false)
608
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)
609
781
  .action(async (dir, options) => {
610
782
  const skip = new Set(options.skip);
611
783
  const shouldPrompt = options.prompt &&
@@ -634,7 +806,11 @@ program
634
806
  console.log(JSON.stringify(report, null, 2));
635
807
  }
636
808
  else {
637
- const text = renderFullCheckReport(report, { quiet: options.quiet, plain: options.plain });
809
+ const text = renderFullCheckReport(report, {
810
+ quiet: options.quiet,
811
+ plain: options.plain,
812
+ color: options.color,
813
+ });
638
814
  if (text)
639
815
  console.log(text);
640
816
  }