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
@@ -11,10 +11,15 @@
11
11
  */
12
12
  import * as fs from "fs";
13
13
  import * as path from "path";
14
+ import pc from "picocolors";
14
15
  import { detectFrameworkMeta } from "../init.js";
15
16
  import { createSanitizer } from "./sanitize.js";
16
17
  const DEFAULT_ENDPOINT = process.env.AZTRX_TELEMETRY_URL || "https://api.aztrx.app/api/telemetry";
17
18
  const UPLOAD_TIMEOUT_MS = 2000;
19
+ /** One dim line on stderr — the only place a detached upload can report. */
20
+ function warn(msg) {
21
+ process.stderr.write(pc.dim(`aztrx: ${msg}\n`));
22
+ }
18
23
  /** In-flight uploads, drained by `flushTelemetry()` before the CLI exits. */
19
24
  const pendingUploads = [];
20
25
  function readFileIfExists(p) {
@@ -56,7 +61,8 @@ function persistDataset(repoRoot, tuples) {
56
61
  fs.appendFileSync(file, lines, "utf-8");
57
62
  return file;
58
63
  }
59
- /** Fire-and-forget upload. Never rejects; bounded by a short abort. */
64
+ /** Fire-and-forget upload. Never rejects; bounded by a short abort. Failures are
65
+ * reported on stderr rather than discarded — silence here reads as success. */
60
66
  export function dispatchTelemetry(envelope, endpoint, apiKey) {
61
67
  const ctrl = new AbortController();
62
68
  const timer = setTimeout(() => ctrl.abort(), UPLOAD_TIMEOUT_MS);
@@ -69,8 +75,20 @@ export function dispatchTelemetry(envelope, endpoint, apiKey) {
69
75
  body: JSON.stringify(envelope),
70
76
  signal: ctrl.signal,
71
77
  })
72
- .then(() => { })
73
- .catch(() => { })
78
+ .then((res) => {
79
+ // `fetch` resolves on 4xx/5xx too — without this check a rejected upload
80
+ // is indistinguishable from a delivered one.
81
+ if (!res.ok)
82
+ warn(`telemetry upload rejected — HTTP ${res.status} from ${endpoint}`);
83
+ })
84
+ .catch((e) => {
85
+ const why = e instanceof Error && e.name === "AbortError"
86
+ ? `no response within ${UPLOAD_TIMEOUT_MS}ms`
87
+ : e instanceof Error
88
+ ? e.message
89
+ : String(e);
90
+ warn(`telemetry upload failed — ${why}`);
91
+ })
74
92
  .finally(() => clearTimeout(timer));
75
93
  }
76
94
  /** Collect + sanitize + persist, and (under `--share-data`) dispatch. Sync on
@@ -89,7 +107,7 @@ export function submitTelemetry(findings, opts) {
89
107
  sentAt: new Date().toISOString(),
90
108
  tuples,
91
109
  };
92
- const apiKey = opts.apiKey ?? process.env.AZTRX_API_KEY;
110
+ const apiKey = opts.apiKey ?? process.env.AZTRX_CLOUD_API_KEY;
93
111
  pendingUploads.push(dispatchTelemetry(envelope, opts.endpoint ?? DEFAULT_ENDPOINT, apiKey));
94
112
  }
95
113
  }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Telemetry sanitizer — the privacy gate before any byte is packaged. A strict
3
+ * superset of the heal redaction layer, applied irreversibly: secrets first
4
+ * (the redaction map is discarded), then URLs, webpack namespaces, and
5
+ * repo-absolute paths. The output must be safe to leave the machine even if the
6
+ * user's app, routes, and file layout are proprietary.
7
+ */
8
+ /**
9
+ * Anonymize a single URL: strip query/fragment/userinfo; keep only a localhost
10
+ * authority (already anonymous) or a literal `<host>` placeholder. The route
11
+ * path is kept — it's structural, not identifying.
12
+ */
13
+ export declare function sanitizeUrl(raw: string): string;
14
+ export interface Sanitizer {
15
+ url: (raw: string) => string;
16
+ text: (raw: string) => string;
17
+ }
18
+ /** Bound to a repo root so repo-absolute paths and the repo name can be scrubbed. */
19
+ export declare function createSanitizer(repoRoot: string): Sanitizer;
@@ -0,0 +1,26 @@
1
+ /** Telemetry payload schema — the data-flywheel columnar tuple. Flat on purpose
2
+ * so a record can be appended to a JSONL dataset as-is and loaded into any
3
+ * columnar store later. Everything in here has already passed the sanitizer. */
4
+ export interface FrameworkMetadata {
5
+ /** Detected framework name, e.g. "Next.js", "Vite", "unknown". */
6
+ framework: string;
7
+ /** The installed version range from package.json, e.g. "^16.3.2". */
8
+ version?: string;
9
+ }
10
+ export interface TelemetryTuple {
11
+ /** Stable stack fingerprint — already a hash, carries no source text. */
12
+ crash_fingerprint: string;
13
+ /** Sanitized minimized Playwright spec (repro steps), or null if none. */
14
+ min_repro_spec: string | null;
15
+ /** Sanitized verified patch diff, or null if healing didn't produce a fix. */
16
+ verified_patch: string | null;
17
+ framework_metadata: FrameworkMetadata;
18
+ /** The model tier that produced the winning patch, or null. */
19
+ model_tier_used: string | null;
20
+ }
21
+ export interface TelemetryEnvelope {
22
+ schema: "aztrx.telemetry/1";
23
+ /** ISO timestamp. */
24
+ sentAt: string;
25
+ tuples: TelemetryTuple[];
26
+ }
@@ -0,0 +1,88 @@
1
+ export type Severity = "crash" | "error" | "warning" | "noise";
2
+ export type FindingType = "uncaught_exception" | "unhandled_rejection" | "console_error" | "network_5xx" | "network_timeout";
3
+ /** A server-side source location, e.g. a stack frame from a 500 response body. */
4
+ export interface ServerFrame {
5
+ /** Raw path string as it appeared in the stack (file:// URL, POSIX, or Windows). */
6
+ filePath: string;
7
+ line: number;
8
+ column: number;
9
+ }
10
+ /** Server-side error captured from a 500 response body by the HTTP fuzzer. */
11
+ export interface ServerErrorInfo {
12
+ /** The server's own error message, best-effort extracted from the body. */
13
+ message: string;
14
+ /** Truncated response body. */
15
+ body: string;
16
+ /** Best-effort source location parsed from a stack trace in the body. */
17
+ frame?: ServerFrame;
18
+ }
19
+ export interface TelemetryErrorPayload {
20
+ type: FindingType;
21
+ rawMessage: string;
22
+ rawStack: string;
23
+ url?: string;
24
+ line?: number;
25
+ column?: number;
26
+ /** Server-side error context (F5-http). Present only for network findings. */
27
+ serverError?: ServerErrorInfo;
28
+ }
29
+ export type ActionType = "click" | "input" | "navigate" | "hover" | "keypress" | "select" | "scroll" | "request";
30
+ /** A raw HTTP request synthesized by the HTTP mutation fuzzer (F5-http). */
31
+ export interface HttpRequestAction {
32
+ method: string;
33
+ /** Absolute URL — matches the interceptor's `res.url()` so fingerprints line up. */
34
+ url: string;
35
+ headers?: Record<string, string>;
36
+ /** Already-serialized body (JSON string, form-encoded, etc.). */
37
+ body?: string;
38
+ }
39
+ export interface RecordedAction {
40
+ type: ActionType;
41
+ /** Selector cascade, most-reliable first: data-testid → text → css path. */
42
+ selectors: string[];
43
+ value?: string;
44
+ timestamp: number;
45
+ postState?: {
46
+ url: string;
47
+ };
48
+ /** Present when `type === "request"` — the hostile HTTP request to replay. */
49
+ request?: HttpRequestAction;
50
+ }
51
+ export interface MappedLocation {
52
+ filePath: string;
53
+ line: number;
54
+ column: number;
55
+ codeContext: string;
56
+ isOwnCode: boolean;
57
+ }
58
+ export type ReproVerdict = "deterministic" | "flaky" | "unreliable";
59
+ export interface ReproReport {
60
+ actions: RecordedAction[];
61
+ specPath: string;
62
+ verdict: ReproVerdict;
63
+ rate: number;
64
+ runs: number;
65
+ reproductions: number;
66
+ }
67
+ export interface Finding {
68
+ id: string;
69
+ fingerprint: string;
70
+ /** Cross-signal root-cause key (see `rootKeyOf` in classifier.ts). Grouped by
71
+ * `collapseSignals` so distinct capture paths of one fault merge into one. */
72
+ rootKey?: string;
73
+ occurrences: number;
74
+ severity: Severity;
75
+ type: FindingType;
76
+ rawMessage: string;
77
+ rawStack: string;
78
+ mappedLocation?: MappedLocation;
79
+ /** Server's own error response, captured by `--http-fuzz` (message + body). */
80
+ serverError?: {
81
+ message: string;
82
+ body: string;
83
+ };
84
+ actionHistory: RecordedAction[];
85
+ repro?: ReproReport;
86
+ /** F10 closed-loop healing result, attached when `--heal` ran for this finding. */
87
+ heal?: import("./heal/types.js").HealResult;
88
+ }
@@ -0,0 +1,23 @@
1
+ import type { Severity } from "./types.js";
2
+ export declare const PALETTE: {
3
+ readonly bg: "#07090d";
4
+ readonly surface: "#0d1117";
5
+ readonly surface2: "#12161e";
6
+ readonly border: "#232a36";
7
+ readonly fg: "#e9edf4";
8
+ readonly muted: "#a6aebb";
9
+ readonly dim: "#5b6573";
10
+ readonly azure: "#4cc2ff";
11
+ readonly azureBright: "#8ad9ff";
12
+ readonly red: "#ff5a5f";
13
+ readonly amber: "#f5a623";
14
+ readonly green: "#43e58a";
15
+ };
16
+ export declare const SEVERITY_COLOR: Record<Severity, string>;
17
+ export declare const BASE_CSS = "\n:root{color-scheme:dark;--bg:#07090d;--surface:#0d1117;--surface-2:#12161e;--border:#232a36;--fg:#e9edf4;--muted:#a6aebb;--dim:#5b6573;--azure:#4cc2ff;--azure-bright:#8ad9ff;--red:#ff5a5f;--amber:#f5a623;--green:#43e58a}\n*{box-sizing:border-box}\nbody{margin:0;background:var(--bg);color:var(--fg);font:14px/1.65 ui-monospace,\"SF Mono\",SFMono-Regular,Menlo,Consolas,\"Liberation Mono\",monospace;-webkit-font-smoothing:antialiased;text-rendering:optimizeLegibility;background-image:linear-gradient(rgba(76,194,255,.035) 1px,transparent 1px),linear-gradient(90deg,rgba(76,194,255,.035) 1px,transparent 1px);background-size:44px 44px}\nmain{max-width:920px;margin:0 auto;padding:40px 24px 80px}\n.hero{margin-bottom:26px}\n.hero svg{width:100%;height:64px;display:block}\n.brand-row{display:flex;align-items:baseline;gap:10px;flex-wrap:wrap;margin-top:16px}\nh1{margin:0;font-size:20px;font-weight:700;letter-spacing:.02em}\nh1 .brand{color:var(--azure)}\nh1 .brand-sub{color:var(--dim)}\n.target{color:var(--dim);font-size:13px;margin-top:4px;word-break:break-all}\n.live{display:inline-flex;align-items:center;gap:7px;font-size:12px;color:var(--muted);margin-top:6px}\n.live-dot{width:8px;height:8px;border-radius:50%;background:var(--green);box-shadow:0 0 8px var(--green);animation:pulse 1.8s ease-in-out infinite}\n@keyframes pulse{0%,100%{opacity:1}50%{opacity:.25}}\n.bar{display:flex;gap:10px;margin:0 0 24px;flex-wrap:wrap}\n.count{font:600 13px/1 ui-monospace,monospace;border:1px solid var(--border);border-radius:8px;padding:7px 12px;color:var(--dim);background:var(--surface)}\n.count b{color:var(--fg);font-weight:600}\n.count b.crash{color:var(--red)}\n.count b.error{color:var(--red)}\n.count b.warning{color:var(--amber)}\n.finding{background:var(--surface);border:1px solid var(--border);border-left:3px solid var(--sev,var(--azure));border-radius:10px;padding:16px 18px;margin-bottom:14px;animation:line-in .35s ease both}\n.finding header{display:flex;align-items:center;gap:10px;flex-wrap:wrap}\n.sev{font:600 11px/1 ui-monospace,monospace;text-transform:uppercase;letter-spacing:.08em;color:var(--sev);border:1px solid var(--sev);border-radius:999px;padding:3px 9px;flex:none}\nh2{font-size:15px;margin:0;font-weight:600;word-break:break-word}\n.loc{color:var(--dim);font-size:12.5px;margin-top:8px}\n.dx{color:var(--azure);font-size:12.5px;margin-top:6px}\n.snippet{background:var(--surface-2);border:1px solid var(--border);border-radius:8px;padding:12px 14px;overflow-x:auto;font:12px/1.6 ui-monospace,monospace;color:var(--muted);margin:12px 0 0;white-space:pre}\n.server{color:var(--amber);font-size:12.5px;margin-top:8px}\n.server-body{background:var(--surface-2);border:1px solid var(--border);border-radius:8px;padding:12px 14px;overflow-x:auto;font:12px/1.6 ui-monospace,monospace;color:var(--muted);margin:8px 0 0;white-space:pre;max-height:240px;overflow-y:auto}\n.occ{color:var(--dim);font-size:12px;margin-top:8px}\n.repro{display:inline-flex;align-items:center;gap:8px;font-size:12px;margin-top:12px;padding:4px 10px;border-radius:6px;border:1px solid}\n.repro.deterministic{color:var(--green);border-color:rgba(67,229,138,.35);background:rgba(67,229,138,.07)}\n.repro.flaky{color:var(--amber);border-color:rgba(245,166,35,.35);background:rgba(245,166,35,.07)}\n.repro.unreliable{color:var(--red);border-color:rgba(255,90,95,.35);background:rgba(255,90,95,.07)}\ndetails{margin-top:12px;color:var(--dim);font-size:12.5px;border-top:1px solid var(--border);padding-top:10px}\nsummary{cursor:pointer;color:var(--muted)}\ndetails ol{margin:8px 0 0;padding-left:22px;display:flex;flex-direction:column;gap:4px}\ndetails code{color:var(--fg)}\n.empty{color:var(--dim);border:1px dashed var(--border);border-radius:10px;padding:24px;text-align:center}\n.foot{color:var(--dim);font-size:12.5px;margin-top:24px}\na{color:var(--azure)}\n@keyframes line-in{from{opacity:0;transform:translateY(4px)}to{opacity:1;transform:translateY(0)}}\n";
18
+ /**
19
+ * The crash seismograph: an azure trace with one red spike per crash. The
20
+ * spikes live inside `<g id="spikes">` so the Studio can append them live as
21
+ * crashes stream in; the report pre-fills them.
22
+ */
23
+ export declare function seismograph(crashes: number): string;
package/dist/core/ui.js CHANGED
@@ -21,11 +21,6 @@ export const SEVERITY_COLOR = {
21
21
  warning: PALETTE.amber,
22
22
  noise: PALETTE.dim,
23
23
  };
24
- export const REPRO_COLOR = {
25
- deterministic: PALETTE.green,
26
- flaky: PALETTE.amber,
27
- unreliable: PALETTE.red,
28
- };
29
24
  export const BASE_CSS = `
30
25
  :root{color-scheme:dark;--bg:#07090d;--surface:#0d1117;--surface-2:#12161e;--border:#232a36;--fg:#e9edf4;--muted:#a6aebb;--dim:#5b6573;--azure:#4cc2ff;--azure-bright:#8ad9ff;--red:#ff5a5f;--amber:#f5a623;--green:#43e58a}
31
26
  *{box-sizing:border-box}
@@ -0,0 +1,14 @@
1
+ import type { Finding, RecordedAction, ReproVerdict } from "./types.js";
2
+ import type { ReplayEngine } from "./replay.js";
3
+ export interface ValidateResult {
4
+ runs: number;
5
+ reproductions: number;
6
+ rate: number;
7
+ verdict: ReproVerdict;
8
+ }
9
+ /**
10
+ * F9 — repro validator. Replays the minimized sequence `runs` times and gates
11
+ * on the flake rate: 100% → deterministic, ≥60% → flaky, else unreliable. This
12
+ * is the difference between "we saw an error" and "we proved the bug".
13
+ */
14
+ export declare function validate(engine: ReplayEngine, url: string, finding: Finding, actions: RecordedAction[], runs?: number): Promise<ValidateResult>;
@@ -0,0 +1 @@
1
+ export declare const VERSION: string;
@@ -0,0 +1,106 @@
1
+ /**
2
+ * Git hook integration — `aztrx-cli hook install | uninstall | run pre-push`.
3
+ *
4
+ * The file that lands in `.git/hooks/` is a short POSIX shim that calls back
5
+ * into the CLI. That split is the design, not a shortcut: a hook carrying its
6
+ * own implementation has to be *reinstalled* to be fixed, and by the time the
7
+ * bug matters the user has already deleted the thing. Here, upgrading
8
+ * `aztrx-cli` upgrades the hook.
9
+ *
10
+ * Everything below is **fail-open**. A missing tool, a shallow clone with no
11
+ * base to diff against, a scan that could not run — none of those are reasons to
12
+ * block someone's push. Only a crash we actually found is. A hook that blocks
13
+ * pushes when it is merely broken gets turned off, and then it catches nothing.
14
+ */
15
+ /** The line that marks a hook file as ours. Used to decide whether installing
16
+ * is safe (never clobber a hook somebody else wrote) and whether uninstalling
17
+ * would delete something we own. */
18
+ export declare const HOOK_MARKER = "aztrx-cli hook run pre-push";
19
+ /** The shim that lands in `.git/hooks/pre-push`.
20
+ *
21
+ * Built as an array of lines joined with `\n` rather than one multi-line
22
+ * template literal, and that is load-bearing: a literal's line breaks are the
23
+ * *source file's* line breaks, so on a checkout with `core.autocrlf=true` this
24
+ * would come out CRLF — and `sh` reads `#!/bin/sh\r` as an interpreter named
25
+ * `/bin/sh\r`, which is not a thing. The hook would fail with "bad
26
+ * interpreter", on Windows only, for reasons invisible in the source. */
27
+ export declare function hookScript(): string;
28
+ export interface PushRef {
29
+ localRef: string;
30
+ localSha: string;
31
+ remoteRef: string;
32
+ remoteSha: string;
33
+ }
34
+ /** git hands a pre-push hook one line per ref on stdin:
35
+ * `<local ref> <local sha> <remote ref> <remote sha>`. An all-zero sha means
36
+ * "does not exist on that side". Malformed rows are dropped rather than
37
+ * guessed at — a ref we cannot read is a ref we must not silently skip. */
38
+ export declare function parsePushLines(stdin: string): PushRef[];
39
+ /** Deleting a remote branch pushes no code. */
40
+ export declare function isBranchDelete(ref: PushRef): boolean;
41
+ /** A branch the remote has never seen. There is no base to diff against, so
42
+ * what is new cannot be known — the conservative answer is "all of it". */
43
+ export declare function isNewBranch(ref: PushRef): boolean;
44
+ export interface PushAnalysis {
45
+ /** Files the push touches, relative to the repo root. `null` when that cannot
46
+ * be known — nothing on stdin, a new branch, a base missing from a shallow
47
+ * clone — which means scan. */
48
+ changed: string[] | null;
49
+ /** Refs that actually carry code. Empty means deletions only. */
50
+ liveRefs: PushRef[];
51
+ /** Why `changed` is unknown, phrased for the user. Empty when it is known.
52
+ * Three different situations land on `null`, and one message covering all of
53
+ * them sent me hunting for a shallow clone that was not there — the actual
54
+ * cause was a wrapper script that had drained stdin. */
55
+ blind: string;
56
+ }
57
+ /** What is this push bringing? Pure but for the injected `diff`, so the branches
58
+ * that are awkward to arrange in a real repo (shallow clone, new branch) are
59
+ * cheap to test. */
60
+ export declare function analyzePush(refs: PushRef[], diff: (base: string, head: string) => string[] | null): PushAnalysis;
61
+ /** Could this file change the app's runtime behaviour? */
62
+ export declare function isAppCode(file: string): boolean;
63
+ export interface PushDecision {
64
+ scan: boolean;
65
+ /** One line for the user explaining the call. */
66
+ reason: string;
67
+ }
68
+ export declare function decidePush(analysis: PushAnalysis, always: boolean): PushDecision;
69
+ /** `git rev-parse --git-path hooks` resolves the hooks directory the way git
70
+ * itself will — through `.git` being a file in a worktree, and through
71
+ * `core.hooksPath` (husky sets it). Installing into `.git/hooks` while
72
+ * `core.hooksPath` points somewhere else produces a hook git never runs, which
73
+ * is the worst possible outcome: the user believes they are covered. */
74
+ export declare function hooksDir(repoRoot: string): string | null;
75
+ /** Which files a range touches, or null when git cannot answer. */
76
+ export declare function diffNames(repoRoot: string, base: string, head: string): string[] | null;
77
+ export type HookAction = "installed" | "updated" | "unchanged" | "refused" | "removed" | "absent";
78
+ export interface HookResult {
79
+ ok: boolean;
80
+ action: HookAction;
81
+ hookPath?: string;
82
+ message: string;
83
+ }
84
+ /** Is this file a shim we wrote (as opposed to a hook the project already had)? */
85
+ export declare function isOurs(hookPath: string): boolean;
86
+ export declare function installHook(repoRoot: string, force?: boolean): HookResult;
87
+ export declare function uninstallHook(repoRoot: string): HookResult;
88
+ export interface PrePushOptions {
89
+ repoRoot: string;
90
+ /** Raw hook stdin. Read from fd 0 when omitted. */
91
+ stdin?: string;
92
+ /** Scan even when nothing app-shaped changed (`--always`). */
93
+ always?: boolean;
94
+ /** Called once, just before the scan starts. The scan itself is silent until
95
+ * it has a result (that is what `--json` means), so without this the push
96
+ * would look hung for the better part of a minute. */
97
+ onProgress?: (message: string) => void;
98
+ }
99
+ export interface PrePushOutcome {
100
+ /** Exit code for the hook process. Non-zero blocks the push. */
101
+ code: number;
102
+ /** Everything to show the user, in order. */
103
+ lines: string[];
104
+ }
105
+ /** `aztrx-cli hook run pre-push` — the body of the shim. */
106
+ export declare function runPrePush(opts: PrePushOptions): Promise<PrePushOutcome>;