aztrx-cli 0.4.5 → 0.5.2
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 +326 -29
- package/dist/cli/help.d.ts +22 -0
- package/dist/cli/repo.d.ts +20 -0
- package/dist/cli/repo.js +50 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +284 -86
- package/dist/core/auth.d.ts +33 -0
- package/dist/core/badge.d.ts +9 -0
- package/dist/core/browser.d.ts +3 -0
- package/dist/core/classifier.d.ts +41 -0
- package/dist/core/cloud/index.d.ts +62 -0
- package/dist/core/cloud/index.js +24 -4
- package/dist/core/devServer.d.ts +90 -0
- package/dist/core/devServer.js +253 -0
- package/dist/core/diagnose.d.ts +18 -0
- package/dist/core/diff.d.ts +31 -0
- package/dist/core/domWalker.d.ts +24 -0
- package/dist/core/domWalker.js +18 -2
- package/dist/core/eventBus.d.ts +59 -0
- package/dist/core/events.d.ts +42 -0
- package/dist/core/fixPr.d.ts +16 -0
- package/dist/core/fixPr.js +11 -1
- package/dist/core/fuzzer.d.ts +24 -0
- package/dist/core/fuzzer.js +1 -1
- package/dist/core/heal/apply.d.ts +28 -0
- package/dist/core/heal/boot.d.ts +74 -0
- package/dist/core/heal/boot.js +126 -23
- package/dist/core/heal/childEnv.d.ts +15 -0
- package/dist/core/heal/gates.d.ts +11 -0
- package/dist/core/heal/index.d.ts +19 -0
- package/dist/core/heal/index.js +53 -23
- package/dist/core/heal/llm.d.ts +43 -0
- package/dist/core/heal/llm.js +22 -2
- package/dist/core/heal/redact.d.ts +22 -0
- package/dist/core/heal/sandbox.d.ts +54 -0
- package/dist/core/heal/sandbox.js +63 -3
- package/dist/core/heal/types.d.ts +102 -0
- package/dist/core/heal/verify.d.ts +37 -0
- package/dist/core/heal/verify.js +28 -14
- package/dist/core/httpFuzzer.d.ts +33 -0
- package/dist/core/init.d.ts +21 -0
- package/dist/core/init.js +5 -6
- package/dist/core/interceptor.d.ts +21 -0
- package/dist/core/llm.d.ts +36 -0
- package/dist/core/llm.js +40 -13
- package/dist/core/minimizer.d.ts +13 -0
- package/dist/core/modernize.d.ts +28 -0
- package/dist/core/modernize.js +2 -2
- package/dist/core/networkGuard.d.ts +13 -0
- package/dist/core/orchestrator.d.ts +86 -0
- package/dist/core/orchestrator.js +5 -2
- package/dist/core/patrol/loop.d.ts +46 -0
- package/dist/core/patrol/pr.d.ts +42 -0
- package/dist/core/patrol/pr.js +9 -6
- package/dist/core/patrol/record.d.ts +51 -0
- package/dist/core/patrol/state.d.ts +39 -0
- package/dist/core/pr.d.ts +3 -0
- package/dist/core/pr.js +15 -11
- package/dist/core/prompt.d.ts +14 -0
- package/dist/core/recorder.d.ts +23 -0
- package/dist/core/recorder.js +1 -1
- package/dist/core/renderMarkdown.d.ts +7 -0
- package/dist/core/replay.d.ts +33 -0
- package/dist/core/replay.js +52 -7
- package/dist/core/report.d.ts +8 -0
- package/dist/core/resolver.d.ts +64 -0
- package/dist/core/resolver.js +135 -17
- package/dist/core/rng.d.ts +2 -0
- package/dist/core/specCompiler.d.ts +17 -0
- package/dist/core/studio.d.ts +6 -0
- package/dist/core/studio.js +0 -3
- package/dist/core/summarize.d.ts +26 -0
- package/dist/core/summarize.js +17 -6
- package/dist/core/swarm.d.ts +97 -0
- package/dist/core/swarm.js +25 -2
- package/dist/core/telemetry/index.d.ts +33 -0
- package/dist/core/telemetry/index.js +22 -4
- package/dist/core/telemetry/sanitize.d.ts +19 -0
- package/dist/core/telemetry/types.d.ts +26 -0
- package/dist/core/types.d.ts +88 -0
- package/dist/core/ui.d.ts +23 -0
- package/dist/core/ui.js +0 -5
- package/dist/core/validator.d.ts +14 -0
- package/dist/core/version.d.ts +1 -0
- package/dist/hooks/index.d.ts +106 -0
- package/dist/hooks/index.js +349 -0
- package/dist/mcp/index.d.ts +58 -0
- package/dist/mcp/index.js +295 -0
- package/dist/mcp/install.d.ts +52 -0
- package/dist/mcp/install.js +204 -0
- package/dist/mcp/protocol.d.ts +106 -0
- package/dist/mcp/protocol.js +156 -0
- package/dist/mcp/tools.d.ts +107 -0
- package/dist/mcp/tools.js +621 -0
- package/dist/next/index.d.ts +70 -0
- package/dist/next/index.js +103 -0
- package/dist/plugins/scan.d.ts +85 -0
- package/dist/plugins/scan.js +188 -0
- package/dist/ui/app.d.ts +11 -0
- package/dist/vite/index.d.ts +60 -0
- package/dist/vite/index.js +56 -0
- package/package.json +44 -6
package/README.md
CHANGED
|
@@ -2,20 +2,25 @@
|
|
|
2
2
|
|
|
3
3
|
> **Catch the runtime crash your Error Boundary hid — and prove it with a test, not a log line.**
|
|
4
4
|
|
|
5
|
-
[](LICENSE)
|
|
5
|
+
[](https://nodejs.org)
|
|
6
|
+
[](https://github.com/Aztrx-AI/aztrx/blob/main/LICENSE)
|
|
7
7
|
|
|
8
8
|
Aztrx AI finds **runtime** bugs, not security holes. It drives your web app like a hostile
|
|
9
9
|
user and catches the crashes that ship to real users — *including ones a React Error Boundary
|
|
10
10
|
swallows* (the errors `window.onerror` never sees). Each crash comes back as an exact source
|
|
11
|
-
line
|
|
11
|
+
line, and with `--repro` an executable **Playwright repro** that replays it. Then it fixes it.
|
|
12
12
|
|
|
13
13
|
```bash
|
|
14
|
-
npx aztrx-cli
|
|
15
|
-
npx aztrx-cli
|
|
14
|
+
npx aztrx-cli # find crashes — no key, no config, no URL to look up
|
|
15
|
+
npx aztrx-cli --fix # fix them — free for common bugs
|
|
16
16
|
```
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
Run it in your project and it works out what your app is, starts your dev server if
|
|
19
|
+
nothing is listening, scans, and stops the server again on the way out. Already have
|
|
20
|
+
one running? It attaches and leaves it alone. Pass a URL (`aztrx-cli run http://…`)
|
|
21
|
+
and it goes exactly there instead.
|
|
22
|
+
|
|
23
|
+

|
|
19
24
|
|
|
20
25
|
---
|
|
21
26
|
|
|
@@ -23,7 +28,7 @@ npx aztrx-cli run http://localhost:3000 --fix # fix them — free for common
|
|
|
23
28
|
|
|
24
29
|
- **Sees swallowed errors.** Error Boundaries and `window.onerror` miss the errors your app *catches*. Aztrx 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.
|
|
25
30
|
- **Explains the crash in one line.** Every crash/error ships with a one-sentence diagnosis — why it happened and what to change (e.g. `the value before `.cart` is undefined — guard with `?.`). Free, no key, right in the terminal and `report.html`.
|
|
26
|
-
- **Proves, not reports.**
|
|
31
|
+
- **Proves, not reports.** With `--repro` (implied by `--fix`), every interaction crash ships with an executable `.spec.ts` repro and a flake-rate verdict — `[deterministic 3/3]`, `[flaky 3/5]`, or `[unreliable]`. A crash that can't be replayed reliably is reported as `[unreliable]` rather than dressed up as proof.
|
|
27
32
|
- **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.
|
|
28
33
|
|
|
29
34
|
---
|
|
@@ -31,15 +36,17 @@ npx aztrx-cli run http://localhost:3000 --fix # fix them — free for common
|
|
|
31
36
|
## Quickstart
|
|
32
37
|
|
|
33
38
|
```bash
|
|
34
|
-
npm i -g aztrx-cli
|
|
39
|
+
npm i -g aztrx-cli # or use npx — no install needed
|
|
35
40
|
|
|
36
|
-
aztrx-cli
|
|
37
|
-
aztrx-cli
|
|
38
|
-
aztrx-cli
|
|
41
|
+
aztrx-cli # 1. find the crashes (no key, no account)
|
|
42
|
+
aztrx-cli --repro # 2. prove them with an executable test
|
|
43
|
+
aztrx-cli --fix # 3. fix them
|
|
39
44
|
```
|
|
40
45
|
|
|
41
|
-
|
|
42
|
-
|
|
46
|
+
Run it from your project root — it detects the framework, finds your dev server or
|
|
47
|
+
boots it (`npm run dev`), and tears the server down when it exits. To point it at a
|
|
48
|
+
server somewhere else, pass the URL: `aztrx-cli run http://localhost:3000`. It drives
|
|
49
|
+
Chromium through Playwright (the first run downloads the browser automatically).
|
|
43
50
|
|
|
44
51
|
---
|
|
45
52
|
|
|
@@ -69,8 +76,10 @@ export AZTRX_MODEL="anthropic/claude-sonnet-5"
|
|
|
69
76
|
```
|
|
70
77
|
|
|
71
78
|
Every fix is redacted, sandboxed in a detached git worktree, compiler-checked, and gated on
|
|
72
|
-
your test suite before you see it.
|
|
73
|
-
`--
|
|
79
|
+
your test suite before you see it. `--fix` writes a patch and stops — nothing is committed to
|
|
80
|
+
the branch you are on. `--pr` is the opt-in that goes further: it commits to a new
|
|
81
|
+
`aztrx/fix-…` branch and opens a merge-ready PR from it. `--regression-test` drops the repro
|
|
82
|
+
into your test dir so the bug can't come back.
|
|
74
83
|
|
|
75
84
|
---
|
|
76
85
|
|
|
@@ -86,11 +95,226 @@ your test suite before you see it. Aztrx never commits. `--pr` opens a merge-rea
|
|
|
86
95
|
| `patrol <url>` | autonomous loop — re-scan, fix, open a PR per bug |
|
|
87
96
|
| `modernize <file>` | rewrite legacy JS/TS into modern idiomatic syntax |
|
|
88
97
|
| `studio` | live dashboard on `localhost:7331` |
|
|
98
|
+
| `hook install` | scan every `git push` — block the ones that crash |
|
|
99
|
+
| `mcp install` | put aztrx inside your editor's agent (Claude Code, Cursor, VS Code) |
|
|
89
100
|
|
|
90
101
|
Full list: `aztrx-cli run --help`, or the [CLI reference](#cli-reference).
|
|
91
102
|
|
|
92
103
|
---
|
|
93
104
|
|
|
105
|
+
## Scan while you develop
|
|
106
|
+
|
|
107
|
+
Add a few lines and your dev server reports its own runtime crashes as you work —
|
|
108
|
+
no second terminal, no separate command to remember. Same plugin either way; only
|
|
109
|
+
the hook differs.
|
|
110
|
+
|
|
111
|
+
### Vite
|
|
112
|
+
|
|
113
|
+
```ts
|
|
114
|
+
// vite.config.ts
|
|
115
|
+
import { aztrx } from "aztrx-cli/vite";
|
|
116
|
+
|
|
117
|
+
export default defineConfig({
|
|
118
|
+
plugins: [aztrx()],
|
|
119
|
+
});
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
npm run dev
|
|
124
|
+
# VITE v8.3.0 ready in 312 ms
|
|
125
|
+
# ➜ Local: http://localhost:5173/
|
|
126
|
+
# [aztrx] 1 crash — first: src/Report.tsx:42
|
|
127
|
+
# [aztrx] run `npx aztrx-cli` for the repro and the fix.
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### Next.js
|
|
131
|
+
|
|
132
|
+
Next has no config object to hang a plugin off, so it uses `instrumentation.ts` —
|
|
133
|
+
the hook Next added for exactly this (Sentry and OpenTelemetry use it too):
|
|
134
|
+
|
|
135
|
+
```ts
|
|
136
|
+
// instrumentation.ts, next to app/ or src/
|
|
137
|
+
export async function register() {
|
|
138
|
+
if (process.env.NEXT_RUNTIME === "nodejs") {
|
|
139
|
+
const { registerAztrx } = await import("aztrx-cli/next");
|
|
140
|
+
registerAztrx();
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
```bash
|
|
146
|
+
npm run dev
|
|
147
|
+
# ▲ Next.js 16.3.2 (Turbopack)
|
|
148
|
+
# - Local: http://localhost:3001
|
|
149
|
+
# [aztrx] 1 crash — first: app/page.tsx:18
|
|
150
|
+
# [aztrx] run `npx aztrx-cli` for the repro and the fix.
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
The `NEXT_RUNTIME` guard is required, not decoration: Next compiles
|
|
154
|
+
`instrumentation.ts` for the edge runtime as well, which has no child processes.
|
|
155
|
+
The plugin itself does nothing outside `next dev` — a scan during `next build` or
|
|
156
|
+
`next start` would point a browser at a real deployment. It finds the port Next
|
|
157
|
+
actually bound, so it still works when 3000 was taken and Next moved to 3001.
|
|
158
|
+
|
|
159
|
+
### Options and behaviour
|
|
160
|
+
|
|
161
|
+
```ts
|
|
162
|
+
// Vite: plugins: [aztrx({ … })] Next.js: registerAztrx({ … }) — same options.
|
|
163
|
+
{
|
|
164
|
+
maxActions: 50, // shallower, faster walk
|
|
165
|
+
fuzz: true, // seeded chaos instead of the deterministic walk
|
|
166
|
+
onResult: (r) => {}, // wire findings into your own tooling
|
|
167
|
+
}
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
Both plugins are a **sentinel, not a reporter**: silent until the scan finishes,
|
|
171
|
+
then one line. Run `aztrx-cli` for the full report, the executable repro, and the
|
|
172
|
+
fix. Findings also land in `.aztrx/` as usual.
|
|
173
|
+
|
|
174
|
+
The scan runs as a **separate process**, killed when the dev server stops. That is
|
|
175
|
+
deliberate: a scanner that crashes, hangs, or leaves a browser behind must never be
|
|
176
|
+
able to take your dev server down with it — and it dies with the parent even if the
|
|
177
|
+
parent is hard-killed. Set `AZTRX_DEV_SCAN=0` to skip a scan without editing the
|
|
178
|
+
config.
|
|
179
|
+
|
|
180
|
+
---
|
|
181
|
+
|
|
182
|
+
## Scan before you push (git hook)
|
|
183
|
+
|
|
184
|
+
One command and every `git push` scans your app first:
|
|
185
|
+
|
|
186
|
+
```bash
|
|
187
|
+
aztrx-cli hook install
|
|
188
|
+
# ✓ pre-push hook installed.
|
|
189
|
+
# .git/hooks/pre-push
|
|
190
|
+
# Every `git push` now scans the app and blocks on a crash.
|
|
191
|
+
# Skip one push with `git push --no-verify`, or a run with AZTRX_HOOK_SKIP=1.
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
A push that found something:
|
|
195
|
+
|
|
196
|
+
```
|
|
197
|
+
[aztrx] 1 changed file: src/Cart.tsx. Scanning the app…
|
|
198
|
+
[aztrx] 1 crash · 1 warning — first: src/Cart.tsx:42
|
|
199
|
+
[aztrx] run `npx aztrx-cli` for the repro and the fix.
|
|
200
|
+
[aztrx] push blocked — 1 crash/error finding in your app.
|
|
201
|
+
[aztrx] fix them and push again; to push anyway, `git push --no-verify`.
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
It scans **your app, not your diff** — a changed file is the *trigger*, not the scope. A crash
|
|
205
|
+
at `Cart.tsx:42` usually comes through a `useCart()` that changed three files away, and a
|
|
206
|
+
diff-shaped scan would look straight past it.
|
|
207
|
+
|
|
208
|
+
### It stays out of the way
|
|
209
|
+
|
|
210
|
+
Push a README and it says so and gets out of the way in about a second:
|
|
211
|
+
|
|
212
|
+
```
|
|
213
|
+
[aztrx] skipped — only docs, metadata or assets changed (1 file).
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
The skip list is deliberately short and boring: `.github/`, `.vscode/`, `.aztrx/`, `LICENSE`,
|
|
217
|
+
`.gitignore`, `.md`, images. Anything arguable — lockfiles, config, `.mdx` — is **not** on it,
|
|
218
|
+
because a wrong skip is a missed crash, which is the one thing this hook exists to prevent.
|
|
219
|
+
When it cannot tell what changed (a branch the remote has never seen, a shallow clone) it
|
|
220
|
+
scans the whole app and says why.
|
|
221
|
+
|
|
222
|
+
| | |
|
|
223
|
+
| --- | --- |
|
|
224
|
+
| `git push --no-verify` | skip this push |
|
|
225
|
+
| `AZTRX_HOOK_SKIP=1 git push` | same, for scripts |
|
|
226
|
+
| `aztrx-cli hook uninstall` | remove it |
|
|
227
|
+
|
|
228
|
+
### It cannot block you by accident
|
|
229
|
+
|
|
230
|
+
Aztrx blocks a push because it found a crash — never because it is broken:
|
|
231
|
+
|
|
232
|
+
- **Not installed, not on PATH** → the push proceeds.
|
|
233
|
+
- **The scan fails, or never finishes** → the push proceeds, with the reason printed.
|
|
234
|
+
`AZTRX_HOOK_TIMEOUT` (ms, default `300000`) caps a single scan.
|
|
235
|
+
- **A failed scan is never reported as clean.** "I could not look" is a different sentence
|
|
236
|
+
from "nothing there", and it stays that way.
|
|
237
|
+
|
|
238
|
+
A hook that blocks pushes when it is merely broken gets turned off, and then it catches
|
|
239
|
+
nothing.
|
|
240
|
+
|
|
241
|
+
The installed file is a short POSIX shim that calls back into the CLI, so `npm i -g
|
|
242
|
+
aztrx-cli@latest` upgrades the hook as well — nothing to reinstall. `hook install` is
|
|
243
|
+
idempotent, installs where git actually looks (so `core.hooksPath` — husky, Lefthook — is
|
|
244
|
+
respected), and refuses to overwrite a `pre-push` hook it did not write unless you pass
|
|
245
|
+
`--force`.
|
|
246
|
+
|
|
247
|
+
**Not handled:** under Yarn PnP there is no `node_modules/aztrx-cli` to find, so the hook
|
|
248
|
+
skips silently. Safe, but useless — use npm, pnpm, or Yarn with `nodeLinker: node-modules`.
|
|
249
|
+
|
|
250
|
+
---
|
|
251
|
+
|
|
252
|
+
## Use aztrx from your editor (MCP)
|
|
253
|
+
|
|
254
|
+
Your editor's agent can write code but cannot run it. It can compile, lint, and typecheck —
|
|
255
|
+
none of which open a page and click the button. So it will tell you a change works when all
|
|
256
|
+
it knows is that the change *parsed*.
|
|
257
|
+
|
|
258
|
+
`aztrx mcp` gives the agent the missing step: it drives the app in a real browser and hands
|
|
259
|
+
back the runtime crashes it produced, with an executable repro attached.
|
|
260
|
+
|
|
261
|
+
```bash
|
|
262
|
+
npx aztrx-cli mcp install
|
|
263
|
+
# ✓ Claude Code — .mcp.json — added.
|
|
264
|
+
# ✓ Cursor — .cursor/mcp.json — added.
|
|
265
|
+
# • VS Code — no .vscode/ in this project
|
|
266
|
+
#
|
|
267
|
+
# Restart your editor — or reload its window — to pick the server up.
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
It writes the config for the editors your project actually uses, and only ever adds one key
|
|
271
|
+
to a file it reads first. Your other MCP servers are left alone. If a config will not parse
|
|
272
|
+
it **refuses** rather than overwriting — a missing comma should never cost you your setup —
|
|
273
|
+
and `--force` is the escape hatch, which saves a `.bak` first. `mcp uninstall` removes only
|
|
274
|
+
our entry and keeps the file.
|
|
275
|
+
|
|
276
|
+
### The three tools
|
|
277
|
+
|
|
278
|
+
| Tool | What it does | Costs |
|
|
279
|
+
| --- | --- | --- |
|
|
280
|
+
| `aztrx_scan` | Drives the app, reports crashes/errors with source locations. Boots the dev server itself if none is running. | tens of seconds |
|
|
281
|
+
| `aztrx_repro` | The minimized steps and the compiled Playwright spec for one finding. Reads the scan that already ran. | free |
|
|
282
|
+
| `aztrx_fix` | Patch → verify in a git worktree by replaying the repro → optionally write it into your tree. | one model call |
|
|
283
|
+
|
|
284
|
+
The scan returns a compact projection with a `scanId`; the detail — the action sequence, the
|
|
285
|
+
spec, the diff — is fetched by handle. A stack trace in the agent's context on every scan is
|
|
286
|
+
how a useful tool becomes an expensive one.
|
|
287
|
+
|
|
288
|
+
`aztrx_fix` needs a key (`ANTHROPIC_API_KEY`, or `AZTRX_API_BASE` + `AZTRX_API_KEY` +
|
|
289
|
+
`AZTRX_MODEL` for any other provider). Without one it returns `no-llm` and attempts nothing.
|
|
290
|
+
Scanning and proving never need a key. Aztrx never commits: `apply: true` writes working-tree
|
|
291
|
+
files, and `git diff` is the review.
|
|
292
|
+
|
|
293
|
+
### What it will not do
|
|
294
|
+
|
|
295
|
+
- **No `--allow-destructive`, `--fuzz`, or `--http-fuzz` over MCP.** The tool drives a real
|
|
296
|
+
browser against an app you are working in. An agent should not be able to reach for
|
|
297
|
+
data-mutating controls because a prompt suggested it. If you want the fuzzer, that is a
|
|
298
|
+
deliberate `aztrx-cli run` in a terminal.
|
|
299
|
+
- **A scan that could not run is an error, never "no findings."** If the app would not boot
|
|
300
|
+
or the browser could not reach it, the tool returns `isError` with the reason — it does not
|
|
301
|
+
return an empty list. An agent that reads `0 crashes` from a scan that never happened will
|
|
302
|
+
tell you the code is fine.
|
|
303
|
+
|
|
304
|
+
### Protocol support
|
|
305
|
+
|
|
306
|
+
MCP changed shape in its `2026-07-28` revision: the `initialize` handshake is gone, replaced
|
|
307
|
+
by per-request metadata. Editors are split across that line — Claude Code speaks the new
|
|
308
|
+
revision, and Cursor still speaks `2025-11-25`.
|
|
309
|
+
|
|
310
|
+
`aztrx mcp` speaks **both** and picks per request, which is why it works in both editors
|
|
311
|
+
today and will keep working as the others move. It is ~550 lines — the protocol layer and the
|
|
312
|
+
server — with zero new dependencies;
|
|
313
|
+
neither official SDK covers both revisions, and the legacy one pulls seventeen runtime
|
|
314
|
+
dependencies (express, cors, jose, ajv…) for a stdio server that needs none of them.
|
|
315
|
+
|
|
316
|
+
---
|
|
317
|
+
|
|
94
318
|
## Autonomous patrol
|
|
95
319
|
|
|
96
320
|
`aztrx patrol` is the looped version of `run --fix`: point it at a running app and it
|
|
@@ -113,6 +337,8 @@ aztrx-cli patrol http://localhost:3000 --batch # group a cycle's fixes into one
|
|
|
113
337
|
| `--batch` | Group all of a cycle's fixes into one PR | one PR per bug |
|
|
114
338
|
| `--once` | Run a single scan then exit | loop forever |
|
|
115
339
|
| `--fuzz` / `--workers <n>` | Detection mode / parallelism (pass-through to `run`) | — |
|
|
340
|
+
| `--login` | Auto-login before each pass — needs `$AZTRX_AUTH_EMAIL` + `$AZTRX_AUTH_PASSWORD`, or `--login-email` / `--login-password` | off |
|
|
341
|
+
| `--storage-state <path>` | Playwright storage-state JSON for authenticated pages | — |
|
|
116
342
|
|
|
117
343
|
Guardrails keep the loop from running away: it only stages the files a patch touched
|
|
118
344
|
(never `git add -A`), dedups by crash fingerprint (a re-scan won't re-open the same PR),
|
|
@@ -123,7 +349,7 @@ backs off from unfixable bugs, and respects a session-wide LLM spend cap.
|
|
|
123
349
|
## Security
|
|
124
350
|
|
|
125
351
|
- **Local-first.** Nothing leaves your machine unless you opt in.
|
|
126
|
-
- **Never
|
|
352
|
+
- **Never touches the branch you are on.** `--fix` and `--heal` land a patch in a detached worktree for your review — `git diff` is the review. The two things that do commit are the two whose entire job is to open a PR: `--pr`, and `patrol`. Both commit to their own `aztrx/fix-…` branch, stage **only** the files a patch touched, and return you to the branch you started on.
|
|
127
353
|
- **Redacted.** Secrets are stripped from the file, error, and stack before any LLM call.
|
|
128
354
|
- **Deny-by-default network.** Off-origin calls are blocked; destructive clicks (delete/pay/logout) are refused.
|
|
129
355
|
- **`.aztrx/` is gitignored** — repros, reports, and patches stay out of history.
|
|
@@ -132,9 +358,8 @@ backs off from unfixable bugs, and respects a session-wide LLM spend cap.
|
|
|
132
358
|
|
|
133
359
|
## Continuous Integration (GitHub Action)
|
|
134
360
|
|
|
135
|
-
Runtime gate on every PR —
|
|
136
|
-
|
|
137
|
-
a crash/error.
|
|
361
|
+
Runtime gate on every PR — runs `aztrx-cli run --fail-on --repro --heal`, posts a comment with
|
|
362
|
+
the repro + patch, and fails on a crash/error.
|
|
138
363
|
|
|
139
364
|
```yaml
|
|
140
365
|
# .github/workflows/ci.yml — composite action, inline
|
|
@@ -142,30 +367,86 @@ on: pull_request
|
|
|
142
367
|
jobs:
|
|
143
368
|
aztrx:
|
|
144
369
|
runs-on: ubuntu-latest
|
|
370
|
+
timeout-minutes: 30 # recommended — see below
|
|
145
371
|
permissions: { contents: read, pull-requests: write }
|
|
146
372
|
steps:
|
|
147
373
|
- uses: actions/checkout@v4
|
|
148
|
-
- uses: Aztrx-AI/aztrx@v0.
|
|
374
|
+
- uses: Aztrx-AI/aztrx@v0.5.1
|
|
149
375
|
with:
|
|
150
|
-
url: http://localhost:3000
|
|
151
|
-
start-command: npm run dev # optional — boot the app in the background
|
|
152
376
|
token: ${{ github.token }}
|
|
153
377
|
anthropic-api-key: ${{ secrets.ANTHROPIC_API_KEY }} # optional — enables --heal
|
|
154
378
|
```
|
|
155
379
|
|
|
380
|
+
That is the whole setup: aztrx detects the framework, boots your dev server, scans, and shuts
|
|
381
|
+
it down again. Already have a server running, or want to control how it starts? Pass a URL and
|
|
382
|
+
the action stays out of the way:
|
|
383
|
+
|
|
384
|
+
```yaml
|
|
385
|
+
with:
|
|
386
|
+
url: http://localhost:3000
|
|
387
|
+
start-command: npm run dev # boot the app in the background first
|
|
388
|
+
wait-for: http://localhost:3000 # defaults to `url`
|
|
389
|
+
token: ${{ github.token }}
|
|
390
|
+
```
|
|
391
|
+
|
|
156
392
|
A status badge (`--badge`) and PR comment (`--pr-comment`) work the same way — regenerate in
|
|
157
393
|
CI on every push.
|
|
158
394
|
|
|
395
|
+
### What the check tells you
|
|
396
|
+
|
|
397
|
+
`uses: Aztrx-AI/aztrx@v0.5.1` runs **aztrx-cli 0.5.1** — the action reads its own version, so
|
|
398
|
+
the tag selects the engine, not just the wrapper.
|
|
399
|
+
|
|
400
|
+
A red check has two different meanings, and the message says which:
|
|
401
|
+
|
|
402
|
+
| | |
|
|
403
|
+
| --- | --- |
|
|
404
|
+
| `aztrx detected crash/error findings` | it scanned, and found something. Fix it, or run `npx aztrx-cli` locally for the repro. |
|
|
405
|
+
| `aztrx did not run (exit code: N)` | the scan never produced a result — a bad version pin, a registry failure, a broken config. **Not** a verdict about your app. The last lines of the run are printed to explain it. |
|
|
406
|
+
|
|
407
|
+
Set `timeout-minutes` on the job. The default `args` include `--repro` and `--heal`, which
|
|
408
|
+
drive your app repeatedly and can call a model — without a ceiling, one hung browser spends
|
|
409
|
+
GitHub's 6-hour job default finding nothing.
|
|
410
|
+
|
|
411
|
+
On a **fork pull request** the token is read-only, so the comment is skipped with a warning and
|
|
412
|
+
the check still reports the scan result. `--heal` needs `ANTHROPIC_API_KEY`, which forks do not
|
|
413
|
+
receive; it skips cleanly rather than failing.
|
|
414
|
+
|
|
415
|
+
Prefer not to wire it inline? The same thing is packaged as a reusable workflow:
|
|
416
|
+
|
|
417
|
+
```yaml
|
|
418
|
+
jobs:
|
|
419
|
+
aztrx:
|
|
420
|
+
uses: Aztrx-AI/aztrx/.github/workflows/aztrx-pr.yml@v0.5.1
|
|
421
|
+
secrets:
|
|
422
|
+
anthropic-api-key: ${{ secrets.ANTHROPIC_API_KEY }}
|
|
423
|
+
```
|
|
424
|
+
|
|
159
425
|
---
|
|
160
426
|
|
|
161
427
|
## CLI reference
|
|
162
428
|
|
|
163
429
|
`aztrx-cli run --help` is grouped by intent (Detect / Prove / Fix / Report & ship / Auth);
|
|
164
430
|
the table below is the complete reference — including flags hidden from `--help` (aliases and
|
|
165
|
-
niche tuning knobs).
|
|
431
|
+
niche tuning knobs). `aztrx-cli --version` (or `-V`) prints the installed version, read from
|
|
432
|
+
the package's own `package.json` so it cannot drift from what npm shipped.
|
|
433
|
+
|
|
434
|
+
The commands that are not `run`:
|
|
435
|
+
|
|
436
|
+
| Command | What it does |
|
|
437
|
+
| --- | --- |
|
|
438
|
+
| `mcp` | Serve the [MCP server](#use-aztrx-from-your-editor-mcp) on stdio (this is what an editor's config runs) |
|
|
439
|
+
| `mcp install [--force]` | Add aztrx to `.mcp.json` / `.cursor/mcp.json` / `.vscode/mcp.json`, merging one key |
|
|
440
|
+
| `mcp uninstall` | Remove only that key |
|
|
441
|
+
| `init` | Scaffold `aztrx.config.ts` |
|
|
442
|
+
| `hook install \| uninstall \| run` | The [pre-push hook](#scan-before-you-push-git-hook) |
|
|
443
|
+
| `patrol [url]` | Autonomous scan → fix → PR loop |
|
|
444
|
+
| `modernize <file>` | Rewrite a legacy file with an LLM |
|
|
445
|
+
| `studio [--port n]` | Live dashboard (`7331`) |
|
|
166
446
|
|
|
167
447
|
| Flag | Description | Default |
|
|
168
448
|
| --- | --- | --- |
|
|
449
|
+
| `--no-boot` | Attach to a running dev server only — never start one yourself | boots when needed |
|
|
169
450
|
| `--fuzz` | Seeded chaos fuzzing instead of the deterministic walk | — |
|
|
170
451
|
| `--http-fuzz` | Server-side mutation fuzzing — hostile requests against the target origin | — |
|
|
171
452
|
| `--http-fuzz-mutations` | With `--http-fuzz`: also send POST/PUT body mutations (default: GET-only) | — |
|
|
@@ -179,7 +460,7 @@ niche tuning knobs).
|
|
|
179
460
|
| `--pr` | Open a merge-ready PR with the verified fixes (with `--fix`) | — |
|
|
180
461
|
| `--lang <code>` | Language for the human-language summary (`en`, `ru`) | `en` |
|
|
181
462
|
| `--upload` | Stream run findings to the cloud ingest backend | — |
|
|
182
|
-
| `--api-key <key>` | Auth key for `--upload` / `--share-data` | `$
|
|
463
|
+
| `--api-key <key>` | Auth key for `--upload` / `--share-data` — distinct from the model provider key | `$AZTRX_CLOUD_API_KEY` |
|
|
183
464
|
| `--cloud-url <url>` | Ingest server base URL | `https://api.aztrx.app` |
|
|
184
465
|
| `--max-actions <n>` | Max actions per pass | `100` |
|
|
185
466
|
| `--seed <n>` | PRNG seed for deterministic fuzz | `42` |
|
|
@@ -194,10 +475,10 @@ niche tuning knobs).
|
|
|
194
475
|
| `--start-command <cmd>` | Command to boot the app for server healing | `scripts.dev` → `scripts.start` |
|
|
195
476
|
| `--pr-comment [path]` | Write a GitHub PR markdown comment | `.aztrx/pr-comment.md` |
|
|
196
477
|
| `--badge [path]` | Write a self-contained SVG status badge | `.aztrx/badge.svg` |
|
|
197
|
-
| `--regression-test [dir]` | Copy validated repro specs into the project test dir | `e2e
|
|
478
|
+
| `--regression-test [dir]` | Copy validated repro specs into the project test dir | first of `e2e/`, `tests/`, `test/`, `__tests__/`; else `.aztrx/regression/` |
|
|
198
479
|
| `--telemetry` | Collect anonymized tuples locally (opt-in) | — |
|
|
199
480
|
| `--share-data` | Also upload the sanitized tuples (opt-in) | — |
|
|
200
|
-
| `--repo <path>` | Root path for sourcemap → source resolution | cwd |
|
|
481
|
+
| `--repo <path>` | Root path for sourcemap → source resolution — must already exist (a path that does not is refused, not created) | cwd |
|
|
201
482
|
| `--allow-host <host>` | Add a host to the network allow-list (repeatable) | — |
|
|
202
483
|
| `--storage-state <path>` | Playwright storage-state for authenticated pages | — |
|
|
203
484
|
| `--auth <path>` | Hidden alias for `--storage-state` | — |
|
|
@@ -209,6 +490,17 @@ niche tuning knobs).
|
|
|
209
490
|
| `--dry-run` | Log planned actions without executing them | — |
|
|
210
491
|
| `--crash-test` | Throw a deliberate error to verify capture | — |
|
|
211
492
|
| `--plain` / `--ui` | Force plain logs / force the live panel | — |
|
|
493
|
+
| `--json` | One JSON document on stdout, nothing else — for editors, plugins, and CI | — |
|
|
494
|
+
|
|
495
|
+
> **The hosted ingest is not live yet.** `--upload` and `--share-data` point at
|
|
496
|
+
> `https://api.aztrx.app`, which does not currently resolve, and the server in
|
|
497
|
+
> `server/` has no deployment. The routes are real and tested —
|
|
498
|
+
> `tests/server-ingest.test.ts` drives the actual `createIngestServer` over HTTP,
|
|
499
|
+
> including the fingerprint dedup — but there is no host behind the default URL.
|
|
500
|
+
> Until there is, point `--cloud-url` at your own instance of `server/`. A failed
|
|
501
|
+
> upload now says so on stderr instead of being discarded, and `--api-key` takes
|
|
502
|
+
> `$AZTRX_CLOUD_API_KEY`: it is deliberately *not* `$AZTRX_API_KEY`, which is your
|
|
503
|
+
> model provider key and has no business on the wire as an ingest credential.
|
|
212
504
|
|
|
213
505
|
---
|
|
214
506
|
|
|
@@ -241,12 +533,17 @@ one seeded runtime bug:
|
|
|
241
533
|
| corpus | detection | deterministic repro |
|
|
242
534
|
| --- | --- | --- |
|
|
243
535
|
| 13 Next.js 16 apps | **13/13 · 100% recall** | **12/12 · 100%** |
|
|
244
|
-
|
|
|
536
|
+
| 13 vanilla archetypes | **13/13 · 100% recall** | **11/12 · 91.7%** |
|
|
537
|
+
|
|
538
|
+
One archetype is `13-swallowed-boundary` — a crash caught by an Error Boundary and
|
|
539
|
+
logged via `console.error`, never rethrown. A `pageerror`-only detector scores
|
|
540
|
+
**0/1** on it. It is the README's headline claim, so it lives in the corpus where a
|
|
541
|
+
regression would fail the benchmark.
|
|
245
542
|
|
|
246
543
|
Reproduce it yourself: `npm run bench` (archetypes) and `cd bench/frameworks && npm run bench`
|
|
247
544
|
(Next.js corpus). Per-case results and scope notes live in
|
|
248
|
-
[`bench/frameworks/RESULTS.md`](bench/frameworks/RESULTS.md) and
|
|
249
|
-
[`bench/RESULTS.md`](bench/RESULTS.md).
|
|
545
|
+
[`bench/frameworks/RESULTS.md`](https://github.com/Aztrx-AI/aztrx/blob/main/bench/frameworks/RESULTS.md) and
|
|
546
|
+
[`bench/RESULTS.md`](https://github.com/Aztrx-AI/aztrx/blob/main/bench/RESULTS.md).
|
|
250
547
|
|
|
251
548
|
## Contributing
|
|
252
549
|
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { Command, Help, Option } from "commander";
|
|
2
|
+
/**
|
|
3
|
+
* Intent-grouped `--help` for the `run` command. Commander v12 has no native
|
|
4
|
+
* option grouping, so we tag each Option with a group and render the sections
|
|
5
|
+
* ourselves (reusing commander's own term/description/wrap helpers for a
|
|
6
|
+
* consistent look).
|
|
7
|
+
*
|
|
8
|
+
* The goal is a funnel the user can actually remember — Detect / Prove / Fix /
|
|
9
|
+
* Report & ship / Auth — with everything else demoted to a compact "Advanced
|
|
10
|
+
* options" block rather than a flat 40-line dump.
|
|
11
|
+
*/
|
|
12
|
+
export type GroupName = "detect" | "prove" | "fix" | "ship" | "auth" | "advanced";
|
|
13
|
+
/** Build a commander Option tagged with a help group (defaults to "advanced"). */
|
|
14
|
+
export declare function opt(flags: string, description: string, group?: GroupName): Option;
|
|
15
|
+
/**
|
|
16
|
+
* Standalone `Help.formatHelp` override, registered via
|
|
17
|
+
* `runCommand.configureHelp({ formatHelp })`. Mirrors commander's built-in
|
|
18
|
+
* layout (Usage / Description / Arguments / Commands) but replaces the flat
|
|
19
|
+
* "Options:" list with named groups, and compresses the advanced flags into a
|
|
20
|
+
* single wrapped line of flag names.
|
|
21
|
+
*/
|
|
22
|
+
export declare function formatHelp(cmd: Command, helper: Help): string;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `--repo` is checked, not trusted.
|
|
3
|
+
*
|
|
4
|
+
* Commander consumes a `<required>` option argument even when the argument
|
|
5
|
+
* *looks like a flag*: `aztrx run --repo --fix` sets `repo` to the literal
|
|
6
|
+
* string `--fix` — measured against commander 12 rather than assumed. From
|
|
7
|
+
* there `path.resolve` turns it into `<cwd>/--fix`, and the first
|
|
8
|
+
* `mkdirSync(..., { recursive: true })` on the run path is happy to create it.
|
|
9
|
+
* So a mistyped invocation does not fail, it *succeeds* — against a project
|
|
10
|
+
* directory the tool invented — and leaves its `.aztrx/` artifacts inside.
|
|
11
|
+
*
|
|
12
|
+
* That is not hypothetical: `C:\Users\dchap\--fix\` existed holding nothing but
|
|
13
|
+
* `.aztrx/`, which is exactly this. An empty directory nobody can explain is a
|
|
14
|
+
* worse failure than an error message, because nothing reports it.
|
|
15
|
+
*/
|
|
16
|
+
/** Why `dir` cannot serve as a project root, or null when it can. Pure, so the
|
|
17
|
+
* wording is testable without spawning a process — the caller prints and exits. */
|
|
18
|
+
export declare function repoPathProblem(dir: string): string | null;
|
|
19
|
+
/** Absolute, existing project root — or exit 1 with the reason. */
|
|
20
|
+
export declare function resolveRepoRoot(raw: string | undefined, cwd?: string): string;
|
package/dist/cli/repo.js
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import pc from "picocolors";
|
|
4
|
+
/**
|
|
5
|
+
* `--repo` is checked, not trusted.
|
|
6
|
+
*
|
|
7
|
+
* Commander consumes a `<required>` option argument even when the argument
|
|
8
|
+
* *looks like a flag*: `aztrx run --repo --fix` sets `repo` to the literal
|
|
9
|
+
* string `--fix` — measured against commander 12 rather than assumed. From
|
|
10
|
+
* there `path.resolve` turns it into `<cwd>/--fix`, and the first
|
|
11
|
+
* `mkdirSync(..., { recursive: true })` on the run path is happy to create it.
|
|
12
|
+
* So a mistyped invocation does not fail, it *succeeds* — against a project
|
|
13
|
+
* directory the tool invented — and leaves its `.aztrx/` artifacts inside.
|
|
14
|
+
*
|
|
15
|
+
* That is not hypothetical: `C:\Users\dchap\--fix\` existed holding nothing but
|
|
16
|
+
* `.aztrx/`, which is exactly this. An empty directory nobody can explain is a
|
|
17
|
+
* worse failure than an error message, because nothing reports it.
|
|
18
|
+
*/
|
|
19
|
+
/** Why `dir` cannot serve as a project root, or null when it can. Pure, so the
|
|
20
|
+
* wording is testable without spawning a process — the caller prints and exits. */
|
|
21
|
+
export function repoPathProblem(dir) {
|
|
22
|
+
let st;
|
|
23
|
+
try {
|
|
24
|
+
st = fs.statSync(dir);
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
// A path whose last segment is itself a flag is the signature of the option
|
|
28
|
+
// having eaten one, and saying so beats leaving the user to re-read their
|
|
29
|
+
// own command line for it.
|
|
30
|
+
const base = path.basename(dir);
|
|
31
|
+
if (base.startsWith("-")) {
|
|
32
|
+
return (`no such directory: ${dir}\n` +
|
|
33
|
+
` \`${base}\` looks like a flag, not a path — did it get taken as the value of \`--repo\`?`);
|
|
34
|
+
}
|
|
35
|
+
return `no such directory: ${dir}`;
|
|
36
|
+
}
|
|
37
|
+
if (!st.isDirectory())
|
|
38
|
+
return `not a directory: ${dir}`;
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
/** Absolute, existing project root — or exit 1 with the reason. */
|
|
42
|
+
export function resolveRepoRoot(raw, cwd = process.cwd()) {
|
|
43
|
+
const dir = path.resolve(cwd, raw ?? ".");
|
|
44
|
+
const problem = repoPathProblem(dir);
|
|
45
|
+
if (problem) {
|
|
46
|
+
console.error(pc.red("✗ ") + problem);
|
|
47
|
+
process.exit(1);
|
|
48
|
+
}
|
|
49
|
+
return dir;
|
|
50
|
+
}
|
package/dist/cli.d.ts
ADDED