@decocms/blocks-cli 7.0.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 (93) hide show
  1. package/package.json +44 -0
  2. package/scripts/analyze-traces.mjs +1117 -0
  3. package/scripts/audit-observability-config.test.ts +446 -0
  4. package/scripts/audit-observability-config.ts +511 -0
  5. package/scripts/deco-migrate-cli.ts +444 -0
  6. package/scripts/fast-deploy-kv.test.ts +131 -0
  7. package/scripts/generate-blocks.test.ts +94 -0
  8. package/scripts/generate-blocks.ts +274 -0
  9. package/scripts/generate-invoke.test.ts +195 -0
  10. package/scripts/generate-invoke.ts +469 -0
  11. package/scripts/generate-loaders.ts +217 -0
  12. package/scripts/generate-schema.ts +1287 -0
  13. package/scripts/generate-sections.ts +237 -0
  14. package/scripts/htmx-analyze.ts +226 -0
  15. package/scripts/lib/blocks-dedupe.test.ts +179 -0
  16. package/scripts/lib/blocks-dedupe.ts +142 -0
  17. package/scripts/lib/cf-kv-rest.ts +78 -0
  18. package/scripts/lib/jsonc.ts +122 -0
  19. package/scripts/lib/kv-snapshot.ts +51 -0
  20. package/scripts/lib/read-decofile.ts +70 -0
  21. package/scripts/lib/sync-helpers.ts +44 -0
  22. package/scripts/migrate/analyzers/htmx-analyze.test.ts +372 -0
  23. package/scripts/migrate/analyzers/htmx-analyze.ts +425 -0
  24. package/scripts/migrate/analyzers/island-classifier.ts +96 -0
  25. package/scripts/migrate/analyzers/loader-inventory.ts +63 -0
  26. package/scripts/migrate/analyzers/section-metadata.ts +147 -0
  27. package/scripts/migrate/analyzers/theme-extractor.ts +122 -0
  28. package/scripts/migrate/colors.ts +46 -0
  29. package/scripts/migrate/config.test.ts +202 -0
  30. package/scripts/migrate/config.ts +186 -0
  31. package/scripts/migrate/phase-analyze.test.ts +63 -0
  32. package/scripts/migrate/phase-analyze.ts +782 -0
  33. package/scripts/migrate/phase-cleanup-audit.test.ts +137 -0
  34. package/scripts/migrate/phase-cleanup-audit.ts +105 -0
  35. package/scripts/migrate/phase-cleanup.test.ts +141 -0
  36. package/scripts/migrate/phase-cleanup.ts +1588 -0
  37. package/scripts/migrate/phase-compile.test.ts +193 -0
  38. package/scripts/migrate/phase-compile.ts +177 -0
  39. package/scripts/migrate/phase-report.ts +243 -0
  40. package/scripts/migrate/phase-scaffold.ts +593 -0
  41. package/scripts/migrate/phase-transform.ts +310 -0
  42. package/scripts/migrate/phase-verify.test.ts +127 -0
  43. package/scripts/migrate/phase-verify.ts +572 -0
  44. package/scripts/migrate/post-cleanup/rules.ts +1708 -0
  45. package/scripts/migrate/post-cleanup/runner.test.ts +1771 -0
  46. package/scripts/migrate/post-cleanup/runner.ts +137 -0
  47. package/scripts/migrate/post-cleanup/shim-classify.test.ts +352 -0
  48. package/scripts/migrate/post-cleanup/shim-classify.ts +246 -0
  49. package/scripts/migrate/post-cleanup/types.ts +106 -0
  50. package/scripts/migrate/source-layout.test.ts +111 -0
  51. package/scripts/migrate/source-layout.ts +103 -0
  52. package/scripts/migrate/templates/app-css.ts +366 -0
  53. package/scripts/migrate/templates/cache-config.ts +26 -0
  54. package/scripts/migrate/templates/commerce-loaders.ts +230 -0
  55. package/scripts/migrate/templates/cursor-rules.test.ts +59 -0
  56. package/scripts/migrate/templates/cursor-rules.ts +70 -0
  57. package/scripts/migrate/templates/hooks.test.ts +141 -0
  58. package/scripts/migrate/templates/hooks.ts +152 -0
  59. package/scripts/migrate/templates/knip-config.ts +27 -0
  60. package/scripts/migrate/templates/lib-utils.test.ts +139 -0
  61. package/scripts/migrate/templates/lib-utils.ts +326 -0
  62. package/scripts/migrate/templates/lockfile-check-yml.test.ts +26 -0
  63. package/scripts/migrate/templates/lockfile-check-yml.ts +66 -0
  64. package/scripts/migrate/templates/package-json.ts +177 -0
  65. package/scripts/migrate/templates/routes.ts +237 -0
  66. package/scripts/migrate/templates/sdk-gen.ts +59 -0
  67. package/scripts/migrate/templates/section-loaders.ts +456 -0
  68. package/scripts/migrate/templates/server-entry.ts +561 -0
  69. package/scripts/migrate/templates/setup.ts +148 -0
  70. package/scripts/migrate/templates/tsconfig.ts +21 -0
  71. package/scripts/migrate/templates/types-gen.ts +174 -0
  72. package/scripts/migrate/templates/ui-components.ts +144 -0
  73. package/scripts/migrate/templates/vite-config.ts +101 -0
  74. package/scripts/migrate/transforms/dead-code.ts +455 -0
  75. package/scripts/migrate/transforms/deno-isms.ts +85 -0
  76. package/scripts/migrate/transforms/fresh-apis.ts +223 -0
  77. package/scripts/migrate/transforms/htmx-on-events.test.ts +305 -0
  78. package/scripts/migrate/transforms/htmx-on-events.ts +193 -0
  79. package/scripts/migrate/transforms/imports.ts +385 -0
  80. package/scripts/migrate/transforms/jsx.ts +317 -0
  81. package/scripts/migrate/transforms/section-conventions.ts +210 -0
  82. package/scripts/migrate/transforms/tailwind.ts +739 -0
  83. package/scripts/migrate/types.ts +244 -0
  84. package/scripts/migrate-blocks-to-kv.ts +104 -0
  85. package/scripts/migrate-post-cleanup.ts +191 -0
  86. package/scripts/migrate-to-cf-observability.test.ts +215 -0
  87. package/scripts/migrate-to-cf-observability.ts +699 -0
  88. package/scripts/migrate.ts +282 -0
  89. package/scripts/smoke-otlp-errorlog.ts +46 -0
  90. package/scripts/smoke-otlp-meter.ts +48 -0
  91. package/scripts/sync-blocks-to-kv.ts +153 -0
  92. package/scripts/tailwind-lint.ts +518 -0
  93. package/tsconfig.json +7 -0
@@ -0,0 +1,511 @@
1
+ #!/usr/bin/env tsx
2
+ /**
3
+ * @decocms/start — observability config audit
4
+ *
5
+ * Read-only auditor for a site's `wrangler.jsonc`. Detects drift away
6
+ * from the canonical Cloudflare-native observability block documented
7
+ * in `docs/observability.md`. CI-friendly: exits 0 on a clean audit, 1
8
+ * on findings.
9
+ *
10
+ * This is the **detect** half of D3 ("audit is the safety net"). The
11
+ * matching **fix** half is `migrate-to-cf-observability.ts`, which can
12
+ * rewrite the block back to canonical with `--write`. Every rule here
13
+ * has a corresponding behavior in the codemod — there is no rule we
14
+ * can detect but not auto-fix.
15
+ *
16
+ * Rules (id — severity — what it catches):
17
+ *
18
+ * observability_missing error No `observability` key at all. CF captures nothing.
19
+ * observability_disabled error `observability.enabled: false`. Master switch off.
20
+ * traces_disabled warn `observability.traces.enabled: false`. No traces in dashboard.
21
+ * logs_disabled warn `observability.logs.enabled: false`. No logs in dashboard.
22
+ * head_sampling_rate_elevated error `traces.head_sampling_rate > 0.01`. Fleet-scale cost risk; see docs/observability.md.
23
+ * logs_head_sampling_rate_low warn `logs.head_sampling_rate < 1`. Sampling info/warn logs loses signal cheaply; errors go via the direct-POST channel.
24
+ * persist_disabled_no_destination error `persist: false` with no destination configured. Data captured then discarded.
25
+ *
26
+ * Phase 6 / D-14 — fleet-config drift rules (live outside the
27
+ * `observability` block but still owned by this audit):
28
+ *
29
+ * version_metadata_binding_missing error Missing `version_metadata` binding. `service.version` won't be stamped — regressions can't be attributed to a deploy.
30
+ * analytics_engine_binding_missing warn No `DECO_METRICS` AE binding. AE meter is off; OTLP meter still works but CF dashboard panels go dark.
31
+ * tail_consumer_missing error No `tail_consumers` entry pointing at `deco-otel-tail`. 100% error-capture is broken.
32
+ * otel_metrics_endpoint_missing warn `DECO_OTEL_METRICS_ENDPOINT` not set on `vars`. OTLP meter is off; only AE works.
33
+ * otel_traces_endpoint_missing warn `DECO_OTEL_TRACES_ENDPOINT` not set on `vars`. Framework `deco.*` spans drop unless CF Traces is on.
34
+ * otel_logs_endpoint_missing warn `DECO_OTEL_LOGS_ENDPOINT` not set on `vars`. Error logs ride CF Destinations only (head-sampled).
35
+ *
36
+ * Usage (from a site directory):
37
+ * npx -p @decocms/start deco-audit-observability # audit cwd (warn mode — exit 0)
38
+ * npx -p @decocms/start deco-audit-observability --source ./ # explicit source dir
39
+ * npx -p @decocms/start deco-audit-observability --json # machine-readable
40
+ * npx -p @decocms/start deco-audit-observability --mode block # error findings exit 1 (CI gate)
41
+ * npx -p @decocms/start deco-audit-observability --github # GitHub Actions annotations
42
+ *
43
+ * Options:
44
+ * --source <dir> Site directory (default: .)
45
+ * --json Emit findings as JSON to stdout
46
+ * --mode <m> Gate hardness: "warn" (default — always exit 0 on findings,
47
+ * just print them) or "block" (exit 1 on any `error` finding).
48
+ * See D-16 in MIGRATION_TOOLING_PLAN.md for the rationale on
49
+ * why warn is the v1 default.
50
+ * --github Emit `::warning::` / `::error::` lines for GitHub Actions
51
+ * annotations in addition to the normal text output.
52
+ * --help, -h Show this message
53
+ *
54
+ * Exit codes:
55
+ * 0 — no findings, or `--mode warn` (the default) regardless of findings
56
+ * 1 — `--mode block` and at least one `error`-severity finding
57
+ * 2 — file invalid / can't parse
58
+ */
59
+
60
+ import * as fs from "node:fs";
61
+ import * as path from "node:path";
62
+ import { parseJsonc } from "./lib/jsonc";
63
+
64
+ export type Severity = "error" | "warn" | "info";
65
+
66
+ export interface Finding {
67
+ id: string;
68
+ severity: Severity;
69
+ message: string;
70
+ /** Suggested remediation — usually a codemod invocation. */
71
+ fix?: string;
72
+ }
73
+
74
+ export type GateMode = "warn" | "block";
75
+
76
+ interface CliOpts {
77
+ source: string;
78
+ json: boolean;
79
+ help: boolean;
80
+ mode: GateMode;
81
+ github: boolean;
82
+ }
83
+
84
+ function parseArgs(argv: string[]): CliOpts {
85
+ const opts: CliOpts = {
86
+ source: ".",
87
+ json: false,
88
+ help: false,
89
+ mode: "warn",
90
+ github: false,
91
+ };
92
+ for (let i = 0; i < argv.length; i++) {
93
+ const flag = argv[i];
94
+ switch (flag) {
95
+ case "--source":
96
+ opts.source = argv[++i] ?? ".";
97
+ break;
98
+ case "--json":
99
+ opts.json = true;
100
+ break;
101
+ case "--mode": {
102
+ const value = argv[++i];
103
+ if (value !== "warn" && value !== "block") {
104
+ console.error(
105
+ `audit: --mode must be "warn" or "block" (got "${value ?? ""}")`,
106
+ );
107
+ process.exit(2);
108
+ }
109
+ opts.mode = value;
110
+ break;
111
+ }
112
+ case "--github":
113
+ opts.github = true;
114
+ break;
115
+ case "--help":
116
+ case "-h":
117
+ opts.help = true;
118
+ break;
119
+ }
120
+ }
121
+ return opts;
122
+ }
123
+
124
+ function showHelp(): void {
125
+ console.log(`
126
+ @decocms/start — observability config audit
127
+
128
+ Read-only check for drift from the canonical Cloudflare-native
129
+ observability block in wrangler.jsonc. Pair with
130
+ \`deco-cf-observability --write\` to auto-fix.
131
+
132
+ Usage:
133
+ npx -p @decocms/start deco-audit-observability [options]
134
+
135
+ Options:
136
+ --source <dir> Site directory (default: .)
137
+ --json Emit findings as JSON
138
+ --mode <m> "warn" (default, exit 0) | "block" (exit 1 on errors)
139
+ --github Emit ::warning::/::error:: lines for GitHub Actions
140
+ --help, -h This message
141
+
142
+ Exit codes:
143
+ 0 no findings, OR --mode warn (default — annotate and move on)
144
+ 1 --mode block AND at least one error-severity finding
145
+ 2 wrangler.jsonc missing or unparseable
146
+
147
+ See D-16 in MIGRATION_TOOLING_PLAN.md for the v1 "warn-only" policy.
148
+ `);
149
+ }
150
+
151
+ interface ObservabilityBlock {
152
+ enabled?: boolean;
153
+ logs?: {
154
+ enabled?: boolean;
155
+ head_sampling_rate?: number;
156
+ persist?: boolean;
157
+ destinations?: unknown[];
158
+ invocation_logs?: boolean;
159
+ };
160
+ traces?: {
161
+ enabled?: boolean;
162
+ head_sampling_rate?: number;
163
+ persist?: boolean;
164
+ destinations?: unknown[];
165
+ };
166
+ }
167
+
168
+ /**
169
+ * Pure audit function. Exported for unit-testing; the CLI wrapper is the
170
+ * thin sliver below.
171
+ */
172
+ export function auditObservabilityBlock(
173
+ obs: ObservabilityBlock | undefined,
174
+ ): Finding[] {
175
+ const findings: Finding[] = [];
176
+
177
+ if (!obs) {
178
+ findings.push({
179
+ id: "observability_missing",
180
+ severity: "error",
181
+ message:
182
+ "wrangler.jsonc has no `observability` block. Cloudflare won't capture logs or traces.",
183
+ fix: "npx -p @decocms/start deco-cf-observability --write",
184
+ });
185
+ return findings;
186
+ }
187
+
188
+ if (obs.enabled === false) {
189
+ findings.push({
190
+ id: "observability_disabled",
191
+ severity: "error",
192
+ message:
193
+ "`observability.enabled: false` — the master switch is off, sub-block flags do nothing.",
194
+ fix: "npx -p @decocms/start deco-cf-observability --write",
195
+ });
196
+ }
197
+
198
+ // ---- traces ----
199
+ if (obs.traces?.enabled === false) {
200
+ findings.push({
201
+ id: "traces_disabled",
202
+ severity: "warn",
203
+ message:
204
+ "`observability.traces.enabled: false` — traces won't reach the CF dashboard or any destination.",
205
+ fix: "npx -p @decocms/start deco-cf-observability --write",
206
+ });
207
+ }
208
+
209
+ const tracesRate = obs.traces?.head_sampling_rate;
210
+ if (typeof tracesRate === "number" && tracesRate > 0.01) {
211
+ findings.push({
212
+ id: "head_sampling_rate_elevated",
213
+ severity: "error",
214
+ message:
215
+ `traces.head_sampling_rate is ${tracesRate} (> 0.01). At fleet scale this is a cost trap; ` +
216
+ `see docs/observability.md → Sampling. If this is intentional and time-bounded (incident, ` +
217
+ `release window), leave a comment in wrangler.jsonc explaining why, then revert.`,
218
+ fix: "npx -p @decocms/start deco-cf-observability --write --traces-rate 0.01",
219
+ });
220
+ }
221
+
222
+ // ---- logs ----
223
+ if (obs.logs?.enabled === false) {
224
+ findings.push({
225
+ id: "logs_disabled",
226
+ severity: "warn",
227
+ message:
228
+ "`observability.logs.enabled: false` — logs won't reach the CF dashboard or any destination.",
229
+ fix: "npx -p @decocms/start deco-cf-observability --write",
230
+ });
231
+ }
232
+
233
+ const logsRate = obs.logs?.head_sampling_rate;
234
+ if (typeof logsRate === "number" && logsRate < 1) {
235
+ findings.push({
236
+ id: "logs_head_sampling_rate_low",
237
+ severity: "warn",
238
+ message:
239
+ `logs.head_sampling_rate is ${logsRate} (< 1). Info/warn logs are cheap and high-signal; ` +
240
+ `error logs already bypass head sampling via the direct-POST channel, so there's little to ` +
241
+ `gain by sampling logs.`,
242
+ fix: "npx -p @decocms/start deco-cf-observability --write --logs-rate 1",
243
+ });
244
+ }
245
+
246
+ // ---- persist / destinations ----
247
+ const hasDestination = (block?: { destinations?: unknown[] }): boolean =>
248
+ Array.isArray(block?.destinations) && block!.destinations!.length > 0;
249
+
250
+ const tracesPersist = obs.traces?.persist ?? true;
251
+ if (
252
+ obs.traces?.enabled !== false &&
253
+ !tracesPersist &&
254
+ !hasDestination(obs.traces)
255
+ ) {
256
+ findings.push({
257
+ id: "persist_disabled_no_destination",
258
+ severity: "error",
259
+ message:
260
+ "traces.persist:false with no destinations — traces are captured and discarded. " +
261
+ "Either set persist:true (CF dashboard storage) or configure a destination.",
262
+ fix: "npx -p @decocms/start deco-cf-observability --write --persist",
263
+ });
264
+ }
265
+
266
+ const logsPersist = obs.logs?.persist ?? true;
267
+ if (
268
+ obs.logs?.enabled !== false &&
269
+ !logsPersist &&
270
+ !hasDestination(obs.logs)
271
+ ) {
272
+ findings.push({
273
+ id: "persist_disabled_no_destination",
274
+ severity: "error",
275
+ message:
276
+ "logs.persist:false with no destinations — logs are captured and discarded. " +
277
+ "Either set persist:true (CF dashboard storage) or configure a destination.",
278
+ fix: "npx -p @decocms/start deco-cf-observability --write --persist",
279
+ });
280
+ }
281
+
282
+ return findings;
283
+ }
284
+
285
+ /**
286
+ * Fleet-config drift rules — owned by the same audit because the
287
+ * cumulative effect of "observability block correct, bindings missing"
288
+ * is identical to "observability block missing" (no data lands in
289
+ * ClickHouse).
290
+ *
291
+ * The CLI composes `auditObservabilityBlock` + `auditFleetBindings`.
292
+ * Both return Finding[]; callers concatenate.
293
+ */
294
+ export interface WranglerLike {
295
+ observability?: ObservabilityBlock;
296
+ version_metadata?: { binding?: string } | unknown;
297
+ analytics_engine_datasets?: Array<{ binding?: string; dataset?: string }> | unknown;
298
+ tail_consumers?: Array<{ service?: string; environment?: string }> | unknown;
299
+ vars?: Record<string, unknown> | unknown;
300
+ }
301
+
302
+ export function auditFleetBindings(wrangler: WranglerLike): Finding[] {
303
+ const findings: Finding[] = [];
304
+
305
+ // version_metadata — required so `service.version` is stamped on every
306
+ // span and log line. Without it, regressions can't be tied to a
307
+ // specific deployment.
308
+ const vmBinding =
309
+ typeof wrangler.version_metadata === "object" &&
310
+ wrangler.version_metadata !== null &&
311
+ "binding" in wrangler.version_metadata
312
+ ? (wrangler.version_metadata as { binding?: string }).binding
313
+ : undefined;
314
+ if (!vmBinding) {
315
+ findings.push({
316
+ id: "version_metadata_binding_missing",
317
+ severity: "error",
318
+ message:
319
+ "wrangler.jsonc is missing a `version_metadata.binding` entry. " +
320
+ "`service.version` won't appear on spans/logs and the deploy-correlation " +
321
+ "panel will be empty. Recommended value: `CF_VERSION_METADATA`.",
322
+ fix: "npx -p @decocms/start deco-cf-observability --write",
323
+ });
324
+ }
325
+
326
+ // DECO_METRICS — Analytics Engine binding. The AE meter is the hot-
327
+ // path CF dashboard view; OTLP works without it but we lose the
328
+ // operator-grade short-window panels.
329
+ const aeDatasets = Array.isArray(wrangler.analytics_engine_datasets)
330
+ ? (wrangler.analytics_engine_datasets as Array<{ binding?: string }>)
331
+ : [];
332
+ const hasMetricsBinding = aeDatasets.some((d) => d?.binding === "DECO_METRICS");
333
+ if (!hasMetricsBinding) {
334
+ findings.push({
335
+ id: "analytics_engine_binding_missing",
336
+ severity: "warn",
337
+ message:
338
+ "wrangler.jsonc has no `analytics_engine_datasets[].binding == 'DECO_METRICS'`. " +
339
+ "The AE meter is off; the hot-path operator dashboards in CF will be empty. " +
340
+ "OTLP metrics keep flowing if `DECO_OTEL_METRICS_ENDPOINT` is set.",
341
+ fix: "npx -p @decocms/start deco-cf-observability --write",
342
+ });
343
+ }
344
+
345
+ // tail_consumers — must list deco-otel-tail. Phase 1 enrichment is
346
+ // useless without the tail consumer firing.
347
+ const tail = Array.isArray(wrangler.tail_consumers)
348
+ ? (wrangler.tail_consumers as Array<{ service?: string }>)
349
+ : [];
350
+ const hasTailConsumer = tail.some((t) => t?.service === "deco-otel-tail");
351
+ if (!hasTailConsumer) {
352
+ findings.push({
353
+ id: "tail_consumer_missing",
354
+ severity: "error",
355
+ message:
356
+ "wrangler.jsonc has no `tail_consumers[].service == 'deco-otel-tail'` entry. " +
357
+ "100% error-capture is broken — only the head-sampled CF Destinations path " +
358
+ "will report errors, and isolate crashes will be invisible.",
359
+ fix: "npx -p @decocms/start deco-cf-observability --write",
360
+ });
361
+ }
362
+
363
+ // DECO_OTEL_*_ENDPOINT env vars. Audit each separately so the message
364
+ // explains which channel is silently no-op.
365
+ const vars =
366
+ typeof wrangler.vars === "object" && wrangler.vars !== null
367
+ ? (wrangler.vars as Record<string, unknown>)
368
+ : {};
369
+ const checkVar = (id: string, name: string, severity: Severity, channel: string) => {
370
+ const v = vars[name];
371
+ if (typeof v !== "string" || v.length === 0) {
372
+ findings.push({
373
+ id,
374
+ severity,
375
+ message:
376
+ `wrangler.jsonc \`vars.${name}\` is not set. ${channel} is a no-op; ` +
377
+ `data captured in this channel never lands in ClickHouse. ` +
378
+ `See docs/observability.md for the canonical endpoints.`,
379
+ fix: "npx -p @decocms/start deco-cf-observability --write",
380
+ });
381
+ }
382
+ };
383
+ checkVar(
384
+ "otel_metrics_endpoint_missing",
385
+ "DECO_OTEL_METRICS_ENDPOINT",
386
+ "warn",
387
+ "OTLP metrics direct-POST",
388
+ );
389
+ checkVar(
390
+ "otel_traces_endpoint_missing",
391
+ "DECO_OTEL_TRACES_ENDPOINT",
392
+ "warn",
393
+ "OTLP traces direct-POST",
394
+ );
395
+ checkVar(
396
+ "otel_logs_endpoint_missing",
397
+ "DECO_OTEL_LOGS_ENDPOINT",
398
+ "warn",
399
+ "OTLP error-log direct-POST",
400
+ );
401
+
402
+ return findings;
403
+ }
404
+
405
+ /**
406
+ * One-stop call for the full wrangler audit — composes the
407
+ * observability-block rules with the fleet-binding rules. Both keep
408
+ * working standalone for fine-grained tests.
409
+ */
410
+ export function auditWranglerConfig(wrangler: WranglerLike): Finding[] {
411
+ return [
412
+ ...auditObservabilityBlock(wrangler.observability),
413
+ ...auditFleetBindings(wrangler),
414
+ ];
415
+ }
416
+
417
+ function findingsToText(file: string, findings: Finding[]): string {
418
+ if (findings.length === 0) {
419
+ return `OK ${file} — observability config looks canonical`;
420
+ }
421
+ const lines = [`Findings in ${file}:`];
422
+ for (const f of findings) {
423
+ lines.push(` [${f.severity.toUpperCase()}] ${f.id}`);
424
+ lines.push(` ${f.message}`);
425
+ if (f.fix) lines.push(` fix: ${f.fix}`);
426
+ }
427
+ lines.push("");
428
+ return lines.join("\n");
429
+ }
430
+
431
+ function main(): void {
432
+ const opts = parseArgs(process.argv.slice(2));
433
+ if (opts.help) {
434
+ showHelp();
435
+ process.exit(0);
436
+ }
437
+
438
+ const file = path.resolve(opts.source, "wrangler.jsonc");
439
+ if (!fs.existsSync(file)) {
440
+ console.error(`audit: ${file} not found`);
441
+ process.exit(2);
442
+ }
443
+
444
+ let parsed: WranglerLike;
445
+ try {
446
+ parsed = parseJsonc(fs.readFileSync(file, "utf8")) as WranglerLike;
447
+ } catch (err) {
448
+ console.error(`audit: ${file} could not be parsed: ${(err as Error).message}`);
449
+ process.exit(2);
450
+ }
451
+
452
+ const findings = auditWranglerConfig(parsed);
453
+
454
+ if (opts.json) {
455
+ process.stdout.write(
456
+ JSON.stringify({ file, mode: opts.mode, findings }, null, 2) + "\n",
457
+ );
458
+ } else {
459
+ process.stdout.write(findingsToText(file, findings) + "\n");
460
+ }
461
+
462
+ if (opts.github) {
463
+ for (const f of findings) {
464
+ // GitHub Actions workflow command. `error` and `warning` annotate the
465
+ // diff; `notice` is informational. We never emit `error` in warn mode
466
+ // even for error-severity rules — the v1 policy is annotate-don't-fail.
467
+ const level = opts.mode === "block" && f.severity === "error"
468
+ ? "error"
469
+ : f.severity === "info" ? "notice" : "warning";
470
+ const msg = `${f.message}${f.fix ? ` (fix: ${f.fix})` : ""}`;
471
+ const escaped = msg.replace(/%/g, "%25").replace(/\r/g, "%0D").replace(
472
+ /\n/g,
473
+ "%0A",
474
+ );
475
+ process.stdout.write(`::${level} title=${f.id}::${escaped}\n`);
476
+ }
477
+ }
478
+
479
+ // Exit policy: D-16 / Phase 6 decision.
480
+ // warn — annotate only; always exit 0 (CI sees the findings but ships)
481
+ // block — exit 1 on any `error`-severity finding
482
+ // The default is `warn` because storefronts are upgraded over weeks; a
483
+ // day-one block would fail PRs that have nothing to do with observability.
484
+ const shouldFail = opts.mode === "block" &&
485
+ findings.some((f) => f.severity === "error");
486
+ process.exit(shouldFail ? 1 : 0);
487
+ }
488
+
489
+ // Only run when invoked directly, not when imported by tests.
490
+ // Works under both CJS (require.main === module) and ESM (import.meta.url
491
+ // matches argv[1]) because the package is `"type": "module"` but the
492
+ // codemod siblings ship .cjs bundles via tsup.
493
+ const isCjsEntry =
494
+ typeof require !== "undefined" &&
495
+ typeof module !== "undefined" &&
496
+ // biome-ignore lint/correctness/noNodejsModules: entry-point check
497
+ require.main === module;
498
+ let isEsmEntry = false;
499
+ try {
500
+ // import.meta is a syntax error in CJS, but we're in an ESM source file.
501
+ isEsmEntry =
502
+ typeof process !== "undefined" &&
503
+ Array.isArray(process.argv) &&
504
+ process.argv[1] !== undefined &&
505
+ import.meta.url === `file://${process.argv[1]}`;
506
+ } catch {
507
+ // ignore in CJS
508
+ }
509
+ if (isCjsEntry || isEsmEntry) {
510
+ main();
511
+ }