dev-diagnosis-cli 0.1.7 → 0.2.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/CHANGELOG.md CHANGED
@@ -1,6 +1,118 @@
1
1
  # Changelog
2
2
 
3
- ## 0.1.7 — unreleased
3
+ ## 0.2.0 — 2026-11-12
4
+
5
+ - New: `dev-diagnosis --mcp` serves the four checks as an MCP server
6
+ on stdio (pure Node.js standard library, no new dependencies): five
7
+ tools (`diagnose_context`, `diagnose_path`, `diagnose_cache`,
8
+ `diagnose_flakes`, `diagnose_all`), each returning the same JSON the
9
+ CLI's `--json` produces. `initialize` (protocol 2024-11-05),
10
+ `tools/list`, `tools/call` and `ping` are supported. `--sandbox
11
+ <dir>` (alias `--root`) restricts every path the tools touch to that
12
+ directory and rejects anything outside it with a plain error.
13
+ - New: `dev-diagnosis --skill-generate` writes a Claude Agent Skills
14
+ SKILL.md (`.claude/skills/dev-diagnosis/SKILL.md`, or
15
+ `~/.claude/skills/dev-diagnosis/SKILL.md` with `--global`) telling an
16
+ agent to run `dev-diagnosis all` when it starts work in a repo, fix
17
+ what FAILs, and re-run to confirm. It refuses to overwrite an
18
+ existing SKILL.md.
19
+ - New: `context` reports what the listed noise costs in tokens —
20
+ `≈ 439k tokens (estimate, ~4 bytes per token)` next to the noise
21
+ list, and `estimatedTokens` (number, or null when there is no noise)
22
+ in the JSON output. The score is unchanged.
23
+ - New: `all` ends with a one-line summary — passed/failed/skipped
24
+ counts, context noise bytes, and the cache saving when the cache
25
+ check ran — in text output and as a `summary` object in `--json`.
26
+ - New: `dev-diagnosis setup` wires the MCP server into the agents on
27
+ your machine (Claude Code, Codex CLI, Cursor, VS Code) in one
28
+ command. It prints a preview of exactly what will be written before
29
+ writing anything: a config that already mentions dev-diagnosis is
30
+ left alone, one that exists without it is merged (other entries
31
+ kept), a missing one is created, and a config it cannot parse is
32
+ left alone and reported. `--dry-run` previews without writing;
33
+ `--check` reports which agents are wired (exit 0 when all are).
34
+ - New: a GitHub Action (`action.yml` at the repo root) runs
35
+ `dev-diagnosis all` in one CI step and fails the job when a check
36
+ fails. It runs the published package, so it stays current. It lives
37
+ at the repo root because the GitHub App cannot push
38
+ `.github/workflows/*`.
39
+ - Fix: `context --fix` (and `all --fix`, and the MCP tools with
40
+ `fix: true`) used to report the pre-fix verdict — exit 1 and FAIL —
41
+ even though the fix had landed and a re-run scores clean. A fix
42
+ command now re-scans after writing `.aiignore` and reports the repo
43
+ as it stands after the fix: the new score, the verdict and the exit
44
+ code all follow the post-fix scan, and `--json` carries the fix
45
+ outcome on the post-fix result. The fix still does not mask other
46
+ failures: `all --fix` exits 1 when flakes, path or cache still fail.
47
+ (Tests 124.)
48
+ - Fix: the test suite now runs serially (`node --test
49
+ --test-concurrency=1`): the cache check times real builds, so test
50
+ files running in parallel skewed its cold-vs-warm timings and
51
+ flipped the verdict on a loaded machine. (122 tests.)
52
+ - Fix: `context --fix` also wrote review suggestions into `.aiignore`
53
+ — a secret-like file costs no score points, so the entry fixed
54
+ nothing, and a basename pattern like `secret.js` hid every
55
+ same-named file in the repo, including harmless test fixtures.
56
+ Review suggestions are now advisory only (`reviewSuggestions` in
57
+ `--json`, printed as such) and `--fix` writes only the entries that
58
+ improve the score; a re-scan after `--fix` still lists the review
59
+ items instead of silently hiding them.
60
+ - Docs: the README now opens with a 22 s terminal screencast
61
+ (`docs/demo.gif`, 92 KB): `all` on the demo repo finds ≈439k tokens
62
+ of noise, `--fix` writes one `.aiignore` entry, a re-run comes back
63
+ 94/100 PASS. Every line is verbatim CLI output captured 2026-11-12;
64
+ the render script is not in the repo (it lives with the run logs).
65
+ - Fix: the suite leaked every temp dir it created (~100 MB per run) —
66
+ after a few runs /tmp filled up and unrelated tests started failing
67
+ on file creation. Temp dirs are now removed after the test that
68
+ created them (`scripts/support.mjs`); the suite passes repeatedly
69
+ with /tmp staying clean.
70
+ - Fix: `context` listed noise that cost no points and told the user to
71
+ fix it — on a 100/100 repo it still printed "Noise to keep away"
72
+ and a `.aiignore` fix for the lockfile, contradicting its own PASS
73
+ verdict. Only noise in a category that actually lost points is now
74
+ listed or suggested (a 95 B lockfile in a 1.7 MB repo loses 0
75
+ points, so it is not something to fix); the score itself is
76
+ unchanged, and the README example now shows the shorter noise list.
77
+ - Fix: the `path` verdict said "missing in the current shell" even
78
+ when the command missed in all four shell contexts — which sounded
79
+ like it might exist somewhere else — and never said where it did
80
+ resolve when another context had it. It now says "not found in any
81
+ shell context" when nothing has it, and "missing in the current
82
+ shell, but resolves in login" (naming the contexts) when another one
83
+ does.
84
+ - Internals: the four checks moved into `lib/checks.mjs`, the one
85
+ place their logic lives — the CLI renders it, the MCP server serves
86
+ it, neither re-implements a verdict. (120 tests.)
87
+ - Docs: the README top is rewritten from the user's side — what the
88
+ noise costs in tokens, the three-step fix, one line per scenario, a
89
+ capability matrix that says what each check cannot find, and
90
+ copy-paste MCP configs for Claude Code and Codex.
91
+
92
+ ## 0.1.8 — 2026-11-12
93
+
94
+ - Fix: `all --json` embedded the build's full stdout (`coldStdout` /
95
+ `warmStdout`) in the cache check, while standalone `cache --json`
96
+ already left it out — a chatty build bloated the machine-readable
97
+ output. Both shapes are now the same: timings and verdict only. The
98
+ `--json` option also now documents `cache` (the usage line and help
99
+ text listed only `context`, `path`, `flakes` and `all`, although
100
+ standalone `cache --json` already worked).
101
+ - New: `context` now also honours `.gitignore` — files and directories
102
+ it lists are excluded from the scan, at the repo root and in nested
103
+ `.gitignore` files alike, so a repo whose build output, logs or data
104
+ dumps are already git-ignored no longer gets flagged for them, and
105
+ `--fix` no longer duplicates those entries into `.aiignore`. The
106
+ common pattern forms work (`*`, `?`, `[...]`, `**`, `/`-anchored
107
+ paths, directory-only `dir/`, `!` negation with last-match-wins);
108
+ `.git/info/exclude` and the global gitignore are not read. (82 tests.)
109
+ - Dogfood: the repo now ships a `.aiignore` excluding the demo fixture
110
+ (`examples/demo-repo/`), so `dev-diagnosis all` on a checkout passes
111
+ its own `context` check (was 77/100 FAIL, now 100/100 PASS). The
112
+ fixture is intentionally noisy test data and stays noisy when scanned
113
+ directly — `npm run example` output is unchanged.
114
+
115
+ ## 0.1.7 — 2026-11-12
4
116
 
5
117
  - Fix: `dev-diagnosis <path>` no longer errors with "unknown command" —
6
118
  the usage line promises `[all] [path]`, but a bare path was treated as a
package/README.md CHANGED
@@ -5,7 +5,47 @@
5
5
  [![license](https://img.shields.io/github/license/WJDdidi-1/dev-doctor.svg)](https://github.com/WJDdidi-1/dev-doctor/blob/main/LICENSE)
6
6
  [![node](https://img.shields.io/node/v/dev-diagnosis-cli.svg)](https://www.npmjs.com/package/dev-diagnosis-cli)
7
7
 
8
- One command that diagnoses your repo, your commands, your cache and your tests. It runs four checks that used to be four separate tools — whether an AI coding agent would read files it should not (`context`), why a CLI command is missing (`path`), whether a build cache actually pays (`cache`), and which tests are flaky (`flakes`) — and prints one report with a verdict per check. It is offline, has no runtime dependencies (Node.js standard library only), and works on Linux, macOS and Windows.
8
+ Every token your agent spends reading a 1.7 MB data dump is money out of your pocket. Every flaky test is CI time your agent sits waiting. Every missing command is a turn your agent burns for nothing. dev-diagnosis finds all three — plus whether your build cache actually saves anything — in one command, offline, with no dependencies.
9
+
10
+ ![Terminal demo: one command finds ≈439k tokens of noise, one fix clears it](docs/demo.gif)
11
+
12
+ ## Try it: three steps, one fix
13
+
14
+ ```sh
15
+ $ dev-diagnosis all
16
+ Files: 9 Size: 1.68 MB Score: 75/100 — Some context noise found
17
+ Noise to keep away from an AI agent:
18
+ data/export.json — large file (1.68 MB)
19
+ Estimated tokens wasted on this noise: ≈ 439k tokens (estimate, ~4 bytes per token)
20
+ FAIL some context noise found (score 75/100)
21
+
22
+ $ dev-diagnosis context --fix
23
+ Created .aiignore with 1 entry: data/export.json
24
+ Re-scanned after fix: Score: 94/100 — Looks clean for an AI coding agent
25
+ PASS context is clean
26
+
27
+ $ dev-diagnosis all
28
+ Files: 9 Size: 1.16 KB Score: 94/100 — Looks clean for an AI coding agent
29
+ PASS context is clean
30
+ ```
31
+
32
+ The noise cost 439k tokens a turn; the fix is one entry in `.aiignore`.
33
+
34
+ ## When you reach for it
35
+
36
+ - Just took over a repo: `dev-diagnosis all`
37
+ - CI goes red, then green on retry, then red again: `dev-diagnosis flakes ci-runs/`
38
+ - Builds feel slow even with a cache: `dev-diagnosis cache -- npm run build`
39
+ - `command not found` in the IDE but works in the terminal: `dev-diagnosis path <name>`
40
+
41
+ ## What each check does — and does not do
42
+
43
+ | Check | Needs | Finds | Cannot find |
44
+ |---|---|---|---|
45
+ | `context` | a directory | files your agent should not read, and what they cost in tokens | whether a large file is actually needed — it flags by size, you decide |
46
+ | `path` | a command name | which shell contexts resolve it, shadowed copies, toolchain installs | why a command that resolves still crashes when it runs |
47
+ | `cache` | a build command and a cache directory | whether the cache pays, cold vs warm | whether a different cache setup would be faster |
48
+ | `flakes` | JUnit XML from two or more runs | flaky, always-failing and passed-on-retry tests, ranked | why a test is flaky — only that it is |
9
49
 
10
50
  ## Install and run
11
51
 
@@ -30,9 +70,77 @@ dev-diagnosis flakes <junit-dir> # flaky tests from JUnit XML
30
70
  dev-diagnosis all [path] --command <name> --flakes <dir> -- <build-command...>
31
71
  ```
32
72
 
33
- `all` always runs `context` on `[path]` (default: the current directory) and runs `path`, `flakes` and `cache` when their input is given. Checks without their input are listed as skipped, with the command that would run them, instead of silently pretending they ran. A build command on a repo with no cache directory to measure is also listed as skipped rather than failing: nothing to measure is not a problem. `--json` gives machine-readable output for `context`, `path`, `flakes` and `all`. In `all --json`, a check that could not run (a missing runs directory, a failing build) reports `{ "error": "<reason>" }` instead of `null`, so a script can tell why the overall result failed. Add `--fix` to `context` (or `all`) to write the suggestions into `<path>/.aiignore` instead of only printing them: entries already there are left alone, nothing else in the file is touched, and a later scan skips everything `.aiignore` lists — so `context --fix` followed by `context` comes back clean.
73
+ `all` always runs `context` on `[path]` (default: the current directory) and runs `path`, `flakes` and `cache` when their input is given. Checks without their input are listed as skipped, with the command that would run them, instead of silently pretending they ran. A build command on a repo with no cache directory to measure is also listed as skipped rather than failing: nothing to measure is not a problem. `--json` gives machine-readable output for `context`, `path`, `cache`, `flakes` and `all`. In `all --json`, a check that could not run (a missing runs directory, a failing build) reports `{ "error": "<reason>" }` instead of `null`, so a script can tell why the overall result failed. Add `--fix` to `context` (or `all`) to write the suggestions into `<path>/.aiignore` instead of only printing them: entries already there are left alone, nothing else in the file is touched, and a later scan skips everything `.aiignore` lists — so `context --fix` followed by `context` comes back clean.
34
74
 
35
- Exit codes: `0` every check that ran passed; `1` a check found something to fix or could not run; `2` usage error.
75
+ `all` ends with a one-line summary: how many checks passed, failed or were skipped, how much context noise was found, and what the cache saved when it ran. In `--json` output the same fields are in the `summary` object.
76
+
77
+ Exit codes: `0` every check that ran passed; `1` a check found something to fix or could not run; `2` usage error. With `--fix`, the exit code follows a re-scan after the fix, so a fix that leaves the repo clean exits `0` — the command reports the repo as it stands after the fix, not as it was before.
78
+
79
+ ## Let your agent run it
80
+
81
+ ### One-command install
82
+
83
+ `dev-diagnosis setup` detects the agents on your machine (Claude Code, Codex CLI, Cursor, VS Code) and writes the MCP config that wires dev-diagnosis into each of them. It prints a preview of exactly what will be written before writing anything: a config that already mentions dev-diagnosis is left alone, a config that exists without it is merged (other entries kept), a missing config is created. A config it cannot parse is left alone and reported.
84
+
85
+ ```sh
86
+ dev-diagnosis setup # preview, then write
87
+ dev-diagnosis setup --dry-run # preview only, write nothing
88
+ dev-diagnosis setup --check # report which agents are wired (exit 0 when all are)
89
+ ```
90
+
91
+ If `dev-diagnosis` itself is not on PATH, setup says so — install it globally (`npm install -g dev-diagnosis-cli`) or the agents will not be able to run it.
92
+
93
+ ### MCP server
94
+
95
+ `dev-diagnosis --mcp` serves the same four checks as MCP tools over stdio — no extra install, no network, the same code the CLI runs. Add it to your agent once:
96
+
97
+ Claude Code (`.mcp.json` in the repo, or `claude mcp add dev-diagnosis -- dev-diagnosis --mcp`):
98
+
99
+ ```json
100
+ {
101
+ "mcpServers": {
102
+ "dev-diagnosis": {
103
+ "command": "dev-diagnosis",
104
+ "args": ["--mcp"]
105
+ }
106
+ }
107
+ }
108
+ ```
109
+
110
+ Codex (`codex mcp add dev-diagnosis -- dev-diagnosis --mcp`, or in `~/.codex/config.toml`):
111
+
112
+ ```toml
113
+ [mcp_servers.dev-diagnosis]
114
+ command = "dev-diagnosis"
115
+ args = ["--mcp"]
116
+ ```
117
+
118
+ To keep the agent inside one repo, add `--sandbox <dir>` (alias `--root`): paths outside it are rejected with a plain error. Five tools: `diagnose_context`, `diagnose_path`, `diagnose_cache`, `diagnose_flakes`, `diagnose_all` — each returns the same JSON the CLI's `--json` produces.
119
+
120
+ ### Agent Skills
121
+
122
+ `dev-diagnosis --skill-generate` writes `.claude/skills/dev-diagnosis/SKILL.md` in the current repo (or `~/.claude/skills/dev-diagnosis/SKILL.md` with `--global`). The skill tells an agent to run `dev-diagnosis all` when it starts work in a repo, fix what FAILs, and re-run to confirm. It refuses to overwrite an existing SKILL.md, so your own edits are safe.
123
+
124
+ ## In CI
125
+
126
+ One step runs the diagnosis and fails the job when a check fails:
127
+
128
+ ```yaml
129
+ jobs:
130
+ diagnose:
131
+ runs-on: ubuntu-latest
132
+ steps:
133
+ - uses: actions/checkout@v4
134
+ - uses: actions/setup-node@v4
135
+ with:
136
+ node-version: 20
137
+ - uses: WJDdidi-1/dev-doctor@main
138
+ with:
139
+ path: .
140
+ args: --command node
141
+ ```
142
+
143
+ `args` is passed straight to `dev-diagnosis all`, so `--command`, `--flakes`, `--fix` and a build command after `--` all work the same way they do on the command line. The action runs the published npm package (`@latest`), so a repo using it always gets the newest checks.
36
144
 
37
145
  ## Example
38
146
 
@@ -51,16 +159,11 @@ Files: 9 Size: 1.68 MB Score: 75/100 — Some context noise found
51
159
  logs: 79 B (0%) — lost 0 of 15
52
160
  Noise to keep away from an AI agent:
53
161
  data/export.json — large file (1.68 MB)
54
- dist/ — generated build output (98 B)
55
- logs/dev.log — local log file (79 B)
56
- package-lock.json — lockfile (95 B)
162
+ Estimated tokens wasted on this noise: ≈ 439k tokens (estimate, ~4 bytes per token)
57
163
  Worth a look before sharing:
58
164
  .env.example — environment template (usually safe to share)
59
165
  Fix: add these to .aiignore (or the agent's ignore list):
60
- *.log
61
166
  data/export.json
62
- dist/
63
- package-lock.json
64
167
  FAIL some context noise found (score 75/100)
65
168
 
66
169
  == path ==
@@ -80,11 +183,12 @@ FAIL 1 flaky, 1 always-failing across 2 runs
80
183
  == cache ==
81
184
  Cache dir: /home/hatch/workspace/dev-doctor-build/dev-doctor/examples/demo-repo/.cache
82
185
  Command: node build.mjs
83
- Cold: 491 ms Warm: 90 ms Saved: 401 ms (82%)
84
- Verdict: cache pays — warm run is 401 ms (82%) faster than cold
85
- PASS cache pays: cold 491 ms, warm 90 ms (warm run is 401 ms (82%) faster than cold)
186
+ Cold: 484 ms Warm: 82 ms Saved: 402 ms (83%)
187
+ Verdict: cache pays — warm run is 402 ms (83%) faster than cold
188
+ PASS cache pays: cold 484 ms, warm 82 ms (warm run is 402 ms (83%) faster than cold)
86
189
 
87
190
  Overall: FAIL — see the checks above
191
+ Summary: 4 checks — 2 passed, 2 failed, 0 skipped; context noise 1.68 MB; cache saved 83%
88
192
  ```
89
193
 
90
194
  The demo repo is intentionally noisy, so `all` exits 1: the context and flakes checks fail while path and cache pass. That is the point of the fixture.
@@ -93,9 +197,9 @@ The demo repo is intentionally noisy, so `all` exits 1: the context and flakes c
93
197
 
94
198
  ### `context` — what an AI agent should not read
95
199
 
96
- Walks the repo (skipping `.git`, `node_modules` and other hidden directories) and classifies every file: build output, large files over 1 MB, lockfiles, logs, binary payloads, secret-like files and environment templates. It prints a 0–100 score, the bytes lost per category, the noisy files, the files worth a look before sharing, and the `.aiignore` entries that would fix it. Build-output directories are reported by their full path from the repo root (`packages/foo/dist/`, not a bare `dist/`) and rolled up into one row per directory, so the entry `--fix` writes matches nested build output too. A large file is fixed by its exact path from the repo root — no glob can single one file out — so `--fix` followed by a re-scan comes back clean even when the noise is a single oversized file. Files the repo's `.aiignore` already lists are excluded from the scan entirely — they are what the agent will not be handed, so they are not noise any more. `context --fix` writes the suggestions into `.aiignore` for you; `--json` includes the fix outcome in the output.
200
+ Walks the repo (skipping `.git`, `node_modules` and other hidden directories) and classifies every file: build output, large files over 1 MB, lockfiles, logs, binary payloads, secret-like files and environment templates. It prints a 0–100 score, the bytes lost per category, the noisy files, the files worth a look before sharing, and the `.aiignore` entries that would fix it. Build-output directories are reported by their full path from the repo root (`packages/foo/dist/`, not a bare `dist/`) and rolled up into one row per directory, so the entry `--fix` writes matches nested build output too. A large file is fixed by its exact path from the repo root — no glob can single one file out — so `--fix` followed by a re-scan comes back clean even when the noise is a single oversized file. Files the repo's `.aiignore` already lists are excluded from the scan entirely — they are what the agent will not be handed, so they are not noise any more. Files and directories listed in `.gitignore` — at the repo root or in a subdirectory — are excluded the same way: they are already invisible to most agents, so flagging them would be noise about noise, and `--fix` will not duplicate them into `.aiignore`. The common pattern forms work (`*`, `?`, `[...]`, `**`, `/`-anchored paths, directory-only `dir/`, and `!` negation with last-match-wins); `.git/info/exclude` and the global gitignore are not read. `context --fix` writes the suggestions into `.aiignore` for you; `--json` includes the fix outcome in the output. Review items — secret-like files and environment templates — are listed for a manual look and are never written by `--fix`: they cost no score points, so the entry would fix nothing, and a basename pattern such as `secret.js` would hide every same-named file in the repo, including test fixtures. They appear as `reviewSuggestions` in `--json`, printed as advisory.
97
201
 
98
- The score is the *noise-only* part of ai-context-doctor's score: 100 minus the category-weighted share of noise bytes (build 40, large 25, lockfile 20, log 15). The full ai-context-doctor score also weighs context volume, one file dominating, file sprawl and binary payloads, so its composite number reads lower on the same repo. The file classification itself is the same.
202
+ The score is the *noise-only* part of ai-context-doctor's score: 100 minus the category-weighted share of noise bytes (build 40, large 25, lockfile 20, log 15). The full ai-context-doctor score also weighs context volume, one file dominating, file sprawl and binary payloads, so its composite number reads lower on the same repo. The file classification itself is the same. Only noise in a category that actually lost points is listed as noise or suggested as a fix: a 95 B lockfile in a 1.7 MB repo loses 0 points, so it is not something to fix — a 100/100 scan lists nothing to fix.
99
203
 
100
204
  ### `path` — why a command is missing
101
205
 
@@ -103,7 +207,7 @@ Runs the lookup in four shell contexts — current, login, non-login and daemon-
103
207
 
104
208
  ### `cache` — does the cache pay
105
209
 
106
- Runs the build command twice: cold, with the cache directory moved aside, and warm, with it present, then reports both timings and the saving. The original cache is restored to its original state afterwards, and anything the cold run produced is thrown away. A cache directory that is the project root, contains it, or lies outside it is refused rather than moved. No network is involved; the check only runs your own build command.
210
+ Runs the build command twice: cold, with the cache directory moved aside, and warm, with it present, then reports both timings and the saving. The original cache is restored to its original state afterwards, and anything the cold run produced is thrown away. A cache directory that is the project root, contains it, or lies outside it is refused rather than moved. No network is involved; the check only runs your own build command. `--json` reports the timings and the verdict without the build's stdout, so a chatty build does not bloat the machine-readable output — in `all --json` the embedded cache check is the same shape.
107
211
 
108
212
  ### `flakes` — which tests are flaky
109
213
 
@@ -112,14 +216,20 @@ Reads JUnit XML across CI runs — each subdirectory of `<junit-dir>` is one run
112
216
  ## Development
113
217
 
114
218
  ```sh
115
- npm test # node:test, no extra dependencies — 72 tests
219
+ npm test # node:test, no extra dependencies — 124 tests, run serially
116
220
  npm run example # run the CLI against the demo fixture
117
221
  ```
118
222
 
223
+ The suite runs serially on purpose (`--test-concurrency=1`): the cache check times real builds, so test files running in parallel would skew its cold-vs-warm timings.
224
+
119
225
  There is nothing to install: the CLI has no runtime dependencies, so
120
226
  `npm test` works straight after cloning. Run the checkout as
121
227
  `node bin/dev-diagnosis.js …` — see Troubleshooting if you reach for
122
- `npx` inside the checkout.
228
+ `npx` inside the checkout. The checkout ships a `.aiignore` that
229
+ excludes the demo fixture (`examples/demo-repo/`), so `dev-diagnosis
230
+ all` on the repo itself passes its own `context` check — the fixture
231
+ is intentionally noisy test data, and it stays noisy when scanned
232
+ directly (`npm run example`).
123
233
 
124
234
  Requires Node.js 18 or later. CI runs the tests and the demo on Node 18, 20, 22 and 24 across Ubuntu, macOS and Windows, and smoke-tests the packed tarball by installing it in a clean directory. Windows is covered by CI only — the Windows-only code paths (the `cmd.exe` retry for `.cmd`/`.bat` shims, `PATHEXT` lookup) have not been run on a real Windows machine.
125
235