aztrx-cli 0.1.1 → 0.2.1

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/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # <img src="media/logo.svg" width="28" height="32" alt="Aztrx logo" align="absmiddle" /> Aztrx AI
2
2
 
3
- > **Autonomous runtime stress-tester, deterministic bug minimizer, and self-healing engine for web applications.**
3
+ > **Autonomous runtime stress-tester, deterministic bug minimizer, human-language explainer, and self-healing engine for web applications.**
4
4
 
5
5
  [![Node.js](https://img.shields.io/badge/Node.js-%3E%3D18-green.svg?style=flat-square)](https://nodejs.org)
6
6
  [![License: Apache 2.0](https://img.shields.io/badge/License-Apache%202.0-blue.svg?style=flat-square)](LICENSE)
@@ -11,111 +11,132 @@ Aztrx AI drives your web app like a hostile user — clicking, entering boundary
11
11
 
12
12
  ---
13
13
 
14
- ## Why Aztrx AI
14
+ ## Run it
15
15
 
16
- - **Sees swallowed errors.** Error Boundaries and `window.onerror` miss the errors your app *catches*. Aztrx AI reads the real throw-site stack off the `Error` object, so a crash you've never seen in your logs becomes a finding you can't ignore.
17
- - **Proves, not reports.** Every crash/error finding ships with an executable `.spec.ts` and a flake-rate verdict — `[deterministic 5/5]`, `[flaky 3/5]`, or `[unreliable]`.
18
- - **Heals, not just finds.** `--heal` generates a patch through an LLM, gates it (redaction + AST safety), compiles it, runs your test suite, and replays it against the repro inside an isolated git worktree — the patch is verified before a human ever sees it.
19
- - **Safe by default.** A deny-by-default network guard blocks off-origin calls, and a destructive-action deny-list refuses to click "delete", "pay", or "logout".
16
+ ```bash
17
+ npx aztrx-cli run http://localhost:3000
18
+ ```
20
19
 
21
- ---
20
+ That's the whole setup — no install, no config, no account, no API key. Point it at your
21
+ running dev server and it finds the crashes. It drives Chromium through Playwright (the
22
+ first run downloads the browser automatically).
23
+
24
+ What that one command gives you:
22
25
 
23
- ## Quickstart
26
+ - **Sees swallowed errors.** Error Boundaries and `window.onerror` miss the errors your app *catches*. Aztrx AI reads the real throw-site stack off the `Error` object — a crash you've never seen in your logs becomes a finding you can't ignore.
27
+ - **Proves, not reports.** Every crash/error finding ships with an executable `.spec.ts` repro and a flake-rate verdict — `[deterministic 5/5]`, `[flaky 3/5]`, or `[unreliable]`.
28
+ - **Safe by default.** A deny-by-default network guard blocks off-origin calls, a destructive-action deny-list refuses to click "delete", "pay", or "logout", and nothing leaves your machine unless you opt in.
24
29
 
25
- Run against any running local dev server no install, no repo clone:
30
+ More ways to run all optional flags on top of the same command:
26
31
 
27
32
  ```bash
28
- npx aztrx-cli run http://localhost:3000 # deterministic walk
29
33
  npx aztrx-cli run http://localhost:3000 --fuzz # seeded chaos (replayable)
30
- npx aztrx-cli run http://localhost:3000 --fuzz --repro # + minimize → compile → validate
31
- npx aztrx-cli run http://localhost:3000 --http-fuzz --repro # server-side 5xx hunt → proof
34
+ npx aztrx-cli run http://localhost:3000 --repro # minimize → compile → validate
35
+ npx aztrx-cli run http://localhost:3000 --http-fuzz # server-side 5xx hunt
32
36
  ```
33
37
 
34
- Install it globally:
38
+ Prefer a global install?
35
39
 
36
40
  ```bash
37
41
  npm i -g aztrx-cli
38
- aztrx-cli run http://localhost:3000 --repro
39
- ```
40
-
41
- Or install from source (contributors):
42
-
43
- ```bash
44
- git clone https://github.com/DanisChaparov/aztrx
45
- cd aztrx
46
- npm install
47
- npm run build
48
- npm link # puts `aztrx-cli` on your PATH
42
+ aztrx-cli run http://localhost:3000
49
43
  ```
50
44
 
51
- Aztrx AI drives Chromium through Playwright — the first run downloads the browser
52
- automatically (`npx playwright install chromium` to force it).
53
-
54
45
  ---
55
46
 
56
- ## Features & Workflows
57
-
58
- ### 1. Initialize configuration
47
+ ## Fix it, not just find it
59
48
 
60
- Scaffold `aztrx.config.ts`, detect the framework + dev port, and seed `.aztrx/` into `.gitignore`:
49
+ The only feature that needs a key. `--fix` hands the crash to an LLM and applies a
50
+ verified patch to your working tree. Set `ANTHROPIC_API_KEY` and run:
61
51
 
62
52
  ```bash
63
- npx aztrx-cli init
53
+ export ANTHROPIC_API_KEY="your-api-key"
54
+ npx aztrx-cli run http://localhost:3000 --fix # find → explain → heal → apply
55
+ npx aztrx-cli run http://localhost:3000 --fix --yes # non-interactive (CI)
64
56
  ```
65
57
 
66
- ### 2. Autonomous healing (`--heal`)
58
+ `--fix` chains find explain → heal, then asks *"Apply the fix?"* (`y/N`). On yes, the
59
+ verified patch lands in your working tree — `git diff` shows the result. Aztrx AI never
60
+ commits.
67
61
 
68
- Locate the crash, hand the redacted context to an LLM, run a TypeScript check and Playwright validation inside an isolated worktree, and write a verified `.patch` file:
62
+ - **`--heal`** the same pipeline, but stops at a verified `.patch` file (no apply).
63
+ - **`--explain`** — a human-language "X-ray" summary of what broke, where, and whether a fix is ready. No key required: it falls back to a deterministic offline summary.
69
64
 
70
- ```bash
71
- export ANTHROPIC_API_KEY="your-api-key"
72
- npx aztrx-cli run http://localhost:3000 --fuzz --repro --heal
73
- ```
65
+ Every patch is redacted, sandboxed in a detached git worktree, compiler-checked, and gated
66
+ on your own test suite before you ever see it.
74
67
 
75
- ### 3. Live studio dashboard
68
+ ---
76
69
 
77
- Inspect real-time telemetry events and triage findings in the built-in web UI:
70
+ ## Advanced
78
71
 
79
- ```bash
80
- npx aztrx-cli studio
81
- # → listening at http://localhost:7331
82
- ```
72
+ Everything else is optional. One line each — the full table is in the [CLI reference](#cli-reference).
83
73
 
84
- ### 4. Cloud & CI ingest (`--upload`)
74
+ ### Fuzz harder
75
+ `--fuzz` breaks the *client*; `--http-fuzz` attacks the *server* — it harvests your app's
76
+ real endpoints and throws hostile requests at them (query overflow, JSON type-confusion,
77
+ header injection), turning every `5xx` into an executable repro. When a 500 body leaks a
78
+ server stack, `--heal` can even fix it by booting the patched app and replaying the repro.
85
79
 
86
- Stream sanitized, deduplicated crash fingerprints and metrics to your team's ingest server:
80
+ ### Parallel swarm
81
+ `--workers 4` fans detection into parallel workers (walk + several fuzz seeds + http-fuzz),
82
+ merged by fingerprint. `--swarm` is a hidden alias for `--workers auto`.
83
+
84
+ ### Authenticated testing
85
+ `--login` detects the login form and signs in before the pass, so every repro runs
86
+ authenticated:
87
87
 
88
88
  ```bash
89
- npx aztrx-cli run http://localhost:3000 --upload --api-key <YOUR_API_KEY> --cloud-url http://localhost:8787
89
+ AZTRX_AUTH_EMAIL=you@example.com AZTRX_AUTH_PASSWORD=secret \
90
+ npx aztrx-cli run http://localhost:3000 --login
90
91
  ```
91
92
 
92
- ### 5. GitHub Action
93
-
94
- Ship a runtime gate on every PR — see [Continuous Integration](#continuous-integration-github-action) below.
93
+ Or `--storage-state <path>` with a saved Playwright state. Use `--login-url` for an
94
+ explicit login page, `--allow-host auth.example.com` for a third-party auth backend.
95
95
 
96
- ### 6. HTTP mutation fuzzing (`--http-fuzz`)
96
+ ### Code modernizer
97
+ `npx aztrx-cli modernize src/legacy.js` rewrites legacy JS/TS into modern idiomatic syntax
98
+ (`var` → `const`, callbacks → `async`/`await`), applied only after you confirm.
97
99
 
98
- The DOM fuzzer (`--fuzz`) breaks the *client*. `--http-fuzz` attacks the *server*: it harvests the endpoints your app actually calls, then throws hostile requests at them — query overflow, JSON type-confusion, header injection, method confusion — and turns every `5xx` into an executable repro.
100
+ ### Live studio
101
+ `npx aztrx-cli studio` — triage findings in a localhost dashboard (binds `127.0.0.1`).
99
102
 
100
- Every `5xx` finding also captures the server's *own* error response — its message and body — and, when the body leaks a stack trace, the server-side source line. So a finding reads "`HTTP 500 /api/cart` … `server: Cannot read properties of undefined` at `app/api/cart/route.ts:14`", not just "something 500'd".
103
+ ### Config & CI
104
+ `npx aztrx-cli init` scaffolds `aztrx.config.ts` and gitignores `.aztrx/`. For a per-PR
105
+ runtime gate, use the [GitHub Action](#continuous-integration-github-action). `--pr-comment`
106
+ / `--badge` write a PR comment / status badge; `--upload --api-key` streams findings to your
107
+ cloud dashboard.
101
108
 
102
- When the 500 body leaks that server stack, `--heal` can also *fix* it: it boots the patched app inside the worktree (via `--start-command`, or auto-detected `scripts.dev`/`scripts.start`), waits for an HTTP readiness signal, and replays the repro against the booted server — so a server patch is verified against a *running* app, not a static file. Server findings whose body does not leak a stack are still reported and repro'd but not healed (there is no file to patch).
109
+ ### Privacy
110
+ Off by default and strictly opt-in. `--telemetry` collects an anonymized tuple locally;
111
+ `--share-data` uploads it. `--upload` streams sanitized findings to the cloud. See
112
+ [Security & data flow](#security--data-flow) for the invariants.
103
113
 
104
114
  ---
105
115
 
106
116
  ## CLI reference
107
117
 
118
+ `aztrx-cli run --help` is grouped by intent (Detect / Prove / Fix / Report & ship / Auth);
119
+ the table below is the complete reference — including flags hidden from `--help` (aliases
120
+ and niche tuning knobs).
121
+
108
122
  | Flag | Description | Default |
109
123
  | --- | --- | --- |
110
124
  | `--fuzz` | Seeded chaos fuzzing instead of the deterministic walk | — |
111
125
  | `--http-fuzz` | Server-side mutation fuzzing — hostile requests against the target origin | — |
112
126
  | `--repro` | Minimize (ddmin) → emit Playwright spec → validate flake rate | — |
113
127
  | `--heal` | Generate + verify an LLM patch (implies `--repro`) | — |
128
+ | `--fix` | Find → explain → heal → apply — the one-command fix (implies `--heal`) | — |
129
+ | `--magic-fix` | Deprecated alias for `--fix` (hidden) | — |
130
+ | `--explain` | Print a human-language summary of the findings | — |
131
+ | `--yes` / `-y` | Auto-apply verified fixes without prompting (with `--fix`) | — |
132
+ | `--lang <code>` | Language for the human-language summary (`en`, `ru`) | `en` |
114
133
  | `--upload` | Stream run findings to the cloud ingest backend | — |
115
134
  | `--api-key <key>` | Auth key for `--upload` / `--share-data` | `$AZTRX_API_KEY` |
116
135
  | `--cloud-url <url>` | Ingest server base URL | `https://api.aztrx.app` |
117
136
  | `--max-actions <n>` | Max actions per pass | `100` |
118
137
  | `--seed <n>` | PRNG seed for deterministic fuzz | `42` |
138
+ | `--workers <n>` | Number of parallel detection workers | `1` |
139
+ | `--swarm` | Hidden alias for `--workers auto` | — |
119
140
  | `--repro-runs <n>` | Flake-rate replay iterations | `3` |
120
141
  | `--heal-model <model>` | Fallback LLM tier | `claude-sonnet-5` |
121
142
  | `--heal-fast-model <model>` | Fast/cheap first tier | `claude-haiku-4-5` |
@@ -129,7 +150,11 @@ When the 500 body leaks that server stack, `--heal` can also *fix* it: it boots
129
150
  | `--share-data` | Also upload the sanitized tuples (opt-in) | — |
130
151
  | `--repo <path>` | Root path for sourcemap → source resolution | cwd |
131
152
  | `--allow-host <host>` | Add a host to the network allow-list (repeatable) | — |
132
- | `--auth <path>` / `--storage-state <path>` | Playwright storage-state for authenticated pages | — |
153
+ | `--storage-state <path>` | Playwright storage-state for authenticated pages (`--auth` is a hidden alias) | — |
154
+ | `--login` | Auto-login before the pass (needs `AZTRX_AUTH_EMAIL`/`AZTRX_AUTH_PASSWORD`) | — |
155
+ | `--login-email <email>` | Email for `--login` (hidden — prefer `$AZTRX_AUTH_EMAIL`) | `$AZTRX_AUTH_EMAIL` |
156
+ | `--login-password <pass>` | Password for `--login` (hidden — prefer `$AZTRX_AUTH_PASSWORD`) | `$AZTRX_AUTH_PASSWORD` |
157
+ | `--login-url <url>` | Explicit login page URL for `--login` (hidden) | current page |
133
158
  | `--fail-on` | Exit `1` if any crash/error finding is present | — |
134
159
  | `--dry-run` | Log planned actions without executing them | — |
135
160
  | `--crash-test` | Throw a deliberate error to verify capture | — |
@@ -156,36 +181,6 @@ Every run writes self-contained artifacts inside `.aztrx/` (gitignored):
156
181
 
157
182
  ---
158
183
 
159
- ## Architecture
160
-
161
- Aztrx AI is a decoupled, event-driven pipeline — modules talk only through an
162
- `EventBus`; the orchestrator wires them together.
163
-
164
- ```
165
- [ CDP interceptor ] ──▶ [ action ring buffer ] ──▶ [ classifier (fingerprint) ]
166
-
167
- [ verified .patch ] ◀── [ LLM healer ] ◀── [ ddmin minimizer ] ◀── [ sourcemap resolver ]
168
-
169
- [ Playwright spec (.spec.ts) ] ──▶ [ flake-rate validator ]
170
- ```
171
-
172
- | Stage | Module | Role |
173
- | --- | --- | --- |
174
- | F1 | `interceptor.ts` | CDP interceptor — captures raw runtime errors and console/network events |
175
- | F2 | `recorder.ts` | Ring buffer of the last 25 actions; selector cascade `data-testid → text → CSS path` |
176
- | F3 | `classifier.ts` | Fingerprints + dedups findings, assigns severity; suppresses `.aztrx/baseline.json` (input) |
177
- | F4 | `resolver.ts` | Maps minified frames to source files, lines, and snippets via sourcemaps |
178
- | F5 | `fuzzer.ts` + `domWalker.ts` | Seeded chaos fuzzer; `domWalker` (F5-lite) discovers interactive elements |
179
- | F6 | `networkGuard.ts` + `domWalker.ts` | Deny-by-default network policy + destructive-action deny-list |
180
- | F7 | `minimizer.ts` | ddmin delta-debugging — eliminates irrelevant actions |
181
- | F8 | `specCompiler.ts` | Emits standalone, clean Playwright `.spec.ts` repro |
182
- | F9 | `validator.ts` | Multi-pass replays → `deterministic` / `flaky` / `unreliable` |
183
- | F10 | `heal/` | Closed-loop healing — redact → generate → AST gate → sandbox → `tsc` → verify |
184
- | F11 | `telemetry/` | Opt-in anonymized crash→repro→patch tuple collection (data flywheel) |
185
- | F12 | `cloud/` | Opt-in cloud sync — streams sanitized findings to the ingest dashboard |
186
-
187
- ---
188
-
189
184
  ## Continuous Integration (GitHub Action)
190
185
 
191
186
  Runtime gate on every PR. The action boots your dev server, runs
@@ -201,7 +196,7 @@ jobs:
201
196
  permissions: { contents: read, pull-requests: write }
202
197
  steps:
203
198
  - uses: actions/checkout@v4
204
- - uses: DanisChaparov/aztrx@e5b89247cb0fba7f5c0febe8fa27f5e1c74cd898
199
+ - uses: DanisChaparov/aztrx@94d1173e6b363bc60e2237775cca11addd39b10f
205
200
  with:
206
201
  url: http://localhost:3000
207
202
  start-command: npm run dev # optional — boot the app in the background
@@ -215,7 +210,7 @@ Or as a reusable workflow:
215
210
  on: pull_request
216
211
  jobs:
217
212
  aztrx:
218
- uses: DanisChaparov/aztrx/.github/workflows/aztrx-pr.yml@e5b89247cb0fba7f5c0febe8fa27f5e1c74cd898
213
+ uses: DanisChaparov/aztrx/.github/workflows/aztrx-pr.yml@94d1173e6b363bc60e2237775cca11addd39b10f
219
214
  with:
220
215
  url: http://localhost:3000
221
216
  start-command: npm run dev
@@ -266,7 +261,7 @@ jobs:
266
261
  sleep 2
267
262
  done
268
263
  - name: Generate badge
269
- run: npx --yes aztrx-cli@0.1.1 run http://localhost:3000 --badge badge.svg
264
+ run: npx --yes aztrx-cli@0.2.1 run http://localhost:3000 --badge badge.svg
270
265
  - name: Commit badge
271
266
  run: |
272
267
  git config user.name "github-actions[bot]"
@@ -291,16 +286,6 @@ aztrx falls back to `claude-sonnet-5` and tries again. Most one-line fixes never
291
286
  pay for the big model. Tiers are configurable via `AZTRX_FAST_MODEL` /
292
287
  `AZTRX_MODEL` or `--heal-fast-model` / `--heal-model`.
293
288
 
294
- ## Telemetry & privacy
295
-
296
- Off by default and strictly opt-in. `--telemetry` collects the anonymized tuple
297
- `[crash_fingerprint, min_repro_spec, verified_patch, framework_metadata,
298
- model_tier_used]` locally (nothing leaves the machine); `--share-data` uploads it
299
- to the telemetry endpoint. Every field passes a sanitizer that irreversibly
300
- strips secrets, anonymizes URLs to `<host>`, and scrubs repo paths to `<repo>`.
301
- Uploads are fire-and-forget, bounded by a 2s timeout, and never affect the exit
302
- code.
303
-
304
289
  ## Security & data flow
305
290
 
306
291
  **Local-first by default.** A run never phones home unless you pass an opt-in
@@ -335,9 +320,19 @@ LLM call.
335
320
  CORS.
336
321
  - **`.aztrx/` is gitignored** on `init` — repro specs, reports, and patches stay
337
322
  out of history.
338
- - **Pinned supply chain.** The GitHub Action pins `aztrx-cli@0.1.1` (never
323
+ - **Pinned supply chain.** The GitHub Action pins `aztrx-cli@0.2.1` (never
339
324
  `@latest`).
340
325
 
326
+ ## Telemetry & privacy
327
+
328
+ Off by default and strictly opt-in. `--telemetry` collects the anonymized tuple
329
+ `[crash_fingerprint, min_repro_spec, verified_patch, framework_metadata,
330
+ model_tier_used]` locally (nothing leaves the machine); `--share-data` uploads it
331
+ to the telemetry endpoint. Every field passes a sanitizer that irreversibly
332
+ strips secrets, anonymizes URLs to `<host>`, and scrubs repo paths to `<repo>`.
333
+ Uploads are fire-and-forget, bounded by a 2s timeout, and never affect the exit
334
+ code.
335
+
341
336
  ---
342
337
 
343
338
  ## Open benchmark
@@ -366,6 +361,37 @@ Full per-case table and scoring notes live in
366
361
 
367
362
  ---
368
363
 
364
+ ## Architecture
365
+
366
+ Aztrx AI is a decoupled, event-driven pipeline — modules talk only through an
367
+ `EventBus`; the orchestrator wires them together.
368
+
369
+ ```
370
+ [ CDP interceptor ] ──▶ [ action ring buffer ] ──▶ [ classifier (fingerprint) ]
371
+
372
+ [ verified .patch ] ◀── [ LLM healer ] ◀── [ ddmin minimizer ] ◀── [ sourcemap resolver ]
373
+
374
+ [ Playwright spec (.spec.ts) ] ──▶ [ flake-rate validator ]
375
+ ```
376
+
377
+ | Stage | Module | Role |
378
+ | --- | --- | --- |
379
+ | F1 | `interceptor.ts` | CDP interceptor — captures raw runtime errors and console/network events |
380
+ | F2 | `recorder.ts` | Ring buffer of the last 25 actions; selector cascade `data-testid → text → CSS path` |
381
+ | F3 | `classifier.ts` | Fingerprints + dedups findings, assigns severity; suppresses `.aztrx/baseline.json` (input) |
382
+ | F4 | `resolver.ts` | Maps minified frames to source files, lines, and snippets via sourcemaps |
383
+ | F5 | `fuzzer.ts` + `domWalker.ts` | Seeded chaos fuzzer; `domWalker` (F5-lite) discovers interactive elements |
384
+ | F6 | `networkGuard.ts` + `domWalker.ts` | Deny-by-default network policy + destructive-action deny-list |
385
+ | F7 | `minimizer.ts` | ddmin delta-debugging — eliminates irrelevant actions |
386
+ | F8 | `specCompiler.ts` | Emits standalone, clean Playwright `.spec.ts` repro |
387
+ | F9 | `validator.ts` | Multi-pass replays → `deterministic` / `flaky` / `unreliable` |
388
+ | F10 | `heal/` | Closed-loop healing — redact → generate → AST gate → sandbox → `tsc` → verify |
389
+ | F11 | `telemetry/` | Opt-in anonymized crash→repro→patch tuple collection (data flywheel) |
390
+ | F12 | `cloud/` | Opt-in cloud sync — streams sanitized findings to the ingest dashboard |
391
+ | F13 | `summarize.ts` + `heal/apply.ts` | Human-language "X-ray" report + opt-in apply of verified patches (`--fix`) |
392
+
393
+ ---
394
+
369
395
  ## Roadmap
370
396
 
371
397
  - [x] Closed-loop healing — redact → generate → gate → sandbox → verify (F10)
@@ -377,6 +403,11 @@ Full per-case table and scoring notes live in
377
403
  - [x] B2B ($29/mo) — Cloud dashboard (api.aztrx.app)
378
404
  - [x] Data flywheel — opt-in anonymized patch-tuple collection (F11)
379
405
  - [x] Server-side healing — heal server `5xx` findings (verify a patch by booting the patched server; requires a leaked server stack + a resolvable start command)
406
+ - [x] Human-language "X-ray" report — `--explain` / `--lang` (LLM + offline fallback)
407
+ - [x] One-click heal & apply — `--fix` (verified patch → working tree, `y/N`, no commit)
408
+ - [x] Autonomous Swarm — parallel detection: walk + multi-seed fuzz + http-fuzz workers, merged by fingerprint
409
+ - [x] Auth auto-login — `--login` walks login forms (synthesize test tokens — next)
410
+ - [x] Code modernizer — LLM-rewrite legacy JS/TS (`modernize`; Python — next)
380
411
 
381
412
  ## Contributing
382
413
 
@@ -393,6 +424,13 @@ node dist/cli.js http://localhost:8901/crash.html --repo fixtures --repro
393
424
  # → one ● crash mapped to crash.html:13:15, minimized to 1 step
394
425
  ```
395
426
 
427
+ ## Support the project
428
+
429
+ If Aztrx AI saved you hours of debugging or helped you ship a clean release, you
430
+ can support the author directly — name a fair price on Polar.sh:
431
+
432
+ **[Donate on Polar.sh →](https://buy.polar.sh/polar_cl_f1vBaxUv3S4fJ0o28GfgzQz7gHDHXkecCQtxY0WqeFs)**
433
+
396
434
  ## License
397
435
 
398
436
  Apache-2.0 © DanisChaparov
@@ -0,0 +1,85 @@
1
+ import { Option } from "commander";
2
+ const GROUP_HEADINGS = {
3
+ detect: "Detect",
4
+ prove: "Prove",
5
+ fix: "Fix",
6
+ ship: "Report & ship",
7
+ auth: "Auth",
8
+ advanced: "Advanced options",
9
+ };
10
+ // Primary groups render first, in this order, with aligned descriptions.
11
+ const PRIMARY_GROUPS = ["detect", "prove", "fix", "ship", "auth"];
12
+ /** Build a commander Option tagged with a help group (defaults to "advanced"). */
13
+ export function opt(flags, description, group = "advanced") {
14
+ const option = new Option(flags, description);
15
+ option.__aztrxGroup = group;
16
+ return option;
17
+ }
18
+ function groupOf(option) {
19
+ return option.__aztrxGroup ?? "advanced";
20
+ }
21
+ /**
22
+ * Standalone `Help.formatHelp` override, registered via
23
+ * `runCommand.configureHelp({ formatHelp })`. Mirrors commander's built-in
24
+ * layout (Usage / Description / Arguments / Commands) but replaces the flat
25
+ * "Options:" list with named groups, and compresses the advanced flags into a
26
+ * single wrapped line of flag names.
27
+ */
28
+ export function formatHelp(cmd, helper) {
29
+ const termWidth = helper.padWidth(cmd, helper);
30
+ const helpWidth = helper.helpWidth || 80;
31
+ const itemIndentWidth = 2;
32
+ const itemSeparatorWidth = 2;
33
+ const formatItem = (term, description) => {
34
+ if (description) {
35
+ const fullText = `${term.padEnd(termWidth + itemSeparatorWidth)}${description}`;
36
+ return helper.wrap(fullText, helpWidth - itemIndentWidth, termWidth + itemSeparatorWidth);
37
+ }
38
+ return term;
39
+ };
40
+ const formatList = (lines) => lines.join("\n").replace(/^/gm, " ".repeat(itemIndentWidth));
41
+ const output = [`Usage: ${helper.commandUsage(cmd)}`, ""];
42
+ const description = helper.commandDescription(cmd);
43
+ if (description.length > 0) {
44
+ output.push(helper.wrap(description, helpWidth, 0), "");
45
+ }
46
+ const argumentList = helper
47
+ .visibleArguments(cmd)
48
+ .map((arg) => formatItem(helper.argumentTerm(arg), helper.argumentDescription(arg)));
49
+ if (argumentList.length > 0) {
50
+ output.push("Arguments:", formatList(argumentList), "");
51
+ }
52
+ // Separate the implicit `-h, --help` so it isn't swallowed by a group.
53
+ const visible = helper.visibleOptions(cmd);
54
+ const helpOption = visible.find((o) => o.short === "-h" && o.long === "--help");
55
+ const grouped = visible.filter((o) => o !== helpOption);
56
+ const buckets = new Map();
57
+ for (const o of grouped) {
58
+ const g = groupOf(o);
59
+ if (!buckets.has(g))
60
+ buckets.set(g, []);
61
+ buckets.get(g).push(o);
62
+ }
63
+ for (const g of PRIMARY_GROUPS) {
64
+ const opts = buckets.get(g);
65
+ if (!opts || opts.length === 0)
66
+ continue;
67
+ const list = opts.map((o) => formatItem(helper.optionTerm(o), helper.optionDescription(o)));
68
+ output.push(`${GROUP_HEADINGS[g]}:`, formatList(list), "");
69
+ }
70
+ const advanced = buckets.get("advanced");
71
+ if (advanced && advanced.length > 0) {
72
+ const names = advanced.map((o) => helper.optionTerm(o)).join(", ");
73
+ output.push(`${GROUP_HEADINGS.advanced}:`, formatList([helper.wrap(names, helpWidth - itemIndentWidth, 0)]), "");
74
+ }
75
+ if (helpOption) {
76
+ output.push(formatList([formatItem(helper.optionTerm(helpOption), helper.optionDescription(helpOption))]), "");
77
+ }
78
+ const commandList = helper
79
+ .visibleCommands(cmd)
80
+ .map((c) => formatItem(helper.subcommandTerm(c), helper.subcommandDescription(c)));
81
+ if (commandList.length > 0) {
82
+ output.push("Commands:", formatList(commandList), "");
83
+ }
84
+ return output.join("\n");
85
+ }