ssrwire 0.3.0 → 0.4.0

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 (46) hide show
  1. package/CHANGELOG.md +24 -1
  2. package/CONTRIBUTING.md +2 -1
  3. package/PUBLISHING.md +15 -15
  4. package/README.md +109 -6
  5. package/dist/audit-report.d.ts +8 -0
  6. package/dist/audit-report.d.ts.map +1 -0
  7. package/dist/audit-report.js +243 -0
  8. package/dist/audit-report.js.map +1 -0
  9. package/dist/audit.d.ts.map +1 -1
  10. package/dist/audit.js +2 -0
  11. package/dist/audit.js.map +1 -1
  12. package/dist/cli.d.ts.map +1 -1
  13. package/dist/cli.js +74 -9
  14. package/dist/cli.js.map +1 -1
  15. package/dist/compare.d.ts +6 -0
  16. package/dist/compare.d.ts.map +1 -0
  17. package/dist/compare.js +720 -0
  18. package/dist/compare.js.map +1 -0
  19. package/dist/comparison-reporters.d.ts +9 -0
  20. package/dist/comparison-reporters.d.ts.map +1 -0
  21. package/dist/comparison-reporters.js +223 -0
  22. package/dist/comparison-reporters.js.map +1 -0
  23. package/dist/config.d.ts.map +1 -1
  24. package/dist/config.js +16 -1
  25. package/dist/config.js.map +1 -1
  26. package/dist/index.d.ts +4 -1
  27. package/dist/index.d.ts.map +1 -1
  28. package/dist/index.js +3 -0
  29. package/dist/index.js.map +1 -1
  30. package/dist/redact.d.ts.map +1 -1
  31. package/dist/redact.js +1 -0
  32. package/dist/redact.js.map +1 -1
  33. package/dist/types.d.ts +83 -1
  34. package/dist/types.d.ts.map +1 -1
  35. package/examples/github-actions.yml +2 -2
  36. package/examples/ssrwire.config.yml +4 -2
  37. package/package.json +1 -1
  38. package/src/audit-report.ts +269 -0
  39. package/src/audit.ts +2 -0
  40. package/src/cli.ts +103 -11
  41. package/src/compare.ts +949 -0
  42. package/src/comparison-reporters.ts +276 -0
  43. package/src/config.ts +15 -1
  44. package/src/index.ts +27 -0
  45. package/src/redact.ts +1 -0
  46. package/src/types.ts +97 -1
package/dist/types.d.ts CHANGED
@@ -30,7 +30,7 @@ export interface DocumentSignals {
30
30
  readonly descriptions: readonly ElementSignal[];
31
31
  readonly canonicals: readonly ElementSignal[];
32
32
  readonly robots: readonly RobotsSignal[];
33
- /** Present on probes produced by SSRWire 0.3.0 and newer. */
33
+ /** Present on current probes; optional for low-level callers and older in-memory results. */
34
34
  readonly socialMetadata?: readonly SocialMetadataSignal[];
35
35
  readonly h1s: readonly ElementSignal[];
36
36
  readonly firstMainText?: ElementSignal;
@@ -103,6 +103,8 @@ export interface TargetExpectations {
103
103
  readonly maxCriticalMs?: number;
104
104
  }
105
105
  export interface AuditTarget {
106
+ /** Stable identity used to match the same target across reports with different origins. */
107
+ readonly id?: string;
106
108
  readonly url: string;
107
109
  readonly expectations: TargetExpectations;
108
110
  }
@@ -170,6 +172,8 @@ export interface AuditSummary {
170
172
  readonly incomplete: number;
171
173
  }
172
174
  export interface AuditResult {
175
+ /** Version of the persisted audit-report contract, independent of the package version. */
176
+ readonly schemaVersion: 1;
173
177
  readonly version: string;
174
178
  readonly generatedAt: string;
175
179
  readonly durationMs: number;
@@ -178,4 +182,82 @@ export interface AuditResult {
178
182
  readonly summary: AuditSummary;
179
183
  }
180
184
  export type ReportFormat = "terminal" | "json" | "sarif";
185
+ export type ComparisonKind = "regression" | "fixed" | "changed";
186
+ export type ComparisonScope = "target" | "agent" | "finding" | "response" | "metadata" | "timing";
187
+ export interface ComparisonChange {
188
+ readonly kind: ComparisonKind;
189
+ readonly scope: ComparisonScope;
190
+ readonly code: string;
191
+ readonly message: string;
192
+ readonly agent?: string;
193
+ readonly field?: string;
194
+ readonly baseline?: string | number | boolean;
195
+ readonly candidate?: string | number | boolean;
196
+ }
197
+ export interface ComparisonTimelineEvent {
198
+ readonly key: string;
199
+ readonly label: string;
200
+ readonly medianMs: number;
201
+ readonly location?: ElementLocation | "mixed";
202
+ readonly observedByByte?: number;
203
+ }
204
+ export interface ComparisonTimelineSnapshot {
205
+ readonly samples: number;
206
+ readonly events: readonly ComparisonTimelineEvent[];
207
+ }
208
+ export interface ComparisonTimelineLane {
209
+ readonly agent: string;
210
+ readonly label: string;
211
+ readonly baseline?: ComparisonTimelineSnapshot;
212
+ readonly candidate?: ComparisonTimelineSnapshot;
213
+ }
214
+ export type TargetComparisonStatus = "matched" | "added" | "removed";
215
+ export interface TargetComparison {
216
+ readonly key: string;
217
+ readonly id?: string;
218
+ readonly status: TargetComparisonStatus;
219
+ readonly baselineUrl?: string;
220
+ readonly candidateUrl?: string;
221
+ readonly changes: readonly ComparisonChange[];
222
+ readonly timelines: readonly ComparisonTimelineLane[];
223
+ }
224
+ export interface AuditReportDescriptor {
225
+ readonly label: string;
226
+ readonly version: string;
227
+ readonly schemaVersion: 1;
228
+ readonly generatedAt: string;
229
+ readonly repeat: number;
230
+ }
231
+ export interface ComparisonThresholds {
232
+ readonly timingRegressionMs: number;
233
+ readonly timingRegressionPercent: number;
234
+ }
235
+ export interface ComparisonSummary {
236
+ readonly targets: number;
237
+ readonly matchedTargets: number;
238
+ readonly addedTargets: number;
239
+ readonly removedTargets: number;
240
+ readonly unchangedTargets: number;
241
+ readonly regressions: number;
242
+ readonly fixed: number;
243
+ readonly changed: number;
244
+ }
245
+ export interface AuditComparison {
246
+ readonly schemaVersion: 1;
247
+ readonly kind: "comparison";
248
+ readonly version: string;
249
+ readonly generatedAt: string;
250
+ readonly baseline: AuditReportDescriptor;
251
+ readonly candidate: AuditReportDescriptor;
252
+ readonly thresholds: ComparisonThresholds;
253
+ readonly results: readonly TargetComparison[];
254
+ readonly summary: ComparisonSummary;
255
+ }
256
+ export interface CompareAuditOptions {
257
+ readonly baselineLabel?: string;
258
+ readonly candidateLabel?: string;
259
+ readonly timingRegressionMs?: number;
260
+ readonly timingRegressionPercent?: number;
261
+ }
262
+ export type ComparisonReportFormat = "terminal" | "json" | "html";
181
263
  //# sourceMappingURL=types.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
@@ -27,7 +27,7 @@ jobs:
27
27
  if: github.event_name == 'pull_request'
28
28
  continue-on-error: true
29
29
  run: >-
30
- npx --yes ssrwire@0.3.0 check
30
+ npx --yes ssrwire@0.4.0 check
31
31
  --config ssrwire.config.yml
32
32
  --format sarif
33
33
  --output reports/ssrwire.sarif
@@ -38,7 +38,7 @@ jobs:
38
38
  if: github.event_name != 'pull_request'
39
39
  continue-on-error: true
40
40
  run: >-
41
- npx --yes ssrwire@0.3.0 check
41
+ npx --yes ssrwire@0.4.0 check
42
42
  --config ssrwire.config.yml
43
43
  --format sarif
44
44
  --output reports/ssrwire.sarif
@@ -1,6 +1,7 @@
1
1
  # Replace these URLs with a small set of high-value SSR routes.
2
2
  targets:
3
- - url: https://example.com/
3
+ - id: home
4
+ url: https://example.com/
4
5
  expectedStatus: 200
5
6
  expectedFinalUrl: https://example.com/
6
7
  require:
@@ -12,7 +13,8 @@ targets:
12
13
  openGraph: true
13
14
  twitterCard: true
14
15
 
15
- - url: https://example.com/pricing/
16
+ - id: pricing
17
+ url: https://example.com/pricing/
16
18
  expectedStatus: 200
17
19
  require:
18
20
  title: true
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ssrwire",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "Inspect streamed SSR HTML, SEO and social metadata timing, and crawler-specific delivery from the command line.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -0,0 +1,269 @@
1
+ import { z } from "zod";
2
+ import type { AuditResult } from "./types.js";
3
+
4
+ export const AUDIT_SCHEMA_VERSION = 1 as const;
5
+
6
+ const nonNegativeNumber = z.number().finite().nonnegative();
7
+ const nonNegativeInteger = z.number().int().nonnegative();
8
+ const elementLocation = z.enum(["head", "body", "document"]);
9
+ const severity = z.enum(["info", "warning", "error"]);
10
+
11
+ const timingMark = z
12
+ .object({
13
+ atMs: nonNegativeNumber,
14
+ observedByByte: nonNegativeInteger,
15
+ })
16
+ .strict();
17
+
18
+ const elementSignal = timingMark
19
+ .extend({
20
+ value: z.string(),
21
+ location: elementLocation,
22
+ })
23
+ .strict();
24
+
25
+ const robotsSignal = elementSignal
26
+ .extend({ audience: z.enum(["robots", "googlebot", "bingbot"]) })
27
+ .strict();
28
+
29
+ const socialMetadataSignal = elementSignal
30
+ .extend({
31
+ property: z.enum([
32
+ "og:title",
33
+ "og:type",
34
+ "og:url",
35
+ "og:image",
36
+ "og:description",
37
+ "twitter:card",
38
+ "twitter:title",
39
+ "twitter:description",
40
+ "twitter:image",
41
+ ]),
42
+ })
43
+ .strict();
44
+
45
+ const jsonLdSignal = timingMark
46
+ .extend({
47
+ location: elementLocation,
48
+ valid: z.boolean().optional(),
49
+ types: z.array(z.string()),
50
+ bytes: nonNegativeInteger,
51
+ analysisLimit: z.string().optional(),
52
+ error: z.string().optional(),
53
+ })
54
+ .strict();
55
+
56
+ const documentSignals = z
57
+ .object({
58
+ title: elementSignal.optional(),
59
+ titles: z.array(elementSignal).optional(),
60
+ descriptions: z.array(elementSignal),
61
+ canonicals: z.array(elementSignal),
62
+ robots: z.array(robotsSignal),
63
+ socialMetadata: z.array(socialMetadataSignal).optional(),
64
+ h1s: z.array(elementSignal),
65
+ firstMainText: elementSignal.optional(),
66
+ jsonLd: z.array(jsonLdSignal),
67
+ headClosed: timingMark.optional(),
68
+ bodyStarted: timingMark.optional(),
69
+ documentClosed: timingMark.optional(),
70
+ })
71
+ .strict();
72
+
73
+ const agentProfile = z
74
+ .object({
75
+ key: z.string().min(1),
76
+ label: z.string().min(1),
77
+ userAgent: z.string(),
78
+ requiresHeadMetadata: z.boolean(),
79
+ })
80
+ .strict();
81
+
82
+ const redirectHop = z
83
+ .object({
84
+ url: z.string(),
85
+ status: z.number().int().min(100).max(599),
86
+ location: z.string(),
87
+ durationMs: nonNegativeNumber,
88
+ })
89
+ .strict();
90
+
91
+ const headerSnapshot = z
92
+ .object({
93
+ values: z.record(z.string(), z.string()),
94
+ setCookiePresent: z.boolean(),
95
+ })
96
+ .strict();
97
+
98
+ const probeTimings = z
99
+ .object({
100
+ headersMs: nonNegativeNumber,
101
+ firstByteMs: nonNegativeNumber.optional(),
102
+ completeMs: nonNegativeNumber.optional(),
103
+ })
104
+ .strict();
105
+
106
+ const probeResult = z
107
+ .object({
108
+ requestedUrl: z.string(),
109
+ finalUrl: z.string(),
110
+ agent: agentProfile,
111
+ status: z.number().int().min(100).max(599).optional(),
112
+ redirects: z.array(redirectHop),
113
+ headers: headerSnapshot,
114
+ timings: probeTimings,
115
+ bytesRead: nonNegativeInteger,
116
+ bodySha256: z.string().optional(),
117
+ signals: documentSignals,
118
+ completion: z.enum([
119
+ "complete",
120
+ "max-bytes-exceeded",
121
+ "timeout",
122
+ "network-error",
123
+ "invalid-response",
124
+ ]),
125
+ error: z.string().optional(),
126
+ sample: z.number().int().positive().optional(),
127
+ })
128
+ .strict();
129
+
130
+ const targetExpectations = z
131
+ .object({
132
+ statuses: z.array(z.number().int().min(100).max(599)).min(1),
133
+ finalUrl: z.string().optional(),
134
+ requireTitle: z.boolean(),
135
+ requireDescription: z.boolean(),
136
+ requireCanonical: z.boolean(),
137
+ requireH1: z.boolean(),
138
+ requireMainText: z.boolean(),
139
+ requireOpenGraph: z.boolean().optional(),
140
+ requireTwitterCard: z.boolean().optional(),
141
+ maxFirstByteMs: z.number().positive().finite().optional(),
142
+ maxCriticalMs: z.number().positive().finite().optional(),
143
+ })
144
+ .strict();
145
+
146
+ const auditTarget = z
147
+ .object({
148
+ id: z.string().min(1).optional(),
149
+ url: z.string(),
150
+ expectations: targetExpectations,
151
+ })
152
+ .strict();
153
+
154
+ const timingStats = z
155
+ .object({
156
+ samples: nonNegativeInteger,
157
+ minMs: nonNegativeNumber,
158
+ medianMs: nonNegativeNumber,
159
+ p95Ms: nonNegativeNumber,
160
+ maxMs: nonNegativeNumber,
161
+ spreadMs: nonNegativeNumber,
162
+ })
163
+ .strict();
164
+
165
+ const stabilityTimings = z
166
+ .object({
167
+ headers: timingStats.optional(),
168
+ firstByte: timingStats.optional(),
169
+ criticalSignals: timingStats.optional(),
170
+ complete: timingStats.optional(),
171
+ })
172
+ .strict();
173
+
174
+ const stabilityVariants = z
175
+ .object({
176
+ completion: nonNegativeInteger,
177
+ status: nonNegativeInteger,
178
+ finalUrl: nonNegativeInteger,
179
+ redirectChain: nonNegativeInteger,
180
+ bodySha256: nonNegativeInteger,
181
+ metadataValues: nonNegativeInteger,
182
+ metadataLocations: nonNegativeInteger,
183
+ })
184
+ .strict();
185
+
186
+ const agentStability = z
187
+ .object({
188
+ agent: agentProfile,
189
+ samples: nonNegativeInteger,
190
+ complete: nonNegativeInteger,
191
+ incomplete: nonNegativeInteger,
192
+ timings: stabilityTimings,
193
+ variants: stabilityVariants,
194
+ })
195
+ .strict();
196
+
197
+ const finding = z
198
+ .object({
199
+ code: z.string().min(1),
200
+ severity,
201
+ message: z.string(),
202
+ url: z.string(),
203
+ agent: z.string().optional(),
204
+ evidence: z.record(z.string(), z.union([z.string(), z.number(), z.boolean()])).optional(),
205
+ })
206
+ .strict();
207
+
208
+ const targetAuditResult = z
209
+ .object({
210
+ target: auditTarget,
211
+ probes: z.array(probeResult),
212
+ findings: z.array(finding),
213
+ stability: z.array(agentStability).optional(),
214
+ })
215
+ .strict();
216
+
217
+ const auditSummary = z
218
+ .object({
219
+ targets: nonNegativeInteger,
220
+ probes: nonNegativeInteger,
221
+ errors: nonNegativeInteger,
222
+ warnings: nonNegativeInteger,
223
+ info: nonNegativeInteger,
224
+ incomplete: nonNegativeInteger,
225
+ })
226
+ .strict();
227
+
228
+ const auditReportSchema = z
229
+ .object({
230
+ schemaVersion: z.literal(AUDIT_SCHEMA_VERSION),
231
+ version: z.string().min(1),
232
+ generatedAt: z.string().min(1),
233
+ durationMs: nonNegativeNumber,
234
+ repeat: z.number().int().min(1).max(10).optional(),
235
+ results: z.array(targetAuditResult),
236
+ summary: auditSummary,
237
+ })
238
+ .strict();
239
+
240
+ export class AuditReportError extends Error {
241
+ public constructor(message: string) {
242
+ super(message);
243
+ this.name = "AuditReportError";
244
+ }
245
+ }
246
+
247
+ export function parseAuditReport(value: unknown, source = "audit report"): AuditResult {
248
+ const parsed = auditReportSchema.safeParse(value);
249
+ if (!parsed.success) {
250
+ const detail = parsed.error.issues
251
+ .slice(0, 5)
252
+ .map((issue) => `${issue.path.join(".") || "report"}: ${issue.message}`)
253
+ .join("; ");
254
+ throw new AuditReportError(`Invalid ${source}: ${detail}`);
255
+ }
256
+
257
+ // The complete runtime schema above is the persisted counterpart of AuditResult.
258
+ return parsed.data as AuditResult;
259
+ }
260
+
261
+ export function parseAuditReportText(text: string, source = "audit report"): AuditResult {
262
+ let value: unknown;
263
+ try {
264
+ value = JSON.parse(text) as unknown;
265
+ } catch {
266
+ throw new AuditReportError(`Could not parse ${source} as JSON.`);
267
+ }
268
+ return parseAuditReport(value, source);
269
+ }
package/src/audit.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { analyzeTarget, summarizeAudit } from "./analyze.js";
2
+ import { AUDIT_SCHEMA_VERSION } from "./audit-report.js";
2
3
  import { probeUrl } from "./http-probe.js";
3
4
  import { redactAudit } from "./redact.js";
4
5
  import { analyzeStability } from "./stability.js";
@@ -186,6 +187,7 @@ export async function runAudit(config: SsrWireConfig): Promise<AuditResult> {
186
187
  });
187
188
 
188
189
  const audit: AuditResult = {
190
+ schemaVersion: AUDIT_SCHEMA_VERSION,
189
191
  version: VERSION,
190
192
  generatedAt: new Date().toISOString(),
191
193
  durationMs: Math.round(performance.now() - started),
package/src/cli.ts CHANGED
@@ -1,10 +1,13 @@
1
- import { access, mkdir, writeFile } from "node:fs/promises";
2
- import { dirname, resolve } from "node:path";
1
+ import { access, mkdir, readFile, writeFile } from "node:fs/promises";
2
+ import { basename, dirname, resolve } from "node:path";
3
3
  import { Command, CommanderError, InvalidArgumentError } from "commander";
4
4
  import { runAudit } from "./audit.js";
5
+ import { AuditReportError, parseAuditReportText } from "./audit-report.js";
6
+ import { compareAudits } from "./compare.js";
7
+ import { renderComparisonReport } from "./comparison-reporters.js";
5
8
  import { ConfigError, loadConfig } from "./config.js";
6
9
  import { renderReport } from "./reporters.js";
7
- import type { ReportFormat } from "./types.js";
10
+ import type { AuditResult, ComparisonReportFormat, ReportFormat } from "./types.js";
8
11
  import { VERSION } from "./version.js";
9
12
 
10
13
  interface CliOptions {
@@ -21,9 +24,19 @@ interface CliOptions {
21
24
  readonly color: boolean;
22
25
  }
23
26
 
27
+ interface CompareCliOptions {
28
+ readonly format: ComparisonReportFormat;
29
+ readonly output?: string;
30
+ readonly failOn: "regression" | "never";
31
+ readonly timingRegressionMs: number;
32
+ readonly timingRegressionPercent: number;
33
+ readonly color: boolean;
34
+ }
35
+
24
36
  const CONFIG_TEMPLATE = `# SSRWire configuration
25
37
  targets:
26
- - url: https://example.com/
38
+ - id: home
39
+ url: https://example.com/
27
40
  expectedStatus: 200
28
41
  require:
29
42
  title: true
@@ -76,6 +89,24 @@ function parseFailOn(value: string): CliOptions["failOn"] {
76
89
  throw new InvalidArgumentError("Expected error, warning, or never.");
77
90
  }
78
91
 
92
+ function parseComparisonFormat(value: string): ComparisonReportFormat {
93
+ if (value === "terminal" || value === "json" || value === "html") return value;
94
+ throw new InvalidArgumentError("Expected terminal, json, or html.");
95
+ }
96
+
97
+ function parseComparisonFailOn(value: string): CompareCliOptions["failOn"] {
98
+ if (value === "regression" || value === "never") return value;
99
+ throw new InvalidArgumentError("Expected regression or never.");
100
+ }
101
+
102
+ function parseNonNegativeNumber(value: string): number {
103
+ const parsed = Number(value);
104
+ if (!Number.isFinite(parsed) || parsed < 0) {
105
+ throw new InvalidArgumentError("Expected a finite non-negative number.");
106
+ }
107
+ return parsed;
108
+ }
109
+
79
110
  function addCheckOptions(command: Command): Command {
80
111
  return command
81
112
  .option("-c, --config <path>", "configuration file")
@@ -120,6 +151,16 @@ async function writeReport(path: string, report: string): Promise<void> {
120
151
  await writeFile(absolute, report, "utf8");
121
152
  }
122
153
 
154
+ async function readAuditFile(path: string, label: string): Promise<AuditResult> {
155
+ let text: string;
156
+ try {
157
+ text = await readFile(resolve(path), "utf8");
158
+ } catch {
159
+ throw new AuditReportError(`Could not read ${label} audit report: ${path}`);
160
+ }
161
+ return parseAuditReportText(text, `${label} audit report`);
162
+ }
163
+
123
164
  async function check(urls: readonly string[], options: CliOptions): Promise<void> {
124
165
  const config = await loadConfig({
125
166
  ...(options.config ? { configPath: options.config } : {}),
@@ -149,6 +190,38 @@ async function check(urls: readonly string[], options: CliOptions): Promise<void
149
190
  process.exitCode = reportExitCode(audit.summary, options.failOn);
150
191
  }
151
192
 
193
+ async function compareReports(
194
+ baselinePath: string,
195
+ candidatePath: string,
196
+ options: CompareCliOptions,
197
+ ): Promise<void> {
198
+ const [baseline, candidate] = await Promise.all([
199
+ readAuditFile(baselinePath, "baseline"),
200
+ readAuditFile(candidatePath, "candidate"),
201
+ ]);
202
+ const comparison = compareAudits(baseline, candidate, {
203
+ baselineLabel: basename(baselinePath),
204
+ candidateLabel: basename(candidatePath),
205
+ timingRegressionMs: options.timingRegressionMs,
206
+ timingRegressionPercent: options.timingRegressionPercent,
207
+ });
208
+ const color =
209
+ options.color &&
210
+ !options.output &&
211
+ Boolean(process.stdout.isTTY) &&
212
+ !Reflect.has(process.env, "NO_COLOR");
213
+ const report = renderComparisonReport(comparison, options.format, { color });
214
+
215
+ if (options.output) {
216
+ await writeReport(options.output, report);
217
+ process.stderr.write(`SSRWire wrote ${options.format} comparison to ${options.output}\n`);
218
+ } else {
219
+ process.stdout.write(report);
220
+ }
221
+
222
+ process.exitCode = options.failOn === "regression" && comparison.summary.regressions > 0 ? 1 : 0;
223
+ }
224
+
152
225
  async function initialize(path: string, force: boolean): Promise<void> {
153
226
  const absolute = resolve(path);
154
227
  if (!force) {
@@ -175,21 +248,40 @@ export async function main(argv: readonly string[] = process.argv): Promise<void
175
248
  .exitOverride()
176
249
  .showHelpAfterError();
177
250
 
178
- addCheckOptions(program)
179
- .argument("[urls...]", "HTTP or HTTPS URLs to inspect")
180
- .action(async (urls: string[], _options: CliOptions, command: Command) => {
181
- await check(urls, optionsFrom(command));
182
- });
183
-
184
251
  addCheckOptions(
185
252
  program
186
- .command("check")
253
+ .command("check", { isDefault: true })
187
254
  .description("inspect one or more SSR responses")
188
255
  .argument("[urls...]", "HTTP or HTTPS URLs to inspect"),
189
256
  ).action(async (urls: string[], _options: CliOptions, command: Command) => {
190
257
  await check(urls, optionsFrom(command));
191
258
  });
192
259
 
260
+ program
261
+ .command("compare")
262
+ .description("compare two SSRWire JSON audit reports")
263
+ .argument("<baseline>", "baseline JSON audit report")
264
+ .argument("<candidate>", "candidate JSON audit report")
265
+ .option("-f, --format <format>", "terminal, json, or html", parseComparisonFormat, "terminal")
266
+ .option("-o, --output <path>", "write the comparison to a file")
267
+ .option("--fail-on <level>", "regression or never", parseComparisonFailOn, "regression")
268
+ .option(
269
+ "--timing-regression-ms <ms>",
270
+ "minimum absolute median slowdown",
271
+ parseNonNegativeNumber,
272
+ 250,
273
+ )
274
+ .option(
275
+ "--timing-regression-percent <percent>",
276
+ "minimum relative median slowdown",
277
+ parseNonNegativeNumber,
278
+ 25,
279
+ )
280
+ .option("--no-color", "disable terminal colors")
281
+ .action(async (baselinePath: string, candidatePath: string, options: CompareCliOptions) => {
282
+ await compareReports(baselinePath, candidatePath, options);
283
+ });
284
+
193
285
  program
194
286
  .command("init")
195
287
  .description("create a documented starter configuration")