--json output
The machine-readable envelope every command can print to stdout.
Pass --json on any command and the tool prints exactly one JSON document to stdout, newline
terminated — on every exit, success or failure, including a usage mistake that never reached a
file — nothing else ever touches stdout under --json. Every human-facing line (progress,
the final summary, missing-file reports, the target-unreachable suggestion sentence) goes to
stderr instead, which is what makes this work:
imagekbreducer compress a.jpg --to 50kb --json | jq -r '.results[0].output'
Envelope shape
interface Envelope {
version: 1
ok: boolean // results.every(r => r.ok) -- never tracked separately, so it can't drift
results: FileResult[]
error?: { code: "usage" | "internal"; message: string } // invocation-level failures only
exitCode?: number // ditto -- the process's own code
}
results has one entry per input the invocation named — including one that was never found.
A missing input becomes its own failure entry rather than just a stderr line; without that, a run
that failed to find one of three files could report "ok": true (an empty check vacuously passes),
which is exactly the shape a --json | jq -e .ok pipeline would misread as success.
An invocation that never reached a file
A usage mistake (exit 2) or an unexpected internal error (exit 1) happens before any input is
processed, so there is no FileResult to carry it. --json still prints its one document — with
results empty, ok stated as false literally rather than derived ([].every(...) is true,
the vacuous-success shape this envelope exists to avoid), and the failure described at the top
level where it actually happened:
{
"version": 1,
"ok": false,
"exitCode": 2,
"error": {
"code": "usage",
"message": "--to is required for compress"
},
"results": []
}
error.code is "usage" for exit 2 and "internal" for exit 1 — the only two values, because
those are the only two failures that can happen with no file in hand. error and exitCode are
absent, never null, on every run that reached at least one input; check for the key. The
human sentence still goes to stderr as always — --json moves the machine-readable channel to
stdout, it has never silenced stderr.
A successful file
{
"input": "shots/a.png",
"output": "dist/a-compressed.png",
"ok": true,
"dryRun": false,
"format": "png",
"width": 1200,
"height": 800,
"originalBytes": 3145728,
"resultBytes": 49812,
"targetBytes": 51200,
"quality": null,
"encodes": 5,
"warnings": [{ "code": "metadata-stripped", "message": "…" }]
}
targetBytesisnull, not omitted, whenever the command had no byte target at all —compressalways has one (--tois required),resize/convertonly have one when--towas given, andcrop/rotatenever take a--toflag and are alwaysnull.qualityisnull, not omitted, for a format with no scalar quality knob — PNG, above.acceptedClosest, present andtrueonly when--accept-closestturned an otherwise-failedtarget-unreachableresult into a written success. Absent (neverfalse) on every ordinary success — check for the key, not its value.resultBytesis the real size of the file written to disk, not merely what the encoder claimed.
A failed file
{
"input": "shots/b.jpg",
"ok": false,
"exitCode": 10,
"error": {
"code": "target-unreachable",
"reason": "FIXED_DIMENSIONS",
"message": "…"
},
"closestBytes": 58203,
"suggestions": [{ "kind": "allow-downscale" }, { "kind": "accept-closest", "bytes": 58203 }],
"partialWritten": false
}
error.code is usually one of the engine’s own ErrorCodes (see Exit codes),
but three values are CLI-side, not engine codes:
error.code |
Where it comes from |
|---|---|
"not-found" |
an input expandInputs couldn’t turn into a real file — a literal path that doesn’t exist, or a glob that matched nothing |
"would-overwrite" |
the computed output already existed and --force/--in-place wasn’t given |
"usage" |
a usage mistake only discoverable per-file, after the file’s own sniffed format is known — today, only strip on a webp missing --reencode, or on a heic source at all |
closestBytes and suggestions are only meaningful for target-unreachable; every other failure
leaves suggestions as an empty array. partialWritten is always false — the one case that
writes a partial file (--accept-closest) is reported as a success entry instead, since it is a
successful write, just not of the winning candidate.
info --json
info prints a third shape under the same envelope, discriminated by "kind": "info" rather than
SuccessResult/FailureResult’s ok:
{
"kind": "info",
"input": "photo.jpg",
"ok": true,
"format": "jpeg",
"width": 800,
"height": 600,
"bytes": 245123,
"orientation": 6,
"hasAlpha": false,
"isAnimated": false,
"hasExif": true,
"hasGps": false,
"hasIptc": false,
"hasXmp": false
}
info never has an output, dryRun, or encoded format-as-output-format — it writes nothing, so
reusing SuccessResult’s shape would either fabricate those fields or make them lie by omission.
Any field inspect() didn’t determine is omitted from this object entirely, never printed as a
fabricated false.
--dry-run --json
The envelope’s byte counts are still the real numbers a real write would have produced —
dryRun: true is the only signal that nothing was actually written.