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
|
@@ -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
|
+
}
|
package/dist/ui/app.d.ts
ADDED
|
@@ -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.
|
|
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": ">=
|
|
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
|
-
"
|
|
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
|
}
|