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.
Files changed (102) hide show
  1. package/README.md +326 -29
  2. package/dist/cli/help.d.ts +22 -0
  3. package/dist/cli/repo.d.ts +20 -0
  4. package/dist/cli/repo.js +50 -0
  5. package/dist/cli.d.ts +2 -0
  6. package/dist/cli.js +284 -86
  7. package/dist/core/auth.d.ts +33 -0
  8. package/dist/core/badge.d.ts +9 -0
  9. package/dist/core/browser.d.ts +3 -0
  10. package/dist/core/classifier.d.ts +41 -0
  11. package/dist/core/cloud/index.d.ts +62 -0
  12. package/dist/core/cloud/index.js +24 -4
  13. package/dist/core/devServer.d.ts +90 -0
  14. package/dist/core/devServer.js +253 -0
  15. package/dist/core/diagnose.d.ts +18 -0
  16. package/dist/core/diff.d.ts +31 -0
  17. package/dist/core/domWalker.d.ts +24 -0
  18. package/dist/core/domWalker.js +18 -2
  19. package/dist/core/eventBus.d.ts +59 -0
  20. package/dist/core/events.d.ts +42 -0
  21. package/dist/core/fixPr.d.ts +16 -0
  22. package/dist/core/fixPr.js +11 -1
  23. package/dist/core/fuzzer.d.ts +24 -0
  24. package/dist/core/fuzzer.js +1 -1
  25. package/dist/core/heal/apply.d.ts +28 -0
  26. package/dist/core/heal/boot.d.ts +74 -0
  27. package/dist/core/heal/boot.js +126 -23
  28. package/dist/core/heal/childEnv.d.ts +15 -0
  29. package/dist/core/heal/gates.d.ts +11 -0
  30. package/dist/core/heal/index.d.ts +19 -0
  31. package/dist/core/heal/index.js +53 -23
  32. package/dist/core/heal/llm.d.ts +43 -0
  33. package/dist/core/heal/llm.js +22 -2
  34. package/dist/core/heal/redact.d.ts +22 -0
  35. package/dist/core/heal/sandbox.d.ts +54 -0
  36. package/dist/core/heal/sandbox.js +63 -3
  37. package/dist/core/heal/types.d.ts +102 -0
  38. package/dist/core/heal/verify.d.ts +37 -0
  39. package/dist/core/heal/verify.js +28 -14
  40. package/dist/core/httpFuzzer.d.ts +33 -0
  41. package/dist/core/init.d.ts +21 -0
  42. package/dist/core/init.js +5 -6
  43. package/dist/core/interceptor.d.ts +21 -0
  44. package/dist/core/llm.d.ts +36 -0
  45. package/dist/core/llm.js +40 -13
  46. package/dist/core/minimizer.d.ts +13 -0
  47. package/dist/core/modernize.d.ts +28 -0
  48. package/dist/core/modernize.js +2 -2
  49. package/dist/core/networkGuard.d.ts +13 -0
  50. package/dist/core/orchestrator.d.ts +86 -0
  51. package/dist/core/orchestrator.js +5 -2
  52. package/dist/core/patrol/loop.d.ts +46 -0
  53. package/dist/core/patrol/pr.d.ts +42 -0
  54. package/dist/core/patrol/pr.js +9 -6
  55. package/dist/core/patrol/record.d.ts +51 -0
  56. package/dist/core/patrol/state.d.ts +39 -0
  57. package/dist/core/pr.d.ts +3 -0
  58. package/dist/core/pr.js +15 -11
  59. package/dist/core/prompt.d.ts +14 -0
  60. package/dist/core/recorder.d.ts +23 -0
  61. package/dist/core/recorder.js +1 -1
  62. package/dist/core/renderMarkdown.d.ts +7 -0
  63. package/dist/core/replay.d.ts +33 -0
  64. package/dist/core/replay.js +52 -7
  65. package/dist/core/report.d.ts +8 -0
  66. package/dist/core/resolver.d.ts +64 -0
  67. package/dist/core/resolver.js +135 -17
  68. package/dist/core/rng.d.ts +2 -0
  69. package/dist/core/specCompiler.d.ts +17 -0
  70. package/dist/core/studio.d.ts +6 -0
  71. package/dist/core/studio.js +0 -3
  72. package/dist/core/summarize.d.ts +26 -0
  73. package/dist/core/summarize.js +17 -6
  74. package/dist/core/swarm.d.ts +97 -0
  75. package/dist/core/swarm.js +25 -2
  76. package/dist/core/telemetry/index.d.ts +33 -0
  77. package/dist/core/telemetry/index.js +22 -4
  78. package/dist/core/telemetry/sanitize.d.ts +19 -0
  79. package/dist/core/telemetry/types.d.ts +26 -0
  80. package/dist/core/types.d.ts +88 -0
  81. package/dist/core/ui.d.ts +23 -0
  82. package/dist/core/ui.js +0 -5
  83. package/dist/core/validator.d.ts +14 -0
  84. package/dist/core/version.d.ts +1 -0
  85. package/dist/hooks/index.d.ts +106 -0
  86. package/dist/hooks/index.js +349 -0
  87. package/dist/mcp/index.d.ts +58 -0
  88. package/dist/mcp/index.js +295 -0
  89. package/dist/mcp/install.d.ts +52 -0
  90. package/dist/mcp/install.js +204 -0
  91. package/dist/mcp/protocol.d.ts +106 -0
  92. package/dist/mcp/protocol.js +156 -0
  93. package/dist/mcp/tools.d.ts +107 -0
  94. package/dist/mcp/tools.js +621 -0
  95. package/dist/next/index.d.ts +70 -0
  96. package/dist/next/index.js +103 -0
  97. package/dist/plugins/scan.d.ts +85 -0
  98. package/dist/plugins/scan.js +188 -0
  99. package/dist/ui/app.d.ts +11 -0
  100. package/dist/vite/index.d.ts +60 -0
  101. package/dist/vite/index.js +56 -0
  102. package/package.json +44 -6
@@ -0,0 +1,70 @@
1
+ /**
2
+ * Next.js dev-server plugin — `aztrx-cli/next`.
3
+ *
4
+ * Next has no `vite.config.ts` to hang a plugin off. What it does have is
5
+ * `instrumentation.ts`, the hook Next added for exactly this kind of
6
+ * instrumentation (Sentry, OpenTelemetry) — it runs once in the server process
7
+ * at startup, in dev and in production alike. So the adapter is a `register()`
8
+ * call, not a config object:
9
+ *
10
+ * // instrumentation.ts, next to app/ or src/
11
+ * export async function register() {
12
+ * if (process.env.NEXT_RUNTIME === "nodejs") {
13
+ * const { registerAztrx } = await import("aztrx-cli/next");
14
+ * registerAztrx();
15
+ * }
16
+ * }
17
+ *
18
+ * Next 15 `register()` may return a promise that Next awaits *before it starts
19
+ * serving*. That makes the obvious implementation — wait for the server, then
20
+ * scan — a deadlock: the server cannot serve until `register()` returns, and
21
+ * `register()` would be waiting on the server. So `registerAztrx` returns
22
+ * immediately and does its waiting detached. It is deliberately not `async`.
23
+ *
24
+ * The scan itself lives in `src/plugins/scan.ts`, shared with the Vite plugin.
25
+ */
26
+ import type { AztrxResult } from "../plugins/scan.js";
27
+ export type { AztrxResult } from "../plugins/scan.js";
28
+ export { summarize } from "../plugins/scan.js";
29
+ export { resolveCliPath } from "../plugins/scan.js";
30
+ export interface NextPluginOptions {
31
+ /** Run a scan once the dev server is up. Default `true` — creating
32
+ * `instrumentation.ts` is already the opt-in. `AZTRX_DEV_SCAN=0` disables it
33
+ * for a single run without touching the file. */
34
+ enabled?: boolean;
35
+ /** The dev server to attack. Almost never needed: the port is derived from
36
+ * your `next dev` script, falling back to Next's default 3000. Set it (or
37
+ * `AZTRX_URL`) when Next picked a different port because 3000 was taken. */
38
+ url?: string;
39
+ /** Max interactions per pass (the CLI's `--max-actions`). */
40
+ maxActions?: number;
41
+ /** Seeded chaos fuzzing instead of the deterministic walk. */
42
+ fuzz?: boolean;
43
+ /** Also fuzz the server's HTTP endpoints. Off by default: it mutates. */
44
+ httpFuzz?: boolean;
45
+ /** Repo root for sourcemap resolution and `.aztrx/` artifacts. Defaults to
46
+ * the Next project's working directory. */
47
+ repoRoot?: string;
48
+ /** Extra CLI flags, for anything not surfaced above. */
49
+ args?: string[];
50
+ /** How long to wait for the dev server to answer before reporting that it
51
+ * never came up. Raise it for a monorepo whose cold compile outlasts the
52
+ * default. */
53
+ readyTimeoutMs?: number;
54
+ /** Replace the default one-line report — e.g. to wire findings into your own
55
+ * tooling. */
56
+ onResult?: (result: AztrxResult) => void;
57
+ /** Called when the scan itself fails (never throws into the dev server). */
58
+ onError?: (message: string) => void;
59
+ }
60
+ /** The URL to attack: explicit option → `AZTRX_URL` → the port Next itself is
61
+ * listening on → the port this project's `next dev` script asks for.
62
+ *
63
+ * `process.env.PORT` is the good one: Next sets it in the instrumentation
64
+ * process to the port it actually bound, *after* resolving collisions. That is
65
+ * the only source that stays correct when Next auto-increments away from 3000
66
+ * because something else already holds it. Exported for tests — the rest is I/O. */
67
+ export declare function resolveNextUrl(repoRoot: string, explicit?: string): string;
68
+ /** Wire Aztrx into a Next.js dev server. Call from `instrumentation.ts`.
69
+ * Returns immediately — see the deadlock note at the top of this file. */
70
+ export declare function registerAztrx(options?: NextPluginOptions): void;
@@ -0,0 +1,103 @@
1
+ /**
2
+ * Next.js dev-server plugin — `aztrx-cli/next`.
3
+ *
4
+ * Next has no `vite.config.ts` to hang a plugin off. What it does have is
5
+ * `instrumentation.ts`, the hook Next added for exactly this kind of
6
+ * instrumentation (Sentry, OpenTelemetry) — it runs once in the server process
7
+ * at startup, in dev and in production alike. So the adapter is a `register()`
8
+ * call, not a config object:
9
+ *
10
+ * // instrumentation.ts, next to app/ or src/
11
+ * export async function register() {
12
+ * if (process.env.NEXT_RUNTIME === "nodejs") {
13
+ * const { registerAztrx } = await import("aztrx-cli/next");
14
+ * registerAztrx();
15
+ * }
16
+ * }
17
+ *
18
+ * Next 15 `register()` may return a promise that Next awaits *before it starts
19
+ * serving*. That makes the obvious implementation — wait for the server, then
20
+ * scan — a deadlock: the server cannot serve until `register()` returns, and
21
+ * `register()` would be waiting on the server. So `registerAztrx` returns
22
+ * immediately and does its waiting detached. It is deliberately not `async`.
23
+ *
24
+ * The scan itself lives in `src/plugins/scan.ts`, shared with the Vite plugin.
25
+ */
26
+ import { planBoot, waitForHttp } from "../core/devServer.js";
27
+ import { resolveRepoRoot, scanDisabled, spawnScan } from "../plugins/scan.js";
28
+ export { summarize } from "../plugins/scan.js";
29
+ // Exposed so a "scan did not produce a result" report can name the path that was
30
+ // actually tried — the one fact needed to tell a resolution bug from a scan bug.
31
+ export { resolveCliPath } from "../plugins/scan.js";
32
+ /** How long to wait for the dev server to answer before giving up. Generous for
33
+ * the same reason as the CLI's attach timeout: Next binds its port well before
34
+ * it can serve, and a cold Turbopack compile is not fast. */
35
+ const READY_TIMEOUT_MS = 60_000;
36
+ /** Next may call `register()` more than once (restarts, worker processes); a
37
+ * second scan of the same server is pure waste. Module-scoped because the
38
+ * duplicate arrives in the same process. */
39
+ let started = false;
40
+ /** The URL to attack: explicit option → `AZTRX_URL` → the port Next itself is
41
+ * listening on → the port this project's `next dev` script asks for.
42
+ *
43
+ * `process.env.PORT` is the good one: Next sets it in the instrumentation
44
+ * process to the port it actually bound, *after* resolving collisions. That is
45
+ * the only source that stays correct when Next auto-increments away from 3000
46
+ * because something else already holds it. Exported for tests — the rest is I/O. */
47
+ export function resolveNextUrl(repoRoot, explicit) {
48
+ if (explicit)
49
+ return explicit;
50
+ if (process.env.AZTRX_URL)
51
+ return process.env.AZTRX_URL;
52
+ if (process.env.PORT)
53
+ return `http://localhost:${process.env.PORT}`;
54
+ // planBoot reads the dev script's `--port`/`-p` and otherwise returns Next's
55
+ // default. It returns null only when there is no dev/start script at all —
56
+ // not our problem to guess at that point, so fall back to the default.
57
+ return `http://localhost:${planBoot(repoRoot)?.port ?? 3000}`;
58
+ }
59
+ /** Wire Aztrx into a Next.js dev server. Call from `instrumentation.ts`.
60
+ * Returns immediately — see the deadlock note at the top of this file. */
61
+ export function registerAztrx(options = {}) {
62
+ // `next build` and `next start` are production; a scan there would drive a
63
+ // browser at a real deployment. Dev only, unconditionally.
64
+ if (process.env.NODE_ENV !== "development")
65
+ return;
66
+ // `instrumentation.ts` is also bundled for the edge runtime, which has no
67
+ // child_process. The caller's `NEXT_RUNTIME` guard is the documented form;
68
+ // this is the one that holds even when they forget it.
69
+ if (process.env.NEXT_RUNTIME && process.env.NEXT_RUNTIME !== "nodejs")
70
+ return;
71
+ if (scanDisabled(options.enabled))
72
+ return;
73
+ if (started)
74
+ return;
75
+ started = true;
76
+ const repoRoot = resolveRepoRoot(options.repoRoot, process.cwd());
77
+ const url = resolveNextUrl(repoRoot, options.url);
78
+ /** Killed on the way out, or the browser outlives the server it was driving.
79
+ * `exit` rather than SIGINT/SIGTERM on purpose: this handler lives inside the
80
+ * *user's* server process, and installing signal handlers there would fight
81
+ * whatever Next (and Sentry, and nodemon) already installed. Next's own
82
+ * Ctrl-C path calls `process.exit()`, which fires this. */
83
+ let scan;
84
+ process.once("exit", () => scan?.stop());
85
+ const timeoutMs = options.readyTimeoutMs ?? READY_TIMEOUT_MS;
86
+ void (async () => {
87
+ if (!(await waitForHttp(url, timeoutMs))) {
88
+ options.onError?.(`no dev server answered at ${url} within ${timeoutMs / 1000}s — ` +
89
+ "if Next picked a different port, set AZTRX_URL or the `url` option.");
90
+ return;
91
+ }
92
+ scan = spawnScan({
93
+ url,
94
+ repoRoot,
95
+ maxActions: options.maxActions,
96
+ fuzz: options.fuzz,
97
+ httpFuzz: options.httpFuzz,
98
+ args: options.args,
99
+ onResult: options.onResult,
100
+ onError: options.onError,
101
+ });
102
+ })();
103
+ }
@@ -0,0 +1,85 @@
1
+ /**
2
+ * The scan child that every dev-server plugin (`aztrx-cli/vite`,
3
+ * `aztrx-cli/next`) runs.
4
+ *
5
+ * The scan is a **child process**, not an in-process call. That is the whole
6
+ * point of the design: a scanner that crashes, hangs, or leaves a browser behind
7
+ * must never be able to take the user's dev server down with it. The child speaks
8
+ * `--json` (exactly one document on stdout), so the parent only parses a result.
9
+ *
10
+ * The plugins are deliberately quiet — one line when the scan finishes, nothing
11
+ * while it runs. The dev server owns the terminal; Aztrx is a guest in it.
12
+ */
13
+ import type { Finding } from "../core/types.js";
14
+ /** The JSON envelope `aztrx-cli --json` prints — see the `if (json)` block in
15
+ * `src/cli.ts`, which serializes `findings` straight from the run.
16
+ *
17
+ * `findings` is the CLI's own `Finding`, not a restatement of it. An earlier
18
+ * hand-written copy of the shape guessed `mappedLocation.file` where the real
19
+ * field is `filePath`, and because the tests were written against the same
20
+ * guess they passed while the plugin printed `first: undefined:18` for every
21
+ * real crash. Borrowing the type makes that class of drift a compile error. */
22
+ export interface AztrxResult {
23
+ version: number;
24
+ url: string;
25
+ repoRoot: string;
26
+ counts: {
27
+ crash: number;
28
+ error: number;
29
+ warning: number;
30
+ };
31
+ findings: Finding[];
32
+ }
33
+ export interface ScanOptions {
34
+ /** The dev server to attack. Omitted only with `boot: true`, where the child
35
+ * works out (and starts) a server itself. */
36
+ url?: string;
37
+ /** Repo root for sourcemap resolution and `.aztrx/` artifacts. */
38
+ repoRoot: string;
39
+ /** Let the child find a dev server, or start the project's own and stop it
40
+ * again. Default `false`: the dev-server plugins run *because* a server is
41
+ * already up, and a second one would be a bug. The git hook passes `true`
42
+ * because during a push there usually is not one — and the booted server is
43
+ * then owned by the child, which already tears down correctly on every exit
44
+ * path including a parent's death. */
45
+ boot?: boolean;
46
+ /** Max interactions per pass (the CLI's `--max-actions`). */
47
+ maxActions?: number;
48
+ /** Seeded chaos fuzzing instead of the deterministic walk. */
49
+ fuzz?: boolean;
50
+ /** Also fuzz the server's HTTP endpoints. Off by default: it mutates. */
51
+ httpFuzz?: boolean;
52
+ /** Extra CLI flags, for anything not surfaced above. */
53
+ args?: string[];
54
+ /** Replaces the default one-line report — e.g. to wire findings into your own
55
+ * tooling. */
56
+ onResult?: (result: AztrxResult) => void;
57
+ /** Called when the scan itself fails. Never throws into the dev server. */
58
+ onError?: (message: string) => void;
59
+ }
60
+ export interface ScanHandle {
61
+ /** Kill the scan and everything below it. Idempotent, synchronous (it is also
62
+ * called from `process.on("exit")`, where only sync work runs), and it
63
+ * suppresses the "no result" error — an interrupted scan has nothing to say. */
64
+ stop: () => void;
65
+ /** Resolves once the child has exited. Never rejects. */
66
+ done: Promise<void>;
67
+ }
68
+ /** Every layout the CLI plausibly lives in, most likely first. Exported for
69
+ * tests — everything below is I/O. */
70
+ export declare function cliCandidates(repoRoot: string): string[];
71
+ /** The first candidate that exists, or undefined. Split from the filesystem so
72
+ * the ordering is testable without a real install to point at. */
73
+ export declare function pickCliPath(candidates: string[], exists?: (p: string) => boolean): string | undefined;
74
+ /** The first candidate that actually exists on disk. */
75
+ export declare function resolveCliPath(repoRoot: string): string;
76
+ /** One line, no colour games — it has to read well in the middle of the dev
77
+ * server's own startup output. */
78
+ export declare function summarize(result: AztrxResult): string;
79
+ export declare function spawnScan(opts: ScanOptions): ScanHandle;
80
+ /** Shared by both plugins: adding the plugin to your config is already the
81
+ * opt-in, and `AZTRX_DEV_SCAN=0` skips a scan for one run without touching it. */
82
+ export declare function scanDisabled(optionsEnabled: boolean | undefined): boolean;
83
+ /** Repo root for sourcemap resolution. The dev server's root is the right
84
+ * default, but a monorepo package root is often deeper than the repo. */
85
+ export declare function resolveRepoRoot(explicit: string | undefined, fallback: string): string;
@@ -0,0 +1,188 @@
1
+ /**
2
+ * The scan child that every dev-server plugin (`aztrx-cli/vite`,
3
+ * `aztrx-cli/next`) runs.
4
+ *
5
+ * The scan is a **child process**, not an in-process call. That is the whole
6
+ * point of the design: a scanner that crashes, hangs, or leaves a browser behind
7
+ * must never be able to take the user's dev server down with it. The child speaks
8
+ * `--json` (exactly one document on stdout), so the parent only parses a result.
9
+ *
10
+ * The plugins are deliberately quiet — one line when the scan finishes, nothing
11
+ * while it runs. The dev server owns the terminal; Aztrx is a guest in it.
12
+ */
13
+ import { spawn } from "child_process";
14
+ import * as fs from "fs";
15
+ import { createRequire } from "module";
16
+ import * as path from "path";
17
+ import { fileURLToPath } from "url";
18
+ import { killTreeSync } from "../core/heal/boot.js";
19
+ /** Where the installed package's `cli.js` is, or undefined if we can't find one.
20
+ * Split out so `resolveCliPath` can try several shapes. */
21
+ function tryResolveCli() {
22
+ // Anchored on `process.cwd()` — the user's project — and not on
23
+ // `import.meta.url`. Both halves matter:
24
+ //
25
+ // * cwd, because a bundler will happily pre-evaluate `require.resolve` when
26
+ // its base is statically known. Turbopack did exactly that to the
27
+ // `import.meta.url` form and inlined a build-time virtual path
28
+ // (`…/[project]/node_modules/aztrx-cli/dist/cli.js`) that does not exist on
29
+ // disk. A base that is only known at runtime cannot be folded.
30
+ // * not `import.meta.url`, because when this module is bundled its URL is the
31
+ // *bundle's* — inside `.next/dev/server/` — and anything relative to it
32
+ // points at build output rather than the installed package.
33
+ const fromCwd = createRequire(path.join(process.cwd(), "index.js"));
34
+ try {
35
+ // `./package.json` is the one subpath the exports map always exposes.
36
+ return path.join(path.dirname(fromCwd.resolve("aztrx-cli/package.json")), "dist", "cli.js");
37
+ }
38
+ catch {
39
+ // No `require.resolve` fallback: under a bundler it is the thing that lies.
40
+ return undefined;
41
+ }
42
+ }
43
+ /** Every layout the CLI plausibly lives in, most likely first. Exported for
44
+ * tests — everything below is I/O. */
45
+ export function cliCandidates(repoRoot) {
46
+ const resolved = tryResolveCli();
47
+ const candidates = [
48
+ resolved,
49
+ // npm's normal shape, read straight off the filesystem. Redundant when
50
+ // `require.resolve` worked; the whole point when it did not.
51
+ path.join(repoRoot, "node_modules", "aztrx-cli", "dist", "cli.js"),
52
+ path.join(process.cwd(), "node_modules", "aztrx-cli", "dist", "cli.js"),
53
+ // A checkout that was never installed into anyone's node_modules — which is
54
+ // how this repo's own tests load the plugins.
55
+ path.join(path.dirname(fileURLToPath(import.meta.url)), "..", "cli.js"),
56
+ ];
57
+ return candidates.filter((c) => Boolean(c));
58
+ }
59
+ /** The first candidate that exists, or undefined. Split from the filesystem so
60
+ * the ordering is testable without a real install to point at. */
61
+ export function pickCliPath(candidates, exists = fs.existsSync) {
62
+ return candidates.find(exists);
63
+ }
64
+ /** The first candidate that actually exists on disk. */
65
+ export function resolveCliPath(repoRoot) {
66
+ const candidates = cliCandidates(repoRoot);
67
+ // Nothing found: hand back the first guess so the spawn error names a real
68
+ // path the user can look at, rather than a placeholder.
69
+ return pickCliPath(candidates) ?? candidates[0];
70
+ }
71
+ /** One line, no colour games — it has to read well in the middle of the dev
72
+ * server's own startup output. */
73
+ export function summarize(result) {
74
+ const { crash, error, warning } = result.counts;
75
+ const total = crash + error + warning;
76
+ if (total === 0)
77
+ return "[aztrx] clean — no runtime crashes found.";
78
+ const bits = [
79
+ crash && `${crash} crash${crash === 1 ? "" : "es"}`,
80
+ error && `${error} error${error === 1 ? "" : "s"}`,
81
+ warning && `${warning} warning${warning === 1 ? "" : "s"}`,
82
+ ].filter(Boolean);
83
+ const first = result.findings.find((f) => f.severity === "crash" || f.severity === "error");
84
+ // Forward slashes so the line reads the same on Windows as everywhere else.
85
+ const where = first?.mappedLocation
86
+ ? ` — first: ${first.mappedLocation.filePath.replace(/\\/g, "/")}:${first.mappedLocation.line}`
87
+ : "";
88
+ return `[aztrx] ${bits.join(" · ")}${where}\n[aztrx] run \`npx aztrx-cli\` for the repro and the fix.`;
89
+ }
90
+ /** What went wrong, in one line, so a failed scan says *why*.
91
+ *
92
+ * Not a fixed slice of either end. A Node crash dump opens with
93
+ * `node:internal/modules/cjs/loader:1451 / throw err; / ^` boilerplate and only
94
+ * *then* names the failure; the tail is `at node:internal/main/...` noise. Both
95
+ * a head-slice and a tail-slice report everything except the cause — so find the
96
+ * line that names it. */
97
+ function stderrTail(stderr) {
98
+ const lines = stderr
99
+ .split("\n")
100
+ .map((l) => l.trim())
101
+ .filter(Boolean);
102
+ if (!lines.length)
103
+ return "";
104
+ const at = lines.findIndex((l) => /(^|\s)\w*Error\b/.test(l));
105
+ return `: ${(at === -1 ? lines.slice(0, 3) : lines.slice(at, at + 2)).join(" ")}`;
106
+ }
107
+ export function spawnScan(opts) {
108
+ const args = [
109
+ resolveCliPath(opts.repoRoot),
110
+ ...(opts.url ? [opts.url] : []),
111
+ "--json",
112
+ // Attach to the running server, never boot another — unless the caller said
113
+ // there isn't one (`boot`), which is the git hook during a push.
114
+ ...(opts.boot ? [] : ["--no-boot"]),
115
+ "--repo",
116
+ opts.repoRoot,
117
+ ...(opts.maxActions !== undefined ? ["--max-actions", String(opts.maxActions)] : []),
118
+ ...(opts.fuzz ? ["--fuzz"] : []),
119
+ ...(opts.httpFuzz ? ["--http-fuzz"] : []),
120
+ ...(opts.args ?? []),
121
+ ];
122
+ let stopping = false;
123
+ let child;
124
+ try {
125
+ child = spawn(process.execPath, args, {
126
+ cwd: opts.repoRoot,
127
+ // Same process-group trick as the boot path: POSIX signals the group
128
+ // (node + its Playwright browser), Windows uses taskkill /T.
129
+ detached: process.platform !== "win32",
130
+ // The IPC slot is not for messages — it is a death signal. The OS closes
131
+ // the channel when the parent goes away, *including* when it is SIGKILLed,
132
+ // which no signal handler can catch. See the watcher in `src/cli.ts`.
133
+ stdio: ["ignore", "pipe", "pipe", "ipc"],
134
+ });
135
+ }
136
+ catch (e) {
137
+ opts.onError?.(e.message);
138
+ return { stop: () => { }, done: Promise.resolve() };
139
+ }
140
+ const pid = child.pid;
141
+ let stdout = "";
142
+ let stderr = "";
143
+ child.stdout?.on("data", (d) => (stdout += d.toString()));
144
+ child.stderr?.on("data", (d) => (stderr += d.toString()));
145
+ child.on("error", (e) => {
146
+ if (!stopping)
147
+ opts.onError?.(e.message);
148
+ });
149
+ const done = new Promise((resolve) => {
150
+ child.on("close", () => {
151
+ if (stopping)
152
+ return resolve();
153
+ let result;
154
+ try {
155
+ result = JSON.parse(stdout);
156
+ }
157
+ catch {
158
+ // A failed scan must not look like a clean one — say so instead of
159
+ // staying silent, which is exactly how a real crash gets missed.
160
+ opts.onError?.(`scan did not produce a result${stderrTail(stderr)}`);
161
+ return resolve();
162
+ }
163
+ if (opts.onResult)
164
+ opts.onResult(result);
165
+ else
166
+ console.log(summarize(result));
167
+ resolve();
168
+ });
169
+ });
170
+ return {
171
+ stop: () => {
172
+ stopping = true;
173
+ if (pid)
174
+ killTreeSync(pid);
175
+ },
176
+ done,
177
+ };
178
+ }
179
+ /** Shared by both plugins: adding the plugin to your config is already the
180
+ * opt-in, and `AZTRX_DEV_SCAN=0` skips a scan for one run without touching it. */
181
+ export function scanDisabled(optionsEnabled) {
182
+ return optionsEnabled === false || process.env.AZTRX_DEV_SCAN === "0";
183
+ }
184
+ /** Repo root for sourcemap resolution. The dev server's root is the right
185
+ * default, but a monorepo package root is often deeper than the repo. */
186
+ export function resolveRepoRoot(explicit, fallback) {
187
+ return path.resolve(explicit ?? fallback);
188
+ }
@@ -0,0 +1,11 @@
1
+ import type { EventBus } from "../core/eventBus.js";
2
+ import type { Finding } from "../core/types.js";
3
+ export interface AztrxAppProps {
4
+ bus: EventBus;
5
+ done: Promise<Finding[]>;
6
+ targetUrl: string;
7
+ repoRoot: string;
8
+ mode: string;
9
+ }
10
+ /** Mount the live terminal panel and resolve once the run (or a failure) ends. */
11
+ export declare function renderTui(props: AztrxAppProps): Promise<void>;
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Vite dev-server plugin — `aztrx-cli/vite`.
3
+ *
4
+ * Drop it into `vite.config.ts` and the dev server reports its own runtime
5
+ * crashes while you work, without a second terminal:
6
+ *
7
+ * import { aztrx } from "aztrx-cli/vite";
8
+ * export default defineConfig({ plugins: [aztrx()] });
9
+ *
10
+ * The scan itself lives in `src/plugins/scan.ts` — a child process every
11
+ * dev-server plugin shares. This file is only the Vite-shaped adapter: when to
12
+ * start, and what to do when the server goes away.
13
+ */
14
+ import type { AztrxResult } from "../plugins/scan.js";
15
+ export type { AztrxResult } from "../plugins/scan.js";
16
+ export { summarize } from "../plugins/scan.js";
17
+ export { resolveCliPath } from "../plugins/scan.js";
18
+ /** The slice of Vite's `DevServer` this plugin actually touches, declared
19
+ * structurally so the plugin stays usable across Vite majors (4 → 8) without
20
+ * pinning a peer dependency's types. */
21
+ export interface ViteDevServerLike {
22
+ config: {
23
+ root: string;
24
+ };
25
+ httpServer: {
26
+ once(event: string, cb: () => void): unknown;
27
+ on(event: string, cb: () => void): unknown;
28
+ } | null;
29
+ resolvedUrls?: {
30
+ local?: string[];
31
+ } | null;
32
+ }
33
+ export interface AztrxPluginOptions {
34
+ /** Run a scan when the dev server starts. Default `true` — adding the plugin
35
+ * to your config is already the opt-in. `AZTRX_DEV_SCAN=0` disables it for a
36
+ * single run without touching the config. */
37
+ enabled?: boolean;
38
+ /** Max interactions per pass (the CLI's `--max-actions`). */
39
+ maxActions?: number;
40
+ /** Seeded chaos fuzzing instead of the deterministic walk. */
41
+ fuzz?: boolean;
42
+ /** Also fuzz the server's HTTP endpoints. Off by default: it mutates. */
43
+ httpFuzz?: boolean;
44
+ /** Repo root for sourcemap resolution and `.aztrx/` artifacts. Defaults to
45
+ * Vite's resolved root. */
46
+ repoRoot?: string;
47
+ /** Extra CLI flags, for anything not surfaced above. */
48
+ args?: string[];
49
+ /** Replace the default one-line report — e.g. to wire findings into your own
50
+ * tooling. */
51
+ onResult?: (result: AztrxResult) => void;
52
+ /** Called when the scan itself fails (never throws into Vite). */
53
+ onError?: (message: string) => void;
54
+ }
55
+ export declare function aztrx(options?: AztrxPluginOptions): {
56
+ name: string;
57
+ apply: "serve";
58
+ configureServer(server: ViteDevServerLike): void;
59
+ };
60
+ export default aztrx;
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Vite dev-server plugin — `aztrx-cli/vite`.
3
+ *
4
+ * Drop it into `vite.config.ts` and the dev server reports its own runtime
5
+ * crashes while you work, without a second terminal:
6
+ *
7
+ * import { aztrx } from "aztrx-cli/vite";
8
+ * export default defineConfig({ plugins: [aztrx()] });
9
+ *
10
+ * The scan itself lives in `src/plugins/scan.ts` — a child process every
11
+ * dev-server plugin shares. This file is only the Vite-shaped adapter: when to
12
+ * start, and what to do when the server goes away.
13
+ */
14
+ import { resolveRepoRoot, scanDisabled, spawnScan } from "../plugins/scan.js";
15
+ export { summarize } from "../plugins/scan.js";
16
+ // Exposed so a "scan did not produce a result" report can name the path that was
17
+ // actually tried — the one fact needed to tell a resolution bug from a scan bug.
18
+ export { resolveCliPath } from "../plugins/scan.js";
19
+ export function aztrx(options = {}) {
20
+ return {
21
+ name: "aztrx",
22
+ // A dev-server sentinel — nothing to scan during a production build.
23
+ apply: "serve",
24
+ configureServer(server) {
25
+ if (scanDisabled(options.enabled))
26
+ return;
27
+ if (!server.httpServer)
28
+ return;
29
+ let scan;
30
+ // Vite calls configureServer before listen(), so this fires once the port
31
+ // is actually bound and `resolvedUrls` is populated — no readiness poll
32
+ // needed here, unlike the Next.js plugin.
33
+ server.httpServer.once("listening", () => {
34
+ const url = server.resolvedUrls?.local?.[0];
35
+ if (!url) {
36
+ options.onError?.("dev server started but Vite reported no local URL");
37
+ return;
38
+ }
39
+ scan = spawnScan({
40
+ url,
41
+ repoRoot: resolveRepoRoot(options.repoRoot, server.config.root),
42
+ maxActions: options.maxActions,
43
+ fuzz: options.fuzz,
44
+ httpFuzz: options.httpFuzz,
45
+ args: options.args,
46
+ onResult: options.onResult,
47
+ onError: options.onError,
48
+ });
49
+ });
50
+ // Ctrl-C on the dev server must take the scan down with it, or a browser
51
+ // is left driving an app that no longer exists.
52
+ server.httpServer.on("close", () => scan?.stop());
53
+ },
54
+ };
55
+ }
56
+ export default aztrx;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aztrx-cli",
3
- "version": "0.4.5",
3
+ "version": "0.5.2",
4
4
  "private": false,
5
5
  "description": "Aztrx AI — runtime stress-tester for web apps. Detect bugs, then prove them with an executable repro.",
6
6
  "license": "Apache-2.0",
@@ -10,6 +10,26 @@
10
10
  "aztrx-cli": "dist/cli.js"
11
11
  },
12
12
  "main": "dist/core/orchestrator.js",
13
+ "types": "dist/core/orchestrator.d.ts",
14
+ "exports": {
15
+ ".": {
16
+ "types": "./dist/core/orchestrator.d.ts",
17
+ "default": "./dist/core/orchestrator.js"
18
+ },
19
+ "./vite": {
20
+ "types": "./dist/vite/index.d.ts",
21
+ "default": "./dist/vite/index.js"
22
+ },
23
+ "./next": {
24
+ "types": "./dist/next/index.d.ts",
25
+ "default": "./dist/next/index.js"
26
+ },
27
+ "./mcp": {
28
+ "types": "./dist/mcp/index.d.ts",
29
+ "default": "./dist/mcp/index.js"
30
+ },
31
+ "./package.json": "./package.json"
32
+ },
13
33
  "files": [
14
34
  "dist",
15
35
  "README.md",
@@ -17,7 +37,7 @@
17
37
  "media"
18
38
  ],
19
39
  "engines": {
20
- "node": ">=18"
40
+ "node": ">=20"
21
41
  },
22
42
  "keywords": [
23
43
  "testing",
@@ -32,13 +52,14 @@
32
52
  ],
33
53
  "repository": {
34
54
  "type": "git",
35
- "url": "https://github.com/Aztrx-AI/aztrx.git"
55
+ "url": "git+https://github.com/Aztrx-AI/aztrx.git"
36
56
  },
37
57
  "scripts": {
38
58
  "build": "tsc",
39
59
  "dev": "node dist/cli.js",
40
60
  "bench": "npm run build && tsx bench/run.ts",
41
- "test": "tsx --test \"tests/**/*.test.ts\"",
61
+ "pretest": "npm run build",
62
+ "test": "node scripts/run-tests.mjs",
42
63
  "typecheck": "tsc --noEmit && tsc -p server/tsconfig.json",
43
64
  "server": "tsx server/index.ts",
44
65
  "server:typecheck": "tsc -p server/tsconfig.json",
@@ -59,6 +80,23 @@
59
80
  "@types/node": "^22.10.0",
60
81
  "@types/pngjs": "^6.0.5",
61
82
  "@types/react": "^18.3.12",
62
- "tsx": "^4.19.0"
63
- }
83
+ "tsx": "^4.19.0",
84
+ "vite": "^8.3.0"
85
+ },
86
+ "peerDependencies": {
87
+ "vite": ">=4"
88
+ },
89
+ "peerDependenciesMeta": {
90
+ "vite": {
91
+ "optional": true
92
+ }
93
+ },
94
+ "directories": {
95
+ "doc": "docs",
96
+ "test": "tests"
97
+ },
98
+ "bugs": {
99
+ "url": "https://github.com/Aztrx-AI/aztrx/issues"
100
+ },
101
+ "homepage": "https://aztrx.app"
64
102
  }