@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,244 @@
1
+ export type Platform =
2
+ | "vtex"
3
+ | "vnda"
4
+ | "shopify"
5
+ | "wake"
6
+ | "linx"
7
+ | "nuvemshop"
8
+ | "magento"
9
+ | "custom";
10
+
11
+ export interface FileRecord {
12
+ /** Relative path from source root */
13
+ path: string;
14
+ /** Absolute path */
15
+ absPath: string;
16
+ /** File category */
17
+ category:
18
+ | "section"
19
+ | "island"
20
+ | "component"
21
+ | "sdk"
22
+ | "loader"
23
+ | "action"
24
+ | "route"
25
+ | "app"
26
+ | "static"
27
+ | "config"
28
+ | "generated"
29
+ | "other";
30
+ /** Whether this file is a re-export wrapper */
31
+ isReExport?: boolean;
32
+ /** The target of the re-export if applicable */
33
+ reExportTarget?: string;
34
+ /** Detected patterns in this file */
35
+ patterns: DetectedPattern[];
36
+ /** Action to take */
37
+ action: "transform" | "delete" | "move" | "scaffold" | "manual-review";
38
+ /** Target path in new structure (relative to project root) */
39
+ targetPath?: string;
40
+ /** Notes for the report */
41
+ notes?: string;
42
+ }
43
+
44
+ export type DetectedPattern =
45
+ | "preact-hooks"
46
+ | "preact-signals"
47
+ | "fresh-runtime"
48
+ | "fresh-server"
49
+ | "deco-hooks"
50
+ | "deco-context"
51
+ | "deco-web"
52
+ | "deco-blocks"
53
+ | "apps-imports"
54
+ | "site-imports"
55
+ | "class-attr"
56
+ | "onInput-handler"
57
+ | "deno-lint-ignore"
58
+ | "npm-prefix"
59
+ | "ts-extension-import"
60
+ | "component-children"
61
+ | "jsx-types"
62
+ | "asset-function"
63
+ | "head-component"
64
+ | "define-app"
65
+ | "invoke-proxy"
66
+ | "use-component"
67
+ | "sections-component-loader";
68
+
69
+ /** Metadata extracted from a section file during analysis */
70
+ export interface SectionMeta {
71
+ /** Relative path from source root (e.g. "sections/Header/Header.tsx") */
72
+ path: string;
73
+ /** Has export const loader or export function loader */
74
+ hasLoader: boolean;
75
+ /** Loader is async */
76
+ loaderIsAsync: boolean;
77
+ /** Has export function LoadingFallback */
78
+ hasLoadingFallback: boolean;
79
+ /** Is a header section (by filename) */
80
+ isHeader: boolean;
81
+ /** Is a footer section (by filename) */
82
+ isFooter: boolean;
83
+ /** Is a theme section (by filename) */
84
+ isTheme: boolean;
85
+ /** Is a shelf/carousel/listing section (by filename or content) */
86
+ isListing: boolean;
87
+ /** Has JSDoc @title */
88
+ hasTitle: boolean;
89
+ /** Has JSDoc @description */
90
+ hasDescription: boolean;
91
+ /** Loader uses ctx.device or similar device detection */
92
+ loaderUsesDevice: boolean;
93
+ /** Loader uses request URL / search params */
94
+ loaderUsesUrl: boolean;
95
+ /** Loader is an Account section (sections/Account/*) */
96
+ isAccountSection: boolean;
97
+ /** Loader only sets ctx.response.status (no real prop enrichment) */
98
+ isStatusOnly: boolean;
99
+ /** Loader sets isMobile (boolean) rather than device (string) */
100
+ usesMobileBoolean: boolean;
101
+ }
102
+
103
+ /** Classification of an island file */
104
+ export interface IslandClassification {
105
+ /** Relative path from source root */
106
+ path: string;
107
+ /** "wrapper" = thin re-export/bridge, "standalone" = has real logic */
108
+ type: "wrapper" | "standalone";
109
+ /** If wrapper, the target component path */
110
+ wrapsComponent?: string;
111
+ /** If standalone, the suggested target path under src/ */
112
+ suggestedTarget: string;
113
+ /** Line count (used as heuristic) */
114
+ lineCount: number;
115
+ }
116
+
117
+ /** Information about a loader file */
118
+ export interface LoaderInfo {
119
+ /** Relative path from source root */
120
+ path: string;
121
+ /** Has export const cache (SWR) */
122
+ hasCache: boolean;
123
+ /** Has export const cacheKey */
124
+ hasCacheKey: boolean;
125
+ /** Maps to a known @decocms/apps equivalent */
126
+ appsEquivalent: string | null;
127
+ /** Is a custom loader that needs dynamic import in commerce-loaders */
128
+ isCustom: boolean;
129
+ /** Detected platform relevance (vtex, shopify, etc.) */
130
+ platformRelevance: Platform | null;
131
+ }
132
+
133
+ export interface MigrationContext {
134
+ sourceDir: string;
135
+ siteName: string;
136
+ platform: Platform;
137
+ /** VTEX account name (e.g. "casaevideonewio") — extracted from source code */
138
+ vtexAccount: string | null;
139
+ gtmId: string | null;
140
+
141
+ /**
142
+ * Per-site config loaded from `.deco-migrate.config.json`. `null` means
143
+ * no config file was present — defaults apply throughout. Imported
144
+ * lazily as `MigrateConfig` to avoid a circular dependency between
145
+ * `types.ts` and `config.ts`.
146
+ */
147
+ config?: import("./config").MigrateConfig | null;
148
+
149
+ /** deno.json import map entries */
150
+ importMap: Record<string, string>;
151
+
152
+ /** npm dependencies discovered from inline npm: imports in source files */
153
+ discoveredNpmDeps: Record<string, string>;
154
+
155
+ /** Theme colors extracted from .deco/blocks CMS config */
156
+ themeColors: Record<string, string>;
157
+ /** Font family from CMS config */
158
+ fontFamily: string | null;
159
+
160
+ /** All categorized source files */
161
+ files: FileRecord[];
162
+
163
+ /** Section metadata extracted during analysis */
164
+ sectionMetas: SectionMeta[];
165
+ /** Island classifications */
166
+ islandClassifications: IslandClassification[];
167
+ /** Map from island path → wrapped component import path (for wrapper islands) */
168
+ islandWrapperTargets: Map<string, string>;
169
+ /** Loader inventory */
170
+ loaderInventory: LoaderInfo[];
171
+
172
+ /** Files created by scaffold phase */
173
+ scaffoldedFiles: string[];
174
+ /** Files transformed */
175
+ transformedFiles: string[];
176
+ /** Files deleted */
177
+ deletedFiles: string[];
178
+ /** Files moved */
179
+ movedFiles: Array<{ from: string; to: string }>;
180
+ /** Items requiring manual review */
181
+ manualReviewItems: ReviewItem[];
182
+ /** Framework findings */
183
+ frameworkFindings: string[];
184
+
185
+ dryRun: boolean;
186
+ verbose: boolean;
187
+ }
188
+
189
+ export interface ReviewItem {
190
+ file: string;
191
+ reason: string;
192
+ severity: "info" | "warning" | "error";
193
+ }
194
+
195
+ export interface TransformResult {
196
+ content: string;
197
+ changed: boolean;
198
+ notes: string[];
199
+ }
200
+
201
+ export function createContext(
202
+ sourceDir: string,
203
+ opts: {
204
+ dryRun?: boolean;
205
+ verbose?: boolean;
206
+ config?: import("./config").MigrateConfig | null;
207
+ } = {},
208
+ ): MigrationContext {
209
+ return {
210
+ sourceDir,
211
+ siteName: "",
212
+ platform: "custom",
213
+ vtexAccount: null,
214
+ gtmId: null,
215
+ config: opts.config ?? null,
216
+ importMap: {},
217
+ discoveredNpmDeps: {},
218
+ themeColors: {},
219
+ fontFamily: null,
220
+ files: [],
221
+ sectionMetas: [],
222
+ islandClassifications: [],
223
+ islandWrapperTargets: new Map(),
224
+ loaderInventory: [],
225
+ scaffoldedFiles: [],
226
+ transformedFiles: [],
227
+ deletedFiles: [],
228
+ movedFiles: [],
229
+ manualReviewItems: [],
230
+ frameworkFindings: [],
231
+ dryRun: opts.dryRun ?? false,
232
+ verbose: opts.verbose ?? false,
233
+ };
234
+ }
235
+
236
+ export function log(ctx: MigrationContext, msg: string) {
237
+ if (ctx.verbose) console.log(` ${msg}`);
238
+ }
239
+
240
+ export function logPhase(phase: string) {
241
+ console.log(`\n${"=".repeat(60)}`);
242
+ console.log(` Phase: ${phase}`);
243
+ console.log(`${"=".repeat(60)}\n`);
244
+ }
@@ -0,0 +1,104 @@
1
+ #!/usr/bin/env tsx
2
+ /**
3
+ * One-shot fast-deploy migration: populate Cloudflare KV from a site's
4
+ * bundled decofile so the worker can serve content KV-first.
5
+ *
6
+ * Reads `.deco/blocks/*.json` (the same merge the block generator does),
7
+ * writes `decofile:current` + `index:revision` to the site's KV namespace via
8
+ * the REST API, then reads both keys back to verify. Run ONCE per site before
9
+ * flipping it to KV-first (i.e. before adding the `DECO_KV` binding +
10
+ * deploying the fast-deploy framework version).
11
+ *
12
+ * Usage (from the site root):
13
+ * # dry-run (default): reads blocks, prints what would be written, no writes
14
+ * CF_ACCOUNT_ID=... CF_KV_NAMESPACE_ID=... CF_API_TOKEN=... \
15
+ * npx -p @decocms/start deco-migrate-blocks-to-kv
16
+ * # apply:
17
+ * ... npx -p @decocms/start deco-migrate-blocks-to-kv --write
18
+ *
19
+ * Options:
20
+ * --blocks-dir <dir> Input blocks dir (default: .deco/blocks)
21
+ * --write Perform the KV writes (otherwise dry-run, exit 0)
22
+ * --help, -h Show this help
23
+ *
24
+ * Env:
25
+ * CF_ACCOUNT_ID, CF_KV_NAMESPACE_ID, CF_API_TOKEN (required with --write)
26
+ *
27
+ * Exit codes: 0 ok / dry-run; 2 error (bad dir, missing env, verify failed)
28
+ */
29
+
30
+ import * as path from "node:path";
31
+ import { createKvRestClient, kvConfigFromEnv } from "./lib/cf-kv-rest";
32
+ import { buildSnapshot, verifySnapshotInKv, writeSnapshotToKv } from "./lib/kv-snapshot";
33
+ import { readDecofileFromDir } from "./lib/read-decofile";
34
+
35
+ function parseArgs(argv: string[]) {
36
+ const has = (f: string) => argv.includes(f);
37
+ const val = (f: string, d: string) => {
38
+ const i = argv.indexOf(f);
39
+ return i >= 0 && argv[i + 1] ? argv[i + 1] : d;
40
+ };
41
+ return {
42
+ help: has("--help") || has("-h"),
43
+ write: has("--write"),
44
+ blocksDir: val("--blocks-dir", ".deco/blocks"),
45
+ };
46
+ }
47
+
48
+ async function main() {
49
+ const opts = parseArgs(process.argv.slice(2));
50
+ if (opts.help) {
51
+ console.log(
52
+ "Usage: deco-migrate-blocks-to-kv [--blocks-dir .deco/blocks] [--write]\n" +
53
+ "Env: CF_ACCOUNT_ID, CF_KV_NAMESPACE_ID, CF_API_TOKEN",
54
+ );
55
+ process.exit(0);
56
+ }
57
+
58
+ const blocksDir = path.resolve(process.cwd(), opts.blocksDir);
59
+
60
+ let blocks: Record<string, unknown>;
61
+ try {
62
+ const result = readDecofileFromDir(blocksDir);
63
+ blocks = result.blocks;
64
+ if (result.collisions.length) {
65
+ console.warn(`warning: ${result.collisions.length} filename collision(s) resolved by tie-break`);
66
+ }
67
+ } catch (e) {
68
+ console.error(`error: ${e instanceof Error ? e.message : String(e)}`);
69
+ process.exit(2);
70
+ }
71
+
72
+ const snap = buildSnapshot(blocks);
73
+ console.log(`decofile: ${snap.count} blocks, revision ${snap.revision}, ${snap.snapshot.length} bytes`);
74
+
75
+ if (!opts.write) {
76
+ console.log("\nDry-run only. Re-run with --write to populate KV.");
77
+ process.exit(0);
78
+ }
79
+
80
+ let client: ReturnType<typeof createKvRestClient>;
81
+ try {
82
+ client = createKvRestClient(kvConfigFromEnv());
83
+ } catch (e) {
84
+ console.error(`error: ${e instanceof Error ? e.message : String(e)}`);
85
+ process.exit(2);
86
+ }
87
+
88
+ try {
89
+ await writeSnapshotToKv(client, snap);
90
+ const verify = await verifySnapshotInKv(client, snap.revision);
91
+ if (!verify.ok) {
92
+ console.error(`error: KV verify failed — ${verify.reason}`);
93
+ process.exit(2);
94
+ }
95
+ } catch (e) {
96
+ console.error(`error: ${e instanceof Error ? e.message : String(e)}`);
97
+ process.exit(2);
98
+ }
99
+
100
+ console.log(`\nwrote + verified decofile:current (rev ${snap.revision}) → KV.`);
101
+ console.log("Next: add the DECO_KV binding in wrangler.toml and deploy the fast-deploy build.");
102
+ }
103
+
104
+ main();
@@ -0,0 +1,191 @@
1
+ #!/usr/bin/env tsx
2
+ /**
3
+ * Post-Migration Cleanup Audit
4
+ *
5
+ * Read-only audit that scans a migrated site for dead code and obsolete
6
+ * boilerplate that the framework now owns. Mirrors the human checklist at
7
+ * `.agents/skills/deco-to-tanstack-migration/references/post-migration-cleanup.md`
8
+ * but turns it into something CI can actually run.
9
+ *
10
+ * Usage (from a migrated site directory):
11
+ * npx -p @decocms/start deco-post-cleanup
12
+ * npx -p @decocms/start deco-post-cleanup --json
13
+ *
14
+ * Options:
15
+ * --source <dir> Site directory to audit (default: current directory)
16
+ * --json Emit machine-readable JSON instead of pretty text
17
+ * --strict Exit code 2 if any warning-severity findings exist
18
+ * --help, -h Show this help
19
+ *
20
+ * This script is intentionally read-only. Auto-fix support (`--fix`) is
21
+ * a planned follow-up — see the SKILL doc.
22
+ */
23
+
24
+ import * as path from "node:path";
25
+ import { banner, bold, cyan, gray, green, red, yellow } from "./migrate/colors";
26
+ import { realFsAdapter, realFsWriter, runAudit } from "./migrate/post-cleanup/runner";
27
+ import type { AuditReport, Severity } from "./migrate/post-cleanup/types";
28
+
29
+ interface CliOpts {
30
+ source: string;
31
+ json: boolean;
32
+ strict: boolean;
33
+ help: boolean;
34
+ fix: boolean;
35
+ }
36
+
37
+ function parseArgs(args: string[]): CliOpts {
38
+ let source = ".";
39
+ let json = false;
40
+ let strict = false;
41
+ let help = false;
42
+ let fix = false;
43
+ for (let i = 0; i < args.length; i++) {
44
+ switch (args[i]) {
45
+ case "--source":
46
+ source = args[++i];
47
+ break;
48
+ case "--json":
49
+ json = true;
50
+ break;
51
+ case "--strict":
52
+ strict = true;
53
+ break;
54
+ case "--fix":
55
+ fix = true;
56
+ break;
57
+ case "--help":
58
+ case "-h":
59
+ help = true;
60
+ break;
61
+ }
62
+ }
63
+ return { source, json, strict, help, fix };
64
+ }
65
+
66
+ function showHelp() {
67
+ console.log(`
68
+ @decocms/start — Post-Migration Cleanup Audit
69
+
70
+ Scans a migrated site for dead code and obsolete boilerplate that the
71
+ framework now owns. Read-only — prints findings, does not modify files.
72
+
73
+ Usage:
74
+ npx -p @decocms/start deco-post-cleanup [options]
75
+
76
+ Options:
77
+ --source <dir> Site directory to audit (default: .)
78
+ --fix Auto-apply mechanical fixes for the safe rules
79
+ (dead-lib-shims, dead-runtime-shim, local-widgets-types,
80
+ vtex-shim-regression swap subset, obsolete-vite-plugins,
81
+ local-framework-duplicate auto-fixable subset,
82
+ lockfile-multiple, package-manager-missing).
83
+ Other rules — including htmx-residue, lockfile-missing,
84
+ lockfile-drift, and the warn-only entries of
85
+ local-framework-duplicate — stay detect-only.
86
+ --json Emit machine-readable JSON instead of pretty text
87
+ --strict Exit code 2 if any warning-severity findings exist
88
+ --help, -h Show this help
89
+
90
+ Examples:
91
+ npx -p @decocms/start deco-post-cleanup
92
+ npx -p @decocms/start deco-post-cleanup --source ./my-site --json
93
+ npx -p @decocms/start deco-post-cleanup --fix
94
+ npx -p @decocms/start deco-post-cleanup --fix --strict # fail CI if anything left
95
+
96
+ See: .agents/skills/deco-to-tanstack-migration/references/post-migration-cleanup.md
97
+ `);
98
+ }
99
+
100
+ function severityColor(sev: Severity, text: string): string {
101
+ if (sev === "warning") return yellow(text);
102
+ return gray(text);
103
+ }
104
+
105
+ function printText(report: AuditReport, fixMode: boolean): void {
106
+ banner(fixMode ? "Post-Migration Cleanup Audit (--fix)" : "Post-Migration Cleanup Audit");
107
+ console.log(` ${gray("Site:")} ${bold(report.site)}`);
108
+ console.log(` ${gray("Findings:")} ${bold(String(report.totalFindings))}`);
109
+ if (fixMode) {
110
+ console.log(` ${gray("Auto-fixed:")} ${bold(String(report.totalFixActions))}`);
111
+ }
112
+ console.log("");
113
+
114
+ let idx = 0;
115
+ for (const summary of report.rules) {
116
+ idx++;
117
+ const count = summary.findings.length;
118
+ const fixCount = summary.fixes?.length ?? 0;
119
+ const headColor = count === 0 ? green : yellow;
120
+ const suffix = fixMode
121
+ ? gray(`(${count} found, ${fixCount} fixed${summary.supportsAutoFix ? "" : ", manual"})`)
122
+ : gray(`(${count} found)`);
123
+ console.log(`${headColor(`[${idx}] ${summary.title}`)} ${suffix}`);
124
+ for (const f of summary.findings) {
125
+ const tag = severityColor(f.severity, `[${f.severity.toUpperCase()}]`);
126
+ console.log(` ${tag} ${bold(f.file)} — ${f.message}`);
127
+ if (f.fix && !summary.fixes) {
128
+ console.log(` ${gray("fix:")} ${f.fix}`);
129
+ }
130
+ }
131
+ if (summary.fixes) {
132
+ for (const a of summary.fixes) {
133
+ console.log(` ${cyan("[FIXED]")} ${bold(a.file)} — ${a.detail}`);
134
+ }
135
+ }
136
+ if (count === 0) console.log(` ${gray("(nothing to clean up)")}`);
137
+ console.log("");
138
+ }
139
+
140
+ const warnings = report.rules
141
+ .flatMap((r) => r.findings)
142
+ .filter((f) => f.severity === "warning").length;
143
+ const infos = report.totalFindings - warnings;
144
+ const tail = fixMode
145
+ ? `${report.totalFindings} finding(s) — ${cyan(`${report.totalFixActions} auto-fixed`)}, ${yellow(`${warnings} warning(s)`)}, ${gray(`${infos} info`)}`
146
+ : `${report.totalFindings} finding(s) — ${yellow(`${warnings} warning(s)`)}, ${gray(`${infos} info`)}`;
147
+ console.log(`${bold("Summary:")} ${tail}`);
148
+ if (report.totalFindings > 0) {
149
+ const hint = fixMode
150
+ ? " Some rules require manual fixes — see post-migration-cleanup.md."
151
+ : " Run with --fix to auto-correct the safe rules, or see post-migration-cleanup.md.";
152
+ console.log(gray(hint));
153
+ }
154
+ }
155
+
156
+ function printJson(report: AuditReport): void {
157
+ console.log(JSON.stringify(report, null, 2));
158
+ }
159
+
160
+ function shouldFail(report: AuditReport, strict: boolean): boolean {
161
+ if (!strict) return false;
162
+ return report.rules.some((r) => r.findings.some((f) => f.severity === "warning"));
163
+ }
164
+
165
+ async function main() {
166
+ const opts = parseArgs(process.argv.slice(2));
167
+ if (opts.help) {
168
+ showHelp();
169
+ process.exit(0);
170
+ }
171
+
172
+ const siteDir = path.resolve(opts.source);
173
+ const report = runAudit(siteDir, realFsAdapter, {
174
+ writer: opts.fix ? realFsWriter : undefined,
175
+ });
176
+
177
+ if (opts.json) {
178
+ printJson(report);
179
+ } else {
180
+ printText(report, opts.fix);
181
+ }
182
+
183
+ if (shouldFail(report, opts.strict)) {
184
+ process.exit(2);
185
+ }
186
+ }
187
+
188
+ main().catch((err) => {
189
+ console.error(red(`Audit failed: ${(err as Error).message}`));
190
+ process.exit(1);
191
+ });