@fragments-sdk/cli 0.14.3 → 0.15.1
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.
- package/README.md +0 -3
- package/dist/{ai-client-I6MDWNYA.js → ai-client-LSLQGOMM.js} +1 -2
- package/dist/bin.js +4745 -3817
- package/dist/bin.js.map +1 -1
- package/dist/{chunk-TXFCEDOC.js → chunk-2WXKALIG.js} +2 -2
- package/dist/{chunk-I34BC3CU.js → chunk-32LIWN2P.js} +1006 -3
- package/dist/chunk-32LIWN2P.js.map +1 -0
- package/dist/chunk-5JF26E55.js +1255 -0
- package/dist/chunk-5JF26E55.js.map +1 -0
- package/dist/{chunk-APTQIBS5.js → chunk-6SQPP47U.js} +153 -1342
- package/dist/chunk-6SQPP47U.js.map +1 -0
- package/dist/chunk-7DZC4YEV.js +294 -0
- package/dist/chunk-7DZC4YEV.js.map +1 -0
- package/dist/{chunk-PJT5IZ37.js → chunk-BJE3425I.js} +19 -52
- package/dist/{chunk-PJT5IZ37.js.map → chunk-BJE3425I.js.map} +1 -1
- package/dist/{chunk-55KERLWL.js → chunk-HQ6A6DTV.js} +1587 -1073
- package/dist/chunk-HQ6A6DTV.js.map +1 -0
- package/dist/chunk-MHIBEEW4.js +511 -0
- package/dist/chunk-MHIBEEW4.js.map +1 -0
- package/dist/{chunk-5A6X2Y73.js → chunk-ONUP6Z4W.js} +25 -13
- package/dist/chunk-ONUP6Z4W.js.map +1 -0
- package/dist/chunk-QCN35LJU.js +630 -0
- package/dist/chunk-QCN35LJU.js.map +1 -0
- package/dist/chunk-T47OLCSF.js +36 -0
- package/dist/chunk-T47OLCSF.js.map +1 -0
- package/dist/codebase-scanner-MQHUZC2G.js +21 -0
- package/dist/converter-7XM3Y6NJ.js +33 -0
- package/dist/converter-7XM3Y6NJ.js.map +1 -0
- package/dist/core/index.js +43 -2
- package/dist/create-IH4R45GE.js +806 -0
- package/dist/create-IH4R45GE.js.map +1 -0
- package/dist/{generate-RYWIPDN2.js → generate-PVOLUAAC.js} +4 -6
- package/dist/{generate-RYWIPDN2.js.map → generate-PVOLUAAC.js.map} +1 -1
- package/dist/govern-scan-OYFZYOQW.js +413 -0
- package/dist/govern-scan-OYFZYOQW.js.map +1 -0
- package/dist/index.d.ts +4 -23
- package/dist/index.js +15 -14
- package/dist/index.js.map +1 -1
- package/dist/{init-WRUSW7R5.js → init-SSGUSP7Z.js} +131 -129
- package/dist/init-SSGUSP7Z.js.map +1 -0
- package/dist/{init-cloud-REQ3XLHO.js → init-cloud-3DNKPWFB.js} +30 -5
- package/dist/{init-cloud-REQ3XLHO.js.map → init-cloud-3DNKPWFB.js.map} +1 -1
- package/dist/mcp-bin.js +5 -37
- package/dist/mcp-bin.js.map +1 -1
- package/dist/node-37AUE74M.js +65 -0
- package/dist/push-contracts-WY32TFP6.js +84 -0
- package/dist/push-contracts-WY32TFP6.js.map +1 -0
- package/dist/scan-PKSYSTRR.js +15 -0
- package/dist/{scan-generate-TFZVL3BT.js → scan-generate-VY27PIOX.js} +340 -52
- package/dist/scan-generate-VY27PIOX.js.map +1 -0
- package/dist/scanner-4KZNOXAK.js +12 -0
- package/dist/{service-HKJ6B7P7.js → service-QJGWUIVL.js} +41 -30
- package/dist/{snapshot-C5DYIGIV.js → snapshot-WIJMEIFT.js} +2 -3
- package/dist/{snapshot-C5DYIGIV.js.map → snapshot-WIJMEIFT.js.map} +1 -1
- package/dist/{static-viewer-DUVC4UIM.js → static-viewer-7QIBQZRC.js} +3 -4
- package/dist/static-viewer-7QIBQZRC.js.map +1 -0
- package/dist/{test-JW7JIDFG.js → test-64Z5BKBA.js} +4 -7
- package/dist/{test-JW7JIDFG.js.map → test-64Z5BKBA.js.map} +1 -1
- package/dist/token-normalizer-TEPOVBPV.js +312 -0
- package/dist/token-normalizer-TEPOVBPV.js.map +1 -0
- package/dist/token-parser-32KOIOFN.js +22 -0
- package/dist/token-parser-32KOIOFN.js.map +1 -0
- package/dist/{tokens-KE73G5JC.js → tokens-NZWFQIAB.js} +10 -9
- package/dist/{tokens-KE73G5JC.js.map → tokens-NZWFQIAB.js.map} +1 -1
- package/dist/tokens-generate-5JQSJ27E.js +85 -0
- package/dist/tokens-generate-5JQSJ27E.js.map +1 -0
- package/dist/tokens-push-HY3KO36V.js +148 -0
- package/dist/tokens-push-HY3KO36V.js.map +1 -0
- package/package.json +8 -6
- package/src/bin.ts +300 -48
- package/src/commands/__fixtures__/shadcn-label-wrapper/package.json +7 -0
- package/src/commands/__fixtures__/shadcn-label-wrapper/src/components/ui/label.contract.json +42 -0
- package/src/commands/__fixtures__/shadcn-label-wrapper/src/components/ui/label.tsx +11 -0
- package/src/commands/__fixtures__/shadcn-label-wrapper/src/components/ui/primitive.contract.json +20 -0
- package/src/commands/__fixtures__/shadcn-label-wrapper/src/components/ui/primitive.tsx +14 -0
- package/src/commands/__fixtures__/shadcn-label-wrapper/tsconfig.app.json +23 -0
- package/src/commands/__tests__/build-freshness.test.ts +231 -0
- package/src/commands/__tests__/create.test.ts +71 -0
- package/src/commands/__tests__/drift-sync.test.ts +1 -1
- package/src/commands/__tests__/govern.test.ts +258 -0
- package/src/commands/__tests__/init.test.ts +113 -0
- package/src/commands/__tests__/scan-generate.test.ts +189 -70
- package/src/commands/__tests__/verify.test.ts +91 -0
- package/src/commands/build.ts +54 -1
- package/src/commands/context.ts +1 -1
- package/src/commands/create.ts +536 -0
- package/src/commands/discover.ts +151 -0
- package/src/commands/doctor.ts +3 -2
- package/src/commands/enhance.ts +3 -1
- package/src/commands/govern-scan.ts +565 -0
- package/src/commands/govern.ts +67 -4
- package/src/commands/init-cloud.ts +32 -4
- package/src/commands/init.ts +152 -28
- package/src/commands/inspect.ts +290 -0
- package/src/commands/migrate-contract.ts +85 -0
- package/src/commands/push-contracts.ts +112 -0
- package/src/commands/scan-generate.ts +439 -51
- package/src/commands/scan.ts +14 -0
- package/src/commands/setup.ts +27 -50
- package/src/commands/sync.ts +2 -2
- package/src/commands/tokens-generate.ts +113 -0
- package/src/commands/tokens-push.ts +199 -0
- package/src/commands/verify.ts +195 -1
- package/src/core/__fixtures__/shadcn-input/input.tsx +7 -0
- package/src/core/__fixtures__/shadcn-input/tsconfig.json +14 -0
- package/src/core/__fixtures__/shadcn-label/label.tsx +11 -0
- package/src/core/__fixtures__/shadcn-label/primitive.tsx +14 -0
- package/src/core/__fixtures__/shadcn-label/tsconfig.json +14 -0
- package/src/core/__fixtures__/shadcn-radix-label/label.tsx +11 -0
- package/src/core/__fixtures__/shadcn-radix-label/node_modules/radix-ui/index.d.ts +12 -0
- package/src/core/__fixtures__/shadcn-radix-label/tsconfig.json +14 -0
- package/src/core/__tests__/contract-parity.test.ts +316 -0
- package/src/core/__tests__/token-resolver.test.ts +1 -1
- package/src/core/component-extractor.test.ts +40 -1
- package/src/core/config.ts +2 -1
- package/src/core/discovery.ts +13 -2
- package/src/core/drift-verifier.ts +123 -0
- package/src/core/extractor-adapter.ts +80 -0
- package/src/index.ts +3 -3
- package/src/mcp/__tests__/projectFields.test.ts +1 -1
- package/src/mcp/utils.ts +1 -50
- package/src/migrate/converter.ts +3 -3
- package/src/migrate/fragment-to-contract.ts +253 -0
- package/src/migrate/report.ts +1 -1
- package/src/scripts/token-benchmark.ts +121 -0
- package/src/service/__tests__/props-extractor.test.ts +94 -0
- package/src/service/__tests__/token-normalizer.test.ts +690 -0
- package/src/service/ast-utils.ts +4 -23
- package/src/service/babel-config.ts +23 -0
- package/src/service/enhance/converter.ts +61 -0
- package/src/service/enhance/props-extractor.ts +25 -8
- package/src/service/enhance/scanner.ts +5 -24
- package/src/service/index.ts +8 -0
- package/src/service/snippet-validation.ts +9 -3
- package/src/service/tailwind-v4-parser.ts +314 -0
- package/src/service/token-normalizer.ts +510 -0
- package/src/service/token-parser.ts +56 -0
- package/src/setup.ts +10 -39
- package/src/shared/index.ts +1 -0
- package/src/shared/project-fields.ts +46 -0
- package/src/theme/__tests__/component-contrast.test.ts +2 -2
- package/src/theme/__tests__/serializer.test.ts +1 -1
- package/src/theme/generator.ts +16 -1
- package/src/theme/schema.ts +8 -0
- package/src/theme/serializer.ts +13 -9
- package/src/theme/types.ts +8 -0
- package/src/validators.ts +1 -2
- package/src/viewer/__tests__/viewer-integration.test.ts +8 -8
- package/src/viewer/style-utils.ts +27 -412
- package/src/viewer/vite-plugin.ts +2 -2
- package/dist/chunk-55KERLWL.js.map +0 -1
- package/dist/chunk-5A6X2Y73.js.map +0 -1
- package/dist/chunk-APTQIBS5.js.map +0 -1
- package/dist/chunk-EYXVAMEX.js +0 -626
- package/dist/chunk-EYXVAMEX.js.map +0 -1
- package/dist/chunk-I34BC3CU.js.map +0 -1
- package/dist/chunk-LOYS64QS.js +0 -2453
- package/dist/chunk-LOYS64QS.js.map +0 -1
- package/dist/chunk-Z7EY4VHE.js +0 -50
- package/dist/chunk-ZKTFKHWN.js +0 -324
- package/dist/chunk-ZKTFKHWN.js.map +0 -1
- package/dist/discovery-VDANZAJ2.js +0 -28
- package/dist/init-WRUSW7R5.js.map +0 -1
- package/dist/sass.node-4XJK6YBF.js +0 -130708
- package/dist/sass.node-4XJK6YBF.js.map +0 -1
- package/dist/scan-YJHQIRKG.js +0 -14
- package/dist/scan-generate-TFZVL3BT.js.map +0 -1
- package/dist/viewer-2TZS3NDL.js +0 -2730
- package/dist/viewer-2TZS3NDL.js.map +0 -1
- package/src/build.ts +0 -612
- package/src/commands/dev.ts +0 -107
- package/src/core/auto-props.ts +0 -464
- package/src/core/component-extractor.ts +0 -1030
- package/src/core/token-resolver.ts +0 -155
- /package/dist/{ai-client-I6MDWNYA.js.map → ai-client-LSLQGOMM.js.map} +0 -0
- /package/dist/{chunk-TXFCEDOC.js.map → chunk-2WXKALIG.js.map} +0 -0
- /package/dist/{chunk-Z7EY4VHE.js.map → codebase-scanner-MQHUZC2G.js.map} +0 -0
- /package/dist/{discovery-VDANZAJ2.js.map → node-37AUE74M.js.map} +0 -0
- /package/dist/{scan-YJHQIRKG.js.map → scan-PKSYSTRR.js.map} +0 -0
- /package/dist/{service-HKJ6B7P7.js.map → scanner-4KZNOXAK.js.map} +0 -0
- /package/dist/{static-viewer-DUVC4UIM.js.map → service-QJGWUIVL.js.map} +0 -0
|
@@ -327,7 +327,34 @@ export async function initCloud(options: InitCloudOptions = {}): Promise<void> {
|
|
|
327
327
|
console.log(pc.green(` ✓ Created ${BRAND.configFile}`));
|
|
328
328
|
}
|
|
329
329
|
|
|
330
|
-
// ── 6.
|
|
330
|
+
// ── 6. Push contracts if available ──────────────────────────────────
|
|
331
|
+
const fragmentsJsonPath = resolve('fragments.json');
|
|
332
|
+
if (existsSync(fragmentsJsonPath)) {
|
|
333
|
+
console.log(pc.dim('\n Found fragments.json — pushing contracts to Cloud...'));
|
|
334
|
+
try {
|
|
335
|
+
const { pushContracts } = await import('@fragments-sdk/govern');
|
|
336
|
+
const raw = readFileSync(fragmentsJsonPath, 'utf-8');
|
|
337
|
+
const parsed = JSON.parse(raw);
|
|
338
|
+
if (parsed.fragments && Array.isArray(parsed.fragments)) {
|
|
339
|
+
const result = await pushContracts({
|
|
340
|
+
contractRegistry: JSON.stringify({ fragments: parsed.fragments }),
|
|
341
|
+
apiKey: auth.apiKey,
|
|
342
|
+
url: cloudUrl,
|
|
343
|
+
});
|
|
344
|
+
if (result.ok) {
|
|
345
|
+
console.log(pc.green(` ✓ Pushed ${parsed.fragments.length} component contracts`));
|
|
346
|
+
} else {
|
|
347
|
+
console.log(pc.yellow(` ⚠ Contract push failed: ${result.error}`));
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
} catch {
|
|
351
|
+
console.log(pc.yellow(' ⚠ Could not push contracts'));
|
|
352
|
+
}
|
|
353
|
+
} else {
|
|
354
|
+
console.log(pc.dim('\n No fragments.json found — run `fragments scan` or `fragments build` to generate contracts'));
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// ── 7. Run first check ────────────────────────────────────────────
|
|
331
358
|
if (!options.skipCheck) {
|
|
332
359
|
console.log(pc.dim('\n Running first governance check...\n'));
|
|
333
360
|
try {
|
|
@@ -346,9 +373,10 @@ export async function initCloud(options: InitCloudOptions = {}): Promise<void> {
|
|
|
346
373
|
}
|
|
347
374
|
}
|
|
348
375
|
|
|
349
|
-
// ──
|
|
376
|
+
// ── 8. Done ───────────────────────────────────────────────────────
|
|
350
377
|
console.log(pc.green('\n ✓ All set!') + ' Your project is connected to Fragments Cloud.\n');
|
|
351
378
|
console.log(pc.dim(` Dashboard: ${cloudUrl}`));
|
|
352
|
-
console.log(pc.dim(' Run checks:
|
|
353
|
-
console.log(pc.dim('
|
|
379
|
+
console.log(pc.dim(' Run checks: fragments govern scan'));
|
|
380
|
+
console.log(pc.dim(' Push contracts: fragments govern push-contracts'));
|
|
381
|
+
console.log(pc.dim(' View config: fragments.config.ts\n'));
|
|
354
382
|
}
|
package/src/commands/init.ts
CHANGED
|
@@ -20,6 +20,49 @@ import {
|
|
|
20
20
|
addTranspilePackages,
|
|
21
21
|
} from "./setup.js";
|
|
22
22
|
|
|
23
|
+
/**
|
|
24
|
+
* Detect existing UI component libraries by reading package.json.
|
|
25
|
+
* Returns the library display name or null if none found.
|
|
26
|
+
*/
|
|
27
|
+
async function detectExistingUILibrary(projectRoot: string): Promise<string | null> {
|
|
28
|
+
let pkg: Record<string, unknown>;
|
|
29
|
+
try {
|
|
30
|
+
const raw = await readFile(join(projectRoot, "package.json"), "utf-8");
|
|
31
|
+
pkg = JSON.parse(raw);
|
|
32
|
+
} catch {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const allDeps: Record<string, string> = {
|
|
37
|
+
...(pkg.dependencies as Record<string, string> || {}),
|
|
38
|
+
...(pkg.devDependencies as Record<string, string> || {}),
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
// shadcn/ui: tailwindcss present + components/ui/ directory
|
|
42
|
+
if (allDeps["tailwindcss"]) {
|
|
43
|
+
const shadcnFiles = await fg(["**/components/ui/*.tsx", "**/components/ui/*.ts"], {
|
|
44
|
+
cwd: projectRoot,
|
|
45
|
+
ignore: ["**/node_modules/**"],
|
|
46
|
+
});
|
|
47
|
+
if (shadcnFiles.length > 0) {
|
|
48
|
+
return "shadcn/ui";
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (allDeps["@mui/material"]) return "Material UI";
|
|
53
|
+
if (allDeps["@chakra-ui/react"]) return "Chakra UI";
|
|
54
|
+
if (allDeps["@mantine/core"]) return "Mantine";
|
|
55
|
+
if (allDeps["antd"]) return "Ant Design";
|
|
56
|
+
|
|
57
|
+
// Radix UI: has @radix-ui/react-* packages but NOT @fragments-sdk/ui
|
|
58
|
+
const hasRadix = Object.keys(allDeps).some((dep) => dep.startsWith("@radix-ui/react-"));
|
|
59
|
+
if (hasRadix && !allDeps["@fragments-sdk/ui"]) {
|
|
60
|
+
return "Radix UI";
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
|
|
23
66
|
export interface InitOptions {
|
|
24
67
|
/** Project root directory */
|
|
25
68
|
projectRoot?: string;
|
|
@@ -43,6 +86,10 @@ export interface InitOptions {
|
|
|
43
86
|
apiKey?: string;
|
|
44
87
|
/** Override AI model for enrichment */
|
|
45
88
|
model?: string;
|
|
89
|
+
/** Generate metadata/governance files without injecting runtime UI */
|
|
90
|
+
metadataOnly?: boolean;
|
|
91
|
+
/** Alias for metadataOnly */
|
|
92
|
+
govern?: boolean;
|
|
46
93
|
}
|
|
47
94
|
|
|
48
95
|
export interface InitResult {
|
|
@@ -342,6 +389,49 @@ export default defineFragment({
|
|
|
342
389
|
`;
|
|
343
390
|
}
|
|
344
391
|
|
|
392
|
+
function generateExampleContract(): string {
|
|
393
|
+
return JSON.stringify({
|
|
394
|
+
$schema: 'https://usefragments.com/schemas/contract.v1.json',
|
|
395
|
+
name: 'Button',
|
|
396
|
+
description: 'Interactive button for triggering actions',
|
|
397
|
+
category: 'Actions',
|
|
398
|
+
status: 'stable',
|
|
399
|
+
sourcePath: 'src/components/Button.tsx',
|
|
400
|
+
exportName: 'Button',
|
|
401
|
+
propsSummary: [
|
|
402
|
+
'variant: primary|secondary|ghost (default: primary)',
|
|
403
|
+
'size: sm|md|lg (default: md)',
|
|
404
|
+
'children: node (required)',
|
|
405
|
+
],
|
|
406
|
+
props: {
|
|
407
|
+
children: { type: 'node', required: true, description: 'Button label content' },
|
|
408
|
+
variant: {
|
|
409
|
+
type: 'enum',
|
|
410
|
+
values: ['primary', 'secondary', 'ghost'],
|
|
411
|
+
default: 'primary',
|
|
412
|
+
description: 'Visual style variant',
|
|
413
|
+
},
|
|
414
|
+
size: {
|
|
415
|
+
type: 'enum',
|
|
416
|
+
values: ['sm', 'md', 'lg'],
|
|
417
|
+
default: 'md',
|
|
418
|
+
description: 'Button size',
|
|
419
|
+
},
|
|
420
|
+
},
|
|
421
|
+
usage: {
|
|
422
|
+
when: ['Triggering an action (save, submit, delete)', 'Form submission', 'Opening dialogs or menus'],
|
|
423
|
+
whenNot: ['Simple navigation (use Link)', 'Toggling state (use Switch)'],
|
|
424
|
+
guidelines: ['Use Primary for the main action in a context', 'Only one Primary button per section'],
|
|
425
|
+
},
|
|
426
|
+
examples: [
|
|
427
|
+
{ name: 'Primary', description: 'Default action button', code: '<Button variant="primary">Save Changes</Button>' },
|
|
428
|
+
{ name: 'Secondary', description: 'Less prominent action', code: '<Button variant="secondary">Cancel</Button>' },
|
|
429
|
+
{ name: 'Ghost', description: 'Minimal visual weight', code: '<Button variant="ghost">Learn More</Button>' },
|
|
430
|
+
],
|
|
431
|
+
provenance: { source: 'manual', verified: false },
|
|
432
|
+
}, null, 2) + '\n';
|
|
433
|
+
}
|
|
434
|
+
|
|
345
435
|
/**
|
|
346
436
|
* Start the dev server
|
|
347
437
|
*/
|
|
@@ -539,7 +629,7 @@ export async function init(options: InitOptions = {}): Promise<InitResult> {
|
|
|
539
629
|
const relScanPath = relative(projectRoot, scanPath);
|
|
540
630
|
const configPath = join(projectRoot, BRAND.configFile);
|
|
541
631
|
const configContent = generateConfig({
|
|
542
|
-
includePaths: [`${relScanPath}/**/*.
|
|
632
|
+
includePaths: [`${relScanPath}/**/*.contract.json`],
|
|
543
633
|
componentPaths: [`${relScanPath}/**/*.tsx`],
|
|
544
634
|
framework: "react",
|
|
545
635
|
});
|
|
@@ -642,7 +732,7 @@ export async function init(options: InitOptions = {}): Promise<InitResult> {
|
|
|
642
732
|
|
|
643
733
|
// Step 5: Create configuration file
|
|
644
734
|
const includePaths: string[] = [
|
|
645
|
-
`${componentPath}/**/*.
|
|
735
|
+
`${componentPath}/**/*.contract.json`,
|
|
646
736
|
];
|
|
647
737
|
|
|
648
738
|
if (scenario === 'stories') {
|
|
@@ -667,33 +757,62 @@ export async function init(options: InitOptions = {}): Promise<InitResult> {
|
|
|
667
757
|
}
|
|
668
758
|
|
|
669
759
|
// Step 6: Auto-inject styles + framework config
|
|
670
|
-
|
|
760
|
+
// Detect existing UI libraries — skip @fragments-sdk/ui runtime if one is found
|
|
761
|
+
const detectedUILib = await detectExistingUILibrary(projectRoot);
|
|
671
762
|
|
|
672
|
-
if
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
}
|
|
680
|
-
}
|
|
681
|
-
|
|
763
|
+
// Check if @fragments-sdk/ui is already a dependency
|
|
764
|
+
let hasFragmentsUI = false;
|
|
765
|
+
try {
|
|
766
|
+
const pkgRaw = await readFile(join(projectRoot, "package.json"), "utf-8");
|
|
767
|
+
const pkgJson = JSON.parse(pkgRaw);
|
|
768
|
+
const allDeps = {
|
|
769
|
+
...(pkgJson.dependencies || {}),
|
|
770
|
+
...(pkgJson.devDependencies || {}),
|
|
771
|
+
};
|
|
772
|
+
hasFragmentsUI = !!allDeps["@fragments-sdk/ui"];
|
|
773
|
+
} catch {
|
|
774
|
+
// no package.json — can't determine, default to injecting
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
const explicitMetadataOnly = !!options.metadataOnly || !!options.govern;
|
|
778
|
+
const skipUIRuntime = explicitMetadataOnly || (detectedUILib !== null && !hasFragmentsUI);
|
|
779
|
+
|
|
780
|
+
if (skipUIRuntime) {
|
|
781
|
+
if (explicitMetadataOnly) {
|
|
782
|
+
console.log(pc.dim(` · Metadata-only mode — skipping @fragments-sdk/ui runtime setup`));
|
|
783
|
+
} else {
|
|
784
|
+
console.log(pc.dim(` · Detected ${detectedUILib} — skipping @fragments-sdk/ui runtime setup`));
|
|
682
785
|
}
|
|
786
|
+
console.log(pc.dim(` · Run '${BRAND.cliCommand} setup' if you want to add @fragments-sdk/ui later`));
|
|
787
|
+
} else {
|
|
788
|
+
const entryFile = await findEntryFile(projectRoot, framework);
|
|
789
|
+
|
|
790
|
+
if (entryFile) {
|
|
791
|
+
try {
|
|
792
|
+
const stylesResult = await addStylesImport(projectRoot, entryFile);
|
|
793
|
+
if (stylesResult.modified) {
|
|
794
|
+
console.log(pc.green(` ✓ Added styles import to ${entryFile}`));
|
|
795
|
+
} else {
|
|
796
|
+
console.log(pc.dim(` · ${stylesResult.message}`));
|
|
797
|
+
}
|
|
798
|
+
} catch (e) {
|
|
799
|
+
errors.push(`Failed to add styles import: ${e instanceof Error ? e.message : e}`);
|
|
800
|
+
}
|
|
683
801
|
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
802
|
+
try {
|
|
803
|
+
const providerResult = await addThemeProvider(projectRoot, entryFile, framework);
|
|
804
|
+
if (providerResult.modified) {
|
|
805
|
+
console.log(pc.green(` ✓ Added ThemeProvider to ${entryFile}`));
|
|
806
|
+
} else {
|
|
807
|
+
console.log(pc.dim(` · ${providerResult.message}`));
|
|
808
|
+
}
|
|
809
|
+
} catch (e) {
|
|
810
|
+
errors.push(`Failed to add ThemeProvider: ${e instanceof Error ? e.message : e}`);
|
|
690
811
|
}
|
|
691
|
-
}
|
|
692
|
-
|
|
812
|
+
} else {
|
|
813
|
+
console.log(pc.yellow(` ! Could not detect entry file — add styles import manually`));
|
|
814
|
+
console.log(pc.dim(` import '@fragments-sdk/ui/styles'`));
|
|
693
815
|
}
|
|
694
|
-
} else {
|
|
695
|
-
console.log(pc.yellow(` ! Could not detect entry file — add styles import manually`));
|
|
696
|
-
console.log(pc.dim(` import '@fragments-sdk/ui/styles'`));
|
|
697
816
|
}
|
|
698
817
|
|
|
699
818
|
// Next.js: add transpilePackages
|
|
@@ -727,13 +846,13 @@ export async function init(options: InitOptions = {}): Promise<InitResult> {
|
|
|
727
846
|
);
|
|
728
847
|
|
|
729
848
|
await writeFile(
|
|
730
|
-
join(exampleDir, "Button.
|
|
731
|
-
|
|
849
|
+
join(exampleDir, "Button.contract.json"),
|
|
850
|
+
generateExampleContract(),
|
|
732
851
|
"utf-8"
|
|
733
852
|
);
|
|
734
853
|
console.log(
|
|
735
854
|
pc.green(
|
|
736
|
-
` ✓ Created ${relative(projectRoot, join(exampleDir, "Button.
|
|
855
|
+
` ✓ Created ${relative(projectRoot, join(exampleDir, "Button.contract.json"))}`
|
|
737
856
|
)
|
|
738
857
|
);
|
|
739
858
|
} catch (e) {
|
|
@@ -775,9 +894,14 @@ export async function init(options: InitOptions = {}): Promise<InitResult> {
|
|
|
775
894
|
|
|
776
895
|
if (startServer) {
|
|
777
896
|
startDevServer(projectRoot);
|
|
897
|
+
} else if (skipUIRuntime) {
|
|
898
|
+
console.log(` ${pc.bold("Get started:")}`);
|
|
899
|
+
console.log(` ${pc.dim("$")} ${BRAND.cliCommand} build`);
|
|
900
|
+
console.log(` ${pc.dim("$")} ${BRAND.cliCommand} verify --ci`);
|
|
901
|
+
console.log();
|
|
778
902
|
} else {
|
|
779
903
|
console.log(` ${pc.bold("Get started:")}`);
|
|
780
|
-
console.log(` ${pc.dim("$")} ${BRAND.cliCommand}
|
|
904
|
+
console.log(` ${pc.dim("$")} ${BRAND.cliCommand} build`);
|
|
781
905
|
console.log();
|
|
782
906
|
|
|
783
907
|
if (!options.configure) {
|
|
@@ -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
|
+
}
|