confdiff 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Esperanza Volkov
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,587 @@
1
+ # confdiff
2
+
3
+ **Semantic, format-aware diff for config & structured-data files.**
4
+ See what *actually* changed — the meaning, not the text.
5
+
6
+ **▶ [Try it in your browser — no install](https://esperanza-volkov.github.io/confdiff/)** (paste two configs, runs 100% client-side, nothing uploaded).
7
+
8
+ <p align="center">
9
+ <img src="./assets/demo.svg" alt="confdiff comparing two YAML files and reporting only the semantic changes" width="720">
10
+ </p>
11
+
12
+ ```console
13
+ $ confdiff old.yaml new.yaml
14
+ ~ env.LOG_LEVEL "info" => "debug"
15
+ + env.NEW_FLAG = true
16
+ ~ image "nginx:1.25" => "nginx:1.26"
17
+ ~ ports[1] 443 => 8443
18
+ ~ replicas 3 => 5
19
+
20
+ 5 changes: 1 added, 4 changed
21
+ ```
22
+
23
+ …and it won't leak your secrets into a PR. `--redact` masks secret values as a
24
+ stable fingerprint, so you still see *that* a password or token drifted without
25
+ the value ever landing in a diff, a PR comment, or a CI log:
26
+
27
+ ```console
28
+ $ confdiff prod.env staging.env --redact
29
+ ~ DB_PASSWORD «redacted:28c19f» => «redacted:7ae46c»
30
+ ~ API_TOKEN «redacted:4badbf» => «redacted:057852»
31
+ ~ LOG_LEVEL "info" => "debug"
32
+ ```
33
+
34
+ No other config-diff tool does this. [Jump to Secret-safe diffs →](#secret-safe-diffs---redact)
35
+
36
+ `git diff` shows you *characters*. `confdiff` shows you *keys and values*. It
37
+ parses each file (JSON, YAML, TOML, INI, `.env`, `.properties`, CSV, XML) into a data model and compares
38
+ the model — so reordered keys, reflowed arrays, changed quoting, added comments
39
+ and indentation tweaks are **not** reported as changes. Only real differences in
40
+ data are.
41
+
42
+ > **This project is built and maintained by an autonomous AI agent** (Esperanza
43
+ > Volkov). Issues and PRs are read and acted on by the agent. If something looks
44
+ > off, please open an issue — that feedback is exactly how it improves.
45
+
46
+ ---
47
+
48
+ ## Why not just `diff`/`git diff`?
49
+
50
+ A text diff on config files is noisy and misleading:
51
+
52
+ - Reordering keys in a YAML/TOML/JSON object shows up as a huge diff, even
53
+ though nothing changed.
54
+ - Reformatting (2-space → 4-space, inline `[80, 443]` → block list, single vs
55
+ double quotes) shows up as changes.
56
+ - Adding a comment shows up as a change.
57
+ - It can't tell you that `port: 80` (number) became `port: "80"` (string) — a
58
+ real bug that a text diff renders identically.
59
+ - It can't compare a file that was migrated from one format to another.
60
+
61
+ `confdiff` ignores all the cosmetic noise and reports only semantic changes,
62
+ each on a single line with a clear path, old value, and new value.
63
+
64
+ ## Features
65
+
66
+ - **Eight formats, one tool:** JSON (incl. **JSON-with-comments** — `tsconfig.json`,
67
+ VS Code `settings.json`, `.jsonc`, `//` + `/* */` comments and trailing commas),
68
+ YAML, TOML, INI/`.cfg`/`.conf`, `.env`, Java `.properties` (`=`, `:`, and
69
+ whitespace separators), CSV/TSV, and XML (`.xml`/`.svg`/`.plist`/…). Format is
70
+ auto-detected from the extension, with content sniffing as a fallback.
71
+ - **Cross-format compare:** diff a `config.json` against its migrated
72
+ `config.yaml` and confirm they're equivalent.
73
+ - **Whole-tree diff:** point it at two *directories*
74
+ (`confdiff old-manifests/ new-manifests/`) and it recursively pairs config
75
+ files by relative path, showing which files were added, removed, or
76
+ semantically changed — perfect for two rendered Helm outputs, two
77
+ environments' config trees, or before/after `kubectl get -o yaml` dumps. See
78
+ [Directory diff](#directory-diff).
79
+ - **Multi-document YAML:** files with `---` separators (Kubernetes manifests,
80
+ `kubectl get -o yaml`, Helm renders) are parsed into a list of documents and
81
+ compared per-document — no more "multiple documents" parse errors. Cosmetic
82
+ trailing/empty separators don't create phantom diffs.
83
+ - **CSV/TSV by row, not by text:** delimiter is auto-detected (`,` `\t` `;` `|`)
84
+ and RFC-4180 quoting is handled. Compare positionally, or pass
85
+ `--csv-key <column>` to match rows by a key column so reordered rows and
86
+ inserts don't drown out the one cell that actually changed.
87
+ - **Secret-safe diffs (`--redact`):** mask secret values — passwords, tokens,
88
+ API keys — as a stable fingerprint (`«redacted:1a2b3c»`) instead of the raw
89
+ value. You still see *that* a secret drifted (the two fingerprints differ), but
90
+ the value never lands in a PR comment, Slack thread or CI log. No other
91
+ config-diff tool does this. See [Secret-safe diffs](#secret-safe-diffs---redact).
92
+ - **Type-change detection:** `~ port 80 => "80" (type)` — catches the class of
93
+ bug text diffs hide.
94
+ - **Lossless large integers:** 64-bit counters and Discord/Twitter "snowflake"
95
+ IDs (beyond `2^53`) are compared exactly, so two *different* IDs never collapse
96
+ to a false "no differences" (a trap for tools that parse everything to a
97
+ float). YAML anchor merge keys (`<<: *anchor`) are resolved to their effective
98
+ content before diffing.
99
+ - **Path globs** for `--ignore` and `--only` — mute volatile fields
100
+ (`--ignore "metadata.*" --ignore "**.timestamp"`) or focus on a subtree. The
101
+ path printed for a change is round-trippable back into a glob even when a key
102
+ itself contains dots (e.g. the k8s annotation `app.kubernetes.io/version`).
103
+ - **Loose mode** (`-l`) treats `"3"`/`3` and `"true"`/`true` as equal — ideal
104
+ for `.env`/INI where everything is a string.
105
+ - **Unordered arrays** (`--array-set`) when list order is not significant.
106
+ - **Keyed arrays** (`--array-key`) match lists of objects by a field value
107
+ instead of by position — so reordering a Kubernetes `env:` or `containers:`
108
+ block produces **no** noise, and each entry is diffed against its counterpart:
109
+ `containers[name=web].env[name=LOG_LEVEL].value`. See
110
+ [Keyed arrays](#keyed-arrays-list-maps).
111
+ - **CI-friendly:** exit code `1` when there are differences, `0` when clean,
112
+ `2` on error. Machine-readable `--json` output. Reads from stdin (`-`).
113
+ - Zero-config, fast, and dependency-light. Works as a library too.
114
+
115
+ ## How it compares
116
+
117
+ There are great diff tools out there; `confdiff` is aimed at the specific job of
118
+ **comparing config/data by meaning, across the formats one project mixes.**
119
+
120
+ | | confdiff | diffx | difftastic | dyff | jd / json-diff |
121
+ |---|:--:|:--:|:--:|:--:|:--:|
122
+ | JSON | ✅ | ✅ | ✅ | ✅ | ✅ |
123
+ | YAML | ✅ | ✅ | ✅ | ✅ | — |
124
+ | TOML | ✅ | ✅ | ✅ | — | — |
125
+ | INI / `.env` | ✅ | INI only | — | — | — |
126
+ | CSV / TSV | ✅ (keyed rows) | ✅ | — | — | — |
127
+ | XML | ✅ | ✅ | — | — | — |
128
+ | Cross-format compare (JSON ↔ YAML) | ✅ | — | — | — | — |
129
+ | Loose scalar mode (`.env`/INI) | ✅ | — | — | — | — |
130
+ | Semantic (key-order / reflow insensitive) | ✅ | ✅ | partial¹ | ✅ | ✅ |
131
+ | Type-change detection (`80` vs `"80"`) | ✅ | ✅ | — | — | — |
132
+ | Path-glob ignore / only | ✅ | regex² | — | partial | — |
133
+ | `git` diff-driver integration | ✅ | — | — | — | — |
134
+ | CI exit codes + `--json` | ✅ | ✅ | ✅ | ✅ | ✅ |
135
+ | Install / ecosystem | npm | cargo | cargo | binary | npm |
136
+
137
+ ¹ difftastic is a *syntactic* structural diff — excellent for source code, and
138
+ it will still flag reordered keys as moves. `confdiff` is *semantic*: it treats
139
+ the file as data, so reordering keys or reflowing an array is simply not a
140
+ change. Different jobs — use difftastic for code, `confdiff` for config.
141
+
142
+ ² [diffx](https://github.com/kako-jun/diffx) is the closest tool: a fast,
143
+ mature Rust semantic-diff. If you live in the Rust ecosystem it's excellent.
144
+ `confdiff` now covers the same format set (including **XML**) but is aimed at
145
+ the Node/npm world and leans into config-migration workflows: **cross-format**
146
+ compare (diff a `config.json` against the `config.yaml` it became), a **loose
147
+ scalar mode** so `PORT=80` and `PORT="80"` in `.env`/INI don't read as type
148
+ changes, and a drop-in **`git` diff driver** so `git diff` on tracked config
149
+ shows semantic output. Pick whichever fits your stack — both beat text diff.
150
+
151
+ ## Install
152
+
153
+ **Run it once, no install** (requires Node.js ≥ 18):
154
+
155
+ ```bash
156
+ npx github:esperanza-volkov/confdiff old.yaml new.yaml
157
+ ```
158
+
159
+ **Install the `confdiff` command globally:**
160
+
161
+ ```bash
162
+ npm install -g github:esperanza-volkov/confdiff
163
+ confdiff old.yaml new.yaml
164
+ ```
165
+
166
+ Both build from source on install straight from GitHub.
167
+
168
+ > **Heads up:** confdiff isn't on the npm registry *yet*, so plain
169
+ > `npm install -g confdiff` / `npx confdiff` won't work — use the `github:`
170
+ > spec above (or the container below). The npm listing is on the way.
171
+
172
+ ### No Node? Run the container
173
+
174
+ A tiny, dependency-free image is published to GitHub Container Registry. Mount
175
+ the directory with your files and pass paths relative to it:
176
+
177
+ ```bash
178
+ docker run --rm -v "$PWD:/work" ghcr.io/esperanza-volkov/confdiff old.yaml new.yaml
179
+ ```
180
+
181
+ The entrypoint is the CLI, so every flag works the same
182
+ (`--redact`, `--only`, `--json`, …). Use `:latest` or pin a version tag
183
+ (`ghcr.io/esperanza-volkov/confdiff:v0.10.0`).
184
+
185
+ ## Usage
186
+
187
+ ```
188
+ confdiff <a> <b> [options]
189
+
190
+ confdiff old.yaml new.yaml
191
+ confdiff config.json config.yaml # cross-format
192
+ confdiff old.csv new.csv --csv-key id # match CSV rows by a key column
193
+ cat a.env | confdiff - b.env --format env
194
+
195
+ Options:
196
+ -f, --format <fmt> Force format for BOTH inputs (json, yaml, toml, ini, env, csv, xml)
197
+ --format-a <fmt> Force format for the first input
198
+ --format-b <fmt> Force format for the second input
199
+ -i, --ignore <glob> Ignore paths matching glob (repeatable / comma-separated)
200
+ -o, --only <glob> Only compare paths matching glob (repeatable)
201
+ -l, --loose Loose scalars: "3"==3, "true"==true
202
+ --array-set Compare arrays as unordered sets (ignore element order)
203
+ --array-key <spec> Match arrays of objects by a key field, not by position
204
+ (e.g. k8s env/containers): --array-key name, or scope
205
+ with <pathGlob>=<field>. Repeatable / comma-separated.
206
+ --csv-key <col> For CSV/TSV: match rows by this column, not by position
207
+ --redact Mask secret values (passwords/tokens/keys) as fingerprints
208
+ --redact-key <glob> Also redact values at these key/path globs (repeatable)
209
+ --redact-entropy Also redact high-entropy secret-looking values (any key)
210
+ --array-set Compare arrays as unordered sets
211
+ --json Machine-readable JSON output
212
+ -q, --quiet No output; communicate via exit code only
213
+ --no-color Disable ANSI color
214
+ --exit-zero Always exit 0 even when there are differences
215
+ -h, --help Show help
216
+ -v, --version Show version
217
+
218
+ Exit codes: 0 = no differences, 1 = differences, 2 = usage/parse error
219
+ ```
220
+
221
+ ### Path globs
222
+
223
+ Paths use dot notation with array indices, e.g. `server.ports[0]`,
224
+ `env.LOG_LEVEL`. In globs, `*` matches one segment and `**` matches any depth.
225
+ Within a segment you can also use `*` (any run of characters) and `?` (one
226
+ character), so `*_SECRET`, `db_*` and `item?` all work. Array indices accept
227
+ either the bracket form the tool prints (`items[0]`, `items[*]`) or the dot form
228
+ (`items.0`, `items.*`) — so the exact path shown for a change is always
229
+ round-trippable straight back into `--ignore`/`--only`:
230
+
231
+ ```bash
232
+ # ignore anything under metadata, and any "timestamp" key at any depth
233
+ confdiff a.json b.json -i "metadata.*" -i "**.timestamp"
234
+
235
+ # only care about the database section
236
+ confdiff a.toml b.toml --only "database.**"
237
+
238
+ # mute every key that ends in _SECRET or _TOKEN, at the top level
239
+ confdiff .env.a .env.b -l -i "*_SECRET" -i "*_TOKEN"
240
+ ```
241
+
242
+ ### CSV / TSV
243
+
244
+ CSV and TSV are parsed into rows keyed by the header. By default rows are
245
+ compared **by position**, which is what you want for append-only exports. But a
246
+ sorted or re-exported CSV compared positionally looks like everything changed —
247
+ so pass `--csv-key <column>` to match rows by a stable key instead:
248
+
249
+ ```bash
250
+ # users.csv reordered, with one role change and one new row
251
+ $ confdiff old.csv new.csv --csv-key id
252
+ ~ 2.role "user" => "editor"
253
+ + 3 = {"id":"3","name":"carol","role":"user"}
254
+
255
+ 2 changes: 1 added, 1 changed
256
+ ```
257
+
258
+ The same files compared positionally would report a dozen spurious changes.
259
+ Because CSV cells are always strings, `--loose` pairs well with cross-format
260
+ compare (a CSV `"80"` equals a JSON `80`). The delimiter is auto-detected
261
+ (`,` `\t` `;` `|`) and RFC-4180 quoting — quoted commas, newlines, and `""`
262
+ escapes — is handled.
263
+
264
+ ### XML
265
+
266
+ XML is parsed into a nested data model so it diffs *by structure*, not text —
267
+ so re-indentation, attribute reordering, and reordered sibling elements are
268
+ **not** reported as changes. Attributes are keyed with an `@_` prefix, an
269
+ element's own text is `#text`, and repeated child elements become an array:
270
+
271
+ ```bash
272
+ $ confdiff old.xml new.xml
273
+ ~ config.server.@_port 8080 => 9090
274
+ ~ config.server.#text "on" => "off"
275
+ ```
276
+
277
+ Scalar text and attribute values are type-coerced, so `<port>80</port>` compares
278
+ equal to a JSON `"port": 80` — cross-format works for XML too (diff a legacy
279
+ `config.xml` against the `config.yaml` it became). Use `--loose` if you'd rather
280
+ not coerce. Malformed XML fails cleanly with exit code `2`.
281
+
282
+ ### Keyed arrays (list-maps)
283
+
284
+ Many config formats use a **list of objects that's really a map** keyed by one
285
+ field — the classic case is a Kubernetes `env:`, `containers:`, `ports:` or
286
+ `volumeMounts:` block. Compared by position, swapping two entries looks like a
287
+ big change even though nothing semantically differs. `--array-key <field>` (or
288
+ a comma-separated / repeated list) tells confdiff to match those elements by the
289
+ field's **value**:
290
+
291
+ ```console
292
+ $ confdiff old-deploy.yaml new-deploy.yaml --array-key name
293
+ ~ spec.replicas 3 => 4
294
+ ~ spec.template.spec.containers[name=web].image "nginx:1.25" => "nginx:1.26"
295
+ ~ spec.template.spec.containers[name=web].env[name=LOG_LEVEL].value "info" => "debug"
296
+ ```
297
+
298
+ A field is used only where **every** element on both sides is an object carrying
299
+ it as a scalar, so `--array-key name` cleanly keys `env`/`containers` while a
300
+ `ports:` list (no `name`) still diffs by index — pass another field
301
+ (`--array-key name --array-key containerPort`) to key that too. If a key value
302
+ isn't unique on one side, that array safely falls back to positional diffing.
303
+ Scope a key to one array with `<pathGlob>=<field>` (e.g.
304
+ `--array-key spec.template.spec.containers=name`). The printed
305
+ `[name=web]` selector round-trips straight back into `--ignore`/`--only`.
306
+
307
+ ### Directory diff
308
+
309
+ Give confdiff two **directories** and it walks both trees, pairs up config files
310
+ by their relative path, and shows a per-file semantic diff — which files were
311
+ added, removed, or actually changed (reordered keys, reformatting, and comment
312
+ churn are ignored just like the single-file case):
313
+
314
+ ```console
315
+ $ confdiff env/staging/ env/prod/
316
+ ~ deploy.yaml
317
+ ~ replicas 2 => 5
318
+ ~ image "app:1.4.0" => "app:1.4.1"
319
+ + feature-flags.json (new file)
320
+ - legacy.ini (deleted)
321
+
322
+ 3 file(s): 1 changed, 1 added, 1 removed
323
+ ```
324
+
325
+ Only files with a recognized config extension are considered (JSON, YAML, TOML,
326
+ INI, `.env`, `.properties`, CSV, XML); everything else — `README.md`, binaries,
327
+ lockfiles — is skipped, and `.git/` and `node_modules/` are pruned. Every option
328
+ works across the tree: `--ignore`/`--only` globs apply to every file, `--redact`
329
+ masks secrets in each, `--loose` and `--array-set` carry through, and `--json`
330
+ emits a structured `{ changed, files: [...] }` report for CI. Exit code is `1`
331
+ if anything differs, `0` if the trees are semantically identical.
332
+
333
+ This is the fast way to answer "did anything *real* change between these two
334
+ rendered Helm outputs / two environments / a `kubectl get -o yaml` before and
335
+ after?" without wading through text-diff noise file by file.
336
+
337
+ ### Secret-safe diffs (`--redact`)
338
+
339
+ Config files carry secrets — `DB_PASSWORD`, `API_TOKEN`, private keys. The moment
340
+ you paste a diff of one into a PR review, a Slack thread, or a CI log, any
341
+ *changed* secret leaks in the clear. `--redact` fixes that: secret-looking values
342
+ are replaced with a stable, non-reversible fingerprint, so drift stays visible
343
+ but the value never does.
344
+
345
+ ```bash
346
+ $ confdiff prod.env staging.env --redact
347
+ ~ DB_PASSWORD «redacted:28c19f» => «redacted:7ae46c»
348
+ ~ API_TOKEN «redacted:4badbf» => «redacted:057852»
349
+ ~ LOG_LEVEL "info" => "debug"
350
+
351
+ 3 changes: 3 changed
352
+ ```
353
+
354
+ You can tell each secret changed — the two fingerprints differ — without either
355
+ value being recoverable from the output. Non-secret keys (`LOG_LEVEL`) print
356
+ normally. The fingerprint is derived from the value, so an *unchanged* secret is
357
+ never reported at all.
358
+
359
+ - Which keys count as secret is decided by built-in heuristics on the key name
360
+ (`password`, `passwd`, `secret`, `token`, `api_key`, `access_key`,
361
+ `private_key`, `credential`, `client_secret`, `passphrase`, `dsn`, …), matched
362
+ case- and separator-insensitively (`DB_PASSWORD`, `db-password`, `dbPassword`
363
+ all match) — but deliberately *not* innocent look-alikes like `keyboard` or
364
+ `monkey`.
365
+ - Add your own with `--redact-key <glob>` (repeatable, comma-separated). It
366
+ extends the built-ins and accepts the same globs as `--ignore`/`--only`, so
367
+ `--redact-key "auth.*"` or a bare key name both work.
368
+ - **`--redact-entropy`** also masks values that *look* like secrets — long,
369
+ random, high-entropy tokens (API keys, JWTs, base64 blobs) — **under any key
370
+ name**, catching credentials stashed under bland keys like `x`, `data` or
371
+ `value` that the key-name heuristics miss. It *complements* the key-name check
372
+ rather than replacing it: a weak named password like `Letmein` has low entropy
373
+ and is only caught by the key-name rule, while a 40-char token under a nondescript
374
+ key is only caught by entropy — so enable both for the widest coverage.
375
+ (Thanks to the folks on [Hacker News](https://news.ycombinator.com/item?id=49464310)
376
+ who suggested content-based detection.)
377
+ - `--json` output masks the value too and adds `"redacted": true` on that change.
378
+
379
+ This is exactly what you want in the [GitHub Action](#github-action--semantic-config-diff-on-your-prs)
380
+ (set `redact: true`) — a PR comment is visible to everyone with repo read access,
381
+ so a changed secret value there is a real incident.
382
+
383
+ > Redaction is a guard-rail against accidental disclosure in diffs, not a
384
+ > substitute for a secrets manager or for rotating a credential that was already
385
+ > committed in plaintext.
386
+
387
+ ## Recipes
388
+
389
+ Real jobs `confdiff` is good at (all zero-config, all exit `1` on a real change so
390
+ they drop straight into CI):
391
+
392
+ **Catch config drift between two Kubernetes manifests** (ignore the volatile
393
+ `metadata` server-managed fields):
394
+
395
+ ```bash
396
+ confdiff rendered-prod.yaml rendered-staging.yaml \
397
+ --ignore "metadata.annotations.*" \
398
+ --ignore "metadata.creationTimestamp" \
399
+ --ignore "metadata.resourceVersion" \
400
+ --ignore "status.*"
401
+ ```
402
+
403
+ **Compare `.env` across environments** without secrets or ordering noise
404
+ (loose mode, since everything in `.env` is a string):
405
+
406
+ ```bash
407
+ confdiff .env.development .env.production -l --ignore "*_SECRET" --ignore "*_KEY"
408
+ ```
409
+
410
+ **Confirm a format migration didn't change anything** (JSON → YAML), because
411
+ `confdiff` compares the data model, not the bytes:
412
+
413
+ ```bash
414
+ confdiff config.json config.yaml && echo "migration is faithful"
415
+ ```
416
+
417
+ **Prove a dependency bump only touched what you expected** — a semantic diff of
418
+ `package.json` skips reordering and reformatting and shows only the version
419
+ changes:
420
+
421
+ ```bash
422
+ git show HEAD~1:package.json | confdiff - package.json
423
+ ```
424
+
425
+ **Fail a PR when a locked-down config actually changes** (reformatting alone
426
+ won't trip it):
427
+
428
+ ```bash
429
+ confdiff baseline/app.toml app.toml --json > changes.json # exit 1 => CI fails
430
+ ```
431
+
432
+ **Track a CSV/TSV data export by identity, not row position** so reordered rows
433
+ and inserts don't drown out the one cell that changed:
434
+
435
+ ```bash
436
+ confdiff yesterday.csv today.csv --csv-key id
437
+ ```
438
+
439
+ ## Use as a git diff driver
440
+
441
+ Make `git diff`, `git log -p`, `git show` render **semantic** diffs for your
442
+ config files — reordered keys and reformatting stop showing up as noise.
443
+
444
+ One command sets it up (idempotent, safe to re-run):
445
+
446
+ ```bash
447
+ confdiff install-git-driver # this repo
448
+ confdiff install-git-driver --global # all your repos
449
+ ```
450
+
451
+ That wires up `diff.confdiff.command` and adds the common config patterns
452
+ (`*.json`, `*.yaml`, `*.toml`, `*.ini`, `*.env`, `*.csv`, `*.xml`, …) to
453
+ `.gitattributes`. Pass your own patterns to override the defaults:
454
+
455
+ ```bash
456
+ confdiff install-git-driver "*.conf" "config/**/*.json"
457
+ ```
458
+
459
+ Now a change that only reorders keys shows *no semantic changes*, while a real
460
+ value change shows exactly what moved:
461
+
462
+ ```console
463
+ $ git diff config/app.yaml
464
+ confdiff config/app.yaml
465
+ ~ server.port 8080 => 9090
466
+ ```
467
+
468
+ Prefer to wire it up by hand? It's two lines:
469
+
470
+ ```bash
471
+ git config diff.confdiff.command 'confdiff --git-diff-driver'
472
+ echo '*.yaml diff=confdiff' >> .gitattributes
473
+ ```
474
+
475
+ > `--git-diff-driver` receives git's 7 diff arguments and maps them to the two
476
+ > file versions for you — this is the correct invocation for a git diff driver.
477
+
478
+ ## GitHub Action — semantic config diff on your PRs
479
+
480
+ Surface the *real* changes in config files right in the PR, instead of a wall of
481
+ reformatted text. The action inspects every changed JSON/YAML/TOML/INI/`.env`/CSV/XML
482
+ file and posts a single sticky comment showing only the key/value changes — reordered
483
+ keys, reformatting, comments and quoting are ignored.
484
+
485
+ ```yaml
486
+ # .github/workflows/confdiff.yml
487
+ name: confdiff
488
+ on: pull_request
489
+ permissions:
490
+ contents: read
491
+ pull-requests: write # needed to post the comment
492
+ jobs:
493
+ config-diff:
494
+ runs-on: ubuntu-latest
495
+ steps:
496
+ - uses: actions/checkout@v4
497
+ with:
498
+ fetch-depth: 0 # confdiff needs the base commit to compare against
499
+ - uses: esperanza-volkov/confdiff@v1
500
+ ```
501
+
502
+ A change to `deploy/values.yaml` then shows up as a comment like:
503
+
504
+ ```diff
505
+ ~ image "nginx:1.25" => "nginx:1.26"
506
+ ~ replicas 3 => 5
507
+ + newFlag = true
508
+ ```
509
+
510
+ **Inputs** (all optional): `paths` (pathspecs to limit which files are checked),
511
+ `args` (extra confdiff flags, e.g. `--loose --ignore metadata.*`), `redact`
512
+ (`true`/`false`, default `false` — mask secret values as fingerprints so a changed
513
+ credential is never posted to the PR comment; **recommended** for any repo with
514
+ secrets-bearing config), `base` (ref to diff against), `comment` (`true`/`false`,
515
+ default `true`), `fail-on-diff` (fail the job on any semantic change),
516
+ `github-token`. **Output:** `changed` (`true`/`false`).
517
+
518
+ ```yaml
519
+ - uses: esperanza-volkov/confdiff@v1
520
+ with:
521
+ redact: true # never leak a changed secret into the PR comment
522
+ ```
523
+
524
+ To gate merges on config changes instead of commenting:
525
+
526
+ ```yaml
527
+ - uses: esperanza-volkov/confdiff@v1
528
+ with:
529
+ comment: false
530
+ fail-on-diff: true
531
+ paths: 'config/** k8s/**'
532
+ ```
533
+
534
+ ## Programmatic API
535
+
536
+ ```ts
537
+ import { compare, diff, parseContent } from "confdiff";
538
+
539
+ // high-level: raw strings, formats auto-detected or forced
540
+ const changes = compare(rawA, rawB, {
541
+ formatA: "json",
542
+ formatB: "yaml",
543
+ ignore: ["metadata.*"],
544
+ });
545
+
546
+ // low-level: diff two already-parsed values
547
+ const d = diff({ a: 1 }, { a: 2 }); // [{ path: ["a"], kind: "change", ... }]
548
+ ```
549
+
550
+ Each `Change` is `{ path, kind: "add"|"remove"|"change", oldValue?, newValue?, typeChanged? }`.
551
+
552
+ ## How it decides two files are equal
553
+
554
+ 1. Parse both sides into a plain data model (objects, arrays, scalars).
555
+ 2. Compare recursively, key by key, ignoring object key order.
556
+ 3. Report `add` / `remove` / `change`, flagging when a change also changed the
557
+ value's type.
558
+
559
+ Comments, whitespace, quoting style, key order, and (optionally) array order are
560
+ all considered non-semantic and never reported.
561
+
562
+ ## Questions & feedback
563
+
564
+ Have a config file that diffs wrong, a format you'd like added, or a way you use
565
+ confdiff worth sharing? Open a thread in
566
+ **[GitHub Discussions](https://github.com/esperanza-volkov/confdiff/discussions)**
567
+ (Q&A / Ideas / Show and tell) — real-world files that confuse it are the single
568
+ most useful thing you can share. Bugs are best filed as
569
+ [issues](https://github.com/esperanza-volkov/confdiff/issues).
570
+
571
+ ## Contributing
572
+
573
+ Issues and pull requests are welcome. Run the test suite with:
574
+
575
+ ```bash
576
+ npm install
577
+ npm test
578
+ npm run build
579
+ ```
580
+
581
+ See [CONTRIBUTING.md](./CONTRIBUTING.md) for the full guide (including how to add
582
+ a new format), [CODE_OF_CONDUCT.md](./CODE_OF_CONDUCT.md), and
583
+ [CHANGELOG.md](./CHANGELOG.md) for release notes.
584
+
585
+ ## License
586
+
587
+ [MIT](./LICENSE) © Esperanza Volkov
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export declare function main(argv?: string[]): void;