@fragments-sdk/cli 0.2.2
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/LICENSE +21 -0
- package/README.md +106 -0
- package/dist/bin.d.ts +1 -0
- package/dist/bin.js +4783 -0
- package/dist/bin.js.map +1 -0
- package/dist/chunk-4FDQSGKX.js +786 -0
- package/dist/chunk-4FDQSGKX.js.map +1 -0
- package/dist/chunk-7H2MMGYG.js +369 -0
- package/dist/chunk-7H2MMGYG.js.map +1 -0
- package/dist/chunk-BSCG3IP7.js +619 -0
- package/dist/chunk-BSCG3IP7.js.map +1 -0
- package/dist/chunk-LY2CFFPY.js +898 -0
- package/dist/chunk-LY2CFFPY.js.map +1 -0
- package/dist/chunk-MUZ6CM66.js +6636 -0
- package/dist/chunk-MUZ6CM66.js.map +1 -0
- package/dist/chunk-OAENNG3G.js +1489 -0
- package/dist/chunk-OAENNG3G.js.map +1 -0
- package/dist/chunk-XHNKNI6J.js +235 -0
- package/dist/chunk-XHNKNI6J.js.map +1 -0
- package/dist/core-DWKLGY4N.js +68 -0
- package/dist/core-DWKLGY4N.js.map +1 -0
- package/dist/generate-4LQNJ7SX.js +249 -0
- package/dist/generate-4LQNJ7SX.js.map +1 -0
- package/dist/index.d.ts +775 -0
- package/dist/index.js +41 -0
- package/dist/index.js.map +1 -0
- package/dist/init-EMVI47QG.js +416 -0
- package/dist/init-EMVI47QG.js.map +1 -0
- package/dist/mcp-bin.d.ts +1 -0
- package/dist/mcp-bin.js +1117 -0
- package/dist/mcp-bin.js.map +1 -0
- package/dist/scan-4YPRF7FV.js +12 -0
- package/dist/scan-4YPRF7FV.js.map +1 -0
- package/dist/service-QSZMZJBJ.js +208 -0
- package/dist/service-QSZMZJBJ.js.map +1 -0
- package/dist/static-viewer-MIPGZ4Z7.js +12 -0
- package/dist/static-viewer-MIPGZ4Z7.js.map +1 -0
- package/dist/test-SQ5ZHXWU.js +1067 -0
- package/dist/test-SQ5ZHXWU.js.map +1 -0
- package/dist/tokens-HSGMYK64.js +173 -0
- package/dist/tokens-HSGMYK64.js.map +1 -0
- package/dist/viewer-YRF4SQE4.js +11101 -0
- package/dist/viewer-YRF4SQE4.js.map +1 -0
- package/package.json +107 -0
- package/src/ai.ts +266 -0
- package/src/analyze.ts +265 -0
- package/src/bin.ts +916 -0
- package/src/build.ts +248 -0
- package/src/commands/a11y.ts +302 -0
- package/src/commands/add.ts +313 -0
- package/src/commands/audit.ts +195 -0
- package/src/commands/baseline.ts +221 -0
- package/src/commands/build.ts +144 -0
- package/src/commands/compare.ts +337 -0
- package/src/commands/context.ts +107 -0
- package/src/commands/dev.ts +107 -0
- package/src/commands/enhance.ts +858 -0
- package/src/commands/generate.ts +391 -0
- package/src/commands/init.ts +531 -0
- package/src/commands/link/figma.ts +645 -0
- package/src/commands/link/index.ts +10 -0
- package/src/commands/link/storybook.ts +267 -0
- package/src/commands/list.ts +49 -0
- package/src/commands/metrics.ts +114 -0
- package/src/commands/reset.ts +242 -0
- package/src/commands/scan.ts +537 -0
- package/src/commands/storygen.ts +207 -0
- package/src/commands/tokens.ts +251 -0
- package/src/commands/validate.ts +93 -0
- package/src/commands/verify.ts +215 -0
- package/src/core/composition.test.ts +262 -0
- package/src/core/composition.ts +255 -0
- package/src/core/config.ts +84 -0
- package/src/core/constants.ts +111 -0
- package/src/core/context.ts +380 -0
- package/src/core/defineSegment.ts +137 -0
- package/src/core/discovery.ts +337 -0
- package/src/core/figma.ts +263 -0
- package/src/core/fragment-types.ts +214 -0
- package/src/core/generators/context.ts +389 -0
- package/src/core/generators/index.ts +23 -0
- package/src/core/generators/registry.ts +364 -0
- package/src/core/generators/typescript-extractor.ts +374 -0
- package/src/core/importAnalyzer.ts +217 -0
- package/src/core/index.ts +149 -0
- package/src/core/loader.ts +155 -0
- package/src/core/node.ts +63 -0
- package/src/core/parser.ts +551 -0
- package/src/core/previewLoader.ts +172 -0
- package/src/core/schema/fragment.schema.json +189 -0
- package/src/core/schema/registry.schema.json +137 -0
- package/src/core/schema.ts +182 -0
- package/src/core/storyAdapter.test.ts +571 -0
- package/src/core/storyAdapter.ts +761 -0
- package/src/core/token-types.ts +287 -0
- package/src/core/types.ts +754 -0
- package/src/diff.ts +323 -0
- package/src/index.ts +43 -0
- package/src/mcp/__tests__/projectFields.test.ts +130 -0
- package/src/mcp/bin.ts +36 -0
- package/src/mcp/index.ts +8 -0
- package/src/mcp/server.ts +1310 -0
- package/src/mcp/utils.ts +54 -0
- package/src/mcp-bin.ts +36 -0
- package/src/migrate/__tests__/argTypes/argTypes.test.ts +189 -0
- package/src/migrate/__tests__/args/args.test.ts +452 -0
- package/src/migrate/__tests__/meta/meta.test.ts +198 -0
- package/src/migrate/__tests__/stories/stories.test.ts +278 -0
- package/src/migrate/__tests__/utils/utils.test.ts +371 -0
- package/src/migrate/__tests__/values/values.test.ts +303 -0
- package/src/migrate/bin.ts +108 -0
- package/src/migrate/converter.ts +658 -0
- package/src/migrate/detect.ts +196 -0
- package/src/migrate/index.ts +45 -0
- package/src/migrate/migrate.ts +163 -0
- package/src/migrate/parser.ts +1136 -0
- package/src/migrate/report.ts +624 -0
- package/src/migrate/types.ts +169 -0
- package/src/screenshot.ts +249 -0
- package/src/service/__tests__/ast-utils.test.ts +426 -0
- package/src/service/__tests__/enhance-scanner.test.ts +200 -0
- package/src/service/__tests__/figma/figma.test.ts +652 -0
- package/src/service/__tests__/metrics-store.test.ts +409 -0
- package/src/service/__tests__/patch-generator.test.ts +186 -0
- package/src/service/__tests__/props-extractor.test.ts +365 -0
- package/src/service/__tests__/token-registry.test.ts +267 -0
- package/src/service/analytics.ts +659 -0
- package/src/service/ast-utils.ts +444 -0
- package/src/service/browser-pool.ts +339 -0
- package/src/service/capture.ts +267 -0
- package/src/service/diff.ts +279 -0
- package/src/service/enhance/aggregator.ts +489 -0
- package/src/service/enhance/cache.ts +275 -0
- package/src/service/enhance/codebase-scanner.ts +357 -0
- package/src/service/enhance/context-generator.ts +529 -0
- package/src/service/enhance/doc-extractor.ts +523 -0
- package/src/service/enhance/index.ts +131 -0
- package/src/service/enhance/props-extractor.ts +665 -0
- package/src/service/enhance/scanner.ts +445 -0
- package/src/service/enhance/storybook-parser.ts +552 -0
- package/src/service/enhance/types.ts +346 -0
- package/src/service/enhance/variant-renderer.ts +479 -0
- package/src/service/figma.ts +1008 -0
- package/src/service/index.ts +249 -0
- package/src/service/metrics-store.ts +333 -0
- package/src/service/patch-generator.ts +349 -0
- package/src/service/report.ts +854 -0
- package/src/service/storage.ts +401 -0
- package/src/service/token-fixes.ts +281 -0
- package/src/service/token-parser.ts +504 -0
- package/src/service/token-registry.ts +721 -0
- package/src/service/utils.ts +172 -0
- package/src/setup.ts +241 -0
- package/src/shared/command-wrapper.ts +81 -0
- package/src/shared/dev-server-client.ts +199 -0
- package/src/shared/index.ts +8 -0
- package/src/shared/segment-loader.ts +59 -0
- package/src/shared/types.ts +147 -0
- package/src/static-viewer.ts +715 -0
- package/src/test/discovery.ts +172 -0
- package/src/test/index.ts +281 -0
- package/src/test/reporters/console.ts +194 -0
- package/src/test/reporters/json.ts +190 -0
- package/src/test/reporters/junit.ts +186 -0
- package/src/test/runner.ts +598 -0
- package/src/test/types.ts +245 -0
- package/src/test/watch.ts +200 -0
- package/src/validators.ts +152 -0
- package/src/viewer/__tests__/jsx-parser.test.ts +502 -0
- package/src/viewer/__tests__/render-utils.test.ts +232 -0
- package/src/viewer/__tests__/style-utils.test.ts +404 -0
- package/src/viewer/bin.ts +86 -0
- package/src/viewer/cli/health.ts +256 -0
- package/src/viewer/cli/index.ts +33 -0
- package/src/viewer/cli/scan.ts +124 -0
- package/src/viewer/cli/utils.ts +174 -0
- package/src/viewer/components/AccessibilityPanel.tsx +1404 -0
- package/src/viewer/components/ActionCapture.tsx +172 -0
- package/src/viewer/components/ActionsPanel.tsx +371 -0
- package/src/viewer/components/App.tsx +638 -0
- package/src/viewer/components/BottomPanel.tsx +224 -0
- package/src/viewer/components/CodePanel.tsx +589 -0
- package/src/viewer/components/CommandPalette.tsx +336 -0
- package/src/viewer/components/ComponentGraph.tsx +394 -0
- package/src/viewer/components/ComponentHeader.tsx +85 -0
- package/src/viewer/components/ContractPanel.tsx +234 -0
- package/src/viewer/components/ErrorBoundary.tsx +85 -0
- package/src/viewer/components/FigmaEmbed.tsx +231 -0
- package/src/viewer/components/FragmentEditor.tsx +485 -0
- package/src/viewer/components/HealthDashboard.tsx +452 -0
- package/src/viewer/components/HmrStatusIndicator.tsx +71 -0
- package/src/viewer/components/Icons.tsx +417 -0
- package/src/viewer/components/InteractionsPanel.tsx +720 -0
- package/src/viewer/components/IsolatedPreviewFrame.tsx +321 -0
- package/src/viewer/components/IsolatedRender.tsx +111 -0
- package/src/viewer/components/KeyboardShortcutsHelp.tsx +89 -0
- package/src/viewer/components/LandingPage.tsx +441 -0
- package/src/viewer/components/Layout.tsx +22 -0
- package/src/viewer/components/LeftSidebar.tsx +391 -0
- package/src/viewer/components/MultiViewportPreview.tsx +429 -0
- package/src/viewer/components/PreviewArea.tsx +404 -0
- package/src/viewer/components/PreviewFrameHost.tsx +310 -0
- package/src/viewer/components/PreviewPane.tsx +150 -0
- package/src/viewer/components/PreviewToolbar.tsx +176 -0
- package/src/viewer/components/PropsEditor.tsx +512 -0
- package/src/viewer/components/PropsTable.tsx +98 -0
- package/src/viewer/components/RelationsSection.tsx +57 -0
- package/src/viewer/components/ResizablePanel.tsx +328 -0
- package/src/viewer/components/RightSidebar.tsx +118 -0
- package/src/viewer/components/ScreenshotButton.tsx +90 -0
- package/src/viewer/components/Sidebar.tsx +169 -0
- package/src/viewer/components/SkeletonLoader.tsx +156 -0
- package/src/viewer/components/StoryRenderer.tsx +128 -0
- package/src/viewer/components/ThemeProvider.tsx +96 -0
- package/src/viewer/components/Toast.tsx +67 -0
- package/src/viewer/components/TokenStylePanel.tsx +708 -0
- package/src/viewer/components/UsageSection.tsx +95 -0
- package/src/viewer/components/VariantMatrix.tsx +350 -0
- package/src/viewer/components/VariantRenderer.tsx +131 -0
- package/src/viewer/components/VariantTabs.tsx +84 -0
- package/src/viewer/components/ViewportSelector.tsx +165 -0
- package/src/viewer/components/_future/CreatePage.tsx +836 -0
- package/src/viewer/composition-renderer.ts +381 -0
- package/src/viewer/constants/index.ts +1 -0
- package/src/viewer/constants/ui.ts +185 -0
- package/src/viewer/entry.tsx +299 -0
- package/src/viewer/hooks/index.ts +2 -0
- package/src/viewer/hooks/useA11yCache.ts +383 -0
- package/src/viewer/hooks/useA11yService.ts +498 -0
- package/src/viewer/hooks/useActions.ts +138 -0
- package/src/viewer/hooks/useAppState.ts +124 -0
- package/src/viewer/hooks/useFigmaIntegration.ts +132 -0
- package/src/viewer/hooks/useHmrStatus.ts +109 -0
- package/src/viewer/hooks/useKeyboardShortcuts.ts +222 -0
- package/src/viewer/hooks/usePreviewBridge.ts +347 -0
- package/src/viewer/hooks/useScrollSpy.ts +78 -0
- package/src/viewer/hooks/useUrlState.ts +330 -0
- package/src/viewer/hooks/useViewSettings.ts +125 -0
- package/src/viewer/index.html +28 -0
- package/src/viewer/index.ts +14 -0
- package/src/viewer/intelligence/healthReport.ts +505 -0
- package/src/viewer/intelligence/styleDrift.ts +340 -0
- package/src/viewer/intelligence/usageScanner.ts +309 -0
- package/src/viewer/jsx-parser.ts +485 -0
- package/src/viewer/postcss.config.js +6 -0
- package/src/viewer/preview-frame-entry.tsx +25 -0
- package/src/viewer/preview-frame.html +109 -0
- package/src/viewer/render-template.html +68 -0
- package/src/viewer/render-utils.ts +170 -0
- package/src/viewer/server.ts +276 -0
- package/src/viewer/style-utils.ts +414 -0
- package/src/viewer/styles/globals.css +355 -0
- package/src/viewer/tailwind.config.js +37 -0
- package/src/viewer/types/a11y.ts +197 -0
- package/src/viewer/utils/a11y-fixes.ts +471 -0
- package/src/viewer/utils/actionExport.ts +372 -0
- package/src/viewer/utils/colorSchemes.ts +201 -0
- package/src/viewer/utils/detectRelationships.ts +256 -0
- package/src/viewer/vite-plugin.ts +2143 -0
package/dist/bin.js
ADDED
|
@@ -0,0 +1,4783 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createRequire } from 'module'; const require = createRequire(import.meta.url);
|
|
3
|
+
import {
|
|
4
|
+
buildFragmentsDir,
|
|
5
|
+
buildSegments,
|
|
6
|
+
runAnalyzeCommand,
|
|
7
|
+
runDiffCommand,
|
|
8
|
+
runScreenshotCommand,
|
|
9
|
+
validateAll,
|
|
10
|
+
validateCoverage,
|
|
11
|
+
validateSchema
|
|
12
|
+
} from "./chunk-4FDQSGKX.js";
|
|
13
|
+
import {
|
|
14
|
+
scan
|
|
15
|
+
} from "./chunk-7H2MMGYG.js";
|
|
16
|
+
import {
|
|
17
|
+
FigmaClient,
|
|
18
|
+
StorageManager,
|
|
19
|
+
checkStorybookRunning,
|
|
20
|
+
createMetricsStore,
|
|
21
|
+
extractPropsFromFile,
|
|
22
|
+
generateComponentContext,
|
|
23
|
+
generatePromptContext,
|
|
24
|
+
generateSystemPrompt,
|
|
25
|
+
generateUserPrompt,
|
|
26
|
+
getScanStats,
|
|
27
|
+
parseAllStories,
|
|
28
|
+
renderAllComponentVariants,
|
|
29
|
+
scanCodebase,
|
|
30
|
+
shutdownSharedPool
|
|
31
|
+
} from "./chunk-MUZ6CM66.js";
|
|
32
|
+
import {
|
|
33
|
+
discoverSegmentFiles,
|
|
34
|
+
loadConfig,
|
|
35
|
+
loadSegmentFile
|
|
36
|
+
} from "./chunk-OAENNG3G.js";
|
|
37
|
+
import {
|
|
38
|
+
generateContext
|
|
39
|
+
} from "./chunk-LY2CFFPY.js";
|
|
40
|
+
import {
|
|
41
|
+
BRAND
|
|
42
|
+
} from "./chunk-XHNKNI6J.js";
|
|
43
|
+
|
|
44
|
+
// src/bin.ts
|
|
45
|
+
import { Command } from "commander";
|
|
46
|
+
import pc20 from "picocolors";
|
|
47
|
+
|
|
48
|
+
// src/commands/validate.ts
|
|
49
|
+
import pc from "picocolors";
|
|
50
|
+
async function validate(options = {}) {
|
|
51
|
+
const { config, configDir } = await loadConfig(options.config);
|
|
52
|
+
console.log(pc.cyan(`
|
|
53
|
+
${BRAND.name} Validator
|
|
54
|
+
`));
|
|
55
|
+
let result;
|
|
56
|
+
if (options.schema) {
|
|
57
|
+
console.log(pc.dim("Running schema validation...\n"));
|
|
58
|
+
result = await validateSchema(config, configDir);
|
|
59
|
+
} else if (options.coverage) {
|
|
60
|
+
console.log(pc.dim("Running coverage validation...\n"));
|
|
61
|
+
result = await validateCoverage(config, configDir);
|
|
62
|
+
} else {
|
|
63
|
+
console.log(pc.dim("Running all validations...\n"));
|
|
64
|
+
result = await validateAll(config, configDir);
|
|
65
|
+
}
|
|
66
|
+
if (result.errors.length > 0) {
|
|
67
|
+
console.log(pc.red(pc.bold("Errors:")));
|
|
68
|
+
for (const error of result.errors) {
|
|
69
|
+
console.log(` ${pc.red("\u2717")} ${pc.bold(error.file)}`);
|
|
70
|
+
console.log(` ${error.message}`);
|
|
71
|
+
if (error.details) {
|
|
72
|
+
console.log(pc.dim(` ${error.details}`));
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
console.log();
|
|
76
|
+
}
|
|
77
|
+
if (result.warnings.length > 0) {
|
|
78
|
+
console.log(pc.yellow(pc.bold("Warnings:")));
|
|
79
|
+
for (const warning of result.warnings) {
|
|
80
|
+
console.log(` ${pc.yellow("\u26A0")} ${pc.bold(warning.file)}`);
|
|
81
|
+
console.log(` ${warning.message}`);
|
|
82
|
+
}
|
|
83
|
+
console.log();
|
|
84
|
+
}
|
|
85
|
+
if (result.valid && result.warnings.length === 0) {
|
|
86
|
+
console.log(pc.green("\u2713 All validations passed\n"));
|
|
87
|
+
} else if (result.valid) {
|
|
88
|
+
console.log(
|
|
89
|
+
pc.yellow(`\u26A0 Passed with ${result.warnings.length} warning(s)
|
|
90
|
+
`)
|
|
91
|
+
);
|
|
92
|
+
} else {
|
|
93
|
+
console.log(pc.red(`\u2717 Failed with ${result.errors.length} error(s)
|
|
94
|
+
`));
|
|
95
|
+
}
|
|
96
|
+
return result;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// src/commands/build.ts
|
|
100
|
+
import pc2 from "picocolors";
|
|
101
|
+
async function build(options = {}) {
|
|
102
|
+
if (options.fromSource) {
|
|
103
|
+
console.log(pc2.cyan(`
|
|
104
|
+
${BRAND.name} Build (from source)
|
|
105
|
+
`));
|
|
106
|
+
console.log(pc2.dim("Using zero-config source extraction pipeline\n"));
|
|
107
|
+
const scanResult = await scan({
|
|
108
|
+
config: options.config,
|
|
109
|
+
output: options.output,
|
|
110
|
+
skipUsage: options.skipUsage,
|
|
111
|
+
skipStorybook: options.skipStorybook,
|
|
112
|
+
verbose: options.verbose
|
|
113
|
+
});
|
|
114
|
+
return {
|
|
115
|
+
success: scanResult.success,
|
|
116
|
+
segmentCount: scanResult.componentCount,
|
|
117
|
+
outputPath: scanResult.outputPath,
|
|
118
|
+
errors: scanResult.errors.map((e) => ({ file: e.component, error: e.error }))
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
const { config, configDir } = await loadConfig(options.config);
|
|
122
|
+
if (options.output) {
|
|
123
|
+
config.outFile = options.output;
|
|
124
|
+
}
|
|
125
|
+
console.log(pc2.cyan(`
|
|
126
|
+
${BRAND.name} Build
|
|
127
|
+
`));
|
|
128
|
+
const errors = [];
|
|
129
|
+
let segmentCount;
|
|
130
|
+
let outputPath;
|
|
131
|
+
let componentCount;
|
|
132
|
+
let registryPath;
|
|
133
|
+
let contextPath;
|
|
134
|
+
if (!options.registryOnly) {
|
|
135
|
+
console.log(pc2.dim("Compiling fragments...\n"));
|
|
136
|
+
const result = await buildSegments(config, configDir);
|
|
137
|
+
if (result.errors.length > 0) {
|
|
138
|
+
console.log(pc2.yellow("Build completed with errors:\n"));
|
|
139
|
+
for (const error of result.errors) {
|
|
140
|
+
console.log(` ${pc2.red("\u2717")} ${error.file}: ${error.error}`);
|
|
141
|
+
errors.push(error);
|
|
142
|
+
}
|
|
143
|
+
console.log();
|
|
144
|
+
}
|
|
145
|
+
segmentCount = result.segmentCount;
|
|
146
|
+
outputPath = result.outputPath;
|
|
147
|
+
console.log(pc2.green(`\u2713 Built ${result.segmentCount} fragment(s)`));
|
|
148
|
+
console.log(pc2.dim(` Output: ${result.outputPath}
|
|
149
|
+
`));
|
|
150
|
+
}
|
|
151
|
+
if (options.registry || options.registryOnly) {
|
|
152
|
+
console.log(pc2.dim("Generating registry and context...\n"));
|
|
153
|
+
const fragmentsResult = await buildFragmentsDir(config, configDir);
|
|
154
|
+
if (fragmentsResult.errors.length > 0) {
|
|
155
|
+
console.log(pc2.yellow("Registry build completed with errors:\n"));
|
|
156
|
+
for (const error of fragmentsResult.errors) {
|
|
157
|
+
console.log(` ${pc2.red("\u2717")} ${error.file}: ${error.error}`);
|
|
158
|
+
errors.push(error);
|
|
159
|
+
}
|
|
160
|
+
console.log();
|
|
161
|
+
}
|
|
162
|
+
componentCount = fragmentsResult.componentCount;
|
|
163
|
+
registryPath = fragmentsResult.registryPath;
|
|
164
|
+
contextPath = fragmentsResult.contextPath;
|
|
165
|
+
console.log(pc2.green(`\u2713 Generated registry with ${fragmentsResult.componentCount} component(s)`));
|
|
166
|
+
console.log(pc2.dim(` Registry: ${fragmentsResult.registryPath}`));
|
|
167
|
+
console.log(pc2.dim(` Context: ${fragmentsResult.contextPath}
|
|
168
|
+
`));
|
|
169
|
+
}
|
|
170
|
+
return {
|
|
171
|
+
success: errors.length === 0,
|
|
172
|
+
segmentCount,
|
|
173
|
+
outputPath,
|
|
174
|
+
componentCount,
|
|
175
|
+
registryPath,
|
|
176
|
+
contextPath,
|
|
177
|
+
errors
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// src/commands/context.ts
|
|
182
|
+
import { readFile } from "fs/promises";
|
|
183
|
+
import { resolve } from "path";
|
|
184
|
+
import pc3 from "picocolors";
|
|
185
|
+
async function context(options = {}) {
|
|
186
|
+
const {
|
|
187
|
+
format = "markdown",
|
|
188
|
+
compact = false,
|
|
189
|
+
includeCode = false,
|
|
190
|
+
includeRelations = false,
|
|
191
|
+
tokensOnly = false
|
|
192
|
+
} = options;
|
|
193
|
+
let segments;
|
|
194
|
+
if (options.input) {
|
|
195
|
+
const inputPath = resolve(process.cwd(), options.input);
|
|
196
|
+
const content2 = await readFile(inputPath, "utf-8");
|
|
197
|
+
const data = JSON.parse(content2);
|
|
198
|
+
segments = Object.values(data.segments);
|
|
199
|
+
} else {
|
|
200
|
+
const { config, configDir } = await loadConfig(options.config);
|
|
201
|
+
const result = await buildSegments(config, configDir);
|
|
202
|
+
if (result.errors.length > 0 && result.segmentCount === 0) {
|
|
203
|
+
console.error(pc3.red("Error: No segments found. Run `segments build` first or fix errors."));
|
|
204
|
+
return { success: false, tokenEstimate: 0 };
|
|
205
|
+
}
|
|
206
|
+
const content2 = await readFile(result.outputPath, "utf-8");
|
|
207
|
+
const data = JSON.parse(content2);
|
|
208
|
+
segments = Object.values(data.segments);
|
|
209
|
+
}
|
|
210
|
+
if (segments.length === 0) {
|
|
211
|
+
console.error(pc3.red("No segments found."));
|
|
212
|
+
return { success: false, tokenEstimate: 0 };
|
|
213
|
+
}
|
|
214
|
+
const { content, tokenEstimate } = generateContext(segments, {
|
|
215
|
+
format,
|
|
216
|
+
compact,
|
|
217
|
+
include: {
|
|
218
|
+
code: includeCode,
|
|
219
|
+
relations: includeRelations
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
if (tokensOnly) {
|
|
223
|
+
console.log(`Estimated tokens: ${tokenEstimate}`);
|
|
224
|
+
} else {
|
|
225
|
+
console.log(content);
|
|
226
|
+
console.error(pc3.dim(`
|
|
227
|
+
[${tokenEstimate} tokens estimated]`));
|
|
228
|
+
}
|
|
229
|
+
return {
|
|
230
|
+
success: true,
|
|
231
|
+
content,
|
|
232
|
+
tokenEstimate
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// src/commands/list.ts
|
|
237
|
+
import pc4 from "picocolors";
|
|
238
|
+
async function list(options = {}) {
|
|
239
|
+
const { config, configDir } = await loadConfig(options.config);
|
|
240
|
+
const files = await discoverSegmentFiles(config, configDir);
|
|
241
|
+
console.log(pc4.cyan(`
|
|
242
|
+
${BRAND.name} - Discovered Fragments
|
|
243
|
+
`));
|
|
244
|
+
if (files.length === 0) {
|
|
245
|
+
console.log(pc4.yellow("No fragment files found.\n"));
|
|
246
|
+
console.log(pc4.dim(`Looking for: ${config.include.join(", ")}`));
|
|
247
|
+
return { success: true, files: [] };
|
|
248
|
+
}
|
|
249
|
+
for (const file of files) {
|
|
250
|
+
console.log(` ${pc4.dim("\u2022")} ${file.relativePath}`);
|
|
251
|
+
}
|
|
252
|
+
console.log(pc4.dim(`
|
|
253
|
+
${files.length} fragment(s) found
|
|
254
|
+
`));
|
|
255
|
+
return { success: true, files };
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// src/commands/reset.ts
|
|
259
|
+
import { stat, rm, unlink } from "fs/promises";
|
|
260
|
+
import { join, relative } from "path";
|
|
261
|
+
import pc5 from "picocolors";
|
|
262
|
+
import fg from "fast-glob";
|
|
263
|
+
async function reset(options = {}) {
|
|
264
|
+
const { yes = false, dryRun = false } = options;
|
|
265
|
+
console.log(pc5.cyan(`
|
|
266
|
+
${BRAND.name} Reset
|
|
267
|
+
`));
|
|
268
|
+
const projectRoot = process.cwd();
|
|
269
|
+
const filesToDelete = [];
|
|
270
|
+
const dirsToDelete = [];
|
|
271
|
+
const dataDir = join(projectRoot, BRAND.dataDir);
|
|
272
|
+
try {
|
|
273
|
+
const dataDirStat = await stat(dataDir);
|
|
274
|
+
if (dataDirStat.isDirectory()) {
|
|
275
|
+
dirsToDelete.push(dataDir);
|
|
276
|
+
}
|
|
277
|
+
} catch {
|
|
278
|
+
}
|
|
279
|
+
const defaultOutFile = join(projectRoot, "segments.json");
|
|
280
|
+
try {
|
|
281
|
+
const fileStat = await stat(defaultOutFile);
|
|
282
|
+
if (fileStat.isFile()) {
|
|
283
|
+
filesToDelete.push(defaultOutFile);
|
|
284
|
+
}
|
|
285
|
+
} catch {
|
|
286
|
+
}
|
|
287
|
+
let segmentPatterns = [`**/*${BRAND.fileExtension}`];
|
|
288
|
+
try {
|
|
289
|
+
const { config } = await loadConfig();
|
|
290
|
+
if (config.outFile && config.outFile !== "segments.json") {
|
|
291
|
+
const customOutFile = join(projectRoot, config.outFile);
|
|
292
|
+
try {
|
|
293
|
+
const fileStat = await stat(customOutFile);
|
|
294
|
+
if (fileStat.isFile()) {
|
|
295
|
+
filesToDelete.push(customOutFile);
|
|
296
|
+
}
|
|
297
|
+
} catch {
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
if (config.include && config.include.length > 0) {
|
|
301
|
+
segmentPatterns = config.include;
|
|
302
|
+
}
|
|
303
|
+
} catch {
|
|
304
|
+
}
|
|
305
|
+
console.log(pc5.dim("Scanning for generated files...\n"));
|
|
306
|
+
for (const pattern of segmentPatterns) {
|
|
307
|
+
const matches = await fg(pattern, {
|
|
308
|
+
cwd: projectRoot,
|
|
309
|
+
ignore: ["**/node_modules/**"],
|
|
310
|
+
absolute: true
|
|
311
|
+
});
|
|
312
|
+
for (const match of matches) {
|
|
313
|
+
if (!filesToDelete.includes(match)) {
|
|
314
|
+
filesToDelete.push(match);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
const mdxFiles = await fg("**/Documentation.mdx", {
|
|
319
|
+
cwd: projectRoot,
|
|
320
|
+
ignore: ["**/node_modules/**"],
|
|
321
|
+
absolute: true
|
|
322
|
+
});
|
|
323
|
+
for (const mdxFile of mdxFiles) {
|
|
324
|
+
if (!filesToDelete.includes(mdxFile)) {
|
|
325
|
+
filesToDelete.push(mdxFile);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
if (filesToDelete.length === 0 && dirsToDelete.length === 0) {
|
|
329
|
+
console.log(pc5.yellow("Nothing to reset. No generated files found.\n"));
|
|
330
|
+
return { success: true, deletedFiles: 0, deletedDirs: 0 };
|
|
331
|
+
}
|
|
332
|
+
console.log(pc5.dim("The following will be deleted:\n"));
|
|
333
|
+
for (const dir of dirsToDelete) {
|
|
334
|
+
const relativePath = relative(projectRoot, dir);
|
|
335
|
+
console.log(` \u{1F4C1} ${relativePath}/`);
|
|
336
|
+
}
|
|
337
|
+
const segmentFiles = filesToDelete.filter((f) => f.endsWith(BRAND.fileExtension));
|
|
338
|
+
const mdxFilesFound = filesToDelete.filter((f) => f.endsWith(".mdx"));
|
|
339
|
+
const otherFiles = filesToDelete.filter(
|
|
340
|
+
(f) => !f.endsWith(BRAND.fileExtension) && !f.endsWith(".mdx")
|
|
341
|
+
);
|
|
342
|
+
if (segmentFiles.length > 0) {
|
|
343
|
+
console.log(` \u{1F4C4} ${segmentFiles.length} segment file(s) (*${BRAND.fileExtension})`);
|
|
344
|
+
if (segmentFiles.length <= 5) {
|
|
345
|
+
for (const f of segmentFiles) {
|
|
346
|
+
console.log(pc5.dim(` ${relative(projectRoot, f)}`));
|
|
347
|
+
}
|
|
348
|
+
} else {
|
|
349
|
+
for (const f of segmentFiles.slice(0, 3)) {
|
|
350
|
+
console.log(pc5.dim(` ${relative(projectRoot, f)}`));
|
|
351
|
+
}
|
|
352
|
+
console.log(pc5.dim(` ... and ${segmentFiles.length - 3} more`));
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
if (mdxFilesFound.length > 0) {
|
|
356
|
+
console.log(` \u{1F4C4} ${mdxFilesFound.length} documentation file(s) (*.mdx)`);
|
|
357
|
+
if (mdxFilesFound.length <= 5) {
|
|
358
|
+
for (const f of mdxFilesFound) {
|
|
359
|
+
console.log(pc5.dim(` ${relative(projectRoot, f)}`));
|
|
360
|
+
}
|
|
361
|
+
} else {
|
|
362
|
+
for (const f of mdxFilesFound.slice(0, 3)) {
|
|
363
|
+
console.log(pc5.dim(` ${relative(projectRoot, f)}`));
|
|
364
|
+
}
|
|
365
|
+
console.log(pc5.dim(` ... and ${mdxFilesFound.length - 3} more`));
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
for (const f of otherFiles) {
|
|
369
|
+
console.log(` \u{1F4C4} ${relative(projectRoot, f)}`);
|
|
370
|
+
}
|
|
371
|
+
const totalCount = filesToDelete.length + dirsToDelete.length;
|
|
372
|
+
console.log(pc5.dim(`
|
|
373
|
+
Total: ${totalCount} item(s)
|
|
374
|
+
`));
|
|
375
|
+
if (dryRun) {
|
|
376
|
+
console.log(pc5.yellow("[Dry run - no files were deleted]\n"));
|
|
377
|
+
return { success: true, deletedFiles: 0, deletedDirs: 0 };
|
|
378
|
+
}
|
|
379
|
+
let proceed = yes;
|
|
380
|
+
if (!proceed) {
|
|
381
|
+
const { confirm } = await import("@inquirer/prompts");
|
|
382
|
+
try {
|
|
383
|
+
proceed = await confirm({
|
|
384
|
+
message: `Delete ${totalCount} item(s)?`,
|
|
385
|
+
default: false
|
|
386
|
+
});
|
|
387
|
+
} catch {
|
|
388
|
+
proceed = false;
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
if (!proceed) {
|
|
392
|
+
console.log(pc5.dim("\nNo changes made.\n"));
|
|
393
|
+
return { success: true, deletedFiles: 0, deletedDirs: 0 };
|
|
394
|
+
}
|
|
395
|
+
console.log();
|
|
396
|
+
let deletedDirs = 0;
|
|
397
|
+
for (const dir of dirsToDelete) {
|
|
398
|
+
try {
|
|
399
|
+
const relativePath = relative(projectRoot, dir);
|
|
400
|
+
await rm(dir, { recursive: true, force: true });
|
|
401
|
+
console.log(` ${pc5.green("\u2713")} Deleted ${relativePath}/`);
|
|
402
|
+
deletedDirs++;
|
|
403
|
+
} catch {
|
|
404
|
+
console.log(` ${pc5.red("\u2717")} Failed: ${relative(projectRoot, dir)}`);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
let deletedCount = 0;
|
|
408
|
+
let failedCount = 0;
|
|
409
|
+
for (const file of filesToDelete) {
|
|
410
|
+
try {
|
|
411
|
+
await unlink(file);
|
|
412
|
+
deletedCount++;
|
|
413
|
+
} catch {
|
|
414
|
+
failedCount++;
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
if (deletedCount > 0) {
|
|
418
|
+
console.log(` ${pc5.green("\u2713")} Deleted ${deletedCount} file(s)`);
|
|
419
|
+
}
|
|
420
|
+
if (failedCount > 0) {
|
|
421
|
+
console.log(` ${pc5.red("\u2717")} Failed to delete ${failedCount} file(s)`);
|
|
422
|
+
}
|
|
423
|
+
console.log(pc5.green(`
|
|
424
|
+
\u2713 Reset complete
|
|
425
|
+
`));
|
|
426
|
+
console.log(pc5.dim(`Config file retained: ${BRAND.configFile}`));
|
|
427
|
+
console.log(pc5.dim(`Run ${pc5.cyan(`${BRAND.cliCommand} init`)} to start fresh
|
|
428
|
+
`));
|
|
429
|
+
return {
|
|
430
|
+
success: true,
|
|
431
|
+
deletedFiles: deletedCount,
|
|
432
|
+
deletedDirs
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// src/commands/dev.ts
|
|
437
|
+
import pc7 from "picocolors";
|
|
438
|
+
|
|
439
|
+
// src/setup.ts
|
|
440
|
+
import pc6 from "picocolors";
|
|
441
|
+
|
|
442
|
+
// src/migrate/migrate.ts
|
|
443
|
+
import { writeFile, mkdir, access, stat as stat2 } from "fs/promises";
|
|
444
|
+
import { join as join2, dirname, relative as relative2 } from "path";
|
|
445
|
+
import fg2 from "fast-glob";
|
|
446
|
+
|
|
447
|
+
// src/migrate/parser.ts
|
|
448
|
+
import { readFile as readFile2 } from "fs/promises";
|
|
449
|
+
async function parseStoryFile(filePath) {
|
|
450
|
+
const content = await readFile2(filePath, "utf-8");
|
|
451
|
+
return parseStoryContent(content, filePath);
|
|
452
|
+
}
|
|
453
|
+
function parseStoryContent(content, filePath) {
|
|
454
|
+
const warnings = [];
|
|
455
|
+
const meta = parseMeta(content, filePath, warnings);
|
|
456
|
+
const argTypes = parseArgTypes(content, warnings);
|
|
457
|
+
const constDeclarations = extractConstDeclarations(content);
|
|
458
|
+
const stories = parseStories(content, meta.componentName, warnings, constDeclarations);
|
|
459
|
+
const confidence = calculateConfidence(meta, argTypes, stories, warnings);
|
|
460
|
+
return {
|
|
461
|
+
filePath,
|
|
462
|
+
meta,
|
|
463
|
+
argTypes,
|
|
464
|
+
stories,
|
|
465
|
+
warnings,
|
|
466
|
+
confidence
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
function calculateConfidence(meta, argTypes, stories, warnings) {
|
|
470
|
+
let score = 1;
|
|
471
|
+
score -= warnings.length * 0.1;
|
|
472
|
+
if (meta.componentName === "Unknown") {
|
|
473
|
+
score -= 0.3;
|
|
474
|
+
}
|
|
475
|
+
if (!meta.componentImport) {
|
|
476
|
+
score -= 0.1;
|
|
477
|
+
}
|
|
478
|
+
if (Object.keys(argTypes).length === 0) {
|
|
479
|
+
score -= 0.1;
|
|
480
|
+
}
|
|
481
|
+
if (stories.length === 0) {
|
|
482
|
+
score -= 0.3;
|
|
483
|
+
}
|
|
484
|
+
const customRenderCount = stories.filter((s) => s.hasCustomRender).length;
|
|
485
|
+
if (customRenderCount > 0) {
|
|
486
|
+
score -= customRenderCount / Math.max(stories.length, 1) * 0.2;
|
|
487
|
+
}
|
|
488
|
+
return Math.max(0, Math.min(1, score));
|
|
489
|
+
}
|
|
490
|
+
function extractConstDeclarations(content) {
|
|
491
|
+
const declarations = /* @__PURE__ */ new Map();
|
|
492
|
+
const constPattern = /const\s+(\w+)(?:\s*:\s*[^=]+)?\s*=\s*\{/g;
|
|
493
|
+
let match;
|
|
494
|
+
while ((match = constPattern.exec(content)) !== null) {
|
|
495
|
+
const varName = match[1];
|
|
496
|
+
if (varName === "meta" || varName === "default" || varName === "Template") {
|
|
497
|
+
continue;
|
|
498
|
+
}
|
|
499
|
+
const braceStart = match.index + match[0].length - 1;
|
|
500
|
+
const braceEnd = findMatchingBraceInContent(content, braceStart, "{", "}");
|
|
501
|
+
if (braceEnd === -1) {
|
|
502
|
+
continue;
|
|
503
|
+
}
|
|
504
|
+
const objectContent = content.slice(braceStart + 1, braceEnd);
|
|
505
|
+
const parsed = parseArgsSimple(objectContent);
|
|
506
|
+
if (Object.keys(parsed).length > 0) {
|
|
507
|
+
declarations.set(varName, parsed);
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
return declarations;
|
|
511
|
+
}
|
|
512
|
+
function parseArgsSimple(content) {
|
|
513
|
+
const args = {};
|
|
514
|
+
const pairs = splitAtTopLevelCommas(content);
|
|
515
|
+
for (const pair of pairs) {
|
|
516
|
+
const trimmed = pair.trim();
|
|
517
|
+
if (!trimmed) continue;
|
|
518
|
+
if (trimmed.startsWith("...")) {
|
|
519
|
+
continue;
|
|
520
|
+
}
|
|
521
|
+
if (/^\w+$/.test(trimmed)) {
|
|
522
|
+
args[trimmed] = `__REF__${trimmed}`;
|
|
523
|
+
continue;
|
|
524
|
+
}
|
|
525
|
+
const colonIndex = trimmed.indexOf(":");
|
|
526
|
+
if (colonIndex > 0) {
|
|
527
|
+
const key = trimmed.slice(0, colonIndex).trim();
|
|
528
|
+
const valueStr = trimmed.slice(colonIndex + 1).trim();
|
|
529
|
+
if (/^\w+$/.test(key)) {
|
|
530
|
+
args[key] = parseArgValue(valueStr);
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
return args;
|
|
535
|
+
}
|
|
536
|
+
function parseMeta(content, filePath, warnings) {
|
|
537
|
+
const result = {
|
|
538
|
+
title: "",
|
|
539
|
+
componentName: ""
|
|
540
|
+
};
|
|
541
|
+
const titleRegex = /title:\s*['"`]([^'"`]+)['"`]/g;
|
|
542
|
+
const titleMatches = [];
|
|
543
|
+
let match;
|
|
544
|
+
while ((match = titleRegex.exec(content)) !== null) {
|
|
545
|
+
titleMatches.push(match[1]);
|
|
546
|
+
}
|
|
547
|
+
const componentPathTitle = titleMatches.find((t) => t.includes("/"));
|
|
548
|
+
const selectedTitle = componentPathTitle ?? titleMatches[0];
|
|
549
|
+
if (selectedTitle) {
|
|
550
|
+
result.title = selectedTitle;
|
|
551
|
+
const segments = result.title.split("/");
|
|
552
|
+
result.componentName = segments[segments.length - 1];
|
|
553
|
+
}
|
|
554
|
+
const componentMatch = content.match(/component:\s*(\w+)/);
|
|
555
|
+
if (componentMatch) {
|
|
556
|
+
if (!result.componentName) {
|
|
557
|
+
result.componentName = componentMatch[1];
|
|
558
|
+
}
|
|
559
|
+
const importMatch = content.match(
|
|
560
|
+
new RegExp(
|
|
561
|
+
`import\\s*{[^}]*\\b${componentMatch[1]}\\b[^}]*}\\s*from\\s*['"\`]([^'"\`]+)['"\`]`
|
|
562
|
+
)
|
|
563
|
+
);
|
|
564
|
+
if (importMatch) {
|
|
565
|
+
result.componentImport = importMatch[1];
|
|
566
|
+
} else {
|
|
567
|
+
const defaultImportMatch = content.match(
|
|
568
|
+
new RegExp(
|
|
569
|
+
`import\\s+${componentMatch[1]}\\s+from\\s*['"\`]([^'"\`]+)['"\`]`
|
|
570
|
+
)
|
|
571
|
+
);
|
|
572
|
+
if (defaultImportMatch) {
|
|
573
|
+
result.componentImport = defaultImportMatch[1];
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
const tagsMatch = content.match(/tags:\s*\[([^\]]+)\]/);
|
|
578
|
+
if (tagsMatch) {
|
|
579
|
+
const tagsContent = tagsMatch[1];
|
|
580
|
+
result.tags = tagsContent.split(",").map((t) => t.trim().replace(/['"`]/g, "")).filter(Boolean);
|
|
581
|
+
}
|
|
582
|
+
const descMatch = content.match(
|
|
583
|
+
/description:\s*\{[^}]*component:\s*['"`]([^'"`]+)['"`]/
|
|
584
|
+
);
|
|
585
|
+
if (descMatch) {
|
|
586
|
+
result.description = descMatch[1];
|
|
587
|
+
}
|
|
588
|
+
if (!result.componentName) {
|
|
589
|
+
const match2 = filePath.match(/([^/\\]+)\.stories\.(tsx?|jsx?|mdx)$/);
|
|
590
|
+
if (match2) {
|
|
591
|
+
result.componentName = match2[1];
|
|
592
|
+
} else {
|
|
593
|
+
result.componentName = "Unknown";
|
|
594
|
+
warnings.push("Could not determine component name");
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
if (!result.title) {
|
|
598
|
+
result.title = `Components/${result.componentName}`;
|
|
599
|
+
warnings.push(`No title found, using default: ${result.title}`);
|
|
600
|
+
}
|
|
601
|
+
return result;
|
|
602
|
+
}
|
|
603
|
+
function parseArgTypes(content, warnings) {
|
|
604
|
+
const result = {};
|
|
605
|
+
const argTypesStart = content.indexOf("argTypes:");
|
|
606
|
+
if (argTypesStart === -1) {
|
|
607
|
+
return result;
|
|
608
|
+
}
|
|
609
|
+
const braceStart = content.indexOf("{", argTypesStart);
|
|
610
|
+
if (braceStart === -1) {
|
|
611
|
+
return result;
|
|
612
|
+
}
|
|
613
|
+
let depth = 1;
|
|
614
|
+
let braceEnd = braceStart + 1;
|
|
615
|
+
while (depth > 0 && braceEnd < content.length) {
|
|
616
|
+
if (content[braceEnd] === "{") depth++;
|
|
617
|
+
if (content[braceEnd] === "}") depth--;
|
|
618
|
+
braceEnd++;
|
|
619
|
+
}
|
|
620
|
+
const argTypesContent = content.slice(braceStart + 1, braceEnd - 1);
|
|
621
|
+
let pos = 0;
|
|
622
|
+
while (pos < argTypesContent.length) {
|
|
623
|
+
while (pos < argTypesContent.length && /[\s,]/.test(argTypesContent[pos])) {
|
|
624
|
+
pos++;
|
|
625
|
+
}
|
|
626
|
+
const nameMatch = argTypesContent.slice(pos).match(/^(\w+)\s*:\s*\{/);
|
|
627
|
+
if (!nameMatch) break;
|
|
628
|
+
const propName = nameMatch[1];
|
|
629
|
+
pos += nameMatch[0].length - 1;
|
|
630
|
+
let propDepth = 1;
|
|
631
|
+
const propStart = pos + 1;
|
|
632
|
+
pos++;
|
|
633
|
+
while (propDepth > 0 && pos < argTypesContent.length) {
|
|
634
|
+
if (argTypesContent[pos] === "{") propDepth++;
|
|
635
|
+
if (argTypesContent[pos] === "}") propDepth--;
|
|
636
|
+
pos++;
|
|
637
|
+
}
|
|
638
|
+
const propContent = argTypesContent.slice(propStart, pos - 1);
|
|
639
|
+
result[propName] = parseArgTypeContent(propContent, warnings);
|
|
640
|
+
}
|
|
641
|
+
return result;
|
|
642
|
+
}
|
|
643
|
+
function parseArgTypeContent(content, warnings) {
|
|
644
|
+
const result = {};
|
|
645
|
+
const controlMatch = content.match(/control:\s*['"`](\w+)['"`]/);
|
|
646
|
+
if (controlMatch) {
|
|
647
|
+
result.control = controlMatch[1];
|
|
648
|
+
} else {
|
|
649
|
+
const controlTypeMatch = content.match(/control:\s*\{[^}]*type:\s*['"`](\w+)['"`]/);
|
|
650
|
+
if (controlTypeMatch) {
|
|
651
|
+
result.control = controlTypeMatch[1];
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
const optionsMatch = content.match(/options:\s*\[([^\]]+)\]/);
|
|
655
|
+
if (optionsMatch) {
|
|
656
|
+
result.options = optionsMatch[1].split(",").map((o) => o.trim().replace(/['"`]/g, "")).filter(Boolean);
|
|
657
|
+
}
|
|
658
|
+
const descMatch = content.match(/description:\s*['"`]([^'"`]+)['"`]/);
|
|
659
|
+
if (descMatch) {
|
|
660
|
+
result.description = descMatch[1];
|
|
661
|
+
}
|
|
662
|
+
const defaultMatch = content.match(/defaultValue:\s*\{[^}]*summary:\s*['"`]([^'"`]+)['"`]/);
|
|
663
|
+
if (defaultMatch) {
|
|
664
|
+
result.defaultValue = defaultMatch[1];
|
|
665
|
+
} else {
|
|
666
|
+
const simpleDefaultMatch = content.match(/defaultValue:\s*['"`]?([^,\s'"`]+)['"`]?/);
|
|
667
|
+
if (simpleDefaultMatch && simpleDefaultMatch[1] !== "{") {
|
|
668
|
+
result.defaultValue = simpleDefaultMatch[1];
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
return result;
|
|
672
|
+
}
|
|
673
|
+
function parseStories(content, componentName, warnings, constDeclarations) {
|
|
674
|
+
const stories = [];
|
|
675
|
+
const storyNames = /* @__PURE__ */ new Set();
|
|
676
|
+
const csf3Pattern = /export\s+const\s+(\w+)(?::\s*\w+)?\s*=\s*\{([^;]*(?:\{[^}]*\}[^;]*)*)\}/g;
|
|
677
|
+
let match;
|
|
678
|
+
while ((match = csf3Pattern.exec(content)) !== null) {
|
|
679
|
+
const storyName = match[1];
|
|
680
|
+
const storyContent = match[2];
|
|
681
|
+
if (storyName === "default" || storyName === "meta") {
|
|
682
|
+
continue;
|
|
683
|
+
}
|
|
684
|
+
if (storyContent.includes("typeof")) {
|
|
685
|
+
continue;
|
|
686
|
+
}
|
|
687
|
+
const story = parseStoryContent2(storyName, storyContent, componentName, warnings, constDeclarations);
|
|
688
|
+
if (story) {
|
|
689
|
+
stories.push(story);
|
|
690
|
+
storyNames.add(storyName);
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
const csf2Pattern = /export\s+const\s+(\w+)\s*=\s*(\w+)\.bind\s*\(\s*\{\s*\}\s*\)/g;
|
|
694
|
+
while ((match = csf2Pattern.exec(content)) !== null) {
|
|
695
|
+
const storyName = match[1];
|
|
696
|
+
const templateName = match[2];
|
|
697
|
+
if (storyNames.has(storyName)) {
|
|
698
|
+
continue;
|
|
699
|
+
}
|
|
700
|
+
if (storyName === "default" || storyName === "meta") {
|
|
701
|
+
continue;
|
|
702
|
+
}
|
|
703
|
+
const argsContent = extractArgsContent(content, storyName);
|
|
704
|
+
const story = {
|
|
705
|
+
name: storyName,
|
|
706
|
+
args: argsContent ? parseArgs(argsContent, constDeclarations) : {},
|
|
707
|
+
hasCustomRender: isCustomTemplate(content, templateName)
|
|
708
|
+
};
|
|
709
|
+
const descPattern = new RegExp(
|
|
710
|
+
`${storyName}\\.parameters\\s*=\\s*\\{[^}]*docs:\\s*\\{[^}]*description:\\s*\\{[^}]*story:\\s*['"\`]([^'"\`]+)['"\`]`
|
|
711
|
+
);
|
|
712
|
+
const descMatch = content.match(descPattern);
|
|
713
|
+
if (descMatch) {
|
|
714
|
+
story.description = descMatch[1];
|
|
715
|
+
}
|
|
716
|
+
stories.push(story);
|
|
717
|
+
storyNames.add(storyName);
|
|
718
|
+
}
|
|
719
|
+
return stories;
|
|
720
|
+
}
|
|
721
|
+
function extractArgsContent(content, storyName) {
|
|
722
|
+
const argsAssignPattern = new RegExp(`${storyName}\\.args\\s*=\\s*\\{`);
|
|
723
|
+
const argsMatch = content.match(argsAssignPattern);
|
|
724
|
+
if (!argsMatch || argsMatch.index === void 0) {
|
|
725
|
+
return null;
|
|
726
|
+
}
|
|
727
|
+
const startPos = argsMatch.index + argsMatch[0].length - 1;
|
|
728
|
+
const closingIndex = findMatchingBraceInContent(content, startPos, "{", "}");
|
|
729
|
+
if (closingIndex === -1) {
|
|
730
|
+
return null;
|
|
731
|
+
}
|
|
732
|
+
return content.slice(startPos + 1, closingIndex);
|
|
733
|
+
}
|
|
734
|
+
function findMatchingBraceInContent(content, startIndex, openChar, closeChar) {
|
|
735
|
+
let depth = 0;
|
|
736
|
+
let inString = null;
|
|
737
|
+
for (let i = startIndex; i < content.length; i++) {
|
|
738
|
+
const char = content[i];
|
|
739
|
+
const prevChar = i > 0 ? content[i - 1] : "";
|
|
740
|
+
if ((char === '"' || char === "'" || char === "`") && prevChar !== "\\") {
|
|
741
|
+
if (inString === char) {
|
|
742
|
+
inString = null;
|
|
743
|
+
} else if (inString === null) {
|
|
744
|
+
inString = char;
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
if (inString === null) {
|
|
748
|
+
if (char === openChar) {
|
|
749
|
+
depth++;
|
|
750
|
+
} else if (char === closeChar) {
|
|
751
|
+
depth--;
|
|
752
|
+
if (depth === 0) {
|
|
753
|
+
return i;
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
return -1;
|
|
759
|
+
}
|
|
760
|
+
function isCustomTemplate(content, templateName) {
|
|
761
|
+
const templatePattern = new RegExp(
|
|
762
|
+
`const\\s+${templateName}[^=]*=\\s*\\([^)]*\\)\\s*=>\\s*([\\s\\S]*?)(?=\\n(?:export|const\\s+\\w+\\s*=)|$)`,
|
|
763
|
+
"m"
|
|
764
|
+
);
|
|
765
|
+
const match = content.match(templatePattern);
|
|
766
|
+
if (!match) {
|
|
767
|
+
return false;
|
|
768
|
+
}
|
|
769
|
+
const templateBody = match[1].trim();
|
|
770
|
+
const simplePatterns = [
|
|
771
|
+
/^<\w+\s+\{\.\.\.args\}\s*\/?>/,
|
|
772
|
+
// <Comp {...args} /> or <Comp {...args}>
|
|
773
|
+
/^\(\s*<\w+\s+\{\.\.\.args\}\s*\/?>\s*\)/
|
|
774
|
+
// (<Comp {...args} />)
|
|
775
|
+
];
|
|
776
|
+
for (const pattern of simplePatterns) {
|
|
777
|
+
if (pattern.test(templateBody)) {
|
|
778
|
+
return false;
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
const customIndicators = [
|
|
782
|
+
"useState",
|
|
783
|
+
"useEffect",
|
|
784
|
+
"useRef",
|
|
785
|
+
"useCallback",
|
|
786
|
+
"useMemo",
|
|
787
|
+
"useContext",
|
|
788
|
+
"return ("
|
|
789
|
+
// Multi-line return with logic
|
|
790
|
+
];
|
|
791
|
+
for (const indicator of customIndicators) {
|
|
792
|
+
if (templateBody.includes(indicator)) {
|
|
793
|
+
return true;
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
const bodyWithoutFirstLine = templateBody.split("\n").slice(1).join("\n");
|
|
797
|
+
if (/const\s+\w+\s*=/.test(bodyWithoutFirstLine)) {
|
|
798
|
+
return true;
|
|
799
|
+
}
|
|
800
|
+
if (templateBody.includes("{...args}") && !templateBody.includes("return")) {
|
|
801
|
+
return false;
|
|
802
|
+
}
|
|
803
|
+
return false;
|
|
804
|
+
}
|
|
805
|
+
function parseStoryContent2(name, content, componentName, warnings, constDeclarations) {
|
|
806
|
+
const result = {
|
|
807
|
+
name,
|
|
808
|
+
args: {}
|
|
809
|
+
};
|
|
810
|
+
const argsMatch = content.match(/args:\s*\{([^}]*(?:\{[^}]*\}[^}]*)*)\}/);
|
|
811
|
+
if (argsMatch) {
|
|
812
|
+
result.args = parseArgs(argsMatch[1], constDeclarations);
|
|
813
|
+
}
|
|
814
|
+
if (content.includes("render:") || content.includes("render(")) {
|
|
815
|
+
result.hasCustomRender = true;
|
|
816
|
+
}
|
|
817
|
+
const descMatch = content.match(/parameters:\s*\{[^}]*docs:\s*\{[^}]*description:\s*\{[^}]*story:\s*['"`]([^'"`]+)['"`]/);
|
|
818
|
+
if (descMatch) {
|
|
819
|
+
result.description = descMatch[1];
|
|
820
|
+
}
|
|
821
|
+
return result;
|
|
822
|
+
}
|
|
823
|
+
function parseArgs(content, constDeclarations) {
|
|
824
|
+
const args = {};
|
|
825
|
+
const pairs = splitAtTopLevelCommas(content);
|
|
826
|
+
for (const pair of pairs) {
|
|
827
|
+
const trimmed = pair.trim();
|
|
828
|
+
if (!trimmed) continue;
|
|
829
|
+
if (trimmed.startsWith("...")) {
|
|
830
|
+
const spreadValue = trimmed.slice(3).trim();
|
|
831
|
+
if (constDeclarations && /^\w+$/.test(spreadValue)) {
|
|
832
|
+
const resolved = constDeclarations.get(spreadValue);
|
|
833
|
+
if (resolved) {
|
|
834
|
+
for (const [key, value] of Object.entries(resolved)) {
|
|
835
|
+
if (!(key in args)) {
|
|
836
|
+
args[key] = value;
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
continue;
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
args["__SPREAD__"] = `__REF__${spreadValue}`;
|
|
843
|
+
continue;
|
|
844
|
+
}
|
|
845
|
+
if (/^\w+$/.test(trimmed)) {
|
|
846
|
+
args[trimmed] = `__REF__${trimmed}`;
|
|
847
|
+
continue;
|
|
848
|
+
}
|
|
849
|
+
const colonIndex = trimmed.indexOf(":");
|
|
850
|
+
if (colonIndex > 0) {
|
|
851
|
+
const key = trimmed.slice(0, colonIndex).trim();
|
|
852
|
+
const valueStr = trimmed.slice(colonIndex + 1).trim();
|
|
853
|
+
if (/^\w+$/.test(key)) {
|
|
854
|
+
args[key] = parseArgValue(valueStr, constDeclarations);
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
return args;
|
|
859
|
+
}
|
|
860
|
+
function splitAtTopLevelCommas(content) {
|
|
861
|
+
const parts = [];
|
|
862
|
+
let current = "";
|
|
863
|
+
let depth = 0;
|
|
864
|
+
let inString = null;
|
|
865
|
+
for (let i = 0; i < content.length; i++) {
|
|
866
|
+
const char = content[i];
|
|
867
|
+
const prevChar = i > 0 ? content[i - 1] : "";
|
|
868
|
+
if ((char === '"' || char === "'" || char === "`") && prevChar !== "\\") {
|
|
869
|
+
if (inString === char) {
|
|
870
|
+
inString = null;
|
|
871
|
+
} else if (inString === null) {
|
|
872
|
+
inString = char;
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
if (inString === null) {
|
|
876
|
+
if (char === "{" || char === "[" || char === "(") {
|
|
877
|
+
depth++;
|
|
878
|
+
} else if (char === "}" || char === "]" || char === ")") {
|
|
879
|
+
depth--;
|
|
880
|
+
} else if (char === "," && depth === 0) {
|
|
881
|
+
parts.push(current);
|
|
882
|
+
current = "";
|
|
883
|
+
continue;
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
current += char;
|
|
887
|
+
}
|
|
888
|
+
if (current.trim()) {
|
|
889
|
+
parts.push(current);
|
|
890
|
+
}
|
|
891
|
+
return parts;
|
|
892
|
+
}
|
|
893
|
+
function resolveEscapeSequences(str) {
|
|
894
|
+
let result = "";
|
|
895
|
+
let i = 0;
|
|
896
|
+
while (i < str.length) {
|
|
897
|
+
if (str[i] === "\\" && i + 1 < str.length) {
|
|
898
|
+
const next = str[i + 1];
|
|
899
|
+
switch (next) {
|
|
900
|
+
case "n":
|
|
901
|
+
result += "\n";
|
|
902
|
+
i += 2;
|
|
903
|
+
break;
|
|
904
|
+
case "t":
|
|
905
|
+
result += " ";
|
|
906
|
+
i += 2;
|
|
907
|
+
break;
|
|
908
|
+
case "r":
|
|
909
|
+
result += "\r";
|
|
910
|
+
i += 2;
|
|
911
|
+
break;
|
|
912
|
+
case "\\":
|
|
913
|
+
result += "\\";
|
|
914
|
+
i += 2;
|
|
915
|
+
break;
|
|
916
|
+
case '"':
|
|
917
|
+
result += '"';
|
|
918
|
+
i += 2;
|
|
919
|
+
break;
|
|
920
|
+
case "'":
|
|
921
|
+
result += "'";
|
|
922
|
+
i += 2;
|
|
923
|
+
break;
|
|
924
|
+
case "`":
|
|
925
|
+
result += "`";
|
|
926
|
+
i += 2;
|
|
927
|
+
break;
|
|
928
|
+
default:
|
|
929
|
+
result += str[i];
|
|
930
|
+
i += 1;
|
|
931
|
+
}
|
|
932
|
+
} else {
|
|
933
|
+
result += str[i];
|
|
934
|
+
i += 1;
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
return result;
|
|
938
|
+
}
|
|
939
|
+
function tryParseStringConcatenation(value) {
|
|
940
|
+
if (!value.includes("+")) {
|
|
941
|
+
return null;
|
|
942
|
+
}
|
|
943
|
+
const parts = [];
|
|
944
|
+
let current = "";
|
|
945
|
+
let inString = null;
|
|
946
|
+
for (let i = 0; i < value.length; i++) {
|
|
947
|
+
const char = value[i];
|
|
948
|
+
const prevChar = i > 0 ? value[i - 1] : "";
|
|
949
|
+
if ((char === '"' || char === "'" || char === "`") && prevChar !== "\\") {
|
|
950
|
+
if (inString === char) {
|
|
951
|
+
inString = null;
|
|
952
|
+
} else if (inString === null) {
|
|
953
|
+
inString = char;
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
if (char === "+" && inString === null) {
|
|
957
|
+
const trimmed = current.trim();
|
|
958
|
+
if (trimmed) {
|
|
959
|
+
parts.push(trimmed);
|
|
960
|
+
}
|
|
961
|
+
current = "";
|
|
962
|
+
continue;
|
|
963
|
+
}
|
|
964
|
+
current += char;
|
|
965
|
+
}
|
|
966
|
+
const lastTrimmed = current.trim();
|
|
967
|
+
if (lastTrimmed) {
|
|
968
|
+
parts.push(lastTrimmed);
|
|
969
|
+
}
|
|
970
|
+
if (parts.length < 2) {
|
|
971
|
+
return null;
|
|
972
|
+
}
|
|
973
|
+
const stringParts = [];
|
|
974
|
+
for (const part of parts) {
|
|
975
|
+
if (part.startsWith('"') && part.endsWith('"') || part.startsWith("'") && part.endsWith("'")) {
|
|
976
|
+
stringParts.push(resolveEscapeSequences(part.slice(1, -1)));
|
|
977
|
+
} else if (part.startsWith("`") && part.endsWith("`")) {
|
|
978
|
+
if (!part.includes("${")) {
|
|
979
|
+
stringParts.push(resolveEscapeSequences(part.slice(1, -1)));
|
|
980
|
+
} else {
|
|
981
|
+
return null;
|
|
982
|
+
}
|
|
983
|
+
} else {
|
|
984
|
+
return null;
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
return stringParts.join("");
|
|
988
|
+
}
|
|
989
|
+
function parseArgValue(value, constDeclarations) {
|
|
990
|
+
value = value.trim();
|
|
991
|
+
value = value.replace(/,\s*$/, "");
|
|
992
|
+
if (!value) return void 0;
|
|
993
|
+
const concatenationResult = tryParseStringConcatenation(value);
|
|
994
|
+
if (concatenationResult !== null) {
|
|
995
|
+
return concatenationResult;
|
|
996
|
+
}
|
|
997
|
+
const asConstMatch = value.match(/^(['"`])(.+?)\1\s+as\s+const$/);
|
|
998
|
+
if (asConstMatch) {
|
|
999
|
+
return resolveEscapeSequences(asConstMatch[2]);
|
|
1000
|
+
}
|
|
1001
|
+
if (value.startsWith("'") && value.endsWith("'") || value.startsWith('"') && value.endsWith('"')) {
|
|
1002
|
+
return resolveEscapeSequences(value.slice(1, -1));
|
|
1003
|
+
}
|
|
1004
|
+
if (value.startsWith("`") && value.endsWith("`")) {
|
|
1005
|
+
return resolveEscapeSequences(value.slice(1, -1));
|
|
1006
|
+
}
|
|
1007
|
+
if (value === "true") return true;
|
|
1008
|
+
if (value === "false") return false;
|
|
1009
|
+
if (/^-?\d+(\.\d+)?$/.test(value)) {
|
|
1010
|
+
return parseFloat(value);
|
|
1011
|
+
}
|
|
1012
|
+
if (value === "null") return null;
|
|
1013
|
+
if (value === "undefined") return void 0;
|
|
1014
|
+
if (value.startsWith("{")) {
|
|
1015
|
+
const closingIndex = findMatchingBrace(value, 0, "{", "}");
|
|
1016
|
+
if (closingIndex !== -1) {
|
|
1017
|
+
const inner = value.slice(1, closingIndex).trim();
|
|
1018
|
+
if (inner) {
|
|
1019
|
+
return parseArgs(inner, constDeclarations);
|
|
1020
|
+
}
|
|
1021
|
+
return {};
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
if (value.startsWith("[")) {
|
|
1025
|
+
const closingIndex = findMatchingBrace(value, 0, "[", "]");
|
|
1026
|
+
if (closingIndex !== -1) {
|
|
1027
|
+
const inner = value.slice(1, closingIndex).trim();
|
|
1028
|
+
if (!inner) return [];
|
|
1029
|
+
const items = splitAtTopLevelCommas(inner);
|
|
1030
|
+
return items.map((item) => parseArgValue(item.trim(), constDeclarations)).filter((v) => v !== void 0);
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
if (value.startsWith("<")) {
|
|
1034
|
+
const trimmed = value.trimEnd();
|
|
1035
|
+
if (trimmed.endsWith("/>") || trimmed.endsWith(">")) {
|
|
1036
|
+
return `__JSX__`;
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
if (value.includes("(") || value.includes("=>")) {
|
|
1040
|
+
return `__EXPR__`;
|
|
1041
|
+
}
|
|
1042
|
+
if (/\s+as\s+[A-Z]/.test(value)) {
|
|
1043
|
+
return `__EXPR__`;
|
|
1044
|
+
}
|
|
1045
|
+
if (/^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(value)) {
|
|
1046
|
+
if (constDeclarations) {
|
|
1047
|
+
const resolved = constDeclarations.get(value);
|
|
1048
|
+
if (resolved !== void 0) {
|
|
1049
|
+
return resolved;
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
return `__REF__${value}`;
|
|
1053
|
+
}
|
|
1054
|
+
if (/^[a-zA-Z_$][a-zA-Z0-9_$]*(\??\.[a-zA-Z_$][a-zA-Z0-9_$]*)+$/.test(value)) {
|
|
1055
|
+
if (constDeclarations) {
|
|
1056
|
+
const parts = value.replace(/\?/g, "").split(".");
|
|
1057
|
+
const baseVar = parts[0];
|
|
1058
|
+
const resolved = constDeclarations.get(baseVar);
|
|
1059
|
+
if (resolved !== void 0) {
|
|
1060
|
+
let current = resolved;
|
|
1061
|
+
for (let i = 1; i < parts.length && current !== void 0; i++) {
|
|
1062
|
+
if (typeof current === "object" && current !== null) {
|
|
1063
|
+
current = current[parts[i]];
|
|
1064
|
+
} else {
|
|
1065
|
+
current = void 0;
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
if (current !== void 0) {
|
|
1069
|
+
return current;
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
return `__REF__${value}`;
|
|
1074
|
+
}
|
|
1075
|
+
if (/^[a-zA-Z_$][a-zA-Z0-9_$]*(\??\.[a-zA-Z_$][a-zA-Z0-9_$]*)*\[\d+\]!?(\??\.[a-zA-Z_$][a-zA-Z0-9_$]*)*$/.test(value)) {
|
|
1076
|
+
return `__REF__${value}`;
|
|
1077
|
+
}
|
|
1078
|
+
if (/^[a-zA-Z_$][a-zA-Z0-9_$]*!$/.test(value)) {
|
|
1079
|
+
return `__REF__${value}`;
|
|
1080
|
+
}
|
|
1081
|
+
return value;
|
|
1082
|
+
}
|
|
1083
|
+
function findMatchingBrace(content, startIndex, openChar, closeChar) {
|
|
1084
|
+
let depth = 0;
|
|
1085
|
+
let inString = null;
|
|
1086
|
+
for (let i = startIndex; i < content.length; i++) {
|
|
1087
|
+
const char = content[i];
|
|
1088
|
+
const prevChar = i > 0 ? content[i - 1] : "";
|
|
1089
|
+
if ((char === '"' || char === "'" || char === "`") && prevChar !== "\\") {
|
|
1090
|
+
if (inString === char) {
|
|
1091
|
+
inString = null;
|
|
1092
|
+
} else if (inString === null) {
|
|
1093
|
+
inString = char;
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
if (inString === null) {
|
|
1097
|
+
if (char === openChar) {
|
|
1098
|
+
depth++;
|
|
1099
|
+
} else if (char === closeChar) {
|
|
1100
|
+
depth--;
|
|
1101
|
+
if (depth === 0) {
|
|
1102
|
+
return i;
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
return -1;
|
|
1108
|
+
}
|
|
1109
|
+
function storyNameToTitle(name) {
|
|
1110
|
+
return name.replace(/([A-Z])/g, " $1").trim().replace(/\s+/g, " ");
|
|
1111
|
+
}
|
|
1112
|
+
function extractCategory(title) {
|
|
1113
|
+
const segments = title.split("/");
|
|
1114
|
+
if (segments.length >= 2) {
|
|
1115
|
+
const category = segments[segments.length - 2];
|
|
1116
|
+
return category.toLowerCase();
|
|
1117
|
+
}
|
|
1118
|
+
return "components";
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
// src/migrate/converter.ts
|
|
1122
|
+
function sanitizeComponentName(name) {
|
|
1123
|
+
return name.split(/[\s-_]+/).map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join("").replace(/[^a-zA-Z0-9]/g, "");
|
|
1124
|
+
}
|
|
1125
|
+
function convertToSegment(parsed) {
|
|
1126
|
+
const warnings = [...parsed.warnings];
|
|
1127
|
+
const todos = [];
|
|
1128
|
+
const category = extractCategory(parsed.meta.title);
|
|
1129
|
+
const componentName = sanitizeComponentName(parsed.meta.componentName);
|
|
1130
|
+
if (!parsed.meta.componentImport) {
|
|
1131
|
+
warnings.push(`No importable component found - story may define component locally`);
|
|
1132
|
+
const outputFile2 = parsed.filePath.replace(/\.stories\.(tsx?|jsx?|mdx)$/, ".segment.tsx");
|
|
1133
|
+
return {
|
|
1134
|
+
sourceFile: parsed.filePath,
|
|
1135
|
+
outputFile: outputFile2,
|
|
1136
|
+
code: "",
|
|
1137
|
+
componentName,
|
|
1138
|
+
category,
|
|
1139
|
+
variantCount: 0,
|
|
1140
|
+
propCount: 0,
|
|
1141
|
+
confidence: 0,
|
|
1142
|
+
todos: [],
|
|
1143
|
+
warnings,
|
|
1144
|
+
success: false
|
|
1145
|
+
};
|
|
1146
|
+
}
|
|
1147
|
+
const props = convertArgTypesToProps(parsed.argTypes);
|
|
1148
|
+
const variants = convertStoriesToVariants(parsed, componentName);
|
|
1149
|
+
todos.push("Add usage.when - scenarios where this component is appropriate");
|
|
1150
|
+
todos.push("Add usage.whenNot - scenarios where alternatives should be used");
|
|
1151
|
+
todos.push("Add usage.guidelines - best practices");
|
|
1152
|
+
todos.push("Add relations - related components");
|
|
1153
|
+
if (Object.keys(props).length > 0) {
|
|
1154
|
+
const propsWithoutConstraints = Object.entries(props).filter(([, p]) => !p.constraints?.length).map(([name]) => name);
|
|
1155
|
+
if (propsWithoutConstraints.length > 0) {
|
|
1156
|
+
todos.push(`Add constraints for props: ${propsWithoutConstraints.slice(0, 3).join(", ")}${propsWithoutConstraints.length > 3 ? "..." : ""}`);
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
const skippedVariants = variants.filter((v) => v.needsManualReview && v.skipReason).map((v) => ({ name: v.name, reason: v.skipReason }));
|
|
1160
|
+
const code = generateSegmentCode({
|
|
1161
|
+
componentName,
|
|
1162
|
+
componentImport: parsed.meta.componentImport,
|
|
1163
|
+
description: parsed.meta.description,
|
|
1164
|
+
category,
|
|
1165
|
+
tags: parsed.meta.tags,
|
|
1166
|
+
props,
|
|
1167
|
+
variants,
|
|
1168
|
+
todos,
|
|
1169
|
+
generated: {
|
|
1170
|
+
source: "storybook",
|
|
1171
|
+
sourceFile: parsed.filePath,
|
|
1172
|
+
confidence: parsed.confidence,
|
|
1173
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1174
|
+
skippedVariants: skippedVariants.length > 0 ? skippedVariants : void 0
|
|
1175
|
+
}
|
|
1176
|
+
});
|
|
1177
|
+
const outputFile = parsed.filePath.replace(/\.stories\.(tsx?|jsx?|mdx)$/, ".segment.tsx");
|
|
1178
|
+
return {
|
|
1179
|
+
sourceFile: parsed.filePath,
|
|
1180
|
+
outputFile,
|
|
1181
|
+
code,
|
|
1182
|
+
componentName,
|
|
1183
|
+
category,
|
|
1184
|
+
variantCount: variants.length,
|
|
1185
|
+
propCount: Object.keys(props).length,
|
|
1186
|
+
confidence: parsed.confidence,
|
|
1187
|
+
todos,
|
|
1188
|
+
warnings,
|
|
1189
|
+
success: true
|
|
1190
|
+
};
|
|
1191
|
+
}
|
|
1192
|
+
function convertArgTypesToProps(argTypes) {
|
|
1193
|
+
const props = {};
|
|
1194
|
+
for (const [name, argType] of Object.entries(argTypes)) {
|
|
1195
|
+
props[name] = convertArgType(name, argType);
|
|
1196
|
+
}
|
|
1197
|
+
return props;
|
|
1198
|
+
}
|
|
1199
|
+
function convertArgType(name, argType) {
|
|
1200
|
+
const prop = {
|
|
1201
|
+
type: inferPropType(argType)
|
|
1202
|
+
};
|
|
1203
|
+
if (argType.description) {
|
|
1204
|
+
prop.description = argType.description;
|
|
1205
|
+
}
|
|
1206
|
+
if (argType.defaultValue !== void 0) {
|
|
1207
|
+
prop.default = argType.defaultValue;
|
|
1208
|
+
}
|
|
1209
|
+
if (argType.required) {
|
|
1210
|
+
prop.required = true;
|
|
1211
|
+
}
|
|
1212
|
+
if (argType.options && argType.options.length > 0) {
|
|
1213
|
+
prop.type = "enum";
|
|
1214
|
+
prop.values = argType.options;
|
|
1215
|
+
}
|
|
1216
|
+
return prop;
|
|
1217
|
+
}
|
|
1218
|
+
function inferPropType(argType) {
|
|
1219
|
+
if (argType.type) {
|
|
1220
|
+
const typeMap = {
|
|
1221
|
+
string: "string",
|
|
1222
|
+
number: "number",
|
|
1223
|
+
boolean: "boolean",
|
|
1224
|
+
object: "object",
|
|
1225
|
+
array: "array",
|
|
1226
|
+
function: "function"
|
|
1227
|
+
};
|
|
1228
|
+
return typeMap[argType.type.toLowerCase()] ?? "custom";
|
|
1229
|
+
}
|
|
1230
|
+
if (argType.control) {
|
|
1231
|
+
const controlMap = {
|
|
1232
|
+
text: "string",
|
|
1233
|
+
number: "number",
|
|
1234
|
+
boolean: "boolean",
|
|
1235
|
+
select: "enum",
|
|
1236
|
+
radio: "enum",
|
|
1237
|
+
"inline-radio": "enum",
|
|
1238
|
+
check: "boolean",
|
|
1239
|
+
"inline-check": "boolean",
|
|
1240
|
+
range: "number",
|
|
1241
|
+
object: "object",
|
|
1242
|
+
array: "array",
|
|
1243
|
+
date: "string",
|
|
1244
|
+
color: "string"
|
|
1245
|
+
};
|
|
1246
|
+
return controlMap[argType.control] ?? "custom";
|
|
1247
|
+
}
|
|
1248
|
+
if (argType.options && argType.options.length > 0) {
|
|
1249
|
+
return "enum";
|
|
1250
|
+
}
|
|
1251
|
+
return "custom";
|
|
1252
|
+
}
|
|
1253
|
+
function convertStoriesToVariants(parsed, componentName) {
|
|
1254
|
+
return parsed.stories.map((story) => {
|
|
1255
|
+
const hasCustomRender = story.hasCustomRender === true;
|
|
1256
|
+
const unrenderableReason = getUnrenderableReason(story.args);
|
|
1257
|
+
const needsManualReview = hasCustomRender || unrenderableReason !== null;
|
|
1258
|
+
let skipReason;
|
|
1259
|
+
if (hasCustomRender) {
|
|
1260
|
+
skipReason = "uses custom render function";
|
|
1261
|
+
} else if (unrenderableReason) {
|
|
1262
|
+
skipReason = unrenderableReason;
|
|
1263
|
+
}
|
|
1264
|
+
const renderCode = hasCustomRender ? `<${componentName} />` : generateRenderCode(componentName, story.args);
|
|
1265
|
+
const description = story.description ?? `${storyNameToTitle(story.name)} variant`;
|
|
1266
|
+
return {
|
|
1267
|
+
name: storyNameToTitle(story.name),
|
|
1268
|
+
description,
|
|
1269
|
+
renderCode,
|
|
1270
|
+
needsManualReview,
|
|
1271
|
+
skipReason
|
|
1272
|
+
};
|
|
1273
|
+
});
|
|
1274
|
+
}
|
|
1275
|
+
function getUnrenderableReason(args, path = "") {
|
|
1276
|
+
for (const [key, value] of Object.entries(args)) {
|
|
1277
|
+
const currentPath = path ? `${path}.${key}` : key;
|
|
1278
|
+
if (typeof value === "string") {
|
|
1279
|
+
if (value === "__JSX__") {
|
|
1280
|
+
return `JSX element in prop "${currentPath}"`;
|
|
1281
|
+
}
|
|
1282
|
+
if (value === "__EXPR__") {
|
|
1283
|
+
return `expression in prop "${currentPath}"`;
|
|
1284
|
+
}
|
|
1285
|
+
if (value.startsWith("__REF__")) {
|
|
1286
|
+
const ref = value.slice(7);
|
|
1287
|
+
return `variable reference "${ref}" in prop "${currentPath}"`;
|
|
1288
|
+
}
|
|
1289
|
+
if (value === "__SPREAD__" || key === "__SPREAD__") {
|
|
1290
|
+
return `spread syntax in args`;
|
|
1291
|
+
}
|
|
1292
|
+
}
|
|
1293
|
+
if (typeof value === "object" && value !== null) {
|
|
1294
|
+
const nestedReason = getUnrenderableReason(value, currentPath);
|
|
1295
|
+
if (nestedReason) {
|
|
1296
|
+
return nestedReason;
|
|
1297
|
+
}
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1300
|
+
return null;
|
|
1301
|
+
}
|
|
1302
|
+
function generateRenderCode(componentName, args) {
|
|
1303
|
+
const entries = Object.entries(args);
|
|
1304
|
+
if (entries.length === 0) {
|
|
1305
|
+
return `<${componentName} />`;
|
|
1306
|
+
}
|
|
1307
|
+
const children = args.children;
|
|
1308
|
+
const otherArgs = Object.entries(args).filter(([k]) => k !== "children");
|
|
1309
|
+
const propsString = otherArgs.map(([key, value]) => formatPropValue(key, value)).filter(Boolean).join(" ");
|
|
1310
|
+
if (children !== void 0) {
|
|
1311
|
+
const childrenStr = formatChildrenValue(children);
|
|
1312
|
+
return propsString ? `<${componentName} ${propsString}>${childrenStr}</${componentName}>` : `<${componentName}>${childrenStr}</${componentName}>`;
|
|
1313
|
+
}
|
|
1314
|
+
return propsString ? `<${componentName} ${propsString} />` : `<${componentName} />`;
|
|
1315
|
+
}
|
|
1316
|
+
function formatChildrenValue(value) {
|
|
1317
|
+
if (typeof value === "string") {
|
|
1318
|
+
if (value === "__JSX__") return "{null /* JSX children */}";
|
|
1319
|
+
if (value === "__EXPR__") return "{null /* expression */}";
|
|
1320
|
+
if (value.startsWith("__REF__")) return `{${value.slice(7)}}`;
|
|
1321
|
+
return value;
|
|
1322
|
+
}
|
|
1323
|
+
return String(value);
|
|
1324
|
+
}
|
|
1325
|
+
function formatPropValue(key, value) {
|
|
1326
|
+
if (value === void 0 || value === null) {
|
|
1327
|
+
return "";
|
|
1328
|
+
}
|
|
1329
|
+
if (typeof value === "string") {
|
|
1330
|
+
if (value === "__JSX__") {
|
|
1331
|
+
return `${key}={undefined /* JSX */}`;
|
|
1332
|
+
}
|
|
1333
|
+
if (value === "__EXPR__") {
|
|
1334
|
+
return `${key}={undefined /* expression */}`;
|
|
1335
|
+
}
|
|
1336
|
+
if (value.startsWith("__REF__")) {
|
|
1337
|
+
return `${key}={${value.slice(7)}}`;
|
|
1338
|
+
}
|
|
1339
|
+
if (value.includes('"') || value.includes("\\") || value.includes("\n")) {
|
|
1340
|
+
return `${key}={"${escapeString(value)}"}`;
|
|
1341
|
+
}
|
|
1342
|
+
return `${key}="${value}"`;
|
|
1343
|
+
}
|
|
1344
|
+
if (typeof value === "boolean") {
|
|
1345
|
+
return value ? key : `${key}={false}`;
|
|
1346
|
+
}
|
|
1347
|
+
if (typeof value === "number") {
|
|
1348
|
+
return `${key}={${value}}`;
|
|
1349
|
+
}
|
|
1350
|
+
if (Array.isArray(value)) {
|
|
1351
|
+
const formatted = formatArrayValue(value);
|
|
1352
|
+
return `${key}={${formatted}}`;
|
|
1353
|
+
}
|
|
1354
|
+
if (typeof value === "object" && value !== null) {
|
|
1355
|
+
const formatted = formatObjectValue(value);
|
|
1356
|
+
return `${key}={${formatted}}`;
|
|
1357
|
+
}
|
|
1358
|
+
return `${key}={${JSON.stringify(value)}}`;
|
|
1359
|
+
}
|
|
1360
|
+
function formatObjectValue(obj) {
|
|
1361
|
+
const entries = Object.entries(obj);
|
|
1362
|
+
if (entries.length === 0) return "{}";
|
|
1363
|
+
const props = entries.map(([k, v]) => {
|
|
1364
|
+
const formatted = formatValueForObject(v);
|
|
1365
|
+
return `${k}: ${formatted}`;
|
|
1366
|
+
}).join(", ");
|
|
1367
|
+
return `{ ${props} }`;
|
|
1368
|
+
}
|
|
1369
|
+
function formatArrayValue(arr) {
|
|
1370
|
+
if (arr.length === 0) return "[]";
|
|
1371
|
+
const items = arr.map((item) => formatValueForObject(item));
|
|
1372
|
+
return `[${items.join(", ")}]`;
|
|
1373
|
+
}
|
|
1374
|
+
function formatValueForObject(value) {
|
|
1375
|
+
if (value === void 0) return "undefined";
|
|
1376
|
+
if (value === null) return "null";
|
|
1377
|
+
if (typeof value === "string") {
|
|
1378
|
+
if (value === "__JSX__") return "undefined /* JSX */";
|
|
1379
|
+
if (value === "__EXPR__") return "undefined /* expression */";
|
|
1380
|
+
if (value.startsWith("__REF__")) return value.slice(7);
|
|
1381
|
+
return `"${escapeString(value)}"`;
|
|
1382
|
+
}
|
|
1383
|
+
if (typeof value === "boolean" || typeof value === "number") {
|
|
1384
|
+
return String(value);
|
|
1385
|
+
}
|
|
1386
|
+
if (Array.isArray(value)) {
|
|
1387
|
+
return formatArrayValue(value);
|
|
1388
|
+
}
|
|
1389
|
+
if (typeof value === "object") {
|
|
1390
|
+
return formatObjectValue(value);
|
|
1391
|
+
}
|
|
1392
|
+
return JSON.stringify(value);
|
|
1393
|
+
}
|
|
1394
|
+
function generateSegmentCode(options) {
|
|
1395
|
+
const {
|
|
1396
|
+
componentName,
|
|
1397
|
+
componentImport,
|
|
1398
|
+
description,
|
|
1399
|
+
category,
|
|
1400
|
+
tags,
|
|
1401
|
+
props,
|
|
1402
|
+
variants,
|
|
1403
|
+
todos,
|
|
1404
|
+
generated
|
|
1405
|
+
} = options;
|
|
1406
|
+
const propsCode = formatPropsCode(props);
|
|
1407
|
+
const variantsCode = formatVariantsCode(componentName, variants);
|
|
1408
|
+
const tagsCode = tags && tags.length > 0 ? `tags: [${tags.map((t) => `"${t}"`).join(", ")}],` : "";
|
|
1409
|
+
const todosComments = todos.length > 0 ? todos.map((t) => ` // TODO: ${t}`).join("\n") + "\n" : "";
|
|
1410
|
+
let generatedCode = "";
|
|
1411
|
+
if (generated) {
|
|
1412
|
+
const skippedCode = generated.skippedVariants && generated.skippedVariants.length > 0 ? `
|
|
1413
|
+
skippedVariants: [
|
|
1414
|
+
${generated.skippedVariants.map((sv) => ` { name: "${escapeString(sv.name)}", reason: "${escapeString(sv.reason)}" },`).join("\n")}
|
|
1415
|
+
],` : "";
|
|
1416
|
+
generatedCode = `
|
|
1417
|
+
_generated: {
|
|
1418
|
+
source: "${generated.source}",
|
|
1419
|
+
sourceFile: "${escapeString(generated.sourceFile)}",
|
|
1420
|
+
confidence: ${generated.confidence.toFixed(2)},
|
|
1421
|
+
timestamp: "${generated.timestamp}",${skippedCode}
|
|
1422
|
+
},
|
|
1423
|
+
`;
|
|
1424
|
+
}
|
|
1425
|
+
return `import { defineSegment } from "@fragments/core";
|
|
1426
|
+
import { ${componentName} } from "${componentImport}";
|
|
1427
|
+
|
|
1428
|
+
export default defineSegment({
|
|
1429
|
+
component: ${componentName},
|
|
1430
|
+
|
|
1431
|
+
meta: {
|
|
1432
|
+
name: "${componentName}",
|
|
1433
|
+
description: "${escapeString(description ?? `${componentName} component`)}",
|
|
1434
|
+
category: "${category}",
|
|
1435
|
+
${tagsCode}
|
|
1436
|
+
// status: undefined, // TODO: Set to stable/beta/deprecated/experimental
|
|
1437
|
+
},
|
|
1438
|
+
|
|
1439
|
+
usage: {
|
|
1440
|
+
// TODO: Add specific use cases - when should developers use this component?
|
|
1441
|
+
when: [
|
|
1442
|
+
${todosComments} ],
|
|
1443
|
+
// TODO: Add anti-patterns - when should developers NOT use this component?
|
|
1444
|
+
whenNot: [],
|
|
1445
|
+
},
|
|
1446
|
+
|
|
1447
|
+
${propsCode}
|
|
1448
|
+
|
|
1449
|
+
relations: [
|
|
1450
|
+
// TODO: Add related components
|
|
1451
|
+
],
|
|
1452
|
+
|
|
1453
|
+
${variantsCode}
|
|
1454
|
+
${generatedCode}});
|
|
1455
|
+
`;
|
|
1456
|
+
}
|
|
1457
|
+
function formatPropsCode(props) {
|
|
1458
|
+
if (Object.keys(props).length === 0) {
|
|
1459
|
+
return " props: {},";
|
|
1460
|
+
}
|
|
1461
|
+
const entries = Object.entries(props).map(([name, prop]) => {
|
|
1462
|
+
const lines = [];
|
|
1463
|
+
lines.push(` ${name}: {`);
|
|
1464
|
+
lines.push(` type: "${prop.type}",`);
|
|
1465
|
+
if (prop.values && prop.values.length > 0) {
|
|
1466
|
+
lines.push(` values: [${prop.values.map((v) => `"${v}"`).join(", ")}],`);
|
|
1467
|
+
}
|
|
1468
|
+
if (prop.default !== void 0) {
|
|
1469
|
+
const defaultVal = typeof prop.default === "string" ? `"${prop.default}"` : String(prop.default);
|
|
1470
|
+
lines.push(` default: ${defaultVal},`);
|
|
1471
|
+
}
|
|
1472
|
+
if (prop.required) {
|
|
1473
|
+
lines.push(` required: true,`);
|
|
1474
|
+
}
|
|
1475
|
+
if (prop.description) {
|
|
1476
|
+
lines.push(` description: "${escapeString(prop.description)}",`);
|
|
1477
|
+
}
|
|
1478
|
+
lines.push(` },`);
|
|
1479
|
+
return lines.join("\n");
|
|
1480
|
+
});
|
|
1481
|
+
return ` props: {
|
|
1482
|
+
${entries.join("\n")}
|
|
1483
|
+
},`;
|
|
1484
|
+
}
|
|
1485
|
+
function formatVariantsCode(componentName, variants) {
|
|
1486
|
+
const renderableVariants = variants.filter((v) => !v.needsManualReview);
|
|
1487
|
+
if (renderableVariants.length === 0) {
|
|
1488
|
+
return " variants: [],";
|
|
1489
|
+
}
|
|
1490
|
+
const entries = renderableVariants.map((variant) => {
|
|
1491
|
+
return ` {
|
|
1492
|
+
name: "${variant.name}",
|
|
1493
|
+
description: "${escapeString(variant.description)}",
|
|
1494
|
+
render: () => ${variant.renderCode},
|
|
1495
|
+
},`;
|
|
1496
|
+
});
|
|
1497
|
+
return ` variants: [
|
|
1498
|
+
${entries.join("\n")}
|
|
1499
|
+
],`;
|
|
1500
|
+
}
|
|
1501
|
+
function escapeString(str) {
|
|
1502
|
+
return str.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n");
|
|
1503
|
+
}
|
|
1504
|
+
|
|
1505
|
+
// src/migrate/detect.ts
|
|
1506
|
+
import { existsSync } from "fs";
|
|
1507
|
+
import { readFile as readFile3 } from "fs/promises";
|
|
1508
|
+
import { join as join3 } from "path";
|
|
1509
|
+
import fg3 from "fast-glob";
|
|
1510
|
+
var CONFIG_FILES = [
|
|
1511
|
+
".storybook/main.ts",
|
|
1512
|
+
".storybook/main.mts",
|
|
1513
|
+
".storybook/main.js",
|
|
1514
|
+
".storybook/main.mjs",
|
|
1515
|
+
".storybook/main.cjs"
|
|
1516
|
+
];
|
|
1517
|
+
var DEFAULT_PATTERNS = [
|
|
1518
|
+
"**/*.stories.@(ts|tsx|js|jsx|mdx)",
|
|
1519
|
+
"**/*.story.@(ts|tsx|js|jsx|mdx)"
|
|
1520
|
+
];
|
|
1521
|
+
async function detectStorybookConfig(projectRoot) {
|
|
1522
|
+
for (const configFile of CONFIG_FILES) {
|
|
1523
|
+
const configPath = join3(projectRoot, configFile);
|
|
1524
|
+
if (existsSync(configPath)) {
|
|
1525
|
+
return parseStorybookConfig(configPath);
|
|
1526
|
+
}
|
|
1527
|
+
}
|
|
1528
|
+
return null;
|
|
1529
|
+
}
|
|
1530
|
+
async function parseStorybookConfig(configPath) {
|
|
1531
|
+
const errors = [];
|
|
1532
|
+
let storyPatterns = [];
|
|
1533
|
+
let framework;
|
|
1534
|
+
let builder;
|
|
1535
|
+
try {
|
|
1536
|
+
const content = await readFile3(configPath, "utf-8");
|
|
1537
|
+
const storiesMatch = content.match(
|
|
1538
|
+
/stories:\s*\[([^\]]+)\]/s
|
|
1539
|
+
);
|
|
1540
|
+
if (storiesMatch) {
|
|
1541
|
+
const storiesContent = storiesMatch[1];
|
|
1542
|
+
const patterns = storiesContent.split(",").map((p) => p.trim()).map((p) => {
|
|
1543
|
+
const cleaned = p.replace(/^['"`]|['"`]$/g, "").trim();
|
|
1544
|
+
return cleaned;
|
|
1545
|
+
}).filter((p) => p && !p.startsWith("//") && !p.startsWith("{"));
|
|
1546
|
+
if (patterns.length > 0) {
|
|
1547
|
+
storyPatterns = patterns;
|
|
1548
|
+
}
|
|
1549
|
+
}
|
|
1550
|
+
const frameworkMatch = content.match(
|
|
1551
|
+
/framework:\s*['"`]([^'"`]+)['"`]/
|
|
1552
|
+
);
|
|
1553
|
+
if (frameworkMatch) {
|
|
1554
|
+
framework = frameworkMatch[1];
|
|
1555
|
+
} else {
|
|
1556
|
+
const frameworkNameMatch = content.match(
|
|
1557
|
+
/framework:\s*\{[^}]*name:\s*['"`]([^'"`]+)['"`]/
|
|
1558
|
+
);
|
|
1559
|
+
if (frameworkNameMatch) {
|
|
1560
|
+
framework = frameworkNameMatch[1];
|
|
1561
|
+
}
|
|
1562
|
+
}
|
|
1563
|
+
const builderMatch = content.match(
|
|
1564
|
+
/builder:\s*['"`]([^'"`]+)['"`]/
|
|
1565
|
+
);
|
|
1566
|
+
if (builderMatch) {
|
|
1567
|
+
builder = builderMatch[1];
|
|
1568
|
+
}
|
|
1569
|
+
} catch (error) {
|
|
1570
|
+
errors.push(
|
|
1571
|
+
`Failed to parse config: ${error instanceof Error ? error.message : String(error)}`
|
|
1572
|
+
);
|
|
1573
|
+
}
|
|
1574
|
+
if (storyPatterns.length === 0) {
|
|
1575
|
+
storyPatterns = DEFAULT_PATTERNS;
|
|
1576
|
+
errors.push("No story patterns found in config, using defaults");
|
|
1577
|
+
}
|
|
1578
|
+
return {
|
|
1579
|
+
configPath,
|
|
1580
|
+
storyPatterns,
|
|
1581
|
+
framework,
|
|
1582
|
+
builder,
|
|
1583
|
+
valid: errors.length === 0 || storyPatterns.length > 0,
|
|
1584
|
+
errors: errors.length > 0 ? errors : void 0
|
|
1585
|
+
};
|
|
1586
|
+
}
|
|
1587
|
+
async function discoverStoryFiles(projectRoot, patterns) {
|
|
1588
|
+
if (!patterns || patterns.length === 0) {
|
|
1589
|
+
const config = await detectStorybookConfig(projectRoot);
|
|
1590
|
+
patterns = config?.storyPatterns ?? DEFAULT_PATTERNS;
|
|
1591
|
+
}
|
|
1592
|
+
const configDir = join3(projectRoot, ".storybook");
|
|
1593
|
+
const resolvedPatterns = patterns.map((p) => {
|
|
1594
|
+
if (p.startsWith("../")) {
|
|
1595
|
+
return join3(configDir, p);
|
|
1596
|
+
}
|
|
1597
|
+
return join3(projectRoot, p);
|
|
1598
|
+
});
|
|
1599
|
+
const files = await fg3(resolvedPatterns, {
|
|
1600
|
+
cwd: projectRoot,
|
|
1601
|
+
absolute: true,
|
|
1602
|
+
ignore: [
|
|
1603
|
+
"**/node_modules/**",
|
|
1604
|
+
"**/dist/**",
|
|
1605
|
+
"**/build/**",
|
|
1606
|
+
"**/.storybook/**"
|
|
1607
|
+
]
|
|
1608
|
+
});
|
|
1609
|
+
return files.sort();
|
|
1610
|
+
}
|
|
1611
|
+
|
|
1612
|
+
// src/setup.ts
|
|
1613
|
+
async function isSegmentsJsonStale(configDir, outFile) {
|
|
1614
|
+
const fs = await import("fs/promises");
|
|
1615
|
+
const path = await import("path");
|
|
1616
|
+
const fg4 = await import("fast-glob");
|
|
1617
|
+
const segmentsJsonPath = path.join(configDir, outFile);
|
|
1618
|
+
try {
|
|
1619
|
+
const segmentsJsonStat = await fs.stat(segmentsJsonPath);
|
|
1620
|
+
const segmentFiles = await fg4.default(`**/*${BRAND.fileExtension}`, {
|
|
1621
|
+
cwd: configDir,
|
|
1622
|
+
ignore: ["**/node_modules/**"],
|
|
1623
|
+
absolute: true
|
|
1624
|
+
});
|
|
1625
|
+
for (const file of segmentFiles) {
|
|
1626
|
+
const stat3 = await fs.stat(file);
|
|
1627
|
+
if (stat3.mtimeMs > segmentsJsonStat.mtimeMs) {
|
|
1628
|
+
return { stale: true, missing: false };
|
|
1629
|
+
}
|
|
1630
|
+
}
|
|
1631
|
+
return { stale: false, missing: false };
|
|
1632
|
+
} catch {
|
|
1633
|
+
return { stale: false, missing: true };
|
|
1634
|
+
}
|
|
1635
|
+
}
|
|
1636
|
+
async function loadSegmentInfo(segmentFiles) {
|
|
1637
|
+
const fs = await import("fs/promises");
|
|
1638
|
+
const segments = [];
|
|
1639
|
+
for (const file of segmentFiles) {
|
|
1640
|
+
try {
|
|
1641
|
+
const content = await fs.readFile(file.absolutePath, "utf-8");
|
|
1642
|
+
const nameMatch = content.match(/name:\s*['"]([^'"]+)['"]/);
|
|
1643
|
+
const hasFigma = /meta:\s*\{[^}]*figma:\s*['"]https?:/.test(content);
|
|
1644
|
+
if (nameMatch) {
|
|
1645
|
+
segments.push({
|
|
1646
|
+
name: nameMatch[1],
|
|
1647
|
+
filePath: file.absolutePath,
|
|
1648
|
+
hasFigma
|
|
1649
|
+
});
|
|
1650
|
+
}
|
|
1651
|
+
} catch {
|
|
1652
|
+
}
|
|
1653
|
+
}
|
|
1654
|
+
return segments;
|
|
1655
|
+
}
|
|
1656
|
+
async function runSetup(options = {}) {
|
|
1657
|
+
const fs = await import("fs/promises");
|
|
1658
|
+
const path = await import("path");
|
|
1659
|
+
const result = {
|
|
1660
|
+
segmentFilesCreated: 0,
|
|
1661
|
+
segmentsBuilt: 0,
|
|
1662
|
+
figmaLinked: 0,
|
|
1663
|
+
errors: []
|
|
1664
|
+
};
|
|
1665
|
+
const log = (msg) => {
|
|
1666
|
+
if (!options.silent) console.log(msg);
|
|
1667
|
+
};
|
|
1668
|
+
try {
|
|
1669
|
+
const { config, configDir } = await loadConfig(options.configPath);
|
|
1670
|
+
log(pc6.dim("Checking for fragment files..."));
|
|
1671
|
+
let segmentFiles = await discoverSegmentFiles(config, configDir);
|
|
1672
|
+
if (segmentFiles.length === 0 && !options.skipStorybook) {
|
|
1673
|
+
log(pc6.yellow("\n No fragment files found"));
|
|
1674
|
+
const sbConfig = await detectStorybookConfig(configDir);
|
|
1675
|
+
if (sbConfig) {
|
|
1676
|
+
log(pc6.dim(` Found Storybook at ${sbConfig.configPath}`));
|
|
1677
|
+
log(pc6.dim(" Converting stories to fragments...\n"));
|
|
1678
|
+
const storyFiles = await discoverStoryFiles(configDir, sbConfig.storyPatterns);
|
|
1679
|
+
if (storyFiles.length > 0) {
|
|
1680
|
+
let converted = 0;
|
|
1681
|
+
for (const storyFile of storyFiles) {
|
|
1682
|
+
try {
|
|
1683
|
+
const parsed = await parseStoryFile(storyFile);
|
|
1684
|
+
const segmentResult = convertToSegment(parsed);
|
|
1685
|
+
await fs.mkdir(path.dirname(segmentResult.outputFile), { recursive: true });
|
|
1686
|
+
await fs.writeFile(segmentResult.outputFile, segmentResult.code);
|
|
1687
|
+
converted++;
|
|
1688
|
+
} catch {
|
|
1689
|
+
}
|
|
1690
|
+
}
|
|
1691
|
+
result.segmentFilesCreated = converted;
|
|
1692
|
+
log(pc6.green(` Generated ${converted} fragment file(s)`));
|
|
1693
|
+
segmentFiles = await discoverSegmentFiles(config, configDir);
|
|
1694
|
+
}
|
|
1695
|
+
} else {
|
|
1696
|
+
log(pc6.dim(" No Storybook config found"));
|
|
1697
|
+
log(pc6.dim(` Run ${pc6.cyan(`${BRAND.cliCommand} add <ComponentName>`)} to create your first fragment`));
|
|
1698
|
+
}
|
|
1699
|
+
} else if (segmentFiles.length > 0) {
|
|
1700
|
+
log(pc6.green(` Found ${segmentFiles.length} fragment file(s)`));
|
|
1701
|
+
}
|
|
1702
|
+
if (segmentFiles.length > 0 && !options.skipBuild) {
|
|
1703
|
+
const outFile = config.outFile || BRAND.outFile;
|
|
1704
|
+
const { stale, missing } = await isSegmentsJsonStale(configDir, outFile);
|
|
1705
|
+
if (missing || stale) {
|
|
1706
|
+
const reason = missing ? "Building" : "Rebuilding";
|
|
1707
|
+
log(pc6.dim(`
|
|
1708
|
+
${reason} ${BRAND.outFile}...`));
|
|
1709
|
+
try {
|
|
1710
|
+
const buildResult = await buildSegments(config, configDir);
|
|
1711
|
+
result.segmentsBuilt = buildResult.segmentCount;
|
|
1712
|
+
if (buildResult.errors.length > 0) {
|
|
1713
|
+
for (const err of buildResult.errors) {
|
|
1714
|
+
result.errors.push(`${err.file}: ${err.error}`);
|
|
1715
|
+
}
|
|
1716
|
+
}
|
|
1717
|
+
log(pc6.green(` Built ${buildResult.segmentCount} fragment(s)`));
|
|
1718
|
+
} catch (error) {
|
|
1719
|
+
result.errors.push(`Build failed: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
1720
|
+
}
|
|
1721
|
+
} else {
|
|
1722
|
+
log(pc6.dim(`
|
|
1723
|
+
${BRAND.outFile} is up to date`));
|
|
1724
|
+
}
|
|
1725
|
+
}
|
|
1726
|
+
if (!options.skipFigma && config.figmaFile && process.env.FIGMA_ACCESS_TOKEN) {
|
|
1727
|
+
const segments = await loadSegmentInfo(segmentFiles);
|
|
1728
|
+
const linkedCount = segments.filter((s) => s.hasFigma).length;
|
|
1729
|
+
if (linkedCount === 0 && segments.length > 0) {
|
|
1730
|
+
log(pc6.dim("\n Figma configured but no fragments linked"));
|
|
1731
|
+
log(pc6.dim(` Run ${pc6.cyan(`${BRAND.cliCommand} link figma --auto`)} to auto-link components`));
|
|
1732
|
+
} else if (linkedCount > 0) {
|
|
1733
|
+
log(pc6.dim(`
|
|
1734
|
+
${linkedCount}/${segments.length} fragment(s) linked to Figma`));
|
|
1735
|
+
}
|
|
1736
|
+
} else if (!options.skipFigma && config.figmaFile && !process.env.FIGMA_ACCESS_TOKEN) {
|
|
1737
|
+
log(pc6.dim("\n Figma file configured but FIGMA_ACCESS_TOKEN not set"));
|
|
1738
|
+
}
|
|
1739
|
+
} catch (error) {
|
|
1740
|
+
result.errors.push(error instanceof Error ? error.message : "Unknown error");
|
|
1741
|
+
}
|
|
1742
|
+
return result;
|
|
1743
|
+
}
|
|
1744
|
+
|
|
1745
|
+
// src/commands/dev.ts
|
|
1746
|
+
async function dev(options = {}) {
|
|
1747
|
+
const {
|
|
1748
|
+
port = 6006,
|
|
1749
|
+
open = true,
|
|
1750
|
+
skipSetup = false,
|
|
1751
|
+
skipStorybook = false,
|
|
1752
|
+
skipFigma = false,
|
|
1753
|
+
skipBuild = false
|
|
1754
|
+
} = options;
|
|
1755
|
+
console.log(pc7.cyan(`
|
|
1756
|
+
${BRAND.name} Dev Server
|
|
1757
|
+
`));
|
|
1758
|
+
if (!skipSetup) {
|
|
1759
|
+
const setupResult = await runSetup({
|
|
1760
|
+
skipStorybook,
|
|
1761
|
+
skipFigma,
|
|
1762
|
+
skipBuild
|
|
1763
|
+
});
|
|
1764
|
+
if (setupResult.errors.length > 0) {
|
|
1765
|
+
console.log(pc7.yellow("\n Setup completed with warnings:"));
|
|
1766
|
+
for (const error of setupResult.errors) {
|
|
1767
|
+
console.log(pc7.dim(` ${error}`));
|
|
1768
|
+
}
|
|
1769
|
+
}
|
|
1770
|
+
}
|
|
1771
|
+
const { createDevServer } = await import("./viewer-YRF4SQE4.js");
|
|
1772
|
+
console.log(pc7.dim("\nStarting dev server..."));
|
|
1773
|
+
const parsedPort = typeof port === "string" ? parseInt(port, 10) : port;
|
|
1774
|
+
try {
|
|
1775
|
+
const server = await createDevServer({
|
|
1776
|
+
port: parsedPort,
|
|
1777
|
+
open,
|
|
1778
|
+
projectRoot: process.cwd()
|
|
1779
|
+
});
|
|
1780
|
+
const address = server.httpServer?.address();
|
|
1781
|
+
const actualPort = typeof address === "object" && address ? address.port : parsedPort;
|
|
1782
|
+
console.log(pc7.green(`
|
|
1783
|
+
Viewer running at http://localhost:${actualPort}/fragments/
|
|
1784
|
+
`));
|
|
1785
|
+
console.log(pc7.dim("Press Ctrl+C to stop\n"));
|
|
1786
|
+
return {
|
|
1787
|
+
success: true,
|
|
1788
|
+
port: typeof actualPort === "number" ? actualPort : parseInt(String(actualPort), 10)
|
|
1789
|
+
};
|
|
1790
|
+
} catch (error) {
|
|
1791
|
+
const errMsg = error instanceof Error ? error.message : String(error);
|
|
1792
|
+
if (errMsg.includes("EADDRINUSE") || errMsg.includes("address already in use")) {
|
|
1793
|
+
console.error(pc7.red(`
|
|
1794
|
+
Port ${parsedPort} is already in use.`));
|
|
1795
|
+
console.error(pc7.dim(` Try a different port: ${BRAND.cliCommand} dev --port ${parsedPort + 1}
|
|
1796
|
+
`));
|
|
1797
|
+
} else if (errMsg.includes("EACCES")) {
|
|
1798
|
+
console.error(pc7.red(`
|
|
1799
|
+
Permission denied for port ${parsedPort}.`));
|
|
1800
|
+
console.error(pc7.dim(` Try a port above 1024: ${BRAND.cliCommand} dev --port 6006
|
|
1801
|
+
`));
|
|
1802
|
+
} else {
|
|
1803
|
+
console.error(pc7.red(`
|
|
1804
|
+
Failed to start dev server: ${errMsg}
|
|
1805
|
+
`));
|
|
1806
|
+
}
|
|
1807
|
+
return { success: false };
|
|
1808
|
+
}
|
|
1809
|
+
}
|
|
1810
|
+
|
|
1811
|
+
// src/commands/compare.ts
|
|
1812
|
+
import pc8 from "picocolors";
|
|
1813
|
+
import { writeFile as writeFile2, mkdir as mkdir2 } from "fs/promises";
|
|
1814
|
+
import { resolve as resolve2, join as join4 } from "path";
|
|
1815
|
+
async function compare(component, options = {}) {
|
|
1816
|
+
const {
|
|
1817
|
+
variant,
|
|
1818
|
+
figma: figmaUrl,
|
|
1819
|
+
threshold = 1,
|
|
1820
|
+
all = false,
|
|
1821
|
+
output,
|
|
1822
|
+
port = 6006
|
|
1823
|
+
} = options;
|
|
1824
|
+
if (!process.env.FIGMA_ACCESS_TOKEN) {
|
|
1825
|
+
console.error(pc8.red("\nFIGMA_ACCESS_TOKEN environment variable required."));
|
|
1826
|
+
console.log(pc8.dim("Generate at: https://www.figma.com/developers/api#access-tokens"));
|
|
1827
|
+
console.log(pc8.dim(" export FIGMA_ACCESS_TOKEN=figd_xxx"));
|
|
1828
|
+
process.exit(1);
|
|
1829
|
+
}
|
|
1830
|
+
const baseUrl = `http://localhost:${port}`;
|
|
1831
|
+
console.log(pc8.cyan(`
|
|
1832
|
+
${BRAND.name} Design Verification
|
|
1833
|
+
`));
|
|
1834
|
+
if (all) {
|
|
1835
|
+
return compareAll(baseUrl, threshold, output);
|
|
1836
|
+
}
|
|
1837
|
+
let componentsToCompare = [];
|
|
1838
|
+
if (component) {
|
|
1839
|
+
componentsToCompare = [component];
|
|
1840
|
+
} else {
|
|
1841
|
+
componentsToCompare = await selectComponents(baseUrl);
|
|
1842
|
+
}
|
|
1843
|
+
if (componentsToCompare.length === 0) {
|
|
1844
|
+
console.log(pc8.dim("\nNo components selected."));
|
|
1845
|
+
return { success: true, passed: 0, failed: 0, skipped: 0 };
|
|
1846
|
+
}
|
|
1847
|
+
if (componentsToCompare.length === 1) {
|
|
1848
|
+
console.log(pc8.dim(`Comparing ${componentsToCompare[0]} to Figma design...
|
|
1849
|
+
`));
|
|
1850
|
+
} else {
|
|
1851
|
+
console.log(pc8.dim(`Comparing ${componentsToCompare.length} components to Figma designs...
|
|
1852
|
+
`));
|
|
1853
|
+
}
|
|
1854
|
+
let passed = 0;
|
|
1855
|
+
let failed = 0;
|
|
1856
|
+
for (const comp of componentsToCompare) {
|
|
1857
|
+
const response = await fetch(`${baseUrl}/segments/compare`, {
|
|
1858
|
+
method: "POST",
|
|
1859
|
+
headers: { "Content-Type": "application/json" },
|
|
1860
|
+
body: JSON.stringify({
|
|
1861
|
+
component: comp,
|
|
1862
|
+
variant,
|
|
1863
|
+
figmaUrl,
|
|
1864
|
+
threshold,
|
|
1865
|
+
figmaToken: process.env.FIGMA_ACCESS_TOKEN
|
|
1866
|
+
})
|
|
1867
|
+
});
|
|
1868
|
+
const result = await response.json();
|
|
1869
|
+
if (result.error) {
|
|
1870
|
+
failed++;
|
|
1871
|
+
console.log(`${pc8.red("\u2717")} ${pc8.bold(comp)} - ${result.error}`);
|
|
1872
|
+
continue;
|
|
1873
|
+
}
|
|
1874
|
+
if (result.match) {
|
|
1875
|
+
passed++;
|
|
1876
|
+
console.log(`${pc8.green("\u2713")} ${pc8.bold(comp)} ${pc8.dim(`${result.diffPercentage}%`)}`);
|
|
1877
|
+
} else {
|
|
1878
|
+
failed++;
|
|
1879
|
+
console.log(`${pc8.red("\u2717")} ${pc8.bold(comp)} ${pc8.yellow(`${result.diffPercentage}%`)} ${pc8.dim(`(threshold: ${threshold}%)`)}`);
|
|
1880
|
+
}
|
|
1881
|
+
if (output && result.rendered && result.figma && result.diff) {
|
|
1882
|
+
await saveImages(output, comp, result);
|
|
1883
|
+
}
|
|
1884
|
+
}
|
|
1885
|
+
if (componentsToCompare.length > 1) {
|
|
1886
|
+
console.log();
|
|
1887
|
+
console.log(pc8.dim("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
1888
|
+
console.log(`
|
|
1889
|
+
${pc8.green(`${passed} passed`)}, ${pc8.red(`${failed} failed`)}
|
|
1890
|
+
`);
|
|
1891
|
+
} else {
|
|
1892
|
+
console.log();
|
|
1893
|
+
}
|
|
1894
|
+
if (output && componentsToCompare.length > 0) {
|
|
1895
|
+
console.log(pc8.dim(`Images saved to: ${output}/
|
|
1896
|
+
`));
|
|
1897
|
+
}
|
|
1898
|
+
return {
|
|
1899
|
+
success: failed === 0,
|
|
1900
|
+
passed,
|
|
1901
|
+
failed,
|
|
1902
|
+
skipped: 0
|
|
1903
|
+
};
|
|
1904
|
+
}
|
|
1905
|
+
async function compareAll(baseUrl, threshold, output) {
|
|
1906
|
+
console.log(pc8.dim("Comparing all components with Figma links...\n"));
|
|
1907
|
+
const contextResp = await fetch(`${baseUrl}/segments/context?format=json`);
|
|
1908
|
+
if (!contextResp.ok) {
|
|
1909
|
+
throw new Error("Failed to fetch segments. Make sure dev server is running.");
|
|
1910
|
+
}
|
|
1911
|
+
const contextText = await contextResp.text();
|
|
1912
|
+
let segments = [];
|
|
1913
|
+
try {
|
|
1914
|
+
const contextData = JSON.parse(contextText);
|
|
1915
|
+
segments = contextData.components || [];
|
|
1916
|
+
} catch {
|
|
1917
|
+
segments = [];
|
|
1918
|
+
}
|
|
1919
|
+
if (segments.length === 0) {
|
|
1920
|
+
console.log(pc8.yellow("No components found with Figma links."));
|
|
1921
|
+
console.log(pc8.dim("Add figma field to your segment definitions:"));
|
|
1922
|
+
console.log(pc8.dim(' meta: { figma: "https://figma.com/file/..." }'));
|
|
1923
|
+
return { success: true, passed: 0, failed: 0, skipped: 0 };
|
|
1924
|
+
}
|
|
1925
|
+
let passed = 0;
|
|
1926
|
+
let failed = 0;
|
|
1927
|
+
let skipped = 0;
|
|
1928
|
+
for (const seg of segments) {
|
|
1929
|
+
if (!seg.figma) {
|
|
1930
|
+
skipped++;
|
|
1931
|
+
console.log(`${pc8.dim("\u23ED\uFE0F")} ${pc8.dim(seg.name)} ${pc8.dim("(no figma link)")}`);
|
|
1932
|
+
continue;
|
|
1933
|
+
}
|
|
1934
|
+
try {
|
|
1935
|
+
const response = await fetch(`${baseUrl}/segments/compare`, {
|
|
1936
|
+
method: "POST",
|
|
1937
|
+
headers: { "Content-Type": "application/json" },
|
|
1938
|
+
body: JSON.stringify({
|
|
1939
|
+
component: seg.name,
|
|
1940
|
+
threshold,
|
|
1941
|
+
figmaToken: process.env.FIGMA_ACCESS_TOKEN
|
|
1942
|
+
})
|
|
1943
|
+
});
|
|
1944
|
+
const result = await response.json();
|
|
1945
|
+
if (result.error) {
|
|
1946
|
+
failed++;
|
|
1947
|
+
console.log(`${pc8.red("\u2717")} ${pc8.bold(seg.name)} - ${result.error}`);
|
|
1948
|
+
} else if (result.match) {
|
|
1949
|
+
passed++;
|
|
1950
|
+
console.log(`${pc8.green("\u2713")} ${pc8.bold(seg.name)} ${pc8.dim(`${result.diffPercentage}%`)}`);
|
|
1951
|
+
} else {
|
|
1952
|
+
failed++;
|
|
1953
|
+
console.log(`${pc8.red("\u2717")} ${pc8.bold(seg.name)} ${pc8.yellow(`${result.diffPercentage}%`)} ${pc8.dim(`(threshold: ${threshold}%)`)}`);
|
|
1954
|
+
}
|
|
1955
|
+
if (output && result.rendered && result.figma && result.diff) {
|
|
1956
|
+
await saveImages(output, seg.name, result);
|
|
1957
|
+
}
|
|
1958
|
+
} catch (error) {
|
|
1959
|
+
failed++;
|
|
1960
|
+
console.log(`${pc8.red("\u2717")} ${pc8.bold(seg.name)} - ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
1961
|
+
}
|
|
1962
|
+
}
|
|
1963
|
+
console.log();
|
|
1964
|
+
console.log(pc8.dim("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
1965
|
+
console.log(`
|
|
1966
|
+
${pc8.green(`${passed} passed`)}, ${pc8.red(`${failed} failed`)}, ${pc8.dim(`${skipped} skipped`)}
|
|
1967
|
+
`);
|
|
1968
|
+
if (output) {
|
|
1969
|
+
console.log(pc8.dim(`Images saved to: ${output}/
|
|
1970
|
+
`));
|
|
1971
|
+
}
|
|
1972
|
+
return {
|
|
1973
|
+
success: failed === 0,
|
|
1974
|
+
passed,
|
|
1975
|
+
failed,
|
|
1976
|
+
skipped
|
|
1977
|
+
};
|
|
1978
|
+
}
|
|
1979
|
+
async function selectComponents(baseUrl) {
|
|
1980
|
+
console.log(pc8.dim("Fetching components with Figma links...\n"));
|
|
1981
|
+
const contextResp = await fetch(`${baseUrl}/segments/context?format=json`);
|
|
1982
|
+
if (!contextResp.ok) {
|
|
1983
|
+
throw new Error("Failed to fetch segments. Make sure dev server is running.");
|
|
1984
|
+
}
|
|
1985
|
+
const contextText = await contextResp.text();
|
|
1986
|
+
let segments = [];
|
|
1987
|
+
try {
|
|
1988
|
+
const contextData = JSON.parse(contextText);
|
|
1989
|
+
segments = (contextData.components || []).filter((s) => s.figma);
|
|
1990
|
+
} catch {
|
|
1991
|
+
segments = [];
|
|
1992
|
+
}
|
|
1993
|
+
if (segments.length === 0) {
|
|
1994
|
+
console.log(pc8.yellow("No components found with Figma links."));
|
|
1995
|
+
console.log(pc8.dim("Add figma field to your segment definitions:"));
|
|
1996
|
+
console.log(pc8.dim(' meta: { figma: "https://figma.com/file/..." }'));
|
|
1997
|
+
return [];
|
|
1998
|
+
}
|
|
1999
|
+
const { checkbox } = await import("@inquirer/prompts");
|
|
2000
|
+
try {
|
|
2001
|
+
const choices = segments.map((seg) => ({
|
|
2002
|
+
name: seg.name,
|
|
2003
|
+
value: seg.name,
|
|
2004
|
+
checked: true
|
|
2005
|
+
}));
|
|
2006
|
+
return await checkbox({
|
|
2007
|
+
message: "Select components to compare:",
|
|
2008
|
+
choices,
|
|
2009
|
+
pageSize: 15
|
|
2010
|
+
});
|
|
2011
|
+
} catch {
|
|
2012
|
+
console.log(pc8.dim("\nNo changes made."));
|
|
2013
|
+
return [];
|
|
2014
|
+
}
|
|
2015
|
+
}
|
|
2016
|
+
async function saveImages(outputDir, component, result) {
|
|
2017
|
+
const dir = resolve2(process.cwd(), outputDir);
|
|
2018
|
+
await mkdir2(dir, { recursive: true });
|
|
2019
|
+
const saveImage = async (data, filename) => {
|
|
2020
|
+
const base64 = data.replace("data:image/png;base64,", "");
|
|
2021
|
+
await writeFile2(join4(dir, filename), Buffer.from(base64, "base64"));
|
|
2022
|
+
};
|
|
2023
|
+
if (result.rendered) {
|
|
2024
|
+
await saveImage(result.rendered, `${component}-rendered.png`);
|
|
2025
|
+
}
|
|
2026
|
+
if (result.figma) {
|
|
2027
|
+
await saveImage(result.figma, `${component}-figma.png`);
|
|
2028
|
+
}
|
|
2029
|
+
if (result.diff) {
|
|
2030
|
+
await saveImage(result.diff, `${component}-diff.png`);
|
|
2031
|
+
}
|
|
2032
|
+
}
|
|
2033
|
+
|
|
2034
|
+
// src/commands/verify.ts
|
|
2035
|
+
import pc10 from "picocolors";
|
|
2036
|
+
|
|
2037
|
+
// src/shared/dev-server-client.ts
|
|
2038
|
+
var DevServerClient = class {
|
|
2039
|
+
baseUrl;
|
|
2040
|
+
timeout;
|
|
2041
|
+
constructor(options) {
|
|
2042
|
+
this.baseUrl = options.baseUrl.replace(/\/$/, "");
|
|
2043
|
+
this.timeout = options.timeout ?? 3e4;
|
|
2044
|
+
}
|
|
2045
|
+
/**
|
|
2046
|
+
* Check if the dev server is reachable
|
|
2047
|
+
*/
|
|
2048
|
+
async ping() {
|
|
2049
|
+
try {
|
|
2050
|
+
const controller = new AbortController();
|
|
2051
|
+
const timeoutId = setTimeout(() => controller.abort(), 5e3);
|
|
2052
|
+
const response = await fetch(`${this.baseUrl}/segments/context?format=json`, {
|
|
2053
|
+
signal: controller.signal
|
|
2054
|
+
});
|
|
2055
|
+
clearTimeout(timeoutId);
|
|
2056
|
+
return response.ok;
|
|
2057
|
+
} catch {
|
|
2058
|
+
return false;
|
|
2059
|
+
}
|
|
2060
|
+
}
|
|
2061
|
+
/**
|
|
2062
|
+
* Get all segments from the context endpoint
|
|
2063
|
+
*/
|
|
2064
|
+
async getSegments() {
|
|
2065
|
+
const response = await this.fetch("/segments/context?format=json");
|
|
2066
|
+
const data = await response.json();
|
|
2067
|
+
const components = data.components || {};
|
|
2068
|
+
return Object.entries(components).map(([name, info]) => ({
|
|
2069
|
+
name,
|
|
2070
|
+
category: info.category || "components",
|
|
2071
|
+
description: info.description,
|
|
2072
|
+
status: info.status,
|
|
2073
|
+
figma: info.figma
|
|
2074
|
+
}));
|
|
2075
|
+
}
|
|
2076
|
+
/**
|
|
2077
|
+
* Get compliance data for a component
|
|
2078
|
+
*/
|
|
2079
|
+
async getCompliance(request) {
|
|
2080
|
+
const response = await this.fetch("/segments/compliance", {
|
|
2081
|
+
method: "POST",
|
|
2082
|
+
headers: { "Content-Type": "application/json" },
|
|
2083
|
+
body: JSON.stringify(request)
|
|
2084
|
+
});
|
|
2085
|
+
const data = await response.json();
|
|
2086
|
+
return data;
|
|
2087
|
+
}
|
|
2088
|
+
/**
|
|
2089
|
+
* Get accessibility results for a component
|
|
2090
|
+
*/
|
|
2091
|
+
async getA11y(component, variant) {
|
|
2092
|
+
const response = await this.fetch("/fragments/a11y", {
|
|
2093
|
+
method: "POST",
|
|
2094
|
+
headers: { "Content-Type": "application/json" },
|
|
2095
|
+
body: JSON.stringify({ component, variant })
|
|
2096
|
+
});
|
|
2097
|
+
const data = await response.json();
|
|
2098
|
+
return data;
|
|
2099
|
+
}
|
|
2100
|
+
/**
|
|
2101
|
+
* Internal fetch wrapper with error handling
|
|
2102
|
+
*/
|
|
2103
|
+
async fetch(path, options) {
|
|
2104
|
+
const url = `${this.baseUrl}${path}`;
|
|
2105
|
+
try {
|
|
2106
|
+
const controller = new AbortController();
|
|
2107
|
+
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
|
2108
|
+
const response = await fetch(url, {
|
|
2109
|
+
...options,
|
|
2110
|
+
signal: controller.signal
|
|
2111
|
+
});
|
|
2112
|
+
clearTimeout(timeoutId);
|
|
2113
|
+
if (!response.ok) {
|
|
2114
|
+
const errorBody = await response.text().catch(() => "");
|
|
2115
|
+
throw new DevServerError(
|
|
2116
|
+
`Server returned ${response.status}: ${response.statusText}`,
|
|
2117
|
+
response.status,
|
|
2118
|
+
errorBody
|
|
2119
|
+
);
|
|
2120
|
+
}
|
|
2121
|
+
return response;
|
|
2122
|
+
} catch (error) {
|
|
2123
|
+
if (error instanceof DevServerError) {
|
|
2124
|
+
throw error;
|
|
2125
|
+
}
|
|
2126
|
+
const errMsg = error instanceof Error && error.cause ? String(error.cause.code || error.message) : error instanceof Error ? error.message : "Unknown error";
|
|
2127
|
+
if (errMsg.includes("ECONNREFUSED") || errMsg.includes("fetch failed")) {
|
|
2128
|
+
throw new DevServerConnectionError(
|
|
2129
|
+
`Cannot connect to dev server at ${this.baseUrl}`,
|
|
2130
|
+
this.baseUrl
|
|
2131
|
+
);
|
|
2132
|
+
}
|
|
2133
|
+
throw error;
|
|
2134
|
+
}
|
|
2135
|
+
}
|
|
2136
|
+
};
|
|
2137
|
+
var DevServerError = class extends Error {
|
|
2138
|
+
constructor(message, statusCode, body) {
|
|
2139
|
+
super(message);
|
|
2140
|
+
this.statusCode = statusCode;
|
|
2141
|
+
this.body = body;
|
|
2142
|
+
this.name = "DevServerError";
|
|
2143
|
+
}
|
|
2144
|
+
};
|
|
2145
|
+
var DevServerConnectionError = class extends Error {
|
|
2146
|
+
constructor(message, serverUrl) {
|
|
2147
|
+
super(message);
|
|
2148
|
+
this.serverUrl = serverUrl;
|
|
2149
|
+
this.name = "DevServerConnectionError";
|
|
2150
|
+
}
|
|
2151
|
+
};
|
|
2152
|
+
function createDevServerClient(port = 6006) {
|
|
2153
|
+
return new DevServerClient({
|
|
2154
|
+
baseUrl: `http://localhost:${port}`
|
|
2155
|
+
});
|
|
2156
|
+
}
|
|
2157
|
+
|
|
2158
|
+
// src/shared/command-wrapper.ts
|
|
2159
|
+
import pc9 from "picocolors";
|
|
2160
|
+
|
|
2161
|
+
// src/commands/verify.ts
|
|
2162
|
+
async function verify(component, options = {}) {
|
|
2163
|
+
const { config: configPath, ci = false, port = 6006 } = options;
|
|
2164
|
+
const { config } = await loadConfig(configPath);
|
|
2165
|
+
const minCompliance = options.minCompliance ?? config.ci?.minCompliance ?? 80;
|
|
2166
|
+
const client = createDevServerClient(port);
|
|
2167
|
+
const results = [];
|
|
2168
|
+
let totalCompliance = 0;
|
|
2169
|
+
let componentCount = 0;
|
|
2170
|
+
if (!ci) {
|
|
2171
|
+
console.log(pc10.cyan(`
|
|
2172
|
+
${BRAND.name} Compliance Verification
|
|
2173
|
+
`));
|
|
2174
|
+
console.log(pc10.dim(`Minimum compliance: ${minCompliance}%
|
|
2175
|
+
`));
|
|
2176
|
+
}
|
|
2177
|
+
const isReachable = await client.ping();
|
|
2178
|
+
if (!isReachable) {
|
|
2179
|
+
throw new DevServerConnectionError(
|
|
2180
|
+
`Cannot connect to dev server at http://localhost:${port}`,
|
|
2181
|
+
`http://localhost:${port}`
|
|
2182
|
+
);
|
|
2183
|
+
}
|
|
2184
|
+
let segments = await client.getSegments();
|
|
2185
|
+
if (component) {
|
|
2186
|
+
segments = segments.filter(
|
|
2187
|
+
(s) => s.name.toLowerCase() === component.toLowerCase()
|
|
2188
|
+
);
|
|
2189
|
+
if (segments.length === 0) {
|
|
2190
|
+
const error = { error: `Component "${component}" not found` };
|
|
2191
|
+
if (ci) {
|
|
2192
|
+
console.log(JSON.stringify(error));
|
|
2193
|
+
} else {
|
|
2194
|
+
console.log(pc10.red(error.error));
|
|
2195
|
+
}
|
|
2196
|
+
process.exit(1);
|
|
2197
|
+
}
|
|
2198
|
+
}
|
|
2199
|
+
for (const seg of segments) {
|
|
2200
|
+
try {
|
|
2201
|
+
const complianceResult = await client.getCompliance({
|
|
2202
|
+
component: seg.name
|
|
2203
|
+
});
|
|
2204
|
+
const passed = complianceResult.compliance >= minCompliance;
|
|
2205
|
+
results.push({
|
|
2206
|
+
component: seg.name,
|
|
2207
|
+
compliance: complianceResult.compliance,
|
|
2208
|
+
passed,
|
|
2209
|
+
violations: complianceResult.violations,
|
|
2210
|
+
totalProperties: complianceResult.totalProperties,
|
|
2211
|
+
hardcoded: complianceResult.hardcoded,
|
|
2212
|
+
usingTokens: complianceResult.usingTokens
|
|
2213
|
+
});
|
|
2214
|
+
totalCompliance += complianceResult.compliance;
|
|
2215
|
+
componentCount++;
|
|
2216
|
+
if (!ci) {
|
|
2217
|
+
const icon = passed ? pc10.green("\u2713") : pc10.red("\u2717");
|
|
2218
|
+
const complianceStr = passed ? pc10.green(`${complianceResult.compliance}%`) : pc10.red(`${complianceResult.compliance}%`);
|
|
2219
|
+
console.log(` ${icon} ${seg.name} ${complianceStr}`);
|
|
2220
|
+
if (!passed && complianceResult.violations.length > 0) {
|
|
2221
|
+
const violationsToShow = complianceResult.violations.slice(0, 3);
|
|
2222
|
+
for (const v of violationsToShow) {
|
|
2223
|
+
console.log(pc10.dim(` - ${v.property}: ${v.issue}`));
|
|
2224
|
+
if (v.suggestion) {
|
|
2225
|
+
console.log(pc10.dim(` ${pc10.cyan("\u2192")} ${v.suggestion}`));
|
|
2226
|
+
}
|
|
2227
|
+
}
|
|
2228
|
+
if (complianceResult.violations.length > 3) {
|
|
2229
|
+
console.log(pc10.dim(` ... and ${complianceResult.violations.length - 3} more`));
|
|
2230
|
+
}
|
|
2231
|
+
}
|
|
2232
|
+
}
|
|
2233
|
+
} catch (error) {
|
|
2234
|
+
results.push({
|
|
2235
|
+
component: seg.name,
|
|
2236
|
+
compliance: 0,
|
|
2237
|
+
passed: false,
|
|
2238
|
+
violations: [{
|
|
2239
|
+
property: "unknown",
|
|
2240
|
+
issue: error instanceof Error ? error.message : "Unknown error",
|
|
2241
|
+
severity: "error"
|
|
2242
|
+
}],
|
|
2243
|
+
totalProperties: 0,
|
|
2244
|
+
hardcoded: 0,
|
|
2245
|
+
usingTokens: 0
|
|
2246
|
+
});
|
|
2247
|
+
if (!ci) {
|
|
2248
|
+
console.log(` ${pc10.red("\u2717")} ${seg.name} ${pc10.red("error")}`);
|
|
2249
|
+
}
|
|
2250
|
+
}
|
|
2251
|
+
}
|
|
2252
|
+
const averageCompliance = componentCount > 0 ? totalCompliance / componentCount : 100;
|
|
2253
|
+
const allPassed = results.every((r) => r.passed);
|
|
2254
|
+
const summary = {
|
|
2255
|
+
passed: allPassed,
|
|
2256
|
+
compliance: Math.round(averageCompliance * 100) / 100,
|
|
2257
|
+
threshold: minCompliance,
|
|
2258
|
+
totalComponents: componentCount,
|
|
2259
|
+
passedComponents: results.filter((r) => r.passed).length,
|
|
2260
|
+
failedComponents: results.filter((r) => !r.passed).length,
|
|
2261
|
+
results,
|
|
2262
|
+
violations: results.flatMap((r) => r.violations.map((v) => ({
|
|
2263
|
+
component: r.component,
|
|
2264
|
+
...v
|
|
2265
|
+
})))
|
|
2266
|
+
};
|
|
2267
|
+
if (ci) {
|
|
2268
|
+
console.log(JSON.stringify(summary, null, 2));
|
|
2269
|
+
} else {
|
|
2270
|
+
console.log();
|
|
2271
|
+
if (allPassed) {
|
|
2272
|
+
console.log(pc10.green(`\u2713 All ${componentCount} component(s) meet ${minCompliance}% compliance threshold`));
|
|
2273
|
+
} else {
|
|
2274
|
+
const failedCount = results.filter((r) => !r.passed).length;
|
|
2275
|
+
console.log(pc10.red(`\u2717 ${failedCount} component(s) below ${minCompliance}% compliance threshold`));
|
|
2276
|
+
}
|
|
2277
|
+
console.log(pc10.dim(` Average compliance: ${summary.compliance}%
|
|
2278
|
+
`));
|
|
2279
|
+
}
|
|
2280
|
+
return summary;
|
|
2281
|
+
}
|
|
2282
|
+
|
|
2283
|
+
// src/commands/audit.ts
|
|
2284
|
+
import pc11 from "picocolors";
|
|
2285
|
+
async function audit(options = {}) {
|
|
2286
|
+
const { config: configPath, json = false, sort = "compliance", port = 6006 } = options;
|
|
2287
|
+
await loadConfig(configPath);
|
|
2288
|
+
const client = createDevServerClient(port);
|
|
2289
|
+
const audits = [];
|
|
2290
|
+
if (!json) {
|
|
2291
|
+
console.log(pc11.cyan(`
|
|
2292
|
+
${BRAND.name} Design System Audit
|
|
2293
|
+
`));
|
|
2294
|
+
}
|
|
2295
|
+
const isReachable = await client.ping();
|
|
2296
|
+
if (!isReachable) {
|
|
2297
|
+
throw new DevServerConnectionError(
|
|
2298
|
+
`Cannot connect to dev server at http://localhost:${port}`,
|
|
2299
|
+
`http://localhost:${port}`
|
|
2300
|
+
);
|
|
2301
|
+
}
|
|
2302
|
+
const segments = await client.getSegments();
|
|
2303
|
+
if (segments.length === 0) {
|
|
2304
|
+
if (json) {
|
|
2305
|
+
console.log(JSON.stringify({ error: "No fragments found", components: [] }));
|
|
2306
|
+
} else {
|
|
2307
|
+
console.log(pc11.yellow("No fragments found.\n"));
|
|
2308
|
+
}
|
|
2309
|
+
return {
|
|
2310
|
+
totalComponents: 0,
|
|
2311
|
+
averageCompliance: 100,
|
|
2312
|
+
worstOffenders: [],
|
|
2313
|
+
components: [],
|
|
2314
|
+
stats: {
|
|
2315
|
+
totalHardcoded: 0,
|
|
2316
|
+
totalTokenMismatches: 0,
|
|
2317
|
+
totalProperties: 0
|
|
2318
|
+
}
|
|
2319
|
+
};
|
|
2320
|
+
}
|
|
2321
|
+
if (!json) {
|
|
2322
|
+
console.log(pc11.dim(`Auditing ${segments.length} component(s)...
|
|
2323
|
+
`));
|
|
2324
|
+
}
|
|
2325
|
+
for (const seg of segments) {
|
|
2326
|
+
try {
|
|
2327
|
+
const complianceResult = await client.getCompliance({
|
|
2328
|
+
component: seg.name
|
|
2329
|
+
});
|
|
2330
|
+
audits.push({
|
|
2331
|
+
name: seg.name,
|
|
2332
|
+
category: seg.category || "uncategorized",
|
|
2333
|
+
compliance: complianceResult.compliance,
|
|
2334
|
+
hardcoded: complianceResult.hardcoded,
|
|
2335
|
+
tokenMismatches: complianceResult.violations.filter(
|
|
2336
|
+
(v) => v.issue.includes("mismatch")
|
|
2337
|
+
).length,
|
|
2338
|
+
totalProperties: complianceResult.totalProperties
|
|
2339
|
+
});
|
|
2340
|
+
} catch (error) {
|
|
2341
|
+
audits.push({
|
|
2342
|
+
name: seg.name,
|
|
2343
|
+
category: seg.category || "uncategorized",
|
|
2344
|
+
compliance: 0,
|
|
2345
|
+
hardcoded: 0,
|
|
2346
|
+
tokenMismatches: 0,
|
|
2347
|
+
totalProperties: 0
|
|
2348
|
+
});
|
|
2349
|
+
}
|
|
2350
|
+
}
|
|
2351
|
+
audits.sort((a, b) => {
|
|
2352
|
+
switch (sort) {
|
|
2353
|
+
case "name":
|
|
2354
|
+
return a.name.localeCompare(b.name);
|
|
2355
|
+
case "hardcoded":
|
|
2356
|
+
return b.hardcoded - a.hardcoded;
|
|
2357
|
+
// Most hardcoded first
|
|
2358
|
+
case "compliance":
|
|
2359
|
+
default:
|
|
2360
|
+
return a.compliance - b.compliance;
|
|
2361
|
+
}
|
|
2362
|
+
});
|
|
2363
|
+
const summary = {
|
|
2364
|
+
totalComponents: audits.length,
|
|
2365
|
+
averageCompliance: audits.length > 0 ? Math.round(audits.reduce((sum, a) => sum + a.compliance, 0) / audits.length * 100) / 100 : 100,
|
|
2366
|
+
worstOffenders: audits.slice(0, 5),
|
|
2367
|
+
components: audits,
|
|
2368
|
+
stats: {
|
|
2369
|
+
totalHardcoded: audits.reduce((sum, a) => sum + a.hardcoded, 0),
|
|
2370
|
+
totalTokenMismatches: audits.reduce((sum, a) => sum + a.tokenMismatches, 0),
|
|
2371
|
+
totalProperties: audits.reduce((sum, a) => sum + a.totalProperties, 0)
|
|
2372
|
+
}
|
|
2373
|
+
};
|
|
2374
|
+
if (json) {
|
|
2375
|
+
console.log(JSON.stringify(summary, null, 2));
|
|
2376
|
+
} else {
|
|
2377
|
+
console.log(pc11.bold("Component".padEnd(30) + "Compliance".padEnd(12) + "Hardcoded".padEnd(12) + "Properties"));
|
|
2378
|
+
console.log(pc11.dim("\u2500".repeat(66)));
|
|
2379
|
+
for (const auditItem of audits) {
|
|
2380
|
+
const complianceColor = auditItem.compliance >= 90 ? pc11.green : auditItem.compliance >= 70 ? pc11.yellow : pc11.red;
|
|
2381
|
+
console.log(
|
|
2382
|
+
auditItem.name.padEnd(30) + complianceColor(`${auditItem.compliance}%`.padEnd(12)) + String(auditItem.hardcoded).padEnd(12) + String(auditItem.totalProperties)
|
|
2383
|
+
);
|
|
2384
|
+
}
|
|
2385
|
+
console.log(pc11.dim("\u2500".repeat(66)));
|
|
2386
|
+
console.log();
|
|
2387
|
+
console.log(pc11.bold("Summary:"));
|
|
2388
|
+
console.log(` Total components: ${audits.length}`);
|
|
2389
|
+
console.log(` Average compliance: ${summary.averageCompliance}%`);
|
|
2390
|
+
console.log(` Total hardcoded values: ${summary.stats.totalHardcoded}`);
|
|
2391
|
+
console.log(` Total properties checked: ${summary.stats.totalProperties}`);
|
|
2392
|
+
if (audits.some((a) => a.compliance < 100)) {
|
|
2393
|
+
console.log();
|
|
2394
|
+
console.log(pc11.dim(`Run ${pc11.cyan(`${BRAND.cliCommand} verify <component>`)} for detailed compliance info.`));
|
|
2395
|
+
}
|
|
2396
|
+
console.log();
|
|
2397
|
+
}
|
|
2398
|
+
return summary;
|
|
2399
|
+
}
|
|
2400
|
+
|
|
2401
|
+
// src/commands/a11y.ts
|
|
2402
|
+
import pc12 from "picocolors";
|
|
2403
|
+
async function a11y(options = {}) {
|
|
2404
|
+
const { config: configPath, json = false, ci = false, component, port = 6006 } = options;
|
|
2405
|
+
await loadConfig(configPath);
|
|
2406
|
+
const client = createDevServerClient(port);
|
|
2407
|
+
const componentResults = [];
|
|
2408
|
+
if (!json) {
|
|
2409
|
+
console.log(pc12.cyan(`
|
|
2410
|
+
${BRAND.name} Accessibility Report
|
|
2411
|
+
`));
|
|
2412
|
+
}
|
|
2413
|
+
const isReachable = await client.ping();
|
|
2414
|
+
if (!isReachable) {
|
|
2415
|
+
throw new DevServerConnectionError(
|
|
2416
|
+
`Cannot connect to dev server at http://localhost:${port}`,
|
|
2417
|
+
`http://localhost:${port}`
|
|
2418
|
+
);
|
|
2419
|
+
}
|
|
2420
|
+
const segments = await client.getSegments();
|
|
2421
|
+
if (segments.length === 0) {
|
|
2422
|
+
if (json) {
|
|
2423
|
+
console.log(JSON.stringify({ error: "No fragments found", components: [] }));
|
|
2424
|
+
} else {
|
|
2425
|
+
console.log(pc12.yellow("No fragments found.\n"));
|
|
2426
|
+
}
|
|
2427
|
+
return {
|
|
2428
|
+
totalComponents: 0,
|
|
2429
|
+
accessibleComponents: 0,
|
|
2430
|
+
accessiblePercent: 100,
|
|
2431
|
+
components: [],
|
|
2432
|
+
totalViolations: 0,
|
|
2433
|
+
totalCritical: 0,
|
|
2434
|
+
totalSerious: 0,
|
|
2435
|
+
totalModerate: 0,
|
|
2436
|
+
totalMinor: 0,
|
|
2437
|
+
passed: true
|
|
2438
|
+
};
|
|
2439
|
+
}
|
|
2440
|
+
const componentsToCheck = component ? segments.filter((s) => s.name.toLowerCase() === component.toLowerCase()) : segments;
|
|
2441
|
+
if (component && componentsToCheck.length === 0) {
|
|
2442
|
+
const error = `Component '${component}' not found. Available: ${segments.map((s) => s.name).join(", ")}`;
|
|
2443
|
+
if (json) {
|
|
2444
|
+
console.log(JSON.stringify({ error }));
|
|
2445
|
+
} else {
|
|
2446
|
+
console.log(pc12.red(error));
|
|
2447
|
+
}
|
|
2448
|
+
throw new Error(error);
|
|
2449
|
+
}
|
|
2450
|
+
if (!json) {
|
|
2451
|
+
console.log(pc12.dim(`Checking ${componentsToCheck.length} component(s) for accessibility issues...
|
|
2452
|
+
`));
|
|
2453
|
+
}
|
|
2454
|
+
for (const seg of componentsToCheck) {
|
|
2455
|
+
try {
|
|
2456
|
+
const a11yResult = await client.getA11y(seg.name);
|
|
2457
|
+
let totalViolations2 = 0;
|
|
2458
|
+
let totalCritical2 = 0;
|
|
2459
|
+
let totalSerious2 = 0;
|
|
2460
|
+
for (const result of a11yResult.results) {
|
|
2461
|
+
totalViolations2 += result.summary.total;
|
|
2462
|
+
totalCritical2 += result.summary.critical;
|
|
2463
|
+
totalSerious2 += result.summary.serious;
|
|
2464
|
+
}
|
|
2465
|
+
let status = "PASS";
|
|
2466
|
+
if (totalCritical2 > 0 || totalSerious2 > 0) {
|
|
2467
|
+
status = "FAIL";
|
|
2468
|
+
} else if (totalViolations2 > 0) {
|
|
2469
|
+
status = "WARN";
|
|
2470
|
+
}
|
|
2471
|
+
componentResults.push({
|
|
2472
|
+
component: seg.name,
|
|
2473
|
+
results: a11yResult.results,
|
|
2474
|
+
status,
|
|
2475
|
+
totalViolations: totalViolations2,
|
|
2476
|
+
totalCritical: totalCritical2,
|
|
2477
|
+
totalSerious: totalSerious2
|
|
2478
|
+
});
|
|
2479
|
+
} catch (error) {
|
|
2480
|
+
componentResults.push({
|
|
2481
|
+
component: seg.name,
|
|
2482
|
+
results: [],
|
|
2483
|
+
status: "FAIL",
|
|
2484
|
+
totalViolations: 0,
|
|
2485
|
+
totalCritical: 0,
|
|
2486
|
+
totalSerious: 0
|
|
2487
|
+
});
|
|
2488
|
+
}
|
|
2489
|
+
}
|
|
2490
|
+
const accessibleComponents = componentResults.filter((c) => c.status !== "FAIL").length;
|
|
2491
|
+
const totalViolations = componentResults.reduce((sum, c) => sum + c.totalViolations, 0);
|
|
2492
|
+
const totalCritical = componentResults.reduce((sum, c) => sum + c.totalCritical, 0);
|
|
2493
|
+
const totalSerious = componentResults.reduce((sum, c) => sum + c.totalSerious, 0);
|
|
2494
|
+
let totalModerate = 0;
|
|
2495
|
+
let totalMinor = 0;
|
|
2496
|
+
for (const comp of componentResults) {
|
|
2497
|
+
for (const result of comp.results) {
|
|
2498
|
+
totalModerate += result.summary.moderate;
|
|
2499
|
+
totalMinor += result.summary.minor;
|
|
2500
|
+
}
|
|
2501
|
+
}
|
|
2502
|
+
const summary = {
|
|
2503
|
+
totalComponents: componentResults.length,
|
|
2504
|
+
accessibleComponents,
|
|
2505
|
+
accessiblePercent: componentResults.length > 0 ? Math.round(accessibleComponents / componentResults.length * 100) : 100,
|
|
2506
|
+
components: componentResults,
|
|
2507
|
+
totalViolations,
|
|
2508
|
+
totalCritical,
|
|
2509
|
+
totalSerious,
|
|
2510
|
+
totalModerate,
|
|
2511
|
+
totalMinor,
|
|
2512
|
+
passed: totalCritical === 0 && totalSerious === 0
|
|
2513
|
+
};
|
|
2514
|
+
if (json) {
|
|
2515
|
+
console.log(JSON.stringify(summary, null, 2));
|
|
2516
|
+
} else {
|
|
2517
|
+
console.log(pc12.bold(
|
|
2518
|
+
"Component".padEnd(20) + "Variants".padEnd(10) + "Violations".padEnd(12) + "Critical".padEnd(10) + "Serious".padEnd(10) + "Status"
|
|
2519
|
+
));
|
|
2520
|
+
console.log(pc12.dim("\u2500".repeat(72)));
|
|
2521
|
+
for (const result of componentResults) {
|
|
2522
|
+
const statusColor = result.status === "PASS" ? pc12.green : result.status === "WARN" ? pc12.yellow : pc12.red;
|
|
2523
|
+
const variantCount = result.results.length || 1;
|
|
2524
|
+
console.log(
|
|
2525
|
+
result.component.padEnd(20) + String(variantCount).padEnd(10) + String(result.totalViolations).padEnd(12) + String(result.totalCritical).padEnd(10) + String(result.totalSerious).padEnd(10) + statusColor(result.status)
|
|
2526
|
+
);
|
|
2527
|
+
}
|
|
2528
|
+
console.log(pc12.dim("\u2500".repeat(72)));
|
|
2529
|
+
console.log();
|
|
2530
|
+
console.log(pc12.bold("Summary:"));
|
|
2531
|
+
console.log(` ${accessibleComponents}/${componentResults.length} components accessible (${summary.accessiblePercent}%)`);
|
|
2532
|
+
console.log(` Total violations: ${totalViolations} (${totalCritical} critical, ${totalSerious} serious, ${totalModerate} moderate, ${totalMinor} minor)`);
|
|
2533
|
+
if (!summary.passed) {
|
|
2534
|
+
console.log();
|
|
2535
|
+
console.log(pc12.red("\u2717 Accessibility check failed - critical/serious violations found"));
|
|
2536
|
+
} else if (totalViolations > 0) {
|
|
2537
|
+
console.log();
|
|
2538
|
+
console.log(pc12.yellow("\u26A0 Minor/moderate violations found - consider fixing for better accessibility"));
|
|
2539
|
+
} else {
|
|
2540
|
+
console.log();
|
|
2541
|
+
console.log(pc12.green("\u2713 All components pass accessibility checks"));
|
|
2542
|
+
}
|
|
2543
|
+
console.log();
|
|
2544
|
+
}
|
|
2545
|
+
if (ci && !summary.passed) {
|
|
2546
|
+
throw new Error(`Accessibility check failed: ${totalCritical} critical, ${totalSerious} serious violations found`);
|
|
2547
|
+
}
|
|
2548
|
+
return summary;
|
|
2549
|
+
}
|
|
2550
|
+
|
|
2551
|
+
// src/commands/storygen.ts
|
|
2552
|
+
import { writeFile as writeFile3, mkdir as mkdir3 } from "fs/promises";
|
|
2553
|
+
import { resolve as resolve3, join as join5, relative as relative3 } from "path";
|
|
2554
|
+
import pc13 from "picocolors";
|
|
2555
|
+
async function storygen(options = {}) {
|
|
2556
|
+
const { config: configPath, output = ".storybook/generated", watch = false } = options;
|
|
2557
|
+
const { config, configDir } = await loadConfig(configPath);
|
|
2558
|
+
console.log(pc13.cyan(`
|
|
2559
|
+
${BRAND.name} Story Generator
|
|
2560
|
+
`));
|
|
2561
|
+
const segmentFiles = await discoverSegmentFiles(config, configDir);
|
|
2562
|
+
if (segmentFiles.length === 0) {
|
|
2563
|
+
console.log(pc13.yellow("No segment files found.\n"));
|
|
2564
|
+
return { success: true, generated: 0, outputDir: output };
|
|
2565
|
+
}
|
|
2566
|
+
const outputDir = resolve3(configDir, output);
|
|
2567
|
+
await mkdir3(outputDir, { recursive: true });
|
|
2568
|
+
let generated = 0;
|
|
2569
|
+
const generateStory = async (file) => {
|
|
2570
|
+
try {
|
|
2571
|
+
const segment = await loadSegmentFile(file.absolutePath);
|
|
2572
|
+
if (!segment) return false;
|
|
2573
|
+
const storyContent = generateCSF3Story(segment, file.relativePath);
|
|
2574
|
+
const storyName = `${segment.meta.name}.stories.tsx`;
|
|
2575
|
+
const storyPath = join5(outputDir, storyName);
|
|
2576
|
+
await writeFile3(storyPath, storyContent);
|
|
2577
|
+
console.log(`${pc13.green("\u2713")} Generated ${storyName}`);
|
|
2578
|
+
return true;
|
|
2579
|
+
} catch (error) {
|
|
2580
|
+
console.log(`${pc13.red("\u2717")} Failed: ${file.relativePath} - ${error instanceof Error ? error.message : error}`);
|
|
2581
|
+
return false;
|
|
2582
|
+
}
|
|
2583
|
+
};
|
|
2584
|
+
console.log(pc13.dim(`Generating stories to ${relative3(process.cwd(), outputDir)}/
|
|
2585
|
+
`));
|
|
2586
|
+
for (const file of segmentFiles) {
|
|
2587
|
+
if (await generateStory(file)) {
|
|
2588
|
+
generated++;
|
|
2589
|
+
}
|
|
2590
|
+
}
|
|
2591
|
+
console.log();
|
|
2592
|
+
console.log(pc13.green(`\u2713 Generated ${generated} story file(s)
|
|
2593
|
+
`));
|
|
2594
|
+
if (watch) {
|
|
2595
|
+
console.log(pc13.dim("Watching for segment changes... (Ctrl+C to stop)\n"));
|
|
2596
|
+
const chokidar = await import("chokidar");
|
|
2597
|
+
const patterns = segmentFiles.map((f) => f.absolutePath);
|
|
2598
|
+
const watcher = chokidar.watch(patterns, {
|
|
2599
|
+
ignoreInitial: true,
|
|
2600
|
+
awaitWriteFinish: { stabilityThreshold: 100 }
|
|
2601
|
+
});
|
|
2602
|
+
watcher.on("change", async (changedPath) => {
|
|
2603
|
+
const file = segmentFiles.find((f) => f.absolutePath === changedPath);
|
|
2604
|
+
if (file) {
|
|
2605
|
+
console.log(pc13.dim(`
|
|
2606
|
+
Changed: ${relative3(process.cwd(), changedPath)}`));
|
|
2607
|
+
await generateStory(file);
|
|
2608
|
+
}
|
|
2609
|
+
});
|
|
2610
|
+
watcher.on("add", async (addedPath) => {
|
|
2611
|
+
console.log(pc13.dim(`
|
|
2612
|
+
Added: ${relative3(process.cwd(), addedPath)}`));
|
|
2613
|
+
const newFiles = await discoverSegmentFiles(config, configDir);
|
|
2614
|
+
const file = newFiles.find((f) => f.absolutePath === addedPath);
|
|
2615
|
+
if (file) {
|
|
2616
|
+
segmentFiles.push(file);
|
|
2617
|
+
await generateStory(file);
|
|
2618
|
+
}
|
|
2619
|
+
});
|
|
2620
|
+
await new Promise(() => {
|
|
2621
|
+
});
|
|
2622
|
+
}
|
|
2623
|
+
return { success: true, generated, outputDir };
|
|
2624
|
+
}
|
|
2625
|
+
function generateCSF3Story(segment, relativePath) {
|
|
2626
|
+
const { meta, variants, props, usage } = segment;
|
|
2627
|
+
const componentName = meta.name;
|
|
2628
|
+
const argTypes = [];
|
|
2629
|
+
if (props) {
|
|
2630
|
+
for (const [propName, propDef] of Object.entries(props)) {
|
|
2631
|
+
let controlType = "text";
|
|
2632
|
+
let controlOptions = "";
|
|
2633
|
+
if (propDef.type === "enum" && propDef.values) {
|
|
2634
|
+
controlType = "select";
|
|
2635
|
+
controlOptions = `,
|
|
2636
|
+
options: ${JSON.stringify(propDef.values)}`;
|
|
2637
|
+
} else if (propDef.type === "boolean") {
|
|
2638
|
+
controlType = "boolean";
|
|
2639
|
+
} else if (propDef.type === "number") {
|
|
2640
|
+
controlType = "number";
|
|
2641
|
+
}
|
|
2642
|
+
argTypes.push(` ${propName}: {
|
|
2643
|
+
control: '${controlType}'${controlOptions},
|
|
2644
|
+
description: ${JSON.stringify(propDef.description || "")},
|
|
2645
|
+
${propDef.default !== void 0 ? `defaultValue: ${JSON.stringify(propDef.default)},` : ""}
|
|
2646
|
+
}`);
|
|
2647
|
+
}
|
|
2648
|
+
}
|
|
2649
|
+
const storyExports = [];
|
|
2650
|
+
for (const variant of variants) {
|
|
2651
|
+
const storyName = variant.name.replace(/[^a-zA-Z0-9]/g, "");
|
|
2652
|
+
storyExports.push(`
|
|
2653
|
+
export const ${storyName}: Story = {
|
|
2654
|
+
name: ${JSON.stringify(variant.name)},
|
|
2655
|
+
${variant.description ? `parameters: { docs: { description: { story: ${JSON.stringify(variant.description)} } } },` : ""}
|
|
2656
|
+
};`);
|
|
2657
|
+
}
|
|
2658
|
+
const usageDoc = usage ? `When to use:
|
|
2659
|
+
${usage.when?.map((w) => `- ${w}`).join("\n") || ""}
|
|
2660
|
+
|
|
2661
|
+
When not to use:
|
|
2662
|
+
${usage.whenNot?.map((w) => `- ${w}`).join("\n") || ""}` : "";
|
|
2663
|
+
return `/**
|
|
2664
|
+
* Auto-generated Storybook stories from ${relativePath}
|
|
2665
|
+
*
|
|
2666
|
+
* DO NOT EDIT - regenerate with: segments storygen
|
|
2667
|
+
*/
|
|
2668
|
+
|
|
2669
|
+
import type { Meta, StoryObj } from '@storybook/react';
|
|
2670
|
+
import { ${componentName} } from '${relativePath.replace(/\.segment\.tsx$/, "/index.js")}';
|
|
2671
|
+
|
|
2672
|
+
const meta: Meta<typeof ${componentName}> = {
|
|
2673
|
+
title: '${meta.category ? `${meta.category.charAt(0).toUpperCase() + meta.category.slice(1)}/` : ""}${componentName}',
|
|
2674
|
+
component: ${componentName},
|
|
2675
|
+
tags: ['autodocs'],
|
|
2676
|
+
parameters: {
|
|
2677
|
+
docs: {
|
|
2678
|
+
description: {
|
|
2679
|
+
component: ${JSON.stringify(meta.description || "")},
|
|
2680
|
+
},
|
|
2681
|
+
},
|
|
2682
|
+
},
|
|
2683
|
+
argTypes: {
|
|
2684
|
+
${argTypes.join(",\n")}
|
|
2685
|
+
},
|
|
2686
|
+
};
|
|
2687
|
+
|
|
2688
|
+
export default meta;
|
|
2689
|
+
type Story = StoryObj<typeof meta>;
|
|
2690
|
+
${storyExports.join("\n")}
|
|
2691
|
+
`;
|
|
2692
|
+
}
|
|
2693
|
+
|
|
2694
|
+
// src/commands/metrics.ts
|
|
2695
|
+
import pc14 from "picocolors";
|
|
2696
|
+
async function metrics(component, options = {}) {
|
|
2697
|
+
const { config: configPath, days = 30, json = false } = options;
|
|
2698
|
+
const { configDir } = await loadConfig(configPath);
|
|
2699
|
+
const store = createMetricsStore(configDir);
|
|
2700
|
+
console.log(pc14.cyan(`
|
|
2701
|
+
${BRAND.name} Compliance Metrics
|
|
2702
|
+
`));
|
|
2703
|
+
const trend = await store.getTrend(component || "all", {
|
|
2704
|
+
days,
|
|
2705
|
+
groupBy: days > 14 ? "week" : "day"
|
|
2706
|
+
});
|
|
2707
|
+
if (json) {
|
|
2708
|
+
console.log(JSON.stringify(trend, null, 2));
|
|
2709
|
+
return { success: true, trend };
|
|
2710
|
+
}
|
|
2711
|
+
const title = component ? `Component: ${component}` : "System-wide";
|
|
2712
|
+
console.log(pc14.bold(title));
|
|
2713
|
+
console.log(pc14.dim(`Last ${days} days
|
|
2714
|
+
`));
|
|
2715
|
+
if (trend.dataPoints.length === 0) {
|
|
2716
|
+
console.log(pc14.yellow("No metrics data found.\n"));
|
|
2717
|
+
console.log(pc14.dim("Metrics are recorded automatically when running verification commands."));
|
|
2718
|
+
console.log(pc14.dim(`Try running: ${pc14.cyan(`${BRAND.cliCommand} verify --ci`)}
|
|
2719
|
+
`));
|
|
2720
|
+
return { success: true, trend };
|
|
2721
|
+
}
|
|
2722
|
+
const sparkline = store.generateSparkline(trend.dataPoints);
|
|
2723
|
+
console.log(pc14.bold("Trend: ") + sparkline);
|
|
2724
|
+
console.log();
|
|
2725
|
+
const trendColor = trend.trend === "improving" ? pc14.green : trend.trend === "declining" ? pc14.red : pc14.dim;
|
|
2726
|
+
const trendIcon = trend.trend === "improving" ? "\u2191" : trend.trend === "declining" ? "\u2193" : "\u2192";
|
|
2727
|
+
console.log(` Average compliance: ${pc14.bold(`${trend.averageCompliance}%`)}`);
|
|
2728
|
+
console.log(` Direction: ${trendColor(`${trendIcon} ${trend.trend}`)}`);
|
|
2729
|
+
console.log(` Data points: ${trend.dataPoints.length}`);
|
|
2730
|
+
console.log();
|
|
2731
|
+
const recent = trend.dataPoints.slice(-5);
|
|
2732
|
+
if (recent.length > 0) {
|
|
2733
|
+
console.log(pc14.dim("Recent data:"));
|
|
2734
|
+
for (const point of recent) {
|
|
2735
|
+
const complianceColor = point.compliance >= 90 ? pc14.green : point.compliance >= 70 ? pc14.yellow : pc14.red;
|
|
2736
|
+
console.log(` ${pc14.dim(point.date)} ${complianceColor(`${point.compliance}%`)} ${pc14.dim(`(${point.violations} violations)`)}`);
|
|
2737
|
+
}
|
|
2738
|
+
console.log();
|
|
2739
|
+
}
|
|
2740
|
+
return { success: true, trend };
|
|
2741
|
+
}
|
|
2742
|
+
|
|
2743
|
+
// src/commands/baseline.ts
|
|
2744
|
+
import { readdir as readdir3, rm as rm2 } from "fs/promises";
|
|
2745
|
+
import { join as join6, relative as relative4 } from "path";
|
|
2746
|
+
import pc15 from "picocolors";
|
|
2747
|
+
async function baseline(action, component, options = {}) {
|
|
2748
|
+
const { config: configPath, variant, all = false, theme = "light", port = 6006 } = options;
|
|
2749
|
+
const { config, configDir } = await loadConfig(configPath);
|
|
2750
|
+
const storage = new StorageManager({
|
|
2751
|
+
projectRoot: configDir,
|
|
2752
|
+
viewport: config.screenshots?.viewport
|
|
2753
|
+
});
|
|
2754
|
+
await storage.initialize();
|
|
2755
|
+
console.log(pc15.cyan(`
|
|
2756
|
+
${BRAND.name} Baseline Manager
|
|
2757
|
+
`));
|
|
2758
|
+
const baseUrl = `http://localhost:${port}`;
|
|
2759
|
+
switch (action) {
|
|
2760
|
+
case "update":
|
|
2761
|
+
return updateBaseline(component, options, config, configDir, baseUrl);
|
|
2762
|
+
case "list":
|
|
2763
|
+
return listBaselines(configDir);
|
|
2764
|
+
case "delete":
|
|
2765
|
+
return deleteBaseline(component, options, configDir);
|
|
2766
|
+
default:
|
|
2767
|
+
console.log(pc15.red(`Unknown action: ${action}`));
|
|
2768
|
+
console.log(pc15.dim("Available actions: update, list, delete\n"));
|
|
2769
|
+
process.exit(1);
|
|
2770
|
+
}
|
|
2771
|
+
}
|
|
2772
|
+
async function updateBaseline(component, options, config, configDir, baseUrl) {
|
|
2773
|
+
const { variant, all = false, theme = "light" } = options;
|
|
2774
|
+
if (!component && !all) {
|
|
2775
|
+
const { select } = await import("@inquirer/prompts");
|
|
2776
|
+
const contextResp = await fetch(`${baseUrl}/fragments/context?format=json`);
|
|
2777
|
+
if (!contextResp.ok) {
|
|
2778
|
+
throw new Error("Failed to fetch fragments. Make sure dev server is running.");
|
|
2779
|
+
}
|
|
2780
|
+
const contextData = JSON.parse(await contextResp.text());
|
|
2781
|
+
const segments = contextData.components || [];
|
|
2782
|
+
if (segments.length === 0) {
|
|
2783
|
+
console.log(pc15.yellow("No components found.\n"));
|
|
2784
|
+
return { success: true, action: "update", count: 0 };
|
|
2785
|
+
}
|
|
2786
|
+
component = await select({
|
|
2787
|
+
message: "Select component to update:",
|
|
2788
|
+
choices: segments.map((s) => ({
|
|
2789
|
+
name: s.name,
|
|
2790
|
+
value: s.name
|
|
2791
|
+
}))
|
|
2792
|
+
});
|
|
2793
|
+
}
|
|
2794
|
+
if (all) {
|
|
2795
|
+
console.log(pc15.dim("Updating all baselines...\n"));
|
|
2796
|
+
await runScreenshotCommand(config, configDir, {
|
|
2797
|
+
theme
|
|
2798
|
+
});
|
|
2799
|
+
console.log(pc15.green("\n\u2713 All baselines updated\n"));
|
|
2800
|
+
} else if (component) {
|
|
2801
|
+
console.log(pc15.dim(`Updating baselines for ${component}...
|
|
2802
|
+
`));
|
|
2803
|
+
await runScreenshotCommand(config, configDir, {
|
|
2804
|
+
component,
|
|
2805
|
+
variant,
|
|
2806
|
+
theme
|
|
2807
|
+
});
|
|
2808
|
+
console.log(pc15.green(`
|
|
2809
|
+
\u2713 Baselines updated for ${component}
|
|
2810
|
+
`));
|
|
2811
|
+
}
|
|
2812
|
+
const baselinesDir = join6(configDir, BRAND.dataDir, "baselines");
|
|
2813
|
+
console.log(pc15.dim(`Baselines directory: ${relative4(process.cwd(), baselinesDir)}
|
|
2814
|
+
`));
|
|
2815
|
+
return { success: true, action: "update" };
|
|
2816
|
+
}
|
|
2817
|
+
async function listBaselines(configDir) {
|
|
2818
|
+
const baselinesDir = join6(configDir, BRAND.dataDir, "baselines");
|
|
2819
|
+
try {
|
|
2820
|
+
const files = await readdir3(baselinesDir, { recursive: true });
|
|
2821
|
+
const pngFiles = files.filter((f) => f.endsWith(".png"));
|
|
2822
|
+
if (pngFiles.length === 0) {
|
|
2823
|
+
console.log(pc15.yellow("No baselines found.\n"));
|
|
2824
|
+
console.log(pc15.dim(`Run ${pc15.cyan(`${BRAND.cliCommand} screenshot`)} to capture baselines.
|
|
2825
|
+
`));
|
|
2826
|
+
return { success: true, action: "list", count: 0 };
|
|
2827
|
+
}
|
|
2828
|
+
console.log(pc15.bold("Baselines:\n"));
|
|
2829
|
+
for (const file of pngFiles) {
|
|
2830
|
+
console.log(` ${file}`);
|
|
2831
|
+
}
|
|
2832
|
+
console.log();
|
|
2833
|
+
console.log(pc15.dim(`Total: ${pngFiles.length} baseline(s)
|
|
2834
|
+
`));
|
|
2835
|
+
return { success: true, action: "list", count: pngFiles.length };
|
|
2836
|
+
} catch {
|
|
2837
|
+
console.log(pc15.yellow("No baselines directory found.\n"));
|
|
2838
|
+
console.log(pc15.dim(`Run ${pc15.cyan(`${BRAND.cliCommand} screenshot`)} to capture baselines.
|
|
2839
|
+
`));
|
|
2840
|
+
return { success: true, action: "list", count: 0 };
|
|
2841
|
+
}
|
|
2842
|
+
}
|
|
2843
|
+
async function deleteBaseline(component, options, configDir) {
|
|
2844
|
+
const { all = false } = options;
|
|
2845
|
+
const baselinesDir = join6(configDir, BRAND.dataDir, "baselines");
|
|
2846
|
+
if (all) {
|
|
2847
|
+
const { confirm } = await import("@inquirer/prompts");
|
|
2848
|
+
const confirmed = await confirm({
|
|
2849
|
+
message: "Delete ALL baselines? This cannot be undone.",
|
|
2850
|
+
default: false
|
|
2851
|
+
});
|
|
2852
|
+
if (!confirmed) {
|
|
2853
|
+
console.log(pc15.dim("\nNo changes made.\n"));
|
|
2854
|
+
return { success: true, action: "delete", count: 0 };
|
|
2855
|
+
}
|
|
2856
|
+
await rm2(baselinesDir, { recursive: true, force: true });
|
|
2857
|
+
console.log(pc15.green("\n\u2713 All baselines deleted\n"));
|
|
2858
|
+
return { success: true, action: "delete" };
|
|
2859
|
+
} else if (component) {
|
|
2860
|
+
const componentDir = join6(baselinesDir, component);
|
|
2861
|
+
try {
|
|
2862
|
+
await rm2(componentDir, { recursive: true, force: true });
|
|
2863
|
+
console.log(pc15.green(`\u2713 Baselines deleted for ${component}
|
|
2864
|
+
`));
|
|
2865
|
+
return { success: true, action: "delete" };
|
|
2866
|
+
} catch {
|
|
2867
|
+
console.log(pc15.yellow(`No baselines found for ${component}.
|
|
2868
|
+
`));
|
|
2869
|
+
return { success: true, action: "delete", count: 0 };
|
|
2870
|
+
}
|
|
2871
|
+
} else {
|
|
2872
|
+
console.log(pc15.yellow("Specify a component name or use --all flag.\n"));
|
|
2873
|
+
return { success: false, action: "delete" };
|
|
2874
|
+
}
|
|
2875
|
+
}
|
|
2876
|
+
|
|
2877
|
+
// src/commands/add.ts
|
|
2878
|
+
import { writeFile as writeFile4, mkdir as mkdir5, access as access2 } from "fs/promises";
|
|
2879
|
+
import { resolve as resolve4, join as join7, relative as relative5 } from "path";
|
|
2880
|
+
import pc16 from "picocolors";
|
|
2881
|
+
async function add(name, options = {}) {
|
|
2882
|
+
console.log(pc16.cyan(`
|
|
2883
|
+
${BRAND.name} Component Scaffold
|
|
2884
|
+
`));
|
|
2885
|
+
let componentName = name;
|
|
2886
|
+
let category = options.category;
|
|
2887
|
+
let template = options.template;
|
|
2888
|
+
let dir = options.dir;
|
|
2889
|
+
const generateComponent = options.component !== false;
|
|
2890
|
+
if (!componentName || !category || !template || !dir) {
|
|
2891
|
+
const { input, select } = await import("@inquirer/prompts");
|
|
2892
|
+
try {
|
|
2893
|
+
if (!componentName) {
|
|
2894
|
+
componentName = await input({
|
|
2895
|
+
message: "Component name:",
|
|
2896
|
+
validate: (value) => {
|
|
2897
|
+
if (!value.trim()) return "Component name is required";
|
|
2898
|
+
if (!/^[A-Za-z][A-Za-z0-9]*$/.test(value)) return "Use PascalCase (e.g., Button, TextField)";
|
|
2899
|
+
return true;
|
|
2900
|
+
}
|
|
2901
|
+
});
|
|
2902
|
+
}
|
|
2903
|
+
if (!template) {
|
|
2904
|
+
template = await select({
|
|
2905
|
+
message: "What type of component is this?",
|
|
2906
|
+
choices: [
|
|
2907
|
+
{ name: "Display - Shows information or status", value: "display" },
|
|
2908
|
+
{ name: "Action - Triggers an action (button, link)", value: "action" },
|
|
2909
|
+
{ name: "Form Input - Accepts user input", value: "form-input" },
|
|
2910
|
+
{ name: "Layout - Organizes content structure", value: "layout" }
|
|
2911
|
+
],
|
|
2912
|
+
default: "display"
|
|
2913
|
+
});
|
|
2914
|
+
}
|
|
2915
|
+
if (!category) {
|
|
2916
|
+
category = await input({
|
|
2917
|
+
message: "Category:",
|
|
2918
|
+
default: template === "form-input" ? "forms" : template === "layout" ? "layout" : "components"
|
|
2919
|
+
});
|
|
2920
|
+
}
|
|
2921
|
+
if (!dir) {
|
|
2922
|
+
dir = await input({
|
|
2923
|
+
message: "Output directory:",
|
|
2924
|
+
default: "src/components"
|
|
2925
|
+
});
|
|
2926
|
+
}
|
|
2927
|
+
} catch {
|
|
2928
|
+
console.log(pc16.dim("\nNo changes made."));
|
|
2929
|
+
process.exit(0);
|
|
2930
|
+
}
|
|
2931
|
+
}
|
|
2932
|
+
componentName = componentName.charAt(0).toUpperCase() + componentName.slice(1);
|
|
2933
|
+
category = category || "components";
|
|
2934
|
+
template = template || "display";
|
|
2935
|
+
dir = dir || "src/components";
|
|
2936
|
+
const componentDir = resolve4(process.cwd(), dir, componentName);
|
|
2937
|
+
const componentFile = join7(componentDir, `${componentName}.tsx`);
|
|
2938
|
+
const segmentFile = join7(componentDir, `${componentName}${BRAND.fileExtension}`);
|
|
2939
|
+
const indexFile = join7(componentDir, "index.ts");
|
|
2940
|
+
try {
|
|
2941
|
+
await access2(componentDir);
|
|
2942
|
+
console.log(pc16.yellow(`Directory already exists: ${relative5(process.cwd(), componentDir)}`));
|
|
2943
|
+
console.log(pc16.dim("Use a different name or remove the existing directory."));
|
|
2944
|
+
process.exit(1);
|
|
2945
|
+
} catch {
|
|
2946
|
+
}
|
|
2947
|
+
await mkdir5(componentDir, { recursive: true });
|
|
2948
|
+
if (generateComponent) {
|
|
2949
|
+
const componentCode = generateComponentStub(componentName, template);
|
|
2950
|
+
await writeFile4(componentFile, componentCode);
|
|
2951
|
+
console.log(`${pc16.green("\u2713")} Created ${relative5(process.cwd(), componentFile)}`);
|
|
2952
|
+
}
|
|
2953
|
+
const segmentCode = generateSegmentStub(componentName, category, template);
|
|
2954
|
+
await writeFile4(segmentFile, segmentCode);
|
|
2955
|
+
console.log(`${pc16.green("\u2713")} Created ${relative5(process.cwd(), segmentFile)}`);
|
|
2956
|
+
const indexCode = `export { ${componentName} } from './${componentName}.js';
|
|
2957
|
+
`;
|
|
2958
|
+
await writeFile4(indexFile, indexCode);
|
|
2959
|
+
console.log(`${pc16.green("\u2713")} Created ${relative5(process.cwd(), indexFile)}`);
|
|
2960
|
+
console.log(pc16.green(`
|
|
2961
|
+
\u2713 Scaffolded ${componentName}
|
|
2962
|
+
`));
|
|
2963
|
+
console.log(pc16.dim("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
2964
|
+
console.log(pc16.bold("\nNext steps:"));
|
|
2965
|
+
if (generateComponent) {
|
|
2966
|
+
console.log(` 1. Implement ${componentName}.tsx`);
|
|
2967
|
+
}
|
|
2968
|
+
console.log(` 2. Fill in usage.when and usage.whenNot in the fragment file`);
|
|
2969
|
+
console.log(` 3. Add variants with different prop combinations`);
|
|
2970
|
+
console.log(` 4. Run ${pc16.cyan(`${BRAND.cliCommand} dev`)} to preview`);
|
|
2971
|
+
console.log();
|
|
2972
|
+
return {
|
|
2973
|
+
success: true,
|
|
2974
|
+
componentPath: generateComponent ? componentFile : void 0,
|
|
2975
|
+
segmentPath: segmentFile,
|
|
2976
|
+
indexPath: indexFile
|
|
2977
|
+
};
|
|
2978
|
+
}
|
|
2979
|
+
function generateComponentStub(name, _template) {
|
|
2980
|
+
const propsInterface = `export interface ${name}Props {
|
|
2981
|
+
/** Content to display */
|
|
2982
|
+
children?: React.ReactNode;
|
|
2983
|
+
/** Additional CSS classes */
|
|
2984
|
+
className?: string;
|
|
2985
|
+
}`;
|
|
2986
|
+
return `import React from 'react';
|
|
2987
|
+
|
|
2988
|
+
${propsInterface}
|
|
2989
|
+
|
|
2990
|
+
/**
|
|
2991
|
+
* ${name} component
|
|
2992
|
+
*
|
|
2993
|
+
* TODO: Implement this component
|
|
2994
|
+
*/
|
|
2995
|
+
export function ${name}({ children, className }: ${name}Props) {
|
|
2996
|
+
return (
|
|
2997
|
+
<div className={className}>
|
|
2998
|
+
{children}
|
|
2999
|
+
</div>
|
|
3000
|
+
);
|
|
3001
|
+
}
|
|
3002
|
+
`;
|
|
3003
|
+
}
|
|
3004
|
+
function generateSegmentStub(name, category, template) {
|
|
3005
|
+
const usageHints = {
|
|
3006
|
+
action: {
|
|
3007
|
+
when: ["User needs to trigger an action", "Form submission is required"],
|
|
3008
|
+
whenNot: ["Navigation is needed (use Link)", "Action is destructive without confirmation"]
|
|
3009
|
+
},
|
|
3010
|
+
"form-input": {
|
|
3011
|
+
when: ["User needs to enter data", "Form field is required"],
|
|
3012
|
+
whenNot: ["Display-only data is shown", "Rich text editing is needed"]
|
|
3013
|
+
},
|
|
3014
|
+
layout: {
|
|
3015
|
+
when: ["Content needs to be organized", "Responsive layout is required"],
|
|
3016
|
+
whenNot: ["Simple inline content is displayed", "Scroll container is needed"]
|
|
3017
|
+
},
|
|
3018
|
+
display: {
|
|
3019
|
+
when: ["Information needs to be presented", "Status or feedback is shown"],
|
|
3020
|
+
whenNot: ["User interaction is required", "Data input is needed"]
|
|
3021
|
+
}
|
|
3022
|
+
};
|
|
3023
|
+
const hints = usageHints[template] || usageHints.display;
|
|
3024
|
+
const scenarioTagHints = {
|
|
3025
|
+
action: ["action.primary", "action.secondary", "form.submit"],
|
|
3026
|
+
"form-input": ["form.input", "form.field", "form.text"],
|
|
3027
|
+
layout: ["layout.container", "layout.section", "content.group"],
|
|
3028
|
+
display: ["display.info", "display.status", "content.text"]
|
|
3029
|
+
};
|
|
3030
|
+
const scenarioTags = scenarioTagHints[template] || scenarioTagHints.display;
|
|
3031
|
+
return `import React from 'react';
|
|
3032
|
+
import { defineSegment } from '@fragments/core';
|
|
3033
|
+
import { ${name} } from './index.js';
|
|
3034
|
+
|
|
3035
|
+
export default defineSegment({
|
|
3036
|
+
component: ${name},
|
|
3037
|
+
|
|
3038
|
+
meta: {
|
|
3039
|
+
name: '${name}',
|
|
3040
|
+
description: 'TODO: Add description',
|
|
3041
|
+
category: '${category}',
|
|
3042
|
+
status: 'experimental',
|
|
3043
|
+
tags: ['${category}'],
|
|
3044
|
+
},
|
|
3045
|
+
|
|
3046
|
+
usage: {
|
|
3047
|
+
when: [
|
|
3048
|
+
'${hints.when[0]}',
|
|
3049
|
+
'${hints.when[1]}',
|
|
3050
|
+
],
|
|
3051
|
+
whenNot: [
|
|
3052
|
+
'${hints.whenNot[0]}',
|
|
3053
|
+
'${hints.whenNot[1]}',
|
|
3054
|
+
],
|
|
3055
|
+
guidelines: [
|
|
3056
|
+
// TODO: Add best practices
|
|
3057
|
+
],
|
|
3058
|
+
accessibility: [
|
|
3059
|
+
// TODO: Add accessibility guidelines
|
|
3060
|
+
],
|
|
3061
|
+
},
|
|
3062
|
+
|
|
3063
|
+
props: {
|
|
3064
|
+
children: {
|
|
3065
|
+
type: 'node',
|
|
3066
|
+
description: 'Content to display',
|
|
3067
|
+
},
|
|
3068
|
+
className: {
|
|
3069
|
+
type: 'string',
|
|
3070
|
+
description: 'Additional CSS classes',
|
|
3071
|
+
},
|
|
3072
|
+
// TODO: Add more props
|
|
3073
|
+
},
|
|
3074
|
+
|
|
3075
|
+
relations: [
|
|
3076
|
+
// TODO: Add related components
|
|
3077
|
+
// { component: 'RelatedComponent', relationship: 'alternative', note: 'Use for...' },
|
|
3078
|
+
],
|
|
3079
|
+
|
|
3080
|
+
contract: {
|
|
3081
|
+
propsSummary: [
|
|
3082
|
+
'children: ReactNode - content to display',
|
|
3083
|
+
'className: string - additional CSS classes',
|
|
3084
|
+
// TODO: Add prop summaries
|
|
3085
|
+
],
|
|
3086
|
+
scenarioTags: [
|
|
3087
|
+
'${scenarioTags[0]}',
|
|
3088
|
+
'${scenarioTags[1]}',
|
|
3089
|
+
// TODO: Add scenario tags for AI agent queries
|
|
3090
|
+
],
|
|
3091
|
+
a11yRules: [
|
|
3092
|
+
// TODO: Add accessibility rule IDs
|
|
3093
|
+
],
|
|
3094
|
+
},
|
|
3095
|
+
|
|
3096
|
+
variants: [
|
|
3097
|
+
{
|
|
3098
|
+
name: 'Default',
|
|
3099
|
+
description: 'Default ${name} appearance',
|
|
3100
|
+
render: () => <${name}>Example content</${name}>,
|
|
3101
|
+
},
|
|
3102
|
+
// TODO: Add more variants
|
|
3103
|
+
// {
|
|
3104
|
+
// name: 'WithProps',
|
|
3105
|
+
// description: '${name} with additional props',
|
|
3106
|
+
// render: () => <${name} someProp="value">Content</${name}>,
|
|
3107
|
+
// },
|
|
3108
|
+
],
|
|
3109
|
+
});
|
|
3110
|
+
`;
|
|
3111
|
+
}
|
|
3112
|
+
|
|
3113
|
+
// src/commands/link/figma.ts
|
|
3114
|
+
import { readFile as readFile4, writeFile as writeFile5 } from "fs/promises";
|
|
3115
|
+
import pc17 from "picocolors";
|
|
3116
|
+
async function linkFigma(figmaUrl, options = {}) {
|
|
3117
|
+
const { config: configPath, auto = false, dryRun = false, variants = true } = options;
|
|
3118
|
+
if (!process.env.FIGMA_ACCESS_TOKEN) {
|
|
3119
|
+
console.error(pc17.red("\nFIGMA_ACCESS_TOKEN environment variable required."));
|
|
3120
|
+
console.log(pc17.dim("Generate at: https://www.figma.com/developers/api#access-tokens"));
|
|
3121
|
+
console.log(pc17.dim(" export FIGMA_ACCESS_TOKEN=figd_xxx"));
|
|
3122
|
+
process.exit(1);
|
|
3123
|
+
}
|
|
3124
|
+
console.log(pc17.cyan(`
|
|
3125
|
+
${BRAND.name} Link Wizard
|
|
3126
|
+
`));
|
|
3127
|
+
const { config, configDir } = await loadConfig(configPath);
|
|
3128
|
+
let fileUrl = figmaUrl || config.figmaFile;
|
|
3129
|
+
if (!fileUrl) {
|
|
3130
|
+
const { input } = await import("@inquirer/prompts");
|
|
3131
|
+
console.log(pc17.dim("Tip: Add `figmaFile` to fragments.config.ts to skip this prompt\n"));
|
|
3132
|
+
try {
|
|
3133
|
+
fileUrl = await input({
|
|
3134
|
+
message: "Enter Figma file URL:",
|
|
3135
|
+
validate: (value) => {
|
|
3136
|
+
if (!value.trim()) return "URL is required";
|
|
3137
|
+
if (!value.includes("figma.com")) return "Please enter a valid Figma URL";
|
|
3138
|
+
return true;
|
|
3139
|
+
}
|
|
3140
|
+
});
|
|
3141
|
+
} catch {
|
|
3142
|
+
console.log(pc17.dim("\nNo changes made."));
|
|
3143
|
+
process.exit(0);
|
|
3144
|
+
}
|
|
3145
|
+
} else if (config.figmaFile && !figmaUrl) {
|
|
3146
|
+
console.log(pc17.dim(`Using Figma file from config: ${config.figmaFile}
|
|
3147
|
+
`));
|
|
3148
|
+
}
|
|
3149
|
+
const figmaClient = new FigmaClient({ accessToken: process.env.FIGMA_ACCESS_TOKEN });
|
|
3150
|
+
console.log(pc17.dim("Parsing Figma URL..."));
|
|
3151
|
+
const { fileKey } = figmaClient.parseFileUrl(fileUrl);
|
|
3152
|
+
console.log(pc17.dim("Fetching components from Figma..."));
|
|
3153
|
+
const figmaData = await figmaClient.getFileComponents(fileKey);
|
|
3154
|
+
const allFigmaComponents = figmaData.componentSets.length > 0 ? figmaData.componentSets : figmaData.components;
|
|
3155
|
+
if (allFigmaComponents.length === 0) {
|
|
3156
|
+
console.log(pc17.yellow("\nNo components found in Figma file."));
|
|
3157
|
+
console.log(pc17.dim("Make sure the file contains published components."));
|
|
3158
|
+
process.exit(0);
|
|
3159
|
+
}
|
|
3160
|
+
const componentType = figmaData.componentSets.length > 0 ? "component set" : "component";
|
|
3161
|
+
console.log(pc17.green(`\u2713 Found ${allFigmaComponents.length} Figma ${componentType}(s) in "${figmaData.fileName}"`));
|
|
3162
|
+
if (figmaData.components.length > 0 && figmaData.componentSets.length > 0) {
|
|
3163
|
+
console.log(pc17.dim(` (${figmaData.components.length} individual components also available)
|
|
3164
|
+
`));
|
|
3165
|
+
} else {
|
|
3166
|
+
console.log();
|
|
3167
|
+
}
|
|
3168
|
+
const segmentFiles = await discoverSegmentFiles(config, configDir);
|
|
3169
|
+
if (segmentFiles.length === 0) {
|
|
3170
|
+
console.log(pc17.yellow("No segment files found in codebase."));
|
|
3171
|
+
console.log(pc17.dim(`Looking for: ${config.include.join(", ")}`));
|
|
3172
|
+
process.exit(0);
|
|
3173
|
+
}
|
|
3174
|
+
console.log(pc17.dim(`Found ${segmentFiles.length} segment file(s)
|
|
3175
|
+
`));
|
|
3176
|
+
const segments = [];
|
|
3177
|
+
for (const file of segmentFiles) {
|
|
3178
|
+
try {
|
|
3179
|
+
const content = await readFile4(file.absolutePath, "utf-8");
|
|
3180
|
+
const nameMatch = content.match(/name:\s*['"]([^'"]+)['"]/);
|
|
3181
|
+
const hasFigma = /meta:\s*\{[^}]*figma:\s*['"]https?:/.test(content);
|
|
3182
|
+
const segmentVariants = extractVariants(content, nameMatch?.[1]);
|
|
3183
|
+
if (nameMatch) {
|
|
3184
|
+
segments.push({
|
|
3185
|
+
name: nameMatch[1],
|
|
3186
|
+
filePath: file.absolutePath,
|
|
3187
|
+
relativePath: file.relativePath,
|
|
3188
|
+
hasFigma,
|
|
3189
|
+
variants: segmentVariants
|
|
3190
|
+
});
|
|
3191
|
+
}
|
|
3192
|
+
} catch {
|
|
3193
|
+
}
|
|
3194
|
+
}
|
|
3195
|
+
const matches = [];
|
|
3196
|
+
const unmatchedSegments = [];
|
|
3197
|
+
for (const segment of segments) {
|
|
3198
|
+
let bestMatch = null;
|
|
3199
|
+
let bestScore = 0;
|
|
3200
|
+
for (const figmaComp of allFigmaComponents) {
|
|
3201
|
+
const score = calculateMatchScore(segment.name, figmaComp.name);
|
|
3202
|
+
if (score > bestScore) {
|
|
3203
|
+
bestMatch = figmaComp;
|
|
3204
|
+
bestScore = score;
|
|
3205
|
+
if (score === 100) break;
|
|
3206
|
+
}
|
|
3207
|
+
}
|
|
3208
|
+
if (bestMatch && bestScore >= 65) {
|
|
3209
|
+
const alreadyLinked = segment.hasFigma;
|
|
3210
|
+
if (alreadyLinked && !auto) {
|
|
3211
|
+
console.log(pc17.dim(`\u23ED\uFE0F ${segment.name} (already linked)`));
|
|
3212
|
+
}
|
|
3213
|
+
matches.push({ segment, figmaComponent: bestMatch, score: bestScore, alreadyLinked });
|
|
3214
|
+
} else {
|
|
3215
|
+
unmatchedSegments.push(segment);
|
|
3216
|
+
}
|
|
3217
|
+
}
|
|
3218
|
+
if (unmatchedSegments.length > 0) {
|
|
3219
|
+
console.log(pc17.dim("Unmatched segments:"));
|
|
3220
|
+
for (const seg of unmatchedSegments) {
|
|
3221
|
+
console.log(` ${pc17.dim("\u2022")} ${seg.name}`);
|
|
3222
|
+
}
|
|
3223
|
+
console.log();
|
|
3224
|
+
}
|
|
3225
|
+
const newMatches = matches.filter((m) => !m.alreadyLinked);
|
|
3226
|
+
const alreadyLinkedMatches = matches.filter((m) => m.alreadyLinked);
|
|
3227
|
+
if (matches.length === 0) {
|
|
3228
|
+
console.log(pc17.yellow("\nNo automatic matches found."));
|
|
3229
|
+
console.log(pc17.dim("You can manually add figma URLs to your segment definitions."));
|
|
3230
|
+
process.exit(0);
|
|
3231
|
+
}
|
|
3232
|
+
if (dryRun) {
|
|
3233
|
+
if (newMatches.length > 0) {
|
|
3234
|
+
console.log(pc17.bold("\nMatched Components:\n"));
|
|
3235
|
+
for (const match of newMatches) {
|
|
3236
|
+
const scoreColor = match.score === 100 ? pc17.green : pc17.yellow;
|
|
3237
|
+
console.log(
|
|
3238
|
+
` ${pc17.green("\u2713")} ${pc17.bold(match.segment.name)} \u2192 ${match.figmaComponent.name} ${scoreColor(`(${Math.round(match.score)}%)`)}`
|
|
3239
|
+
);
|
|
3240
|
+
}
|
|
3241
|
+
}
|
|
3242
|
+
console.log(pc17.yellow("\n[Dry run - no files were updated]"));
|
|
3243
|
+
return { success: true, updated: 0, variantUpdates: 0 };
|
|
3244
|
+
}
|
|
3245
|
+
let selectedMatches = newMatches;
|
|
3246
|
+
if (newMatches.length > 0) {
|
|
3247
|
+
if (auto) {
|
|
3248
|
+
console.log(pc17.bold("\nMatched Components:\n"));
|
|
3249
|
+
for (const match of newMatches) {
|
|
3250
|
+
const scoreColor = match.score === 100 ? pc17.green : pc17.yellow;
|
|
3251
|
+
console.log(
|
|
3252
|
+
` ${pc17.green("\u2713")} ${pc17.bold(match.segment.name)} \u2192 ${match.figmaComponent.name} ${scoreColor(`(${Math.round(match.score)}%)`)}`
|
|
3253
|
+
);
|
|
3254
|
+
}
|
|
3255
|
+
} else {
|
|
3256
|
+
const { checkbox } = await import("@inquirer/prompts");
|
|
3257
|
+
console.log(pc17.bold("\nMatched Components:\n"));
|
|
3258
|
+
console.log(pc17.dim("Use \u2191/\u2193 to navigate, Space to toggle, Enter to confirm\n"));
|
|
3259
|
+
const choices = newMatches.map((match) => {
|
|
3260
|
+
const scoreColor = match.score === 100 ? pc17.green : pc17.yellow;
|
|
3261
|
+
return {
|
|
3262
|
+
name: `${pc17.bold(match.segment.name)} \u2192 ${match.figmaComponent.name} ${scoreColor(`(${Math.round(match.score)}%)`)}`,
|
|
3263
|
+
value: match,
|
|
3264
|
+
checked: true
|
|
3265
|
+
};
|
|
3266
|
+
});
|
|
3267
|
+
try {
|
|
3268
|
+
selectedMatches = await checkbox({
|
|
3269
|
+
message: "Select components to link:",
|
|
3270
|
+
choices,
|
|
3271
|
+
pageSize: 20
|
|
3272
|
+
});
|
|
3273
|
+
} catch {
|
|
3274
|
+
console.log(pc17.dim("\nNo changes made."));
|
|
3275
|
+
return { success: true, updated: 0, variantUpdates: 0 };
|
|
3276
|
+
}
|
|
3277
|
+
}
|
|
3278
|
+
}
|
|
3279
|
+
const allSelectedMatches = [...selectedMatches, ...alreadyLinkedMatches];
|
|
3280
|
+
let updated = 0;
|
|
3281
|
+
for (const match of selectedMatches) {
|
|
3282
|
+
if (match.alreadyLinked) continue;
|
|
3283
|
+
try {
|
|
3284
|
+
let content = await readFile4(match.segment.filePath, "utf-8");
|
|
3285
|
+
const figmaUrlToInsert = figmaClient.buildNodeUrl(
|
|
3286
|
+
match.figmaComponent.file_key,
|
|
3287
|
+
match.figmaComponent.node_id,
|
|
3288
|
+
figmaData.fileName
|
|
3289
|
+
);
|
|
3290
|
+
if (/meta:\s*\{[^}]*figma:/.test(content)) {
|
|
3291
|
+
content = content.replace(
|
|
3292
|
+
/(meta:\s*\{[^}]*figma:\s*['"])([^'"]*)['"]/,
|
|
3293
|
+
`$1${figmaUrlToInsert}'`
|
|
3294
|
+
);
|
|
3295
|
+
} else {
|
|
3296
|
+
content = content.replace(
|
|
3297
|
+
/(meta:\s*\{[^}]*category:\s*['"][^'"]*['"],?)/,
|
|
3298
|
+
`$1
|
|
3299
|
+
figma: '${figmaUrlToInsert}',`
|
|
3300
|
+
);
|
|
3301
|
+
}
|
|
3302
|
+
await writeFile5(match.segment.filePath, content);
|
|
3303
|
+
updated++;
|
|
3304
|
+
console.log(` ${pc17.green("\u2713")} Updated ${match.segment.relativePath}`);
|
|
3305
|
+
} catch (error) {
|
|
3306
|
+
console.log(` ${pc17.red("\u2717")} Failed to update ${match.segment.relativePath}: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
3307
|
+
}
|
|
3308
|
+
}
|
|
3309
|
+
if (updated > 0) {
|
|
3310
|
+
console.log(pc17.green(`
|
|
3311
|
+
\u2713 Updated ${updated} segment file(s)
|
|
3312
|
+
`));
|
|
3313
|
+
}
|
|
3314
|
+
let variantUpdates = 0;
|
|
3315
|
+
if (variants && figmaData.componentSets.length > 0) {
|
|
3316
|
+
variantUpdates = await linkVariants(
|
|
3317
|
+
allSelectedMatches,
|
|
3318
|
+
figmaData,
|
|
3319
|
+
figmaClient,
|
|
3320
|
+
fileKey
|
|
3321
|
+
);
|
|
3322
|
+
}
|
|
3323
|
+
console.log(pc17.dim("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
3324
|
+
console.log(pc17.bold("\nNext steps:"));
|
|
3325
|
+
console.log(` 1. Run ${pc17.cyan(`${BRAND.cliCommand} compare --all`)} to verify designs`);
|
|
3326
|
+
console.log(` 2. Add figmaProps mappings for prop-level linking`);
|
|
3327
|
+
console.log();
|
|
3328
|
+
return { success: true, updated, variantUpdates };
|
|
3329
|
+
}
|
|
3330
|
+
function extractVariants(content, componentName) {
|
|
3331
|
+
const variants = [];
|
|
3332
|
+
const variantsArrayMatch = content.match(/variants:\s*\[/);
|
|
3333
|
+
if (variantsArrayMatch && variantsArrayMatch.index !== void 0) {
|
|
3334
|
+
const variantsStart = variantsArrayMatch.index + variantsArrayMatch[0].length;
|
|
3335
|
+
let bracketCount = 1;
|
|
3336
|
+
let variantsEnd = variantsStart;
|
|
3337
|
+
while (bracketCount > 0 && variantsEnd < content.length) {
|
|
3338
|
+
if (content[variantsEnd] === "[") bracketCount++;
|
|
3339
|
+
if (content[variantsEnd] === "]") bracketCount--;
|
|
3340
|
+
variantsEnd++;
|
|
3341
|
+
}
|
|
3342
|
+
const variantsSection = content.slice(variantsStart, variantsEnd - 1);
|
|
3343
|
+
const variantNameRegex = /\{\s*\n?\s*name:\s*['"]([^'"]+)['"]/g;
|
|
3344
|
+
let variantMatch;
|
|
3345
|
+
while ((variantMatch = variantNameRegex.exec(variantsSection)) !== null) {
|
|
3346
|
+
const variantName = variantMatch[1];
|
|
3347
|
+
if (componentName && variantName === componentName) continue;
|
|
3348
|
+
const objectStart = variantMatch.index;
|
|
3349
|
+
let braceCount = 1;
|
|
3350
|
+
let objectEnd = objectStart + 1;
|
|
3351
|
+
while (braceCount > 0 && objectEnd < variantsSection.length) {
|
|
3352
|
+
if (variantsSection[objectEnd] === "{") braceCount++;
|
|
3353
|
+
if (variantsSection[objectEnd] === "}") braceCount--;
|
|
3354
|
+
objectEnd++;
|
|
3355
|
+
}
|
|
3356
|
+
const objectContent = variantsSection.slice(objectStart, objectEnd);
|
|
3357
|
+
const variantHasFigma = /figma:\s*['"]https?:/.test(objectContent);
|
|
3358
|
+
variants.push({
|
|
3359
|
+
name: variantName,
|
|
3360
|
+
hasFigma: variantHasFigma
|
|
3361
|
+
});
|
|
3362
|
+
}
|
|
3363
|
+
}
|
|
3364
|
+
return variants;
|
|
3365
|
+
}
|
|
3366
|
+
function calculateMatchScore(segmentName, figmaName) {
|
|
3367
|
+
const normalizeForMatch = (s) => s.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
3368
|
+
const normalizedSegment = normalizeForMatch(segmentName);
|
|
3369
|
+
const normalizedFigma = normalizeForMatch(figmaName);
|
|
3370
|
+
if (normalizedSegment === normalizedFigma) {
|
|
3371
|
+
return 100;
|
|
3372
|
+
}
|
|
3373
|
+
if (normalizedFigma.startsWith(normalizedSegment)) {
|
|
3374
|
+
const coverage = normalizedSegment.length / normalizedFigma.length;
|
|
3375
|
+
return Math.max(85, coverage * 100);
|
|
3376
|
+
}
|
|
3377
|
+
if (normalizedSegment.startsWith(normalizedFigma)) {
|
|
3378
|
+
const coverage = normalizedFigma.length / normalizedSegment.length;
|
|
3379
|
+
return Math.max(80, coverage * 100);
|
|
3380
|
+
}
|
|
3381
|
+
const getWords = (s) => {
|
|
3382
|
+
return s.replace(/([a-z])([A-Z])/g, "$1 $2").replace(/[^a-zA-Z0-9]+/g, " ").toLowerCase().split(/\s+/).filter((w) => w.length > 0);
|
|
3383
|
+
};
|
|
3384
|
+
const segmentWords = getWords(segmentName);
|
|
3385
|
+
const figmaWords = getWords(figmaName);
|
|
3386
|
+
const allSegmentWordsInFigma = segmentWords.every(
|
|
3387
|
+
(sw) => figmaWords.some((fw) => fw === sw || fw.startsWith(sw) || sw.startsWith(fw))
|
|
3388
|
+
);
|
|
3389
|
+
if (allSegmentWordsInFigma && segmentWords.length > 0) {
|
|
3390
|
+
const wordOverlap = segmentWords.length / Math.max(segmentWords.length, figmaWords.length);
|
|
3391
|
+
return Math.max(75, wordOverlap * 95);
|
|
3392
|
+
}
|
|
3393
|
+
if (normalizedFigma.includes(normalizedSegment)) {
|
|
3394
|
+
return 70;
|
|
3395
|
+
}
|
|
3396
|
+
if (normalizedSegment.includes(normalizedFigma)) {
|
|
3397
|
+
return 65;
|
|
3398
|
+
}
|
|
3399
|
+
return 0;
|
|
3400
|
+
}
|
|
3401
|
+
async function linkVariants(matches, figmaData, figmaClient, fileKey) {
|
|
3402
|
+
console.log(pc17.bold("\nLinking variants...\n"));
|
|
3403
|
+
const matchedComponentSets = matches.map((m) => m.figmaComponent).filter((c) => figmaData.componentSets.some((cs) => cs.key === c.key));
|
|
3404
|
+
if (matchedComponentSets.length === 0) {
|
|
3405
|
+
return 0;
|
|
3406
|
+
}
|
|
3407
|
+
console.log(pc17.dim("Fetching Figma variants..."));
|
|
3408
|
+
const componentSetVariants = await figmaClient.getComponentSetVariants(fileKey, matchedComponentSets);
|
|
3409
|
+
let variantUpdates = 0;
|
|
3410
|
+
const { select } = await import("@inquirer/prompts");
|
|
3411
|
+
const normalizeForMatch = (s) => s.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
3412
|
+
const escapeRegExp = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
3413
|
+
for (const match of matches) {
|
|
3414
|
+
const csWithVariants = componentSetVariants.find(
|
|
3415
|
+
(cs) => cs.componentSet.key === match.figmaComponent.key
|
|
3416
|
+
);
|
|
3417
|
+
if (!csWithVariants || csWithVariants.variants.length === 0) {
|
|
3418
|
+
continue;
|
|
3419
|
+
}
|
|
3420
|
+
const segmentVariants = match.segment.variants.filter((v) => !v.hasFigma);
|
|
3421
|
+
if (segmentVariants.length === 0) {
|
|
3422
|
+
console.log(pc17.dim(` \u23ED\uFE0F ${match.segment.name}: all variants already linked`));
|
|
3423
|
+
continue;
|
|
3424
|
+
}
|
|
3425
|
+
console.log(pc17.dim(` ${match.segment.name}: ${csWithVariants.variants.length} Figma variants`));
|
|
3426
|
+
for (const segmentVariant of segmentVariants) {
|
|
3427
|
+
const variantMatches = [];
|
|
3428
|
+
for (const fv of csWithVariants.variants) {
|
|
3429
|
+
const normalizedSegment = normalizeForMatch(segmentVariant.name);
|
|
3430
|
+
for (const value of fv.values) {
|
|
3431
|
+
const normalizedValue = normalizeForMatch(value);
|
|
3432
|
+
if (normalizedSegment === normalizedValue) {
|
|
3433
|
+
variantMatches.push({ figmaVariant: fv, score: 100 });
|
|
3434
|
+
break;
|
|
3435
|
+
} else if (normalizedValue.includes(normalizedSegment)) {
|
|
3436
|
+
variantMatches.push({ figmaVariant: fv, score: 85 });
|
|
3437
|
+
} else if (normalizedSegment.includes(normalizedValue)) {
|
|
3438
|
+
variantMatches.push({ figmaVariant: fv, score: 75 });
|
|
3439
|
+
}
|
|
3440
|
+
}
|
|
3441
|
+
}
|
|
3442
|
+
variantMatches.sort((a, b) => b.score - a.score);
|
|
3443
|
+
if (variantMatches.length > 0 && variantMatches[0].score === 100) {
|
|
3444
|
+
const bestMatch = variantMatches[0];
|
|
3445
|
+
const variantUrl = figmaClient.buildNodeUrl(
|
|
3446
|
+
match.figmaComponent.file_key,
|
|
3447
|
+
bestMatch.figmaVariant.node_id,
|
|
3448
|
+
figmaData.fileName
|
|
3449
|
+
);
|
|
3450
|
+
try {
|
|
3451
|
+
let content = await readFile4(match.segment.filePath, "utf-8");
|
|
3452
|
+
const namePattern = new RegExp(
|
|
3453
|
+
`(name:\\s*['"]${escapeRegExp(segmentVariant.name)}['"],?)`,
|
|
3454
|
+
"g"
|
|
3455
|
+
);
|
|
3456
|
+
let replaced = false;
|
|
3457
|
+
content = content.replace(namePattern, (matchedStr) => {
|
|
3458
|
+
if (replaced) return matchedStr;
|
|
3459
|
+
replaced = true;
|
|
3460
|
+
return `${matchedStr}
|
|
3461
|
+
figma: '${variantUrl}',`;
|
|
3462
|
+
});
|
|
3463
|
+
await writeFile5(match.segment.filePath, content);
|
|
3464
|
+
variantUpdates++;
|
|
3465
|
+
console.log(
|
|
3466
|
+
` ${pc17.green("\u2713")} ${segmentVariant.name} \u2192 ${bestMatch.figmaVariant.name}`
|
|
3467
|
+
);
|
|
3468
|
+
} catch (error) {
|
|
3469
|
+
console.log(
|
|
3470
|
+
` ${pc17.red("\u2717")} ${segmentVariant.name}: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
3471
|
+
);
|
|
3472
|
+
}
|
|
3473
|
+
} else if (variantMatches.length > 0) {
|
|
3474
|
+
const choices = [
|
|
3475
|
+
...variantMatches.slice(0, 4).map((m) => ({
|
|
3476
|
+
name: `${m.figmaVariant.name} (${m.score}%)`,
|
|
3477
|
+
value: m.figmaVariant
|
|
3478
|
+
})),
|
|
3479
|
+
{ name: "Skip this variant", value: null }
|
|
3480
|
+
];
|
|
3481
|
+
try {
|
|
3482
|
+
const selectedVariant = await select({
|
|
3483
|
+
message: ` Match for "${segmentVariant.name}":`,
|
|
3484
|
+
choices
|
|
3485
|
+
});
|
|
3486
|
+
if (selectedVariant) {
|
|
3487
|
+
const variantUrl = figmaClient.buildNodeUrl(
|
|
3488
|
+
match.figmaComponent.file_key,
|
|
3489
|
+
selectedVariant.node_id,
|
|
3490
|
+
figmaData.fileName
|
|
3491
|
+
);
|
|
3492
|
+
let content = await readFile4(match.segment.filePath, "utf-8");
|
|
3493
|
+
const namePattern = new RegExp(
|
|
3494
|
+
`(name:\\s*['"]${escapeRegExp(segmentVariant.name)}['"],?)`,
|
|
3495
|
+
"g"
|
|
3496
|
+
);
|
|
3497
|
+
let replaced = false;
|
|
3498
|
+
content = content.replace(namePattern, (matchedStr) => {
|
|
3499
|
+
if (replaced) return matchedStr;
|
|
3500
|
+
replaced = true;
|
|
3501
|
+
return `${matchedStr}
|
|
3502
|
+
figma: '${variantUrl}',`;
|
|
3503
|
+
});
|
|
3504
|
+
await writeFile5(match.segment.filePath, content);
|
|
3505
|
+
variantUpdates++;
|
|
3506
|
+
console.log(
|
|
3507
|
+
` ${pc17.green("\u2713")} ${segmentVariant.name} \u2192 ${selectedVariant.name}`
|
|
3508
|
+
);
|
|
3509
|
+
} else {
|
|
3510
|
+
console.log(` ${pc17.dim("\u23ED\uFE0F")} ${segmentVariant.name} (skipped)`);
|
|
3511
|
+
}
|
|
3512
|
+
} catch {
|
|
3513
|
+
console.log(` ${pc17.dim("\u23ED\uFE0F")} ${segmentVariant.name} (cancelled)`);
|
|
3514
|
+
}
|
|
3515
|
+
} else {
|
|
3516
|
+
console.log(` ${pc17.yellow("?")} ${segmentVariant.name}: no matching Figma variant`);
|
|
3517
|
+
}
|
|
3518
|
+
}
|
|
3519
|
+
}
|
|
3520
|
+
if (variantUpdates > 0) {
|
|
3521
|
+
console.log(pc17.green(`
|
|
3522
|
+
\u2713 Linked ${variantUpdates} variant(s)
|
|
3523
|
+
`));
|
|
3524
|
+
} else {
|
|
3525
|
+
console.log(pc17.dim("\nNo variant updates made.\n"));
|
|
3526
|
+
}
|
|
3527
|
+
return variantUpdates;
|
|
3528
|
+
}
|
|
3529
|
+
|
|
3530
|
+
// src/commands/link/storybook.ts
|
|
3531
|
+
import { writeFile as writeFile6, mkdir as mkdir6 } from "fs/promises";
|
|
3532
|
+
import { join as join8, dirname as dirname3, relative as relative6 } from "path";
|
|
3533
|
+
import pc18 from "picocolors";
|
|
3534
|
+
async function linkStorybook(options = {}) {
|
|
3535
|
+
const { out, yes = false, dryRun = false, verbose = false } = options;
|
|
3536
|
+
console.log(pc18.cyan(`
|
|
3537
|
+
${BRAND.name} Storybook Link
|
|
3538
|
+
`));
|
|
3539
|
+
const projectRoot = process.cwd();
|
|
3540
|
+
console.log(pc18.dim("Detecting Storybook configuration..."));
|
|
3541
|
+
const sbConfig = await detectStorybookConfig(projectRoot);
|
|
3542
|
+
if (!sbConfig) {
|
|
3543
|
+
console.log(pc18.yellow("\nNo Storybook configuration found."));
|
|
3544
|
+
console.log(pc18.dim("Looking for: .storybook/main.ts, .storybook/main.js, etc."));
|
|
3545
|
+
console.log(pc18.dim("\nUse --config to specify a custom path."));
|
|
3546
|
+
process.exit(1);
|
|
3547
|
+
}
|
|
3548
|
+
console.log(pc18.green(`\u2713 Found: ${sbConfig.configPath}`));
|
|
3549
|
+
if (sbConfig.framework) {
|
|
3550
|
+
console.log(pc18.dim(` Framework: ${sbConfig.framework}`));
|
|
3551
|
+
}
|
|
3552
|
+
if (sbConfig.errors?.length) {
|
|
3553
|
+
for (const err of sbConfig.errors) {
|
|
3554
|
+
console.log(pc18.yellow(` Warning: ${err}`));
|
|
3555
|
+
}
|
|
3556
|
+
}
|
|
3557
|
+
console.log(pc18.dim("\nDiscovering story files..."));
|
|
3558
|
+
const storyFiles = await discoverStoryFiles(projectRoot, sbConfig.storyPatterns);
|
|
3559
|
+
if (storyFiles.length === 0) {
|
|
3560
|
+
console.log(pc18.yellow("\nNo story files found."));
|
|
3561
|
+
console.log(pc18.dim(`Patterns: ${sbConfig.storyPatterns.join(", ")}`));
|
|
3562
|
+
process.exit(1);
|
|
3563
|
+
}
|
|
3564
|
+
console.log(pc18.green(`\u2713 Found ${storyFiles.length} story file(s)
|
|
3565
|
+
`));
|
|
3566
|
+
const previews = [];
|
|
3567
|
+
let parseErrors = 0;
|
|
3568
|
+
const total = storyFiles.length;
|
|
3569
|
+
console.log(pc18.dim(`Analyzing ${total} stories...
|
|
3570
|
+
`));
|
|
3571
|
+
for (let i = 0; i < storyFiles.length; i++) {
|
|
3572
|
+
const storyFile = storyFiles[i];
|
|
3573
|
+
const relativePath = relative6(projectRoot, storyFile);
|
|
3574
|
+
process.stdout.write(`\r ${pc18.dim(`[${i + 1}/${total}]`)} ${relativePath.slice(0, 60).padEnd(60)}`);
|
|
3575
|
+
try {
|
|
3576
|
+
const parsed = await parseStoryFile(storyFile);
|
|
3577
|
+
const result = convertToSegment(parsed);
|
|
3578
|
+
const outputFile = out ? join8(out, relativePath.replace(/\.stories\.(tsx?|jsx?)$/, BRAND.fileExtension)) : result.outputFile;
|
|
3579
|
+
previews.push({
|
|
3580
|
+
componentName: result.componentName,
|
|
3581
|
+
sourceFile: relativePath,
|
|
3582
|
+
outputFile: relative6(projectRoot, outputFile),
|
|
3583
|
+
variantCount: result.variantCount,
|
|
3584
|
+
propCount: result.propCount,
|
|
3585
|
+
confidence: result.confidence,
|
|
3586
|
+
warnings: result.warnings
|
|
3587
|
+
});
|
|
3588
|
+
} catch (error) {
|
|
3589
|
+
parseErrors++;
|
|
3590
|
+
if (verbose) {
|
|
3591
|
+
process.stdout.write("\n");
|
|
3592
|
+
console.log(pc18.red(`\u2717 ${relativePath}: ${error instanceof Error ? error.message : "Parse error"}`));
|
|
3593
|
+
}
|
|
3594
|
+
}
|
|
3595
|
+
}
|
|
3596
|
+
process.stdout.write("\r" + " ".repeat(80) + "\r");
|
|
3597
|
+
console.log(pc18.bold("Preview:\n"));
|
|
3598
|
+
console.log(pc18.dim("\u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510"));
|
|
3599
|
+
console.log(pc18.dim("\u2502") + " Component".padEnd(20) + pc18.dim("\u2502") + " Stories".padEnd(10) + pc18.dim("\u2502") + " Props".padEnd(8) + pc18.dim("\u2502") + " Confidence".padEnd(13) + pc18.dim("\u2502"));
|
|
3600
|
+
console.log(pc18.dim("\u251C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524"));
|
|
3601
|
+
for (const item of previews) {
|
|
3602
|
+
const confColor = item.confidence >= 0.8 ? pc18.green : item.confidence >= 0.5 ? pc18.yellow : pc18.red;
|
|
3603
|
+
const confLabel = item.confidence >= 0.8 ? "high" : item.confidence >= 0.5 ? "medium" : "low";
|
|
3604
|
+
console.log(
|
|
3605
|
+
pc18.dim("\u2502") + ` ${item.componentName}`.padEnd(20).slice(0, 20) + pc18.dim("\u2502") + ` ${item.variantCount}`.padEnd(10) + pc18.dim("\u2502") + ` ${item.propCount}`.padEnd(8) + pc18.dim("\u2502") + ` ${confColor(confLabel)}`.padEnd(13 + (confColor === pc18.green ? 10 : confColor === pc18.yellow ? 11 : 9)) + pc18.dim("\u2502")
|
|
3606
|
+
);
|
|
3607
|
+
}
|
|
3608
|
+
console.log(pc18.dim("\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518"));
|
|
3609
|
+
if (parseErrors > 0) {
|
|
3610
|
+
console.log(pc18.yellow(`
|
|
3611
|
+
${parseErrors} file(s) could not be parsed (use --verbose for details)`));
|
|
3612
|
+
}
|
|
3613
|
+
if (previews.length === 0) {
|
|
3614
|
+
console.log(pc18.yellow("\nNo stories could be parsed successfully."));
|
|
3615
|
+
return { success: true, generated: 0 };
|
|
3616
|
+
}
|
|
3617
|
+
if (dryRun) {
|
|
3618
|
+
console.log(pc18.dim(`
|
|
3619
|
+
${previews.length} segment file(s) would be created`));
|
|
3620
|
+
console.log(pc18.yellow("\n[Dry run - no files were written]"));
|
|
3621
|
+
return { success: true, generated: 0 };
|
|
3622
|
+
}
|
|
3623
|
+
let selectedPreviews = previews;
|
|
3624
|
+
if (yes) {
|
|
3625
|
+
console.log(pc18.dim(`
|
|
3626
|
+
${previews.length} segment file(s) will be created`));
|
|
3627
|
+
} else {
|
|
3628
|
+
const { checkbox } = await import("@inquirer/prompts");
|
|
3629
|
+
console.log(pc18.dim("\nUse \u2191/\u2193 to navigate, Space to toggle, Enter to confirm\n"));
|
|
3630
|
+
const choices = previews.map((item) => {
|
|
3631
|
+
const confColor = item.confidence >= 0.8 ? pc18.green : item.confidence >= 0.5 ? pc18.yellow : pc18.red;
|
|
3632
|
+
const confLabel = item.confidence >= 0.8 ? "high" : item.confidence >= 0.5 ? "medium" : "low";
|
|
3633
|
+
return {
|
|
3634
|
+
name: `${pc18.bold(item.componentName)} ${pc18.dim(`(${item.variantCount} stories, ${confLabel} confidence)`)}`,
|
|
3635
|
+
value: item,
|
|
3636
|
+
checked: true
|
|
3637
|
+
};
|
|
3638
|
+
});
|
|
3639
|
+
try {
|
|
3640
|
+
selectedPreviews = await checkbox({
|
|
3641
|
+
message: "Select stories to convert:",
|
|
3642
|
+
choices,
|
|
3643
|
+
pageSize: 15
|
|
3644
|
+
});
|
|
3645
|
+
} catch {
|
|
3646
|
+
console.log(pc18.dim("\nNo changes made."));
|
|
3647
|
+
return { success: true, generated: 0 };
|
|
3648
|
+
}
|
|
3649
|
+
if (selectedPreviews.length === 0) {
|
|
3650
|
+
console.log(pc18.dim("\nNo stories selected. No changes made."));
|
|
3651
|
+
return { success: true, generated: 0 };
|
|
3652
|
+
}
|
|
3653
|
+
}
|
|
3654
|
+
const genTotal = selectedPreviews.length;
|
|
3655
|
+
console.log(pc18.dim(`
|
|
3656
|
+
Generating ${genTotal} segment file(s)...
|
|
3657
|
+
`));
|
|
3658
|
+
let generated = 0;
|
|
3659
|
+
let genErrors = 0;
|
|
3660
|
+
for (let i = 0; i < selectedPreviews.length; i++) {
|
|
3661
|
+
const preview = selectedPreviews[i];
|
|
3662
|
+
const storyFile = join8(projectRoot, preview.sourceFile);
|
|
3663
|
+
try {
|
|
3664
|
+
const parsed = await parseStoryFile(storyFile);
|
|
3665
|
+
const result = convertToSegment(parsed);
|
|
3666
|
+
const outputFile = out ? join8(projectRoot, out, preview.sourceFile.replace(/\.stories\.(tsx?|jsx?)$/, BRAND.fileExtension)) : result.outputFile;
|
|
3667
|
+
await mkdir6(dirname3(outputFile), { recursive: true });
|
|
3668
|
+
await writeFile6(outputFile, result.code);
|
|
3669
|
+
generated++;
|
|
3670
|
+
console.log(`${pc18.dim(`[${i + 1}/${genTotal}]`)} ${pc18.green("\u2713")} ${result.componentName}`);
|
|
3671
|
+
} catch (error) {
|
|
3672
|
+
genErrors++;
|
|
3673
|
+
console.log(`${pc18.dim(`[${i + 1}/${genTotal}]`)} ${pc18.red("\u2717")} ${preview.componentName}`);
|
|
3674
|
+
}
|
|
3675
|
+
}
|
|
3676
|
+
console.log(pc18.green(`
|
|
3677
|
+
\u2713 Generated ${generated} segment file(s)
|
|
3678
|
+
`));
|
|
3679
|
+
console.log(pc18.dim("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
3680
|
+
console.log(pc18.bold("\nNext steps:"));
|
|
3681
|
+
console.log(` 1. Review generated ${BRAND.fileExtension} files`);
|
|
3682
|
+
console.log(` 2. Fill in usage.when and usage.whenNot fields`);
|
|
3683
|
+
console.log(` 3. Run ${pc18.cyan(`${BRAND.cliCommand} build`)} to compile`);
|
|
3684
|
+
console.log(` 4. Run ${pc18.cyan(`${BRAND.cliCommand} dev`)} to view your design system`);
|
|
3685
|
+
console.log();
|
|
3686
|
+
return { success: true, generated };
|
|
3687
|
+
}
|
|
3688
|
+
|
|
3689
|
+
// src/commands/enhance.ts
|
|
3690
|
+
import pc19 from "picocolors";
|
|
3691
|
+
import { readFile as readFile5, writeFile as writeFile7 } from "fs/promises";
|
|
3692
|
+
import { resolve as resolve5, relative as relative7, join as join9 } from "path";
|
|
3693
|
+
var DEFAULT_MODELS = {
|
|
3694
|
+
anthropic: "claude-sonnet-4-20250514",
|
|
3695
|
+
openai: "gpt-4o",
|
|
3696
|
+
none: ""
|
|
3697
|
+
};
|
|
3698
|
+
async function enhance(options = {}) {
|
|
3699
|
+
const {
|
|
3700
|
+
config: configPath,
|
|
3701
|
+
component,
|
|
3702
|
+
yes = false,
|
|
3703
|
+
dryRun = false,
|
|
3704
|
+
format = "interactive",
|
|
3705
|
+
provider = detectProvider(options),
|
|
3706
|
+
apiKey = getApiKey(provider, options.apiKey),
|
|
3707
|
+
model = options.model || DEFAULT_MODELS[provider],
|
|
3708
|
+
root = process.cwd(),
|
|
3709
|
+
contextOnly = false,
|
|
3710
|
+
renderVariants = false,
|
|
3711
|
+
storybookUrl = "http://localhost:6006"
|
|
3712
|
+
} = options;
|
|
3713
|
+
const isInteractive = format === "interactive";
|
|
3714
|
+
const isQuiet = format === "quiet";
|
|
3715
|
+
const isContextMode = contextOnly || format === "context" || provider === "none";
|
|
3716
|
+
const config = await loadConfig(configPath);
|
|
3717
|
+
const rootDir = resolve5(root);
|
|
3718
|
+
if (isInteractive) {
|
|
3719
|
+
console.log(pc19.cyan(`
|
|
3720
|
+
${BRAND.name} AI Enhancement
|
|
3721
|
+
`));
|
|
3722
|
+
if (isContextMode) {
|
|
3723
|
+
console.log(pc19.dim("Running in context-only mode (for use with IDE AI like Cursor)\n"));
|
|
3724
|
+
} else {
|
|
3725
|
+
console.log(pc19.dim(`Using ${provider} API with model: ${model}
|
|
3726
|
+
`));
|
|
3727
|
+
}
|
|
3728
|
+
}
|
|
3729
|
+
if (!isContextMode && !apiKey) {
|
|
3730
|
+
const envVar = provider === "openai" ? "OPENAI_API_KEY" : "ANTHROPIC_API_KEY";
|
|
3731
|
+
const error = `API key required. Set ${envVar} or use --api-key, or use --context-only for IDE AI mode`;
|
|
3732
|
+
if (format === "json") {
|
|
3733
|
+
console.log(JSON.stringify({ success: false, error }));
|
|
3734
|
+
} else if (!isQuiet) {
|
|
3735
|
+
console.error(pc19.red("Error:"), error);
|
|
3736
|
+
console.log(pc19.dim("\nTip: Use --context-only to generate prompts for Cursor/Copilot/etc."));
|
|
3737
|
+
}
|
|
3738
|
+
return { success: false, enhanced: [], totalTokens: 0, estimatedCost: 0 };
|
|
3739
|
+
}
|
|
3740
|
+
if (isInteractive) {
|
|
3741
|
+
console.log(pc19.dim("Phase 1: Scanning codebase for usage patterns..."));
|
|
3742
|
+
}
|
|
3743
|
+
let usageAnalysis;
|
|
3744
|
+
try {
|
|
3745
|
+
usageAnalysis = await scanCodebase({
|
|
3746
|
+
rootDir,
|
|
3747
|
+
useCache: true,
|
|
3748
|
+
onProgress: (progress) => {
|
|
3749
|
+
if (isInteractive && progress.phase === "scanning") {
|
|
3750
|
+
const pct = Math.round(progress.current / progress.total * 100);
|
|
3751
|
+
process.stdout.write(`\r Scanning: ${pct}% (${progress.current}/${progress.total})`);
|
|
3752
|
+
}
|
|
3753
|
+
}
|
|
3754
|
+
});
|
|
3755
|
+
if (isInteractive) {
|
|
3756
|
+
process.stdout.write("\r" + " ".repeat(60) + "\r");
|
|
3757
|
+
const stats = getScanStats(usageAnalysis);
|
|
3758
|
+
console.log(pc19.green(` Found ${stats.totalUsages} usages across ${stats.totalFiles} files`));
|
|
3759
|
+
}
|
|
3760
|
+
} catch (error) {
|
|
3761
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
3762
|
+
if (format === "json") {
|
|
3763
|
+
console.log(JSON.stringify({ success: false, error: `Scan failed: ${msg}` }));
|
|
3764
|
+
} else if (!isQuiet) {
|
|
3765
|
+
console.error(pc19.red("Scan failed:"), msg);
|
|
3766
|
+
}
|
|
3767
|
+
return { success: false, enhanced: [], totalTokens: 0, estimatedCost: 0 };
|
|
3768
|
+
}
|
|
3769
|
+
if (isInteractive) {
|
|
3770
|
+
console.log(pc19.dim("Phase 2: Parsing Storybook stories..."));
|
|
3771
|
+
}
|
|
3772
|
+
let storyFiles;
|
|
3773
|
+
try {
|
|
3774
|
+
storyFiles = await parseAllStories(rootDir);
|
|
3775
|
+
if (isInteractive) {
|
|
3776
|
+
console.log(pc19.green(` Found ${storyFiles.size} story files`));
|
|
3777
|
+
}
|
|
3778
|
+
} catch {
|
|
3779
|
+
storyFiles = /* @__PURE__ */ new Map();
|
|
3780
|
+
if (isInteractive) {
|
|
3781
|
+
console.log(pc19.yellow(" No Storybook stories found"));
|
|
3782
|
+
}
|
|
3783
|
+
}
|
|
3784
|
+
if (isInteractive) {
|
|
3785
|
+
console.log(pc19.dim("Phase 3: Loading fragment files..."));
|
|
3786
|
+
}
|
|
3787
|
+
const segmentFiles = await findSegmentFiles(rootDir);
|
|
3788
|
+
if (segmentFiles.length === 0) {
|
|
3789
|
+
const msg = "No fragment files found";
|
|
3790
|
+
if (format === "json") {
|
|
3791
|
+
console.log(JSON.stringify({ success: false, error: msg }));
|
|
3792
|
+
} else if (!isQuiet) {
|
|
3793
|
+
console.log(pc19.yellow(msg));
|
|
3794
|
+
}
|
|
3795
|
+
return { success: false, enhanced: [], totalTokens: 0, estimatedCost: 0 };
|
|
3796
|
+
}
|
|
3797
|
+
if (isInteractive) {
|
|
3798
|
+
console.log(pc19.green(` Found ${segmentFiles.length} fragment files`));
|
|
3799
|
+
}
|
|
3800
|
+
let componentsToEnhance;
|
|
3801
|
+
if (component && component !== "all") {
|
|
3802
|
+
componentsToEnhance = [component];
|
|
3803
|
+
} else {
|
|
3804
|
+
componentsToEnhance = segmentFiles.map((f) => extractComponentName(f));
|
|
3805
|
+
}
|
|
3806
|
+
if (isInteractive) {
|
|
3807
|
+
console.log(pc19.dim("Phase 4: Extracting props from TypeScript interfaces..."));
|
|
3808
|
+
}
|
|
3809
|
+
const propsExtractions = /* @__PURE__ */ new Map();
|
|
3810
|
+
for (const compName of componentsToEnhance) {
|
|
3811
|
+
const segmentFile = segmentFiles.find((f) => extractComponentName(f) === compName);
|
|
3812
|
+
if (!segmentFile) continue;
|
|
3813
|
+
const segmentDir = segmentFile.replace(/\.segment\.(tsx?|jsx?)$/, "");
|
|
3814
|
+
const possiblePaths = [
|
|
3815
|
+
`${segmentDir}.tsx`,
|
|
3816
|
+
`${segmentDir}.ts`,
|
|
3817
|
+
`${segmentDir}/index.tsx`,
|
|
3818
|
+
`${segmentDir}/index.ts`,
|
|
3819
|
+
join9(rootDir, "src", "components", `${compName}.tsx`),
|
|
3820
|
+
join9(rootDir, "src", "components", compName, `${compName}.tsx`),
|
|
3821
|
+
join9(rootDir, "src", "components", compName, "index.tsx")
|
|
3822
|
+
];
|
|
3823
|
+
for (const srcPath of possiblePaths) {
|
|
3824
|
+
try {
|
|
3825
|
+
const fs = await import("fs");
|
|
3826
|
+
if (fs.existsSync(srcPath)) {
|
|
3827
|
+
const extraction = await extractPropsFromFile(srcPath, {
|
|
3828
|
+
propsTypeName: `${compName}Props`
|
|
3829
|
+
});
|
|
3830
|
+
if (extraction.success) {
|
|
3831
|
+
propsExtractions.set(compName, extraction);
|
|
3832
|
+
break;
|
|
3833
|
+
}
|
|
3834
|
+
}
|
|
3835
|
+
} catch {
|
|
3836
|
+
}
|
|
3837
|
+
}
|
|
3838
|
+
}
|
|
3839
|
+
if (isInteractive) {
|
|
3840
|
+
const propsCount = Array.from(propsExtractions.values()).reduce((sum, ext) => sum + ext.props.length, 0);
|
|
3841
|
+
console.log(pc19.green(` Extracted ${propsCount} props from ${propsExtractions.size} component files`));
|
|
3842
|
+
}
|
|
3843
|
+
const renderedVariants = /* @__PURE__ */ new Map();
|
|
3844
|
+
if (renderVariants) {
|
|
3845
|
+
if (isInteractive) {
|
|
3846
|
+
console.log(pc19.dim("Phase 5: Rendering variants from Storybook..."));
|
|
3847
|
+
}
|
|
3848
|
+
const storybookRunning = await checkStorybookRunning(storybookUrl);
|
|
3849
|
+
if (!storybookRunning) {
|
|
3850
|
+
if (isInteractive) {
|
|
3851
|
+
console.log(pc19.yellow(` Storybook not running at ${storybookUrl}. Skipping variant rendering.`));
|
|
3852
|
+
console.log(pc19.dim(` Start Storybook with: npm run storybook (or yarn storybook)`));
|
|
3853
|
+
}
|
|
3854
|
+
} else {
|
|
3855
|
+
let totalVariants = 0;
|
|
3856
|
+
let failedVariants = 0;
|
|
3857
|
+
for (const compName of componentsToEnhance) {
|
|
3858
|
+
try {
|
|
3859
|
+
const result = await renderAllComponentVariants(storybookUrl, compName);
|
|
3860
|
+
if (result.variants.length > 0) {
|
|
3861
|
+
renderedVariants.set(compName, result.variants);
|
|
3862
|
+
totalVariants += result.variants.length;
|
|
3863
|
+
}
|
|
3864
|
+
failedVariants += result.failed.length;
|
|
3865
|
+
} catch (err) {
|
|
3866
|
+
if (isInteractive) {
|
|
3867
|
+
console.log(pc19.dim(` ${compName}: Failed to render (${err.message})`));
|
|
3868
|
+
}
|
|
3869
|
+
}
|
|
3870
|
+
}
|
|
3871
|
+
if (isInteractive) {
|
|
3872
|
+
if (totalVariants > 0) {
|
|
3873
|
+
console.log(pc19.green(` Rendered ${totalVariants} variants from ${renderedVariants.size} components`));
|
|
3874
|
+
}
|
|
3875
|
+
if (failedVariants > 0) {
|
|
3876
|
+
console.log(pc19.yellow(` ${failedVariants} variant(s) failed to render`));
|
|
3877
|
+
}
|
|
3878
|
+
}
|
|
3879
|
+
await shutdownSharedPool();
|
|
3880
|
+
}
|
|
3881
|
+
}
|
|
3882
|
+
const contexts = [];
|
|
3883
|
+
for (const compName of componentsToEnhance) {
|
|
3884
|
+
const analysis = usageAnalysis.components[compName];
|
|
3885
|
+
const stories = storyFiles.get(compName);
|
|
3886
|
+
const segmentFile = segmentFiles.find((f) => extractComponentName(f) === compName);
|
|
3887
|
+
const propsExtraction = propsExtractions.get(compName);
|
|
3888
|
+
if (!segmentFile) continue;
|
|
3889
|
+
const context2 = generateComponentContext(
|
|
3890
|
+
compName,
|
|
3891
|
+
analysis,
|
|
3892
|
+
void 0,
|
|
3893
|
+
stories,
|
|
3894
|
+
propsExtraction
|
|
3895
|
+
);
|
|
3896
|
+
contexts.push({ name: compName, context: context2, segmentFile });
|
|
3897
|
+
}
|
|
3898
|
+
if (isContextMode) {
|
|
3899
|
+
return handleContextOnlyMode(contexts, format, isInteractive);
|
|
3900
|
+
}
|
|
3901
|
+
if (isInteractive) {
|
|
3902
|
+
console.log(pc19.dim(`
|
|
3903
|
+
Phase 6: Generating AI enhancements for ${componentsToEnhance.length} component(s)...
|
|
3904
|
+
`));
|
|
3905
|
+
}
|
|
3906
|
+
const enhanced = [];
|
|
3907
|
+
let totalTokens = 0;
|
|
3908
|
+
const aiClient = await createAIClient(provider, apiKey);
|
|
3909
|
+
for (const { name: compName, context: context2, segmentFile } of contexts) {
|
|
3910
|
+
if (!context2.usageAnalysis || context2.usageAnalysis.totalUsages < 2) {
|
|
3911
|
+
enhanced.push({
|
|
3912
|
+
componentName: compName,
|
|
3913
|
+
added: { when: [], whenNot: [] },
|
|
3914
|
+
confidence: 0,
|
|
3915
|
+
tokensUsed: 0,
|
|
3916
|
+
skipped: true,
|
|
3917
|
+
reason: context2.usageAnalysis?.totalUsages === 1 ? "Only 1 usage found" : "No usage data found"
|
|
3918
|
+
});
|
|
3919
|
+
if (isInteractive) {
|
|
3920
|
+
console.log(pc19.dim(` ${compName}: Skipped (insufficient data)`));
|
|
3921
|
+
}
|
|
3922
|
+
continue;
|
|
3923
|
+
}
|
|
3924
|
+
if (isInteractive) {
|
|
3925
|
+
process.stdout.write(` ${compName}: Generating...`);
|
|
3926
|
+
}
|
|
3927
|
+
try {
|
|
3928
|
+
const result = await generateEnhancement(aiClient, provider, model, context2);
|
|
3929
|
+
enhanced.push({
|
|
3930
|
+
componentName: compName,
|
|
3931
|
+
added: result.suggestions,
|
|
3932
|
+
confidence: result.confidence,
|
|
3933
|
+
tokensUsed: result.tokensUsed,
|
|
3934
|
+
skipped: false
|
|
3935
|
+
});
|
|
3936
|
+
totalTokens += result.tokensUsed;
|
|
3937
|
+
if (isInteractive) {
|
|
3938
|
+
process.stdout.write(`\r ${compName}: ${pc19.green("Done")} (${result.suggestions.when.length} when, ${result.suggestions.whenNot.length} whenNot)
|
|
3939
|
+
`);
|
|
3940
|
+
}
|
|
3941
|
+
} catch (error) {
|
|
3942
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
3943
|
+
enhanced.push({
|
|
3944
|
+
componentName: compName,
|
|
3945
|
+
added: { when: [], whenNot: [] },
|
|
3946
|
+
confidence: 0,
|
|
3947
|
+
tokensUsed: 0,
|
|
3948
|
+
skipped: true,
|
|
3949
|
+
reason: `AI error: ${msg}`
|
|
3950
|
+
});
|
|
3951
|
+
if (isInteractive) {
|
|
3952
|
+
process.stdout.write(`\r ${compName}: ${pc19.red("Failed")} - ${msg}
|
|
3953
|
+
`);
|
|
3954
|
+
}
|
|
3955
|
+
}
|
|
3956
|
+
}
|
|
3957
|
+
const estimatedCost = calculateCost(provider, totalTokens);
|
|
3958
|
+
if (!dryRun) {
|
|
3959
|
+
if (isInteractive) {
|
|
3960
|
+
console.log(pc19.dim("\nPhase 7: Updating segment files..."));
|
|
3961
|
+
}
|
|
3962
|
+
for (const result of enhanced) {
|
|
3963
|
+
if (result.skipped || result.added.when.length === 0 && result.added.whenNot.length === 0) {
|
|
3964
|
+
continue;
|
|
3965
|
+
}
|
|
3966
|
+
const segmentFile = segmentFiles.find((f) => extractComponentName(f) === result.componentName);
|
|
3967
|
+
if (!segmentFile) continue;
|
|
3968
|
+
try {
|
|
3969
|
+
await updateSegmentFile(segmentFile, result.added);
|
|
3970
|
+
if (isInteractive) {
|
|
3971
|
+
console.log(pc19.green(` Updated: ${relative7(rootDir, segmentFile)}`));
|
|
3972
|
+
}
|
|
3973
|
+
} catch {
|
|
3974
|
+
if (isInteractive) {
|
|
3975
|
+
console.log(pc19.red(` Failed to update: ${relative7(rootDir, segmentFile)}`));
|
|
3976
|
+
}
|
|
3977
|
+
}
|
|
3978
|
+
}
|
|
3979
|
+
}
|
|
3980
|
+
const successCount = enhanced.filter((e) => !e.skipped).length;
|
|
3981
|
+
if (format === "json") {
|
|
3982
|
+
console.log(JSON.stringify({
|
|
3983
|
+
success: true,
|
|
3984
|
+
enhanced,
|
|
3985
|
+
totalTokens,
|
|
3986
|
+
estimatedCost
|
|
3987
|
+
}, null, 2));
|
|
3988
|
+
} else if (isInteractive) {
|
|
3989
|
+
console.log();
|
|
3990
|
+
console.log(pc19.bold("Summary:"));
|
|
3991
|
+
console.log(` Components processed: ${componentsToEnhance.length}`);
|
|
3992
|
+
console.log(` Successfully enhanced: ${successCount}`);
|
|
3993
|
+
console.log(` Skipped: ${enhanced.filter((e) => e.skipped).length}`);
|
|
3994
|
+
console.log(` Total tokens used: ${totalTokens}`);
|
|
3995
|
+
console.log(` Estimated cost: $${estimatedCost.toFixed(4)}`);
|
|
3996
|
+
if (dryRun) {
|
|
3997
|
+
console.log();
|
|
3998
|
+
console.log(pc19.yellow("Dry run - no files were modified"));
|
|
3999
|
+
}
|
|
4000
|
+
console.log();
|
|
4001
|
+
}
|
|
4002
|
+
return {
|
|
4003
|
+
success: true,
|
|
4004
|
+
enhanced,
|
|
4005
|
+
totalTokens,
|
|
4006
|
+
estimatedCost
|
|
4007
|
+
};
|
|
4008
|
+
}
|
|
4009
|
+
function handleContextOnlyMode(contexts, format, isInteractive) {
|
|
4010
|
+
const systemPrompt = generateSystemPrompt();
|
|
4011
|
+
const componentContexts = [];
|
|
4012
|
+
for (const { name, context: context2 } of contexts) {
|
|
4013
|
+
if (!context2.usageAnalysis || context2.usageAnalysis.totalUsages < 2) {
|
|
4014
|
+
continue;
|
|
4015
|
+
}
|
|
4016
|
+
componentContexts.push(generatePromptContext(context2));
|
|
4017
|
+
}
|
|
4018
|
+
if (componentContexts.length === 0) {
|
|
4019
|
+
if (format === "json") {
|
|
4020
|
+
console.log(JSON.stringify({ success: false, error: "No components with sufficient usage data" }));
|
|
4021
|
+
} else if (isInteractive) {
|
|
4022
|
+
console.log(pc19.yellow("\nNo components with sufficient usage data to analyze."));
|
|
4023
|
+
}
|
|
4024
|
+
return { success: false, enhanced: [], totalTokens: 0, estimatedCost: 0 };
|
|
4025
|
+
}
|
|
4026
|
+
const fullContext = `${systemPrompt}
|
|
4027
|
+
|
|
4028
|
+
---
|
|
4029
|
+
|
|
4030
|
+
${componentContexts.join("\n\n---\n\n")}
|
|
4031
|
+
|
|
4032
|
+
---
|
|
4033
|
+
|
|
4034
|
+
Based on the usage analysis above, generate "when" and "whenNot" documentation for each component.
|
|
4035
|
+
|
|
4036
|
+
For each component, provide your response in JSON format:
|
|
4037
|
+
\`\`\`json
|
|
4038
|
+
{
|
|
4039
|
+
"componentName": "...",
|
|
4040
|
+
"when": ["scenario 1", "scenario 2", ...],
|
|
4041
|
+
"whenNot": ["anti-pattern 1", ...]
|
|
4042
|
+
}
|
|
4043
|
+
\`\`\``;
|
|
4044
|
+
if (format === "json") {
|
|
4045
|
+
console.log(JSON.stringify({
|
|
4046
|
+
success: true,
|
|
4047
|
+
context: fullContext,
|
|
4048
|
+
instructions: "Copy this prompt into your IDE AI (Cursor, Copilot, etc.) to generate suggestions"
|
|
4049
|
+
}, null, 2));
|
|
4050
|
+
} else {
|
|
4051
|
+
console.log(pc19.bold("\n\u{1F4CB} AI Context Generated\n"));
|
|
4052
|
+
console.log(pc19.dim("Copy the following prompt into your IDE AI (Cursor, Copilot, etc.):\n"));
|
|
4053
|
+
console.log(pc19.dim("\u2500".repeat(60)));
|
|
4054
|
+
console.log(fullContext);
|
|
4055
|
+
console.log(pc19.dim("\u2500".repeat(60)));
|
|
4056
|
+
console.log();
|
|
4057
|
+
console.log(pc19.green("Tip: In Cursor, press Cmd+L to open chat and paste this prompt."));
|
|
4058
|
+
console.log(pc19.dim("After getting suggestions, manually update your segment files."));
|
|
4059
|
+
console.log();
|
|
4060
|
+
}
|
|
4061
|
+
return {
|
|
4062
|
+
success: true,
|
|
4063
|
+
enhanced: [],
|
|
4064
|
+
totalTokens: 0,
|
|
4065
|
+
estimatedCost: 0,
|
|
4066
|
+
context: fullContext
|
|
4067
|
+
};
|
|
4068
|
+
}
|
|
4069
|
+
function detectProvider(options) {
|
|
4070
|
+
if (options.contextOnly) return "none";
|
|
4071
|
+
if (options.provider) return options.provider;
|
|
4072
|
+
if (options.apiKey) {
|
|
4073
|
+
if (options.apiKey.startsWith("sk-ant-")) return "anthropic";
|
|
4074
|
+
if (options.apiKey.startsWith("sk-")) return "openai";
|
|
4075
|
+
}
|
|
4076
|
+
if (process.env.ANTHROPIC_API_KEY) return "anthropic";
|
|
4077
|
+
if (process.env.OPENAI_API_KEY) return "openai";
|
|
4078
|
+
return "none";
|
|
4079
|
+
}
|
|
4080
|
+
function getApiKey(provider, explicitKey) {
|
|
4081
|
+
if (explicitKey) return explicitKey;
|
|
4082
|
+
if (provider === "anthropic") return process.env.ANTHROPIC_API_KEY;
|
|
4083
|
+
if (provider === "openai") return process.env.OPENAI_API_KEY;
|
|
4084
|
+
return void 0;
|
|
4085
|
+
}
|
|
4086
|
+
async function createAIClient(provider, apiKey) {
|
|
4087
|
+
if (provider === "anthropic") {
|
|
4088
|
+
const Anthropic = (await import("@anthropic-ai/sdk")).default;
|
|
4089
|
+
return new Anthropic({ apiKey });
|
|
4090
|
+
}
|
|
4091
|
+
if (provider === "openai") {
|
|
4092
|
+
const OpenAI = (await import("openai")).default;
|
|
4093
|
+
return new OpenAI({ apiKey });
|
|
4094
|
+
}
|
|
4095
|
+
throw new Error(`Unknown provider: ${provider}`);
|
|
4096
|
+
}
|
|
4097
|
+
async function generateEnhancement(client, provider, model, context2) {
|
|
4098
|
+
const systemPrompt = generateSystemPrompt();
|
|
4099
|
+
const userPrompt = generateUserPrompt(context2);
|
|
4100
|
+
if (provider === "anthropic") {
|
|
4101
|
+
return generateWithAnthropic(client, model, systemPrompt, userPrompt, context2);
|
|
4102
|
+
}
|
|
4103
|
+
if (provider === "openai") {
|
|
4104
|
+
return generateWithOpenAI(client, model, systemPrompt, userPrompt, context2);
|
|
4105
|
+
}
|
|
4106
|
+
throw new Error(`Unknown provider: ${provider}`);
|
|
4107
|
+
}
|
|
4108
|
+
async function generateWithAnthropic(client, model, systemPrompt, userPrompt, context2) {
|
|
4109
|
+
const anthropic = client;
|
|
4110
|
+
const response = await anthropic.messages.create({
|
|
4111
|
+
model,
|
|
4112
|
+
max_tokens: 1024,
|
|
4113
|
+
system: systemPrompt,
|
|
4114
|
+
messages: [{ role: "user", content: userPrompt }]
|
|
4115
|
+
});
|
|
4116
|
+
const content = response.content[0];
|
|
4117
|
+
if (content.type !== "text") {
|
|
4118
|
+
throw new Error("Unexpected response type");
|
|
4119
|
+
}
|
|
4120
|
+
const suggestions = parseAIResponse(content.text);
|
|
4121
|
+
const tokensUsed = (response.usage?.input_tokens || 0) + (response.usage?.output_tokens || 0);
|
|
4122
|
+
return {
|
|
4123
|
+
suggestions,
|
|
4124
|
+
confidence: calculateConfidence2(context2, suggestions),
|
|
4125
|
+
tokensUsed
|
|
4126
|
+
};
|
|
4127
|
+
}
|
|
4128
|
+
async function generateWithOpenAI(client, model, systemPrompt, userPrompt, context2) {
|
|
4129
|
+
const openai = client;
|
|
4130
|
+
const response = await openai.chat.completions.create({
|
|
4131
|
+
model,
|
|
4132
|
+
max_tokens: 1024,
|
|
4133
|
+
messages: [
|
|
4134
|
+
{ role: "system", content: systemPrompt },
|
|
4135
|
+
{ role: "user", content: userPrompt }
|
|
4136
|
+
]
|
|
4137
|
+
});
|
|
4138
|
+
const content = response.choices[0]?.message?.content;
|
|
4139
|
+
if (!content) {
|
|
4140
|
+
throw new Error("No response from OpenAI");
|
|
4141
|
+
}
|
|
4142
|
+
const suggestions = parseAIResponse(content);
|
|
4143
|
+
const tokensUsed = (response.usage?.prompt_tokens || 0) + (response.usage?.completion_tokens || 0);
|
|
4144
|
+
return {
|
|
4145
|
+
suggestions,
|
|
4146
|
+
confidence: calculateConfidence2(context2, suggestions),
|
|
4147
|
+
tokensUsed
|
|
4148
|
+
};
|
|
4149
|
+
}
|
|
4150
|
+
function parseAIResponse(text) {
|
|
4151
|
+
try {
|
|
4152
|
+
const jsonMatch = text.match(/```json\n?([\s\S]*?)\n?```/) || text.match(/\{[\s\S]*\}/);
|
|
4153
|
+
const jsonStr = jsonMatch ? jsonMatch[1] || jsonMatch[0] : text;
|
|
4154
|
+
const parsed = JSON.parse(jsonStr);
|
|
4155
|
+
return {
|
|
4156
|
+
when: Array.isArray(parsed.when) ? parsed.when : [],
|
|
4157
|
+
whenNot: Array.isArray(parsed.whenNot) ? parsed.whenNot : []
|
|
4158
|
+
};
|
|
4159
|
+
} catch {
|
|
4160
|
+
return extractSuggestionsFromText(text);
|
|
4161
|
+
}
|
|
4162
|
+
}
|
|
4163
|
+
function extractSuggestionsFromText(text) {
|
|
4164
|
+
const when = [];
|
|
4165
|
+
const whenNot = [];
|
|
4166
|
+
const lines = text.split("\n");
|
|
4167
|
+
let currentSection = null;
|
|
4168
|
+
for (const line of lines) {
|
|
4169
|
+
const trimmed = line.trim();
|
|
4170
|
+
if (/^(when|use when|when to use)/i.test(trimmed)) {
|
|
4171
|
+
currentSection = "when";
|
|
4172
|
+
continue;
|
|
4173
|
+
}
|
|
4174
|
+
if (/^(when not|do not use|avoid|whenNot)/i.test(trimmed)) {
|
|
4175
|
+
currentSection = "whenNot";
|
|
4176
|
+
continue;
|
|
4177
|
+
}
|
|
4178
|
+
if (currentSection && /^[-*]\s+/.test(trimmed)) {
|
|
4179
|
+
const item = trimmed.replace(/^[-*]\s+/, "").trim();
|
|
4180
|
+
if (item && item.length > 10) {
|
|
4181
|
+
if (currentSection === "when") {
|
|
4182
|
+
when.push(item);
|
|
4183
|
+
} else {
|
|
4184
|
+
whenNot.push(item);
|
|
4185
|
+
}
|
|
4186
|
+
}
|
|
4187
|
+
}
|
|
4188
|
+
}
|
|
4189
|
+
return { when, whenNot };
|
|
4190
|
+
}
|
|
4191
|
+
function calculateConfidence2(context2, suggestions) {
|
|
4192
|
+
let confidence = 50;
|
|
4193
|
+
if (context2.usageAnalysis.totalUsages > 10) confidence += 20;
|
|
4194
|
+
if (context2.storybook && context2.storybook.stories.length > 0) confidence += 15;
|
|
4195
|
+
if (suggestions.when.length >= 2) confidence += 10;
|
|
4196
|
+
if (suggestions.whenNot.length >= 1) confidence += 5;
|
|
4197
|
+
return Math.min(confidence, 100);
|
|
4198
|
+
}
|
|
4199
|
+
function calculateCost(provider, tokens) {
|
|
4200
|
+
const costsPer1M = {
|
|
4201
|
+
anthropic: 3,
|
|
4202
|
+
// Claude Sonnet
|
|
4203
|
+
openai: 5,
|
|
4204
|
+
// GPT-4o
|
|
4205
|
+
none: 0
|
|
4206
|
+
};
|
|
4207
|
+
return tokens / 1e6 * costsPer1M[provider];
|
|
4208
|
+
}
|
|
4209
|
+
async function findSegmentFiles(dir) {
|
|
4210
|
+
const fg4 = await import("fast-glob");
|
|
4211
|
+
return fg4.default(["**/*.segment.tsx", "**/*.segment.ts"], {
|
|
4212
|
+
cwd: dir,
|
|
4213
|
+
absolute: true,
|
|
4214
|
+
ignore: ["**/node_modules/**", "**/dist/**"]
|
|
4215
|
+
});
|
|
4216
|
+
}
|
|
4217
|
+
function extractComponentName(filePath) {
|
|
4218
|
+
const match = filePath.match(/([^/\\]+)\.segment\.(tsx?|jsx?)$/);
|
|
4219
|
+
return match ? match[1] : "";
|
|
4220
|
+
}
|
|
4221
|
+
async function updateSegmentFile(filePath, suggestions) {
|
|
4222
|
+
const content = await readFile5(filePath, "utf-8");
|
|
4223
|
+
let updated = content;
|
|
4224
|
+
if (suggestions.when.length > 0) {
|
|
4225
|
+
const whenItems = suggestions.when.map((s) => ` "${s}"`).join(",\n");
|
|
4226
|
+
const whenMatch = updated.match(/when:\s*\[\s*([^\]]*)\]/);
|
|
4227
|
+
if (whenMatch) {
|
|
4228
|
+
const existingContent = whenMatch[1].trim();
|
|
4229
|
+
if (existingContent) {
|
|
4230
|
+
updated = updated.replace(
|
|
4231
|
+
/when:\s*\[\s*([^\]]*)\]/,
|
|
4232
|
+
`when: [
|
|
4233
|
+
${existingContent},
|
|
4234
|
+
${whenItems}
|
|
4235
|
+
]`
|
|
4236
|
+
);
|
|
4237
|
+
} else {
|
|
4238
|
+
updated = updated.replace(
|
|
4239
|
+
/when:\s*\[\s*\]/,
|
|
4240
|
+
`when: [
|
|
4241
|
+
${whenItems}
|
|
4242
|
+
]`
|
|
4243
|
+
);
|
|
4244
|
+
}
|
|
4245
|
+
}
|
|
4246
|
+
}
|
|
4247
|
+
if (suggestions.whenNot.length > 0) {
|
|
4248
|
+
const whenNotItems = suggestions.whenNot.map((s) => ` "${s}"`).join(",\n");
|
|
4249
|
+
const whenNotMatch = updated.match(/whenNot:\s*\[\s*([^\]]*)\]/);
|
|
4250
|
+
if (whenNotMatch) {
|
|
4251
|
+
const existingContent = whenNotMatch[1].trim();
|
|
4252
|
+
if (existingContent) {
|
|
4253
|
+
updated = updated.replace(
|
|
4254
|
+
/whenNot:\s*\[\s*([^\]]*)\]/,
|
|
4255
|
+
`whenNot: [
|
|
4256
|
+
${existingContent},
|
|
4257
|
+
${whenNotItems}
|
|
4258
|
+
]`
|
|
4259
|
+
);
|
|
4260
|
+
} else {
|
|
4261
|
+
updated = updated.replace(
|
|
4262
|
+
/whenNot:\s*\[\s*\]/,
|
|
4263
|
+
`whenNot: [
|
|
4264
|
+
${whenNotItems}
|
|
4265
|
+
]`
|
|
4266
|
+
);
|
|
4267
|
+
}
|
|
4268
|
+
}
|
|
4269
|
+
}
|
|
4270
|
+
if (updated !== content) {
|
|
4271
|
+
await writeFile7(filePath, updated, "utf-8");
|
|
4272
|
+
}
|
|
4273
|
+
}
|
|
4274
|
+
|
|
4275
|
+
// src/bin.ts
|
|
4276
|
+
var program = new Command();
|
|
4277
|
+
program.name(BRAND.cliCommand).description(`${BRAND.name} - Design system documentation and compliance tool`).version("0.1.0");
|
|
4278
|
+
program.command("validate").description("Validate fragment files").option("-c, --config <path>", "Path to config file").option("--schema", "Validate fragment schema only").option("--coverage", "Validate coverage only").action(async (options) => {
|
|
4279
|
+
try {
|
|
4280
|
+
const result = await validate(options);
|
|
4281
|
+
if (!result.valid) {
|
|
4282
|
+
process.exit(1);
|
|
4283
|
+
}
|
|
4284
|
+
} catch (error) {
|
|
4285
|
+
console.error(pc20.red("Error:"), error instanceof Error ? error.message : error);
|
|
4286
|
+
process.exit(1);
|
|
4287
|
+
}
|
|
4288
|
+
});
|
|
4289
|
+
program.command("build").description(`Build compiled ${BRAND.outFile} and ${BRAND.dataDir}/ directory`).option("-c, --config <path>", "Path to config file").option("-o, --output <path>", "Output file path").option("--registry", `Also generate ${BRAND.dataDir}/${BRAND.registryFile} and ${BRAND.contextFile}`).option("--registry-only", `Only generate ${BRAND.dataDir}/ directory (skip ${BRAND.outFile})`).option("--from-source", "Build from source code (zero-config, no fragment files needed)").option("--skip-usage", "Skip usage analysis when building from source").option("--skip-storybook", "Skip Storybook parsing when building from source").option("-v, --verbose", "Verbose output").action(async (options) => {
|
|
4290
|
+
try {
|
|
4291
|
+
const result = await build({
|
|
4292
|
+
config: options.config,
|
|
4293
|
+
output: options.output,
|
|
4294
|
+
registry: options.registry,
|
|
4295
|
+
registryOnly: options.registryOnly,
|
|
4296
|
+
fromSource: options.fromSource,
|
|
4297
|
+
skipUsage: options.skipUsage,
|
|
4298
|
+
skipStorybook: options.skipStorybook,
|
|
4299
|
+
verbose: options.verbose
|
|
4300
|
+
});
|
|
4301
|
+
if (!result.success) {
|
|
4302
|
+
process.exit(1);
|
|
4303
|
+
}
|
|
4304
|
+
} catch (error) {
|
|
4305
|
+
console.error(pc20.red("Error:"), error instanceof Error ? error.message : error);
|
|
4306
|
+
process.exit(1);
|
|
4307
|
+
}
|
|
4308
|
+
});
|
|
4309
|
+
program.command("context").description("Generate AI-ready context for your design system").option("-c, --config <path>", "Path to config file").option("-i, --input <path>", `Path to ${BRAND.outFile} (builds if not provided)`).option("-f, --format <format>", "Output format (markdown/json)", "markdown").option("--compact", "Minimal output for token efficiency").option("--code", "Include code examples").option("--relations", "Include component relationships").option("--tokens", "Only output token estimate").action(async (options) => {
|
|
4310
|
+
try {
|
|
4311
|
+
const result = await context({
|
|
4312
|
+
config: options.config,
|
|
4313
|
+
input: options.input,
|
|
4314
|
+
format: options.format,
|
|
4315
|
+
compact: options.compact,
|
|
4316
|
+
includeCode: options.code,
|
|
4317
|
+
includeRelations: options.relations,
|
|
4318
|
+
tokensOnly: options.tokens
|
|
4319
|
+
});
|
|
4320
|
+
if (!result.success) {
|
|
4321
|
+
process.exit(1);
|
|
4322
|
+
}
|
|
4323
|
+
} catch (error) {
|
|
4324
|
+
console.error(pc20.red("Error:"), error instanceof Error ? error.message : error);
|
|
4325
|
+
process.exit(1);
|
|
4326
|
+
}
|
|
4327
|
+
});
|
|
4328
|
+
program.command("ai").description("Generate context optimized for AI assistants (Claude Desktop)").option("-c, --config <path>", "Path to config file").option("--live", "Connect to running dev server for live data").option("-p, --port <port>", "Dev server port", "6006").action(async (options) => {
|
|
4329
|
+
try {
|
|
4330
|
+
if (options.live) {
|
|
4331
|
+
const baseUrl = `http://localhost:${options.port}`;
|
|
4332
|
+
const response = await fetch(`${baseUrl}/${BRAND.nameLower}/context?format=markdown`);
|
|
4333
|
+
if (!response.ok) {
|
|
4334
|
+
throw new Error("Failed to fetch context. Make sure dev server is running.");
|
|
4335
|
+
}
|
|
4336
|
+
const contextContent = await response.text();
|
|
4337
|
+
console.log(contextContent);
|
|
4338
|
+
} else {
|
|
4339
|
+
const result = await context({
|
|
4340
|
+
config: options.config,
|
|
4341
|
+
format: "markdown",
|
|
4342
|
+
compact: false,
|
|
4343
|
+
includeCode: true,
|
|
4344
|
+
includeRelations: true
|
|
4345
|
+
});
|
|
4346
|
+
if (!result.success) {
|
|
4347
|
+
process.exit(1);
|
|
4348
|
+
}
|
|
4349
|
+
}
|
|
4350
|
+
} catch (error) {
|
|
4351
|
+
console.error(pc20.red("Error:"), error instanceof Error ? error.message : error);
|
|
4352
|
+
process.exit(1);
|
|
4353
|
+
}
|
|
4354
|
+
});
|
|
4355
|
+
program.command("list").description("List all discovered fragment files").option("-c, --config <path>", "Path to config file").action(async (options) => {
|
|
4356
|
+
try {
|
|
4357
|
+
await list(options);
|
|
4358
|
+
} catch (error) {
|
|
4359
|
+
console.error(pc20.red("Error:"), error instanceof Error ? error.message : error);
|
|
4360
|
+
process.exit(1);
|
|
4361
|
+
}
|
|
4362
|
+
});
|
|
4363
|
+
program.command("reset").description("Reset to initial state (delete all generated files)").option("-y, --yes", "Skip confirmation prompt").option("--dry-run", "Show what would be deleted without deleting").action(async (options) => {
|
|
4364
|
+
try {
|
|
4365
|
+
await reset(options);
|
|
4366
|
+
} catch (error) {
|
|
4367
|
+
console.error(pc20.red("Error:"), error instanceof Error ? error.message : error);
|
|
4368
|
+
process.exit(1);
|
|
4369
|
+
}
|
|
4370
|
+
});
|
|
4371
|
+
var linkCommand = program.command("link").description("Link external resources (Figma designs, Storybook stories) to fragments");
|
|
4372
|
+
linkCommand.command("figma").argument("[figma-url]", "Figma file URL to link components from").description("Interactive wizard to link Figma components to code").option("-c, --config <path>", "Path to config file").option("--auto", "Auto-link matching components without prompts").option("--dry-run", "Show matches without updating files").option("--no-variants", "Skip linking individual variants to their Figma frames").action(async (figmaUrl, options) => {
|
|
4373
|
+
try {
|
|
4374
|
+
await linkFigma(figmaUrl, {
|
|
4375
|
+
config: options.config,
|
|
4376
|
+
auto: options.auto,
|
|
4377
|
+
dryRun: options.dryRun,
|
|
4378
|
+
variants: options.variants
|
|
4379
|
+
});
|
|
4380
|
+
} catch (error) {
|
|
4381
|
+
console.error(pc20.red("Error:"), error instanceof Error ? error.message : error);
|
|
4382
|
+
process.exit(1);
|
|
4383
|
+
}
|
|
4384
|
+
});
|
|
4385
|
+
linkCommand.command("storybook").description("Bootstrap fragments from existing Storybook stories").option("-c, --config <path>", "Path to .storybook/main.* config").option("-o, --out <dir>", "Output directory for fragment files").option("--yes", "Skip confirmation prompts").option("--dry-run", "Preview what would be generated without writing files").option("--include <glob>", "Only process stories matching glob").option("--exclude <glob>", "Skip stories matching glob").action(async (options) => {
|
|
4386
|
+
try {
|
|
4387
|
+
await linkStorybook({
|
|
4388
|
+
config: options.config,
|
|
4389
|
+
out: options.out,
|
|
4390
|
+
yes: options.yes,
|
|
4391
|
+
dryRun: options.dryRun,
|
|
4392
|
+
include: options.include,
|
|
4393
|
+
exclude: options.exclude
|
|
4394
|
+
});
|
|
4395
|
+
} catch (error) {
|
|
4396
|
+
console.error(pc20.red("Error:"), error instanceof Error ? error.message : error);
|
|
4397
|
+
process.exit(1);
|
|
4398
|
+
}
|
|
4399
|
+
});
|
|
4400
|
+
program.command("dev").description("Start the development server with live component rendering").option("-p, --port <port>", "Port to run on", "6006").option("--no-open", "Do not open browser").option("--skip-setup", "Skip auto-setup (Storybook import, build, Figma link)").option("--skip-storybook", "Skip auto-importing from Storybook").option("--skip-figma", "Skip Figma link check").option("--skip-build", `Skip auto-building ${BRAND.outFile}`).action(async (options) => {
|
|
4401
|
+
try {
|
|
4402
|
+
await dev({
|
|
4403
|
+
port: options.port,
|
|
4404
|
+
open: options.open,
|
|
4405
|
+
skipSetup: options.skipSetup,
|
|
4406
|
+
skipStorybook: options.skipStorybook,
|
|
4407
|
+
skipFigma: options.skipFigma,
|
|
4408
|
+
skipBuild: options.skipBuild
|
|
4409
|
+
});
|
|
4410
|
+
} catch (error) {
|
|
4411
|
+
console.error(pc20.red("Error:"), error instanceof Error ? error.message : error);
|
|
4412
|
+
if (error instanceof Error && error.stack) {
|
|
4413
|
+
console.error(pc20.dim(error.stack));
|
|
4414
|
+
}
|
|
4415
|
+
process.exit(1);
|
|
4416
|
+
}
|
|
4417
|
+
});
|
|
4418
|
+
program.command("screenshot").description("Capture screenshots of component variants").option("-c, --config <path>", "Path to config file").option("--component <name>", "Capture specific component only").option("--variant <name>", "Capture specific variant only").option("--theme <theme>", "Theme to capture (light/dark)", "light").option("--update", "Update existing baselines").option("--width <pixels>", "Viewport width", parseInt).option("--height <pixels>", "Viewport height", parseInt).option("--ci", "CI mode - no interactive prompts").action(async (options) => {
|
|
4419
|
+
try {
|
|
4420
|
+
const { config, configDir } = await loadConfig(options.config);
|
|
4421
|
+
const result = await runScreenshotCommand(config, configDir, {
|
|
4422
|
+
component: options.component,
|
|
4423
|
+
variant: options.variant,
|
|
4424
|
+
theme: options.theme,
|
|
4425
|
+
update: options.update,
|
|
4426
|
+
ci: options.ci,
|
|
4427
|
+
width: options.width,
|
|
4428
|
+
height: options.height
|
|
4429
|
+
});
|
|
4430
|
+
if (!result.success) {
|
|
4431
|
+
process.exit(1);
|
|
4432
|
+
}
|
|
4433
|
+
} catch (error) {
|
|
4434
|
+
console.error(pc20.red("Error:"), error instanceof Error ? error.message : error);
|
|
4435
|
+
process.exit(1);
|
|
4436
|
+
}
|
|
4437
|
+
});
|
|
4438
|
+
program.command("diff").argument("[component]", "Component name to diff (optional)").description("Compare current renders against baselines").option("-c, --config <path>", "Path to config file").option("--variant <name>", "Compare specific variant only").option("--theme <theme>", "Theme to compare (light/dark)", "light").option("--threshold <percent>", "Diff threshold percentage", parseFloat).option("--ci", "CI mode - exit 1 on differences").option("--open", "Open diff images").action(async (component, options) => {
|
|
4439
|
+
try {
|
|
4440
|
+
const { config, configDir } = await loadConfig(options.config);
|
|
4441
|
+
const result = await runDiffCommand(config, configDir, {
|
|
4442
|
+
component,
|
|
4443
|
+
variant: options.variant,
|
|
4444
|
+
theme: options.theme,
|
|
4445
|
+
threshold: options.threshold,
|
|
4446
|
+
ci: options.ci,
|
|
4447
|
+
open: options.open
|
|
4448
|
+
});
|
|
4449
|
+
if (!result.success && options.ci) {
|
|
4450
|
+
process.exit(1);
|
|
4451
|
+
}
|
|
4452
|
+
} catch (error) {
|
|
4453
|
+
console.error(pc20.red("Error:"), error instanceof Error ? error.message : error);
|
|
4454
|
+
process.exit(1);
|
|
4455
|
+
}
|
|
4456
|
+
});
|
|
4457
|
+
program.command("compare").argument("[component]", "Component name to compare").description("Compare component renders against Figma designs").option("-c, --config <path>", "Path to config file").option("--variant <name>", "Compare specific variant").option("--figma <url>", "Figma frame URL (uses fragment figma link if not provided)").option("--threshold <percent>", "Diff threshold percentage", parseFloat, 1).option("--all", "Compare all components with Figma links").option("--output <dir>", "Save diff images to directory").option("-p, --port <port>", "Dev server port", "6006").action(async (component, options) => {
|
|
4458
|
+
try {
|
|
4459
|
+
const result = await compare(component, {
|
|
4460
|
+
config: options.config,
|
|
4461
|
+
variant: options.variant,
|
|
4462
|
+
figma: options.figma,
|
|
4463
|
+
threshold: options.threshold,
|
|
4464
|
+
all: options.all,
|
|
4465
|
+
output: options.output,
|
|
4466
|
+
port: options.port
|
|
4467
|
+
});
|
|
4468
|
+
if (!result.success) {
|
|
4469
|
+
process.exit(1);
|
|
4470
|
+
}
|
|
4471
|
+
} catch (error) {
|
|
4472
|
+
console.error(pc20.red("Error:"), error instanceof Error ? error.message : error);
|
|
4473
|
+
console.log(pc20.dim(`
|
|
4474
|
+
Make sure the dev server is running: ${BRAND.cliCommand} dev`));
|
|
4475
|
+
process.exit(1);
|
|
4476
|
+
}
|
|
4477
|
+
});
|
|
4478
|
+
program.command("analyze").description("Analyze design system and generate report").option("-c, --config <path>", "Path to config file").option("-f, --format <format>", "Output format (html/json/console)", "html").option("-o, --output <path>", "Output file path").option("--open", "Open report in browser after generation").option("--ci", "CI mode - exit with code based on score").option("--min-score <score>", "Minimum score to pass in CI mode", parseFloat).action(async (options) => {
|
|
4479
|
+
try {
|
|
4480
|
+
const { config, configDir } = await loadConfig(options.config);
|
|
4481
|
+
const result = await runAnalyzeCommand(config, configDir, {
|
|
4482
|
+
format: options.format,
|
|
4483
|
+
output: options.output,
|
|
4484
|
+
open: options.open,
|
|
4485
|
+
ci: options.ci,
|
|
4486
|
+
minScore: options.minScore
|
|
4487
|
+
});
|
|
4488
|
+
if (!result.success) {
|
|
4489
|
+
process.exit(1);
|
|
4490
|
+
}
|
|
4491
|
+
} catch (error) {
|
|
4492
|
+
console.error(pc20.red("Error:"), error instanceof Error ? error.message : error);
|
|
4493
|
+
process.exit(1);
|
|
4494
|
+
}
|
|
4495
|
+
});
|
|
4496
|
+
program.command("verify").argument("[component]", "Component name to verify (optional, verifies all if omitted)").description("Verify component compliance for CI pipelines").option("-c, --config <path>", "Path to config file").option("--ci", "CI mode - output JSON and exit non-zero on failure").option("--min-compliance <percent>", "Minimum compliance percentage (default: 80)", parseFloat, 80).option("-p, --port <port>", "Dev server port", "6006").action(async (component, options) => {
|
|
4497
|
+
try {
|
|
4498
|
+
const summary = await verify(component, {
|
|
4499
|
+
config: options.config,
|
|
4500
|
+
ci: options.ci,
|
|
4501
|
+
minCompliance: options.minCompliance,
|
|
4502
|
+
port: options.port
|
|
4503
|
+
});
|
|
4504
|
+
if (!summary.passed && options.ci) {
|
|
4505
|
+
process.exit(1);
|
|
4506
|
+
}
|
|
4507
|
+
} catch (error) {
|
|
4508
|
+
if (options.ci) {
|
|
4509
|
+
console.log(JSON.stringify({ error: error instanceof Error ? error.message : "Verification failed" }));
|
|
4510
|
+
} else {
|
|
4511
|
+
console.error(pc20.red("Error:"), error instanceof Error ? error.message : error);
|
|
4512
|
+
}
|
|
4513
|
+
process.exit(1);
|
|
4514
|
+
}
|
|
4515
|
+
});
|
|
4516
|
+
program.command("audit").description("Scan all fragments and show compliance metrics across the design system").option("-c, --config <path>", "Path to config file").option("--json", "Output machine-readable JSON format").option("--sort <field>", "Sort by field: compliance, name, hardcoded (default: compliance)", "compliance").option("-p, --port <port>", "Dev server port", "6006").action(async (options) => {
|
|
4517
|
+
try {
|
|
4518
|
+
await audit({
|
|
4519
|
+
config: options.config,
|
|
4520
|
+
json: options.json,
|
|
4521
|
+
sort: options.sort,
|
|
4522
|
+
port: options.port
|
|
4523
|
+
});
|
|
4524
|
+
} catch (error) {
|
|
4525
|
+
if (options.json) {
|
|
4526
|
+
console.log(JSON.stringify({ error: error instanceof Error ? error.message : "Audit failed" }));
|
|
4527
|
+
} else {
|
|
4528
|
+
console.error(pc20.red("Error:"), error instanceof Error ? error.message : error);
|
|
4529
|
+
}
|
|
4530
|
+
process.exit(1);
|
|
4531
|
+
}
|
|
4532
|
+
});
|
|
4533
|
+
program.command("a11y").description("Run accessibility checks on all component variants").option("-c, --config <path>", "Path to config file").option("--json", "Output results as JSON").option("--ci", "CI mode (exit code 1 if any critical/serious violations)").option("--component <name>", "Check specific component only").option("-p, --port <port>", "Dev server port", "6006").action(async (options) => {
|
|
4534
|
+
try {
|
|
4535
|
+
await a11y({
|
|
4536
|
+
config: options.config,
|
|
4537
|
+
json: options.json,
|
|
4538
|
+
ci: options.ci,
|
|
4539
|
+
component: options.component,
|
|
4540
|
+
port: options.port
|
|
4541
|
+
});
|
|
4542
|
+
} catch (error) {
|
|
4543
|
+
if (options.json) {
|
|
4544
|
+
console.log(JSON.stringify({ error: error instanceof Error ? error.message : "A11y check failed" }));
|
|
4545
|
+
} else {
|
|
4546
|
+
console.error(pc20.red("Error:"), error instanceof Error ? error.message : error);
|
|
4547
|
+
}
|
|
4548
|
+
process.exit(1);
|
|
4549
|
+
}
|
|
4550
|
+
});
|
|
4551
|
+
program.command("enhance").description("AI-powered documentation generation from codebase analysis").option("-c, --config <path>", "Path to config file").option("--component <name>", 'Enhance specific component (or "all")').option("--yes", "Skip confirmation prompts").option("--dry-run", "Only analyze, do not modify files").option("--format <format>", "Output format: interactive, json, quiet, context", "interactive").option("--provider <provider>", "AI provider: anthropic, openai (auto-detected from API key)").option("--api-key <key>", "API key (or use ANTHROPIC_API_KEY/OPENAI_API_KEY env)").option("--model <model>", "AI model to use (default: claude-sonnet-4 for anthropic, gpt-4o for openai)").option("--root <dir>", "Root directory to scan", process.cwd()).option("--context-only", "Output context for IDE AI (Cursor, Copilot) without calling API").action(async (options) => {
|
|
4552
|
+
try {
|
|
4553
|
+
await enhance({
|
|
4554
|
+
config: options.config,
|
|
4555
|
+
component: options.component,
|
|
4556
|
+
yes: options.yes,
|
|
4557
|
+
dryRun: options.dryRun,
|
|
4558
|
+
format: options.format,
|
|
4559
|
+
provider: options.provider,
|
|
4560
|
+
apiKey: options.apiKey,
|
|
4561
|
+
model: options.model,
|
|
4562
|
+
root: options.root,
|
|
4563
|
+
contextOnly: options.contextOnly
|
|
4564
|
+
});
|
|
4565
|
+
} catch (error) {
|
|
4566
|
+
if (options.format === "json") {
|
|
4567
|
+
console.log(JSON.stringify({ success: false, error: error instanceof Error ? error.message : "Enhance failed" }));
|
|
4568
|
+
} else {
|
|
4569
|
+
console.error(pc20.red("Error:"), error instanceof Error ? error.message : error);
|
|
4570
|
+
}
|
|
4571
|
+
process.exit(1);
|
|
4572
|
+
}
|
|
4573
|
+
});
|
|
4574
|
+
program.command("scan").description(`Zero-config ${BRAND.outFile} generation from source code`).option("-c, --config <path>", "Path to config file").option("-o, --output <path>", "Output file path", BRAND.outFile).option("--patterns <patterns...>", "Component file patterns to scan").option("--barrel <files...>", "Barrel export files to parse").option("--usage-dir <dir>", "Directory to scan for usage patterns").option("--skip-usage", "Skip usage pattern analysis").option("--skip-storybook", "Skip Storybook story parsing").option("-v, --verbose", "Verbose output").action(async (options) => {
|
|
4575
|
+
try {
|
|
4576
|
+
const result = await scan({
|
|
4577
|
+
config: options.config,
|
|
4578
|
+
output: options.output,
|
|
4579
|
+
componentPatterns: options.patterns,
|
|
4580
|
+
barrelFiles: options.barrel,
|
|
4581
|
+
usageDir: options.usageDir,
|
|
4582
|
+
skipUsage: options.skipUsage,
|
|
4583
|
+
skipStorybook: options.skipStorybook,
|
|
4584
|
+
verbose: options.verbose
|
|
4585
|
+
});
|
|
4586
|
+
if (!result.success) {
|
|
4587
|
+
process.exit(1);
|
|
4588
|
+
}
|
|
4589
|
+
} catch (error) {
|
|
4590
|
+
console.error(pc20.red("Error:"), error instanceof Error ? error.message : error);
|
|
4591
|
+
process.exit(1);
|
|
4592
|
+
}
|
|
4593
|
+
});
|
|
4594
|
+
program.command("storygen").description("Generate Storybook stories from fragment definitions").option("-c, --config <path>", "Path to config file").option("-o, --output <dir>", "Output directory", ".storybook/generated").option("--watch", "Watch for segment changes and regenerate").option("--format <format>", "Story format (csf3)", "csf3").action(async (options) => {
|
|
4595
|
+
try {
|
|
4596
|
+
await storygen({
|
|
4597
|
+
config: options.config,
|
|
4598
|
+
output: options.output,
|
|
4599
|
+
watch: options.watch,
|
|
4600
|
+
format: options.format
|
|
4601
|
+
});
|
|
4602
|
+
} catch (error) {
|
|
4603
|
+
console.error(pc20.red("Error:"), error instanceof Error ? error.message : error);
|
|
4604
|
+
process.exit(1);
|
|
4605
|
+
}
|
|
4606
|
+
});
|
|
4607
|
+
program.command("metrics").argument("[component]", "Component name (optional, shows system-wide if omitted)").description("View compliance trends over time").option("-c, --config <path>", "Path to config file").option("--days <number>", "Number of days to look back", parseInt, 30).option("--json", "Output JSON format").action(async (component, options) => {
|
|
4608
|
+
try {
|
|
4609
|
+
await metrics(component, {
|
|
4610
|
+
config: options.config,
|
|
4611
|
+
days: options.days,
|
|
4612
|
+
json: options.json
|
|
4613
|
+
});
|
|
4614
|
+
} catch (error) {
|
|
4615
|
+
console.error(pc20.red("Error:"), error instanceof Error ? error.message : error);
|
|
4616
|
+
process.exit(1);
|
|
4617
|
+
}
|
|
4618
|
+
});
|
|
4619
|
+
program.command("baseline").description("Manage visual regression baselines").argument("<action>", "Action to perform: update, list, delete").argument("[component]", "Component name (optional for update/delete)").option("-c, --config <path>", "Path to config file").option("--variant <name>", "Specific variant to update").option("--all", "Update/delete all baselines").option("--theme <theme>", "Theme for baseline (light/dark)", "light").option("-p, --port <port>", "Dev server port", "6006").action(async (action, component, options) => {
|
|
4620
|
+
try {
|
|
4621
|
+
await baseline(action, component, {
|
|
4622
|
+
config: options.config,
|
|
4623
|
+
variant: options.variant,
|
|
4624
|
+
all: options.all,
|
|
4625
|
+
theme: options.theme,
|
|
4626
|
+
port: options.port
|
|
4627
|
+
});
|
|
4628
|
+
} catch (error) {
|
|
4629
|
+
console.error(pc20.red("Error:"), error instanceof Error ? error.message : error);
|
|
4630
|
+
if (action === "update") {
|
|
4631
|
+
console.log(pc20.dim(`
|
|
4632
|
+
Make sure the dev server is running: ${BRAND.cliCommand} dev`));
|
|
4633
|
+
}
|
|
4634
|
+
process.exit(1);
|
|
4635
|
+
}
|
|
4636
|
+
});
|
|
4637
|
+
program.command("view").description(`Generate a static HTML viewer for ${BRAND.outFile}`).option("-i, --input <path>", `Path to ${BRAND.outFile}`, BRAND.outFile).option("-o, --output <path>", "Output HTML file path", BRAND.viewerHtmlFile).option("--open", "Open in browser after generation").action(async (options) => {
|
|
4638
|
+
try {
|
|
4639
|
+
const { generateViewerFromJson } = await import("./static-viewer-MIPGZ4Z7.js");
|
|
4640
|
+
const fs = await import("fs/promises");
|
|
4641
|
+
const path = await import("path");
|
|
4642
|
+
const inputPath = path.resolve(process.cwd(), options.input);
|
|
4643
|
+
const outputPath = path.resolve(process.cwd(), options.output);
|
|
4644
|
+
console.log(pc20.cyan(`
|
|
4645
|
+
${BRAND.name} Viewer Generator
|
|
4646
|
+
`));
|
|
4647
|
+
try {
|
|
4648
|
+
await fs.access(inputPath);
|
|
4649
|
+
} catch {
|
|
4650
|
+
console.log(pc20.red(`Error: ${options.input} not found.`));
|
|
4651
|
+
console.log(pc20.dim(`
|
|
4652
|
+
Run ${pc20.cyan(`${BRAND.cliCommand} build`)} first to generate ${BRAND.outFile}
|
|
4653
|
+
`));
|
|
4654
|
+
process.exit(1);
|
|
4655
|
+
}
|
|
4656
|
+
console.log(pc20.dim(`Reading: ${options.input}`));
|
|
4657
|
+
const html = await generateViewerFromJson(inputPath);
|
|
4658
|
+
await fs.writeFile(outputPath, html);
|
|
4659
|
+
console.log(pc20.green(`
|
|
4660
|
+
\u2713 Generated: ${options.output}
|
|
4661
|
+
`));
|
|
4662
|
+
if (options.open) {
|
|
4663
|
+
const { exec } = await import("child_process");
|
|
4664
|
+
const openCmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
|
|
4665
|
+
exec(`${openCmd} "${outputPath}"`);
|
|
4666
|
+
}
|
|
4667
|
+
} catch (error) {
|
|
4668
|
+
console.error(pc20.red("Error:"), error instanceof Error ? error.message : error);
|
|
4669
|
+
process.exit(1);
|
|
4670
|
+
}
|
|
4671
|
+
});
|
|
4672
|
+
program.command("add").argument("[name]", 'Component name (e.g., "Button", "TextField")').description("Scaffold a new component with fragment file").option("-c, --category <category>", "Component category").option("-d, --dir <directory>", "Output directory").option("-t, --template <template>", "Template to use (action, form-input, layout, display)").option("--no-component", "Only generate fragment file, skip component stub").action(async (name, options) => {
|
|
4673
|
+
try {
|
|
4674
|
+
await add(name, {
|
|
4675
|
+
category: options.category,
|
|
4676
|
+
dir: options.dir,
|
|
4677
|
+
template: options.template,
|
|
4678
|
+
component: options.component
|
|
4679
|
+
});
|
|
4680
|
+
} catch (error) {
|
|
4681
|
+
console.error(pc20.red("Error:"), error instanceof Error ? error.message : error);
|
|
4682
|
+
process.exit(1);
|
|
4683
|
+
}
|
|
4684
|
+
});
|
|
4685
|
+
program.command("init").description("Initialize fragments in a project (interactive by default)").option("--force", "Overwrite existing config").option("-y, --yes", "Non-interactive mode - auto-detect and use defaults").action(async (options) => {
|
|
4686
|
+
try {
|
|
4687
|
+
const { init } = await import("./init-EMVI47QG.js");
|
|
4688
|
+
const result = await init({
|
|
4689
|
+
projectRoot: process.cwd(),
|
|
4690
|
+
force: options.force,
|
|
4691
|
+
yes: options.yes
|
|
4692
|
+
});
|
|
4693
|
+
if (!result.success) {
|
|
4694
|
+
console.error(pc20.red("\nInit failed with errors:"));
|
|
4695
|
+
for (const error of result.errors) {
|
|
4696
|
+
console.error(pc20.red(` - ${error}`));
|
|
4697
|
+
}
|
|
4698
|
+
process.exit(1);
|
|
4699
|
+
}
|
|
4700
|
+
} catch (error) {
|
|
4701
|
+
console.error(pc20.red("Error:"), error instanceof Error ? error.message : error);
|
|
4702
|
+
process.exit(1);
|
|
4703
|
+
}
|
|
4704
|
+
});
|
|
4705
|
+
program.command("tokens").description("Discover and list design tokens from CSS/SCSS files").option("-c, --config <path>", "Path to config file").option("--json", "Output as JSON").option("--categories", "Group tokens by category").option("--theme <theme>", "Filter by theme name").option("--category <category>", "Filter by category (color, spacing, typography, etc.)").option("--verbose", "Show all tokens (no truncation)").action(async (options) => {
|
|
4706
|
+
try {
|
|
4707
|
+
const { tokens } = await import("./tokens-HSGMYK64.js");
|
|
4708
|
+
const result = await tokens({
|
|
4709
|
+
config: options.config,
|
|
4710
|
+
json: options.json,
|
|
4711
|
+
categories: options.categories,
|
|
4712
|
+
theme: options.theme,
|
|
4713
|
+
category: options.category,
|
|
4714
|
+
verbose: options.verbose
|
|
4715
|
+
});
|
|
4716
|
+
if (!result.success) {
|
|
4717
|
+
process.exit(1);
|
|
4718
|
+
}
|
|
4719
|
+
} catch (error) {
|
|
4720
|
+
console.error(pc20.red("Error:"), error instanceof Error ? error.message : error);
|
|
4721
|
+
process.exit(1);
|
|
4722
|
+
}
|
|
4723
|
+
});
|
|
4724
|
+
program.command("generate").description("Generate fragment files from component source code").argument("[component]", "Specific component name to generate (optional)").option("--force", "Overwrite existing fragment files").option("--pattern <glob>", "Pattern for component files", "src/components/**/*.tsx").action(async (component, options) => {
|
|
4725
|
+
try {
|
|
4726
|
+
const { generate } = await import("./generate-4LQNJ7SX.js");
|
|
4727
|
+
const result = await generate({
|
|
4728
|
+
projectRoot: process.cwd(),
|
|
4729
|
+
component,
|
|
4730
|
+
force: options.force,
|
|
4731
|
+
componentPattern: options.pattern
|
|
4732
|
+
});
|
|
4733
|
+
if (!result.success) {
|
|
4734
|
+
console.error(pc20.red("\nGenerate completed with errors"));
|
|
4735
|
+
process.exit(1);
|
|
4736
|
+
}
|
|
4737
|
+
} catch (error) {
|
|
4738
|
+
console.error(pc20.red("Error:"), error instanceof Error ? error.message : error);
|
|
4739
|
+
process.exit(1);
|
|
4740
|
+
}
|
|
4741
|
+
});
|
|
4742
|
+
program.command("test").description("Run interaction tests for fragments with play functions").option("-c, --config <path>", "Path to config file").option("--component <name>", "Filter by component name").option("--tags <tags>", "Filter by tags (comma-separated)").option("--grep <pattern>", "Filter by variant name pattern").option("--exclude <pattern>", "Exclude tests matching pattern").option("--parallel <count>", "Number of parallel browser contexts", parseInt, 4).option("--timeout <ms>", "Timeout per test in milliseconds", parseInt, 3e4).option("--retries <count>", "Number of retries for failed tests", parseInt, 0).option("--bail", "Stop on first failure").option("--browser <name>", "Browser to use (chromium, firefox, webkit)", "chromium").option("--headed", "Run in headed mode (show browser)").option("--a11y", "Run accessibility checks with axe-core").option("--visual", "Capture screenshots for visual regression").option("--update-snapshots", "Update visual snapshots").option("--watch", "Watch mode - re-run on file changes").option("--reporters <names>", "Reporters to use (console, junit, json)", "console").option("-o, --output <dir>", "Output directory for results", "./test-results").option("--server-url <url>", "URL of running dev server (skips starting server)").option("-p, --port <port>", "Port for dev server", parseInt, 6006).option("--ci", "CI mode - non-interactive, exit with code 1 on failure").option("--list", "List available tests without running them").action(async (options) => {
|
|
4743
|
+
try {
|
|
4744
|
+
const { config, configDir } = await loadConfig(options.config);
|
|
4745
|
+
const { runTestCommand, listTests } = await import("./test-SQ5ZHXWU.js");
|
|
4746
|
+
if (options.list) {
|
|
4747
|
+
await listTests(config, configDir, {
|
|
4748
|
+
component: options.component,
|
|
4749
|
+
tags: options.tags,
|
|
4750
|
+
grep: options.grep,
|
|
4751
|
+
exclude: options.exclude
|
|
4752
|
+
});
|
|
4753
|
+
return;
|
|
4754
|
+
}
|
|
4755
|
+
const exitCode = await runTestCommand(config, configDir, {
|
|
4756
|
+
component: options.component,
|
|
4757
|
+
tags: options.tags,
|
|
4758
|
+
grep: options.grep,
|
|
4759
|
+
exclude: options.exclude,
|
|
4760
|
+
parallel: options.parallel,
|
|
4761
|
+
timeout: options.timeout,
|
|
4762
|
+
retries: options.retries,
|
|
4763
|
+
bail: options.bail,
|
|
4764
|
+
browser: options.browser,
|
|
4765
|
+
headless: !options.headed,
|
|
4766
|
+
a11y: options.a11y,
|
|
4767
|
+
visual: options.visual,
|
|
4768
|
+
updateSnapshots: options.updateSnapshots,
|
|
4769
|
+
watch: options.watch,
|
|
4770
|
+
reporters: options.reporters,
|
|
4771
|
+
output: options.output,
|
|
4772
|
+
serverUrl: options.serverUrl,
|
|
4773
|
+
port: options.port,
|
|
4774
|
+
ci: options.ci
|
|
4775
|
+
});
|
|
4776
|
+
process.exit(exitCode);
|
|
4777
|
+
} catch (error) {
|
|
4778
|
+
console.error(pc20.red("Error:"), error instanceof Error ? error.message : error);
|
|
4779
|
+
process.exit(1);
|
|
4780
|
+
}
|
|
4781
|
+
});
|
|
4782
|
+
program.parse();
|
|
4783
|
+
//# sourceMappingURL=bin.js.map
|