aztrx-cli 0.4.4 → 0.4.5

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.
@@ -72,6 +72,14 @@ export async function run(options) {
72
72
  if (!ui)
73
73
  console.log(parts.join(" "));
74
74
  };
75
+ // Important hints/warnings (destructive mode, login-wall, wrong --repo) must be
76
+ // visible in the TUI too — `say` is a no-op there. Emit on the bus always, and
77
+ // print to stdout only in the linear (non-UI) path so it isn't duplicated.
78
+ const notice = (message, level = "hint") => {
79
+ bus.emit("notice", { message, level, ts: Date.now() });
80
+ if (!ui)
81
+ console.log(level === "danger" ? pc.red(message) : pc.yellow(message));
82
+ };
75
83
  const emitPhase = (phase, detail) => bus.emit("phase", { phase, detail, ts: Date.now() });
76
84
  say(pc.cyan(`\nAztrx AI v${VERSION} — Runtime Detector`));
77
85
  say(pc.dim(`Target: ${url}`));
@@ -85,8 +93,7 @@ export async function run(options) {
85
93
  if (options.storageState)
86
94
  say(pc.dim(`Auth: ${options.storageState}`));
87
95
  if (options.allowDestructive) {
88
- say(pc.yellow(pc.bold("⚠ DESTRUCTIVE MODE — delete/pay/logout/checkout controls are ENABLED.")));
89
- say(pc.yellow(" This can mutate real data. Run only against a disposable/dev instance you own."));
96
+ notice("⚠ DESTRUCTIVE MODE — delete/pay/logout/checkout controls are ENABLED. This can mutate real data. Run only against a disposable/dev instance you own.", "danger");
90
97
  }
91
98
  say("");
92
99
  emitPhase("launch", url);
@@ -149,7 +156,7 @@ export async function run(options) {
149
156
  say(pc.dim(`\nWalked ${totalActions} action(s).\n`));
150
157
  }
151
158
  if (sawLoginForm && !options.login) {
152
- say(pc.yellow("Hint: this app has a login form — re-run with --login to test the authenticated app."));
159
+ notice("Hint: this app has a login form — re-run with --login to test the authenticated app.", "hint");
153
160
  }
154
161
  // F7 → F8 → F9: minimize each finding, compile an executable spec, validate
155
162
  // the flake rate. Only crash/error findings with a recorded action history.
@@ -317,10 +324,15 @@ export async function run(options) {
317
324
  }
318
325
  }
319
326
  else if (candidates.length > 0) {
320
- // Reproducible crashes, but none mapped to source (wrong --repo?) — tell the
321
- // user rather than silently doing nothing. Already-handled fingerprints are
322
- // excluded above, so this only fires for genuinely new findings.
323
- say(pc.yellow(`Found ${candidates.length} reproducible crash(es) but couldn't map them to source files. Run from your project root (or pass --repo <dir>) so --fix can read the code.`));
327
+ // Reproducible crashes, but none mapped to a readable source file (wrong
328
+ // --repo?) — tell the user rather than silently doing nothing. Already-handled
329
+ // fingerprints are excluded above, so this only fires for genuinely new
330
+ // findings. Name the files it looked for so the hint is actionable.
331
+ const missing = candidates
332
+ .map((f) => f.mappedLocation?.filePath || f.rawMessage.split("\n")[0].slice(0, 40))
333
+ .filter(Boolean)
334
+ .slice(0, 3);
335
+ notice(`Found ${candidates.length} reproducible crash(es) but couldn't read their source (${missing.join(", ")}). Run from your project root (or pass --repo <dir>) so --fix can read the code.`, "warning");
324
336
  }
325
337
  }
326
338
  const reportPath = writeReport(repoRoot, url, findings, options.lang);
package/dist/ui/app.js CHANGED
@@ -54,6 +54,8 @@ function reducer(state, msg) {
54
54
  ...state,
55
55
  findings: state.findings.map((f) => f.fingerprint === msg.heal.finding.fingerprint ? msg.heal.finding : f),
56
56
  };
57
+ case "notice":
58
+ return { ...state, notices: [...state.notices, msg.notice] };
57
59
  default:
58
60
  return state;
59
61
  }
@@ -68,6 +70,7 @@ const initialState = {
68
70
  routes: [],
69
71
  navigations: 0,
70
72
  done: false,
73
+ notices: [],
71
74
  };
72
75
  function useAztrx(bus) {
73
76
  const [state, dispatch] = useReducer(reducer, initialState);
@@ -86,6 +89,7 @@ function useAztrx(bus) {
86
89
  bus.on("route", (r) => dispatch({ type: "route", url: r.url })),
87
90
  bus.on("repro", (r) => dispatch({ type: "repro", repro: r })),
88
91
  bus.on("heal", (h) => dispatch({ type: "heal", heal: h })),
92
+ bus.on("notice", (n) => dispatch({ type: "notice", notice: n })),
89
93
  ];
90
94
  return () => offs.forEach((off) => off());
91
95
  }, [bus]);
@@ -119,6 +123,15 @@ function FindingRow({ finding, repro }) {
119
123
  return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(SeverityMark, { severity: finding.severity }), _jsx(Text, { color: C.dim, children: " " }), _jsx(Text, { color: C.fg, children: firstLine(finding.rawMessage) })] }), finding.mappedLocation ? (_jsxs(Text, { color: C.dim, children: [" ", finding.mappedLocation.filePath, ":", finding.mappedLocation.line, ":", finding.mappedLocation.column] })) : null, dx ? (_jsxs(Text, { color: C.azure, children: [" ↳ ", dx] })) : null, repro ? (_jsxs(Box, { children: [_jsx(Text, { color: C.dim, children: " " }), _jsx(ReproBadge, { repro: repro }), _jsxs(Text, { color: C.dim, children: [" ", "spec ", repro.specPath, " \u00B7 ", repro.steps, "/", repro.totalSteps, " steps"] })] })) : null, finding.heal ? (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { color: finding.heal.status === "healed" ? C.green : C.amber, children: [" ", finding.heal.status === "healed" ? "✓" : "◐", " ", finding.heal.status, finding.heal.model ? ` · ${finding.heal.model}` : ""] }), finding.heal.status === "healed" && finding.heal.hunks.length > 0 ? (_jsx(DiffView, { hunks: finding.heal.hunks, filePath: finding.heal.filePath })) : null] })) : null] }));
120
124
  }
121
125
  const SPINNER = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
126
+ const NOTICE_STYLE = {
127
+ hint: { color: C.azure, glyph: "💡" },
128
+ warning: { color: C.amber, glyph: "⚠" },
129
+ danger: { color: C.red, glyph: "⚠" },
130
+ };
131
+ function NoticeRow({ notice }) {
132
+ const style = NOTICE_STYLE[notice.level];
133
+ return (_jsxs(Text, { color: style.color, children: [style.glyph, " ", notice.message] }));
134
+ }
122
135
  function DiffView({ hunks, filePath }) {
123
136
  const groups = diffHunks(hunks);
124
137
  return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { color: C.dim, children: [" ", filePath] }), groups.map((group, gi) => (_jsx(Box, { flexDirection: "column", children: group.map((l, i) => (_jsxs(Box, { children: [_jsxs(Text, { color: l.type === "add" ? C.green : C.red, children: [" ", l.type === "add" ? "+" : "-", " "] }), _jsx(Text, { children: l.tokens.map((t, j) => {
@@ -150,7 +163,7 @@ function AztrxApp({ bus, done, targetUrl, repoRoot, mode }) {
150
163
  }, [done, exit]);
151
164
  const phase = PHASE_LABEL[state.phase];
152
165
  const currentRoute = state.routes[state.routes.length - 1] ?? targetUrl;
153
- return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Text, { color: C.azure, bold: true, children: "Aztrx AI" }), _jsx(Text, { color: C.dim, children: " \u2014 Runtime Detector" }), _jsxs(Text, { color: C.dim, children: [" v", VERSION] })] }), _jsxs(Text, { color: C.dim, children: [" target ", targetUrl, " repo ", repoRoot] }), _jsxs(Text, { color: C.dim, children: [" mode ", mode] }), _jsx(Text, { color: C.dim, children: "\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500" }), _jsxs(Box, { marginTop: 1, children: [_jsxs(Text, { color: phase.color, children: [state.phase === "done" ? "✓" : SPINNER[spin], " ", phase.text.replace(/^[◉✓]\s*/, "")] }), _jsx(Text, { color: C.dim, children: " " }), _jsx(Text, { color: C.azureBright, bold: true, children: rate.toFixed(1) }), _jsx(Text, { color: C.dim, children: " ops/s \u00B7 " }), _jsx(Text, { color: C.fg, children: state.actions }), _jsx(Text, { color: C.dim, children: " actions \u00B7 " }), _jsx(Text, { color: C.fg, children: state.clicks }), _jsx(Text, { color: C.dim, children: " clicks" })] }), _jsxs(Box, { children: [_jsx(Text, { color: C.dim, children: " route " }), _jsx(Text, { color: C.muted, children: currentRoute }), _jsxs(Text, { color: C.dim, children: [" \u00B7 ", state.routes.length, " route(s)"] })] }), state.findings.length > 0 ? (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsxs(Text, { color: C.muted, bold: true, children: ["findings (", state.findings.length, ")"] }), state.findings.map((f) => (_jsx(FindingRow, { finding: f, repro: state.repros[f.fingerprint] }, f.fingerprint)))] })) : null, state.noise > 0 ? (_jsxs(Text, { color: C.dim, children: [" \u25B8 ", state.noise, " noise event(s) suppressed"] })) : null] }));
166
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Text, { color: C.azure, bold: true, children: "Aztrx AI" }), _jsx(Text, { color: C.dim, children: " \u2014 Runtime Detector" }), _jsxs(Text, { color: C.dim, children: [" v", VERSION] })] }), _jsxs(Text, { color: C.dim, children: [" target ", targetUrl, " repo ", repoRoot] }), _jsxs(Text, { color: C.dim, children: [" mode ", mode] }), _jsx(Text, { color: C.dim, children: "\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500" }), _jsxs(Box, { marginTop: 1, children: [_jsxs(Text, { color: phase.color, children: [state.phase === "done" ? "✓" : SPINNER[spin], " ", phase.text.replace(/^[◉✓]\s*/, "")] }), _jsx(Text, { color: C.dim, children: " " }), _jsx(Text, { color: C.azureBright, bold: true, children: rate.toFixed(1) }), _jsx(Text, { color: C.dim, children: " ops/s \u00B7 " }), _jsx(Text, { color: C.fg, children: state.actions }), _jsx(Text, { color: C.dim, children: " actions \u00B7 " }), _jsx(Text, { color: C.fg, children: state.clicks }), _jsx(Text, { color: C.dim, children: " clicks" })] }), _jsxs(Box, { children: [_jsx(Text, { color: C.dim, children: " route " }), _jsx(Text, { color: C.muted, children: currentRoute }), _jsxs(Text, { color: C.dim, children: [" \u00B7 ", state.routes.length, " route(s)"] })] }), state.notices.length > 0 ? (_jsx(Box, { flexDirection: "column", marginTop: 1, children: state.notices.map((n, i) => (_jsx(NoticeRow, { notice: n }, i))) })) : null, state.findings.length > 0 ? (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsxs(Text, { color: C.muted, bold: true, children: ["findings (", state.findings.length, ")"] }), state.findings.map((f) => (_jsx(FindingRow, { finding: f, repro: state.repros[f.fingerprint] }, f.fingerprint)))] })) : null, state.noise > 0 ? (_jsxs(Text, { color: C.dim, children: [" \u25B8 ", state.noise, " noise event(s) suppressed"] })) : null] }));
154
167
  }
155
168
  /** Mount the live terminal panel and resolve once the run (or a failure) ends. */
156
169
  export function renderTui(props) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aztrx-cli",
3
- "version": "0.4.4",
3
+ "version": "0.4.5",
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",