@fragments-sdk/cli 0.14.2 → 0.15.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 (135) hide show
  1. package/README.md +0 -3
  2. package/dist/bin.js +4290 -3754
  3. package/dist/bin.js.map +1 -1
  4. package/dist/{chunk-TXFCEDOC.js → chunk-2WXKALIG.js} +2 -2
  5. package/dist/{chunk-I34BC3CU.js → chunk-32LIWN2P.js} +1006 -3
  6. package/dist/chunk-32LIWN2P.js.map +1 -0
  7. package/dist/{chunk-55KERLWL.js → chunk-65WSVDV5.js} +314 -89
  8. package/dist/chunk-65WSVDV5.js.map +1 -0
  9. package/dist/chunk-7DZC4YEV.js +294 -0
  10. package/dist/chunk-7DZC4YEV.js.map +1 -0
  11. package/dist/{chunk-LOYS64QS.js → chunk-7WHVW72L.js} +230 -19
  12. package/dist/chunk-7WHVW72L.js.map +1 -0
  13. package/dist/{chunk-PJT5IZ37.js → chunk-BJE3425I.js} +19 -52
  14. package/dist/{chunk-PJT5IZ37.js.map → chunk-BJE3425I.js.map} +1 -1
  15. package/dist/{chunk-5A6X2Y73.js → chunk-CZD3AD4Q.js} +12 -11
  16. package/dist/chunk-CZD3AD4Q.js.map +1 -0
  17. package/dist/{chunk-EYXVAMEX.js → chunk-MN3TJ3D5.js} +72 -3
  18. package/dist/chunk-MN3TJ3D5.js.map +1 -0
  19. package/dist/chunk-QCN35LJU.js +630 -0
  20. package/dist/chunk-QCN35LJU.js.map +1 -0
  21. package/dist/chunk-T47OLCSF.js +36 -0
  22. package/dist/chunk-T47OLCSF.js.map +1 -0
  23. package/dist/{chunk-APTQIBS5.js → chunk-XJQ5BIWI.js} +144 -1049
  24. package/dist/chunk-XJQ5BIWI.js.map +1 -0
  25. package/dist/codebase-scanner-VOTPXRYW.js +22 -0
  26. package/dist/converter-JLINP7CJ.js +34 -0
  27. package/dist/converter-JLINP7CJ.js.map +1 -0
  28. package/dist/core/index.js +43 -1
  29. package/dist/{generate-RYWIPDN2.js → generate-A4FP5426.js} +3 -4
  30. package/dist/{generate-RYWIPDN2.js.map → generate-A4FP5426.js.map} +1 -1
  31. package/dist/govern-scan-UCBZR6D6.js +280 -0
  32. package/dist/govern-scan-UCBZR6D6.js.map +1 -0
  33. package/dist/index.d.ts +2 -1
  34. package/dist/index.js +11 -11
  35. package/dist/{init-WRUSW7R5.js → init-HGSM35XA.js} +131 -128
  36. package/dist/init-HGSM35XA.js.map +1 -0
  37. package/dist/{init-cloud-REQ3XLHO.js → init-cloud-MQ6GRJAZ.js} +2 -2
  38. package/dist/mcp-bin.js +5 -36
  39. package/dist/mcp-bin.js.map +1 -1
  40. package/dist/scan-VNNKACG2.js +15 -0
  41. package/dist/{scan-generate-TFZVL3BT.js → scan-generate-TWRHNU5M.js} +335 -46
  42. package/dist/scan-generate-TWRHNU5M.js.map +1 -0
  43. package/dist/scanner-7LAZYPWZ.js +13 -0
  44. package/dist/{service-HKJ6B7P7.js → service-FHQU7YS7.js} +27 -23
  45. package/dist/{snapshot-C5DYIGIV.js → snapshot-KQEQ6XHL.js} +2 -2
  46. package/dist/{static-viewer-DUVC4UIM.js → static-viewer-63PG6FWY.js} +3 -3
  47. package/dist/static-viewer-63PG6FWY.js.map +1 -0
  48. package/dist/{test-JW7JIDFG.js → test-UQYUCZIS.js} +4 -6
  49. package/dist/{test-JW7JIDFG.js.map → test-UQYUCZIS.js.map} +1 -1
  50. package/dist/{tokens-KE73G5JC.js → tokens-6GYKDV6U.js} +6 -5
  51. package/dist/{tokens-KE73G5JC.js.map → tokens-6GYKDV6U.js.map} +1 -1
  52. package/dist/tokens-generate-VTZV5EEW.js +86 -0
  53. package/dist/tokens-generate-VTZV5EEW.js.map +1 -0
  54. package/package.json +6 -6
  55. package/src/bin.ts +210 -48
  56. package/src/build.ts +130 -6
  57. package/src/commands/__fixtures__/shadcn-label-wrapper/package.json +7 -0
  58. package/src/commands/__fixtures__/shadcn-label-wrapper/src/components/ui/label.contract.json +42 -0
  59. package/src/commands/__fixtures__/shadcn-label-wrapper/src/components/ui/label.tsx +11 -0
  60. package/src/commands/__fixtures__/shadcn-label-wrapper/src/components/ui/primitive.contract.json +20 -0
  61. package/src/commands/__fixtures__/shadcn-label-wrapper/src/components/ui/primitive.tsx +14 -0
  62. package/src/commands/__fixtures__/shadcn-label-wrapper/tsconfig.app.json +23 -0
  63. package/src/commands/__tests__/init.test.ts +113 -0
  64. package/src/commands/__tests__/scan-generate.test.ts +188 -69
  65. package/src/commands/__tests__/verify.test.ts +91 -0
  66. package/src/commands/discover.ts +151 -0
  67. package/src/commands/enhance.ts +3 -1
  68. package/src/commands/govern-scan.ts +386 -0
  69. package/src/commands/govern.ts +2 -2
  70. package/src/commands/init.ts +152 -28
  71. package/src/commands/inspect.ts +290 -0
  72. package/src/commands/migrate-contract.ts +85 -0
  73. package/src/commands/scan-generate.ts +438 -50
  74. package/src/commands/scan.ts +1 -0
  75. package/src/commands/setup.ts +27 -50
  76. package/src/commands/tokens-generate.ts +113 -0
  77. package/src/commands/verify.ts +195 -1
  78. package/src/core/__fixtures__/shadcn-input/input.tsx +7 -0
  79. package/src/core/__fixtures__/shadcn-input/tsconfig.json +14 -0
  80. package/src/core/__fixtures__/shadcn-label/label.tsx +11 -0
  81. package/src/core/__fixtures__/shadcn-label/primitive.tsx +14 -0
  82. package/src/core/__fixtures__/shadcn-label/tsconfig.json +14 -0
  83. package/src/core/__fixtures__/shadcn-radix-label/label.tsx +11 -0
  84. package/src/core/__fixtures__/shadcn-radix-label/node_modules/radix-ui/index.d.ts +12 -0
  85. package/src/core/__fixtures__/shadcn-radix-label/tsconfig.json +14 -0
  86. package/src/core/__tests__/contract-parity.test.ts +316 -0
  87. package/src/core/component-extractor.test.ts +39 -0
  88. package/src/core/component-extractor.ts +92 -1
  89. package/src/core/config.ts +2 -1
  90. package/src/core/discovery.ts +13 -2
  91. package/src/core/drift-verifier.ts +123 -0
  92. package/src/core/extractor-adapter.ts +80 -0
  93. package/src/mcp/__tests__/projectFields.test.ts +1 -1
  94. package/src/mcp/utils.ts +1 -50
  95. package/src/migrate/converter.ts +3 -3
  96. package/src/migrate/fragment-to-contract.ts +253 -0
  97. package/src/migrate/report.ts +1 -1
  98. package/src/scripts/token-benchmark.ts +121 -0
  99. package/src/service/__tests__/props-extractor.test.ts +94 -0
  100. package/src/service/__tests__/token-normalizer.test.ts +690 -0
  101. package/src/service/ast-utils.ts +4 -23
  102. package/src/service/babel-config.ts +23 -0
  103. package/src/service/enhance/converter.ts +61 -0
  104. package/src/service/enhance/props-extractor.ts +25 -8
  105. package/src/service/enhance/scanner.ts +5 -24
  106. package/src/service/snippet-validation.ts +9 -3
  107. package/src/service/token-normalizer.ts +510 -0
  108. package/src/shared/index.ts +1 -0
  109. package/src/shared/project-fields.ts +46 -0
  110. package/src/viewer/__tests__/viewer-integration.test.ts +8 -8
  111. package/src/viewer/preview-adapter.ts +116 -0
  112. package/src/viewer/style-utils.ts +27 -412
  113. package/src/viewer/vite-plugin.ts +2 -2
  114. package/dist/chunk-55KERLWL.js.map +0 -1
  115. package/dist/chunk-5A6X2Y73.js.map +0 -1
  116. package/dist/chunk-APTQIBS5.js.map +0 -1
  117. package/dist/chunk-EYXVAMEX.js.map +0 -1
  118. package/dist/chunk-I34BC3CU.js.map +0 -1
  119. package/dist/chunk-LOYS64QS.js.map +0 -1
  120. package/dist/chunk-ZKTFKHWN.js +0 -324
  121. package/dist/chunk-ZKTFKHWN.js.map +0 -1
  122. package/dist/discovery-VDANZAJ2.js +0 -28
  123. package/dist/init-WRUSW7R5.js.map +0 -1
  124. package/dist/scan-YJHQIRKG.js +0 -14
  125. package/dist/scan-generate-TFZVL3BT.js.map +0 -1
  126. package/dist/viewer-2TZS3NDL.js +0 -2730
  127. package/dist/viewer-2TZS3NDL.js.map +0 -1
  128. package/src/commands/dev.ts +0 -107
  129. /package/dist/{chunk-TXFCEDOC.js.map → chunk-2WXKALIG.js.map} +0 -0
  130. /package/dist/{discovery-VDANZAJ2.js.map → codebase-scanner-VOTPXRYW.js.map} +0 -0
  131. /package/dist/{init-cloud-REQ3XLHO.js.map → init-cloud-MQ6GRJAZ.js.map} +0 -0
  132. /package/dist/{scan-YJHQIRKG.js.map → scan-VNNKACG2.js.map} +0 -0
  133. /package/dist/{service-HKJ6B7P7.js.map → scanner-7LAZYPWZ.js.map} +0 -0
  134. /package/dist/{static-viewer-DUVC4UIM.js.map → service-FHQU7YS7.js.map} +0 -0
  135. /package/dist/{snapshot-C5DYIGIV.js.map → snapshot-KQEQ6XHL.js.map} +0 -0
@@ -0,0 +1,290 @@
1
+ /**
2
+ * `fragments inspect` — inspect a single component from fragments.json.
3
+ *
4
+ * Loads the compiled output, finds the named component, and prints
5
+ * a human-readable summary or JSON depending on flags. Supports field
6
+ * filtering (dot notation), variant filtering, and verbosity levels.
7
+ */
8
+
9
+ import pc from 'picocolors';
10
+ import { readFile } from 'node:fs/promises';
11
+ import { resolve } from 'node:path';
12
+ import type { CompiledFragmentsFile } from '../core/index.js';
13
+ import { BRAND } from '../core/index.js';
14
+ import { loadConfig } from '../core/node.js';
15
+ import { projectFields } from '../shared/project-fields.js';
16
+
17
+ // ---------------------------------------------------------------------------
18
+ // Types
19
+ // ---------------------------------------------------------------------------
20
+
21
+ export interface InspectCommandOptions {
22
+ config?: string;
23
+ fields?: string;
24
+ variant?: string;
25
+ verbosity?: 'compact' | 'standard' | 'full';
26
+ maxExamples?: number;
27
+ json?: boolean;
28
+ }
29
+
30
+ // ---------------------------------------------------------------------------
31
+ // Helpers
32
+ // ---------------------------------------------------------------------------
33
+
34
+ /**
35
+ * Simple Levenshtein distance for "did you mean?" suggestions.
36
+ */
37
+ function levenshtein(a: string, b: string): number {
38
+ const m = a.length;
39
+ const n = b.length;
40
+ const dp: number[][] = Array.from({ length: m + 1 }, () => Array(n + 1).fill(0) as number[]);
41
+ for (let i = 0; i <= m; i++) dp[i][0] = i;
42
+ for (let j = 0; j <= n; j++) dp[0][j] = j;
43
+ for (let i = 1; i <= m; i++) {
44
+ for (let j = 1; j <= n; j++) {
45
+ dp[i][j] = a[i - 1] === b[j - 1]
46
+ ? dp[i - 1][j - 1]
47
+ : 1 + Math.min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]);
48
+ }
49
+ }
50
+ return dp[m][n];
51
+ }
52
+
53
+ function findClosestMatch(input: string, candidates: string[], maxDistance = 3): string | null {
54
+ const inputLower = input.toLowerCase();
55
+ let bestMatch: string | null = null;
56
+ let bestDist = maxDistance + 1;
57
+
58
+ for (const candidate of candidates) {
59
+ const dist = levenshtein(inputLower, candidate.toLowerCase());
60
+ if (dist < bestDist) {
61
+ bestDist = dist;
62
+ bestMatch = candidate;
63
+ } else if (dist === bestDist && bestMatch) {
64
+ if (Math.abs(candidate.length - input.length) < Math.abs(bestMatch.length - input.length)) {
65
+ bestMatch = candidate;
66
+ }
67
+ }
68
+ }
69
+
70
+ return bestDist <= maxDistance ? bestMatch : null;
71
+ }
72
+
73
+ // ---------------------------------------------------------------------------
74
+ // Command implementation
75
+ // ---------------------------------------------------------------------------
76
+
77
+ export async function inspect(
78
+ component: string,
79
+ options: InspectCommandOptions,
80
+ ): Promise<void> {
81
+ const { config, configDir } = await loadConfig(options.config);
82
+ const outputPath = resolve(configDir, config.outFile ?? BRAND.outFile);
83
+
84
+ let data: CompiledFragmentsFile;
85
+ try {
86
+ const content = await readFile(outputPath, 'utf-8');
87
+ data = JSON.parse(content) as CompiledFragmentsFile;
88
+ } catch {
89
+ console.error(
90
+ pc.red(`Error: Could not load ${BRAND.outFile}. Run \`${BRAND.cliCommand} build\` first.`),
91
+ );
92
+ process.exit(1);
93
+ }
94
+
95
+ // Find fragment (case-insensitive)
96
+ const fragment = Object.values(data.fragments).find(
97
+ (s) => s.meta.name.toLowerCase() === component.toLowerCase(),
98
+ );
99
+
100
+ if (!fragment) {
101
+ const allNames = Object.values(data.fragments).map((s) => s.meta.name);
102
+ const closest = findClosestMatch(component, allNames);
103
+ const suggestion = closest
104
+ ? ` Did you mean "${closest}"?`
105
+ : '';
106
+ console.error(
107
+ pc.red(`Error: Component "${component}" not found.${suggestion}`),
108
+ );
109
+ console.error(
110
+ pc.dim(`Use \`${BRAND.cliCommand} discover\` to see available components.`),
111
+ );
112
+ process.exit(1);
113
+ }
114
+
115
+ const verbosity = options.verbosity ?? 'standard';
116
+
117
+ // --- Variant filtering ---
118
+ let variants = fragment.variants;
119
+ if (options.variant) {
120
+ const query = options.variant.toLowerCase();
121
+ let filtered = variants.filter((v) => v.name.toLowerCase() === query);
122
+ if (filtered.length === 0) {
123
+ filtered = variants.filter((v) => v.name.toLowerCase().startsWith(query));
124
+ }
125
+ if (filtered.length === 0) {
126
+ filtered = variants.filter((v) => v.name.toLowerCase().includes(query));
127
+ }
128
+ if (filtered.length > 0) {
129
+ variants = filtered;
130
+ } else {
131
+ console.error(
132
+ pc.red(
133
+ `Error: Variant "${options.variant}" not found for ${component}. ` +
134
+ `Available: ${fragment.variants.map((v) => v.name).join(', ')}`,
135
+ ),
136
+ );
137
+ process.exit(1);
138
+ }
139
+ }
140
+ if (options.maxExamples && options.maxExamples > 0) {
141
+ variants = variants.slice(0, options.maxExamples);
142
+ }
143
+
144
+ // --- Build full result ---
145
+ const propsReference = Object.entries(fragment.props ?? {}).map(([propName, prop]) => ({
146
+ name: propName,
147
+ type: prop.type,
148
+ required: prop.required,
149
+ default: prop.default,
150
+ description: prop.description,
151
+ }));
152
+
153
+ const propConstraints = Object.entries(fragment.props ?? {})
154
+ .filter(([, prop]) => prop.constraints && prop.constraints.length > 0)
155
+ .map(([pName, prop]) => ({
156
+ prop: pName,
157
+ constraints: prop.constraints,
158
+ }));
159
+
160
+ const examples = variants.map((variant) => ({
161
+ variant: variant.name,
162
+ description: variant.description,
163
+ code: variant.code ?? `<${fragment.meta.name} />`,
164
+ }));
165
+
166
+ const fullResult = {
167
+ meta: fragment.meta,
168
+ props: fragment.props,
169
+ variants: fragment.variants,
170
+ relations: fragment.relations,
171
+ contract: fragment.contract,
172
+ generated: fragment._generated,
173
+ guidelines: {
174
+ when: fragment.usage?.when ?? [],
175
+ whenNot: fragment.usage?.whenNot ?? [],
176
+ guidelines: fragment.usage?.guidelines ?? [],
177
+ accessibility: fragment.usage?.accessibility ?? [],
178
+ propConstraints,
179
+ alternatives: fragment.relations
180
+ ?.filter((r) => r.relationship === 'alternative')
181
+ .map((r) => ({ component: r.component, note: r.note })) ?? [],
182
+ },
183
+ examples: {
184
+ import: `import { ${fragment.meta.name} } from '${data.packageName ?? BRAND.nameLower}';`,
185
+ code: examples,
186
+ propsReference,
187
+ },
188
+ };
189
+
190
+ // --- Apply verbosity + field filtering ---
191
+ const fieldsArray = options.fields
192
+ ? options.fields.split(',').map((f) => f.trim())
193
+ : undefined;
194
+
195
+ // Alias legacy field paths
196
+ const aliasMap: Record<string, string> = { usage: 'guidelines' };
197
+ const resolvedFields = fieldsArray?.map((f) => {
198
+ const parts = f.split('.');
199
+ if (aliasMap[parts[0]]) parts[0] = aliasMap[parts[0]];
200
+ return parts.join('.');
201
+ });
202
+
203
+ let result: unknown;
204
+ if (verbosity === 'compact' && !resolvedFields?.length) {
205
+ result = {
206
+ meta: fullResult.meta,
207
+ propNames: Object.keys(fragment.props ?? {}),
208
+ variantNames: fragment.variants.map((v) => v.name),
209
+ };
210
+ } else {
211
+ result = resolvedFields && resolvedFields.length > 0
212
+ ? projectFields(fullResult as unknown as Record<string, unknown>, resolvedFields)
213
+ : fullResult;
214
+ }
215
+
216
+ // --- Output ---
217
+ if (options.json) {
218
+ console.log(JSON.stringify(result, null, 2));
219
+ return;
220
+ }
221
+
222
+ // Human-readable output
223
+ const meta = fragment.meta;
224
+ console.log(pc.bold(`\n${meta.name}\n`));
225
+ console.log(` ${pc.cyan('Category:')} ${meta.category}`);
226
+ console.log(` ${pc.cyan('Status:')} ${meta.status ?? 'stable'}`);
227
+ if (meta.description) {
228
+ console.log(` ${pc.cyan('Description:')} ${meta.description}`);
229
+ }
230
+ if (meta.tags && meta.tags.length > 0) {
231
+ console.log(` ${pc.cyan('Tags:')} ${meta.tags.join(', ')}`);
232
+ }
233
+
234
+ // Props table
235
+ const propEntries = Object.entries(fragment.props ?? {});
236
+ if (propEntries.length > 0) {
237
+ console.log(pc.bold('\n Props\n'));
238
+ console.log(
239
+ pc.dim(
240
+ ` ${'Name'.padEnd(24)} ${'Type'.padEnd(20)} ${'Required'.padEnd(10)} ${'Default'}`,
241
+ ),
242
+ );
243
+ console.log(pc.dim(` ${'─'.repeat(70)}`));
244
+
245
+ const displayProps = verbosity === 'compact' ? propEntries.slice(0, 5) : propEntries;
246
+ for (const [name, prop] of displayProps) {
247
+ const propName = name.length > 22 ? name.slice(0, 19) + '...' : name;
248
+ const propType = (prop.type ?? '').length > 18
249
+ ? (prop.type ?? '').slice(0, 15) + '...'
250
+ : (prop.type ?? '');
251
+ console.log(
252
+ ` ${pc.cyan(propName.padEnd(24))} ${propType.padEnd(20)} ${(prop.required ? 'yes' : 'no').padEnd(10)} ${pc.dim(String(prop.default ?? '-'))}`,
253
+ );
254
+ }
255
+ if (verbosity === 'compact' && propEntries.length > 5) {
256
+ console.log(pc.dim(` ... and ${propEntries.length - 5} more`));
257
+ }
258
+ }
259
+
260
+ // Variants list
261
+ if (variants.length > 0) {
262
+ console.log(pc.bold('\n Variants\n'));
263
+ for (const v of variants) {
264
+ console.log(` ${pc.yellow(v.name)}${v.description ? pc.dim(` — ${v.description}`) : ''}`);
265
+ }
266
+ }
267
+
268
+ // Usage guidelines (standard/full only)
269
+ if (verbosity !== 'compact') {
270
+ const when = fragment.usage?.when ?? [];
271
+ const whenNot = fragment.usage?.whenNot ?? [];
272
+ if (when.length > 0 || whenNot.length > 0) {
273
+ console.log(pc.bold('\n Usage Guidelines\n'));
274
+ if (when.length > 0) {
275
+ console.log(` ${pc.green('When to use:')}`);
276
+ for (const w of when) {
277
+ console.log(` ${pc.dim('•')} ${w}`);
278
+ }
279
+ }
280
+ if (whenNot.length > 0) {
281
+ console.log(` ${pc.red('When NOT to use:')}`);
282
+ for (const w of whenNot) {
283
+ console.log(` ${pc.dim('•')} ${w}`);
284
+ }
285
+ }
286
+ }
287
+ }
288
+
289
+ console.log();
290
+ }
@@ -0,0 +1,85 @@
1
+ /**
2
+ * CLI command: fragments migrate-contract
3
+ *
4
+ * Converts .fragment.tsx files to .contract.json format.
5
+ */
6
+
7
+ import pc from 'picocolors';
8
+ import fg from 'fast-glob';
9
+ import { resolve } from 'node:path';
10
+ import { loadConfig } from '../core/node.js';
11
+ import { migrateFragmentToContract } from '../migrate/fragment-to-contract.js';
12
+
13
+ export interface MigrateContractOptions {
14
+ config?: string;
15
+ glob?: string;
16
+ dryRun?: boolean;
17
+ tsconfig?: string;
18
+ }
19
+
20
+ export async function migrateContract(options: MigrateContractOptions): Promise<{
21
+ migrated: number;
22
+ failed: number;
23
+ warnings: number;
24
+ }> {
25
+ const { config, configDir } = await loadConfig(options.config);
26
+ const pattern = options.glob ?? 'src/**/*.fragment.tsx';
27
+
28
+ console.log(pc.blue(`Migrating fragment files matching: ${pattern}`));
29
+ if (options.dryRun) {
30
+ console.log(pc.yellow('(dry run — no files will be written)'));
31
+ }
32
+
33
+ const files = await fg(pattern, { cwd: configDir, absolute: true });
34
+
35
+ if (files.length === 0) {
36
+ console.log(pc.yellow('No fragment files found matching pattern.'));
37
+ return { migrated: 0, failed: 0, warnings: 0 };
38
+ }
39
+
40
+ console.log(pc.dim(`Found ${files.length} fragment file(s)`));
41
+
42
+ let migrated = 0;
43
+ let failed = 0;
44
+ let totalWarnings = 0;
45
+
46
+ // Find tsconfig
47
+ const tsconfigPath = options.tsconfig
48
+ ? resolve(options.tsconfig)
49
+ : undefined;
50
+
51
+ for (const file of files) {
52
+ try {
53
+ const result = await migrateFragmentToContract(file, configDir, {
54
+ dryRun: options.dryRun,
55
+ tsconfigPath,
56
+ });
57
+
58
+ migrated++;
59
+ const verb = options.dryRun ? 'Would migrate' : 'Migrated';
60
+ console.log(pc.green(` ✓ ${verb}: ${result.contractPath}`));
61
+
62
+ if (result.warnings.length > 0) {
63
+ totalWarnings += result.warnings.length;
64
+ for (const w of result.warnings) {
65
+ console.log(pc.yellow(` ⚠ ${w}`));
66
+ }
67
+ }
68
+ } catch (error) {
69
+ failed++;
70
+ console.log(pc.red(` ✗ Failed: ${file}`));
71
+ console.log(pc.dim(` ${error instanceof Error ? error.message : String(error)}`));
72
+ }
73
+ }
74
+
75
+ console.log('');
76
+ console.log(pc.bold('Migration summary:'));
77
+ console.log(` ${pc.green(`${migrated} migrated`)} ${pc.red(`${failed} failed`)} ${pc.yellow(`${totalWarnings} warnings`)}`);
78
+
79
+ if (!options.dryRun && migrated > 0) {
80
+ console.log('');
81
+ console.log(pc.dim('Run `fragments build` to verify migrated contracts compile correctly.'));
82
+ }
83
+
84
+ return { migrated, failed, warnings: totalWarnings };
85
+ }