@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.
Files changed (259) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +106 -0
  3. package/dist/bin.d.ts +1 -0
  4. package/dist/bin.js +4783 -0
  5. package/dist/bin.js.map +1 -0
  6. package/dist/chunk-4FDQSGKX.js +786 -0
  7. package/dist/chunk-4FDQSGKX.js.map +1 -0
  8. package/dist/chunk-7H2MMGYG.js +369 -0
  9. package/dist/chunk-7H2MMGYG.js.map +1 -0
  10. package/dist/chunk-BSCG3IP7.js +619 -0
  11. package/dist/chunk-BSCG3IP7.js.map +1 -0
  12. package/dist/chunk-LY2CFFPY.js +898 -0
  13. package/dist/chunk-LY2CFFPY.js.map +1 -0
  14. package/dist/chunk-MUZ6CM66.js +6636 -0
  15. package/dist/chunk-MUZ6CM66.js.map +1 -0
  16. package/dist/chunk-OAENNG3G.js +1489 -0
  17. package/dist/chunk-OAENNG3G.js.map +1 -0
  18. package/dist/chunk-XHNKNI6J.js +235 -0
  19. package/dist/chunk-XHNKNI6J.js.map +1 -0
  20. package/dist/core-DWKLGY4N.js +68 -0
  21. package/dist/core-DWKLGY4N.js.map +1 -0
  22. package/dist/generate-4LQNJ7SX.js +249 -0
  23. package/dist/generate-4LQNJ7SX.js.map +1 -0
  24. package/dist/index.d.ts +775 -0
  25. package/dist/index.js +41 -0
  26. package/dist/index.js.map +1 -0
  27. package/dist/init-EMVI47QG.js +416 -0
  28. package/dist/init-EMVI47QG.js.map +1 -0
  29. package/dist/mcp-bin.d.ts +1 -0
  30. package/dist/mcp-bin.js +1117 -0
  31. package/dist/mcp-bin.js.map +1 -0
  32. package/dist/scan-4YPRF7FV.js +12 -0
  33. package/dist/scan-4YPRF7FV.js.map +1 -0
  34. package/dist/service-QSZMZJBJ.js +208 -0
  35. package/dist/service-QSZMZJBJ.js.map +1 -0
  36. package/dist/static-viewer-MIPGZ4Z7.js +12 -0
  37. package/dist/static-viewer-MIPGZ4Z7.js.map +1 -0
  38. package/dist/test-SQ5ZHXWU.js +1067 -0
  39. package/dist/test-SQ5ZHXWU.js.map +1 -0
  40. package/dist/tokens-HSGMYK64.js +173 -0
  41. package/dist/tokens-HSGMYK64.js.map +1 -0
  42. package/dist/viewer-YRF4SQE4.js +11101 -0
  43. package/dist/viewer-YRF4SQE4.js.map +1 -0
  44. package/package.json +107 -0
  45. package/src/ai.ts +266 -0
  46. package/src/analyze.ts +265 -0
  47. package/src/bin.ts +916 -0
  48. package/src/build.ts +248 -0
  49. package/src/commands/a11y.ts +302 -0
  50. package/src/commands/add.ts +313 -0
  51. package/src/commands/audit.ts +195 -0
  52. package/src/commands/baseline.ts +221 -0
  53. package/src/commands/build.ts +144 -0
  54. package/src/commands/compare.ts +337 -0
  55. package/src/commands/context.ts +107 -0
  56. package/src/commands/dev.ts +107 -0
  57. package/src/commands/enhance.ts +858 -0
  58. package/src/commands/generate.ts +391 -0
  59. package/src/commands/init.ts +531 -0
  60. package/src/commands/link/figma.ts +645 -0
  61. package/src/commands/link/index.ts +10 -0
  62. package/src/commands/link/storybook.ts +267 -0
  63. package/src/commands/list.ts +49 -0
  64. package/src/commands/metrics.ts +114 -0
  65. package/src/commands/reset.ts +242 -0
  66. package/src/commands/scan.ts +537 -0
  67. package/src/commands/storygen.ts +207 -0
  68. package/src/commands/tokens.ts +251 -0
  69. package/src/commands/validate.ts +93 -0
  70. package/src/commands/verify.ts +215 -0
  71. package/src/core/composition.test.ts +262 -0
  72. package/src/core/composition.ts +255 -0
  73. package/src/core/config.ts +84 -0
  74. package/src/core/constants.ts +111 -0
  75. package/src/core/context.ts +380 -0
  76. package/src/core/defineSegment.ts +137 -0
  77. package/src/core/discovery.ts +337 -0
  78. package/src/core/figma.ts +263 -0
  79. package/src/core/fragment-types.ts +214 -0
  80. package/src/core/generators/context.ts +389 -0
  81. package/src/core/generators/index.ts +23 -0
  82. package/src/core/generators/registry.ts +364 -0
  83. package/src/core/generators/typescript-extractor.ts +374 -0
  84. package/src/core/importAnalyzer.ts +217 -0
  85. package/src/core/index.ts +149 -0
  86. package/src/core/loader.ts +155 -0
  87. package/src/core/node.ts +63 -0
  88. package/src/core/parser.ts +551 -0
  89. package/src/core/previewLoader.ts +172 -0
  90. package/src/core/schema/fragment.schema.json +189 -0
  91. package/src/core/schema/registry.schema.json +137 -0
  92. package/src/core/schema.ts +182 -0
  93. package/src/core/storyAdapter.test.ts +571 -0
  94. package/src/core/storyAdapter.ts +761 -0
  95. package/src/core/token-types.ts +287 -0
  96. package/src/core/types.ts +754 -0
  97. package/src/diff.ts +323 -0
  98. package/src/index.ts +43 -0
  99. package/src/mcp/__tests__/projectFields.test.ts +130 -0
  100. package/src/mcp/bin.ts +36 -0
  101. package/src/mcp/index.ts +8 -0
  102. package/src/mcp/server.ts +1310 -0
  103. package/src/mcp/utils.ts +54 -0
  104. package/src/mcp-bin.ts +36 -0
  105. package/src/migrate/__tests__/argTypes/argTypes.test.ts +189 -0
  106. package/src/migrate/__tests__/args/args.test.ts +452 -0
  107. package/src/migrate/__tests__/meta/meta.test.ts +198 -0
  108. package/src/migrate/__tests__/stories/stories.test.ts +278 -0
  109. package/src/migrate/__tests__/utils/utils.test.ts +371 -0
  110. package/src/migrate/__tests__/values/values.test.ts +303 -0
  111. package/src/migrate/bin.ts +108 -0
  112. package/src/migrate/converter.ts +658 -0
  113. package/src/migrate/detect.ts +196 -0
  114. package/src/migrate/index.ts +45 -0
  115. package/src/migrate/migrate.ts +163 -0
  116. package/src/migrate/parser.ts +1136 -0
  117. package/src/migrate/report.ts +624 -0
  118. package/src/migrate/types.ts +169 -0
  119. package/src/screenshot.ts +249 -0
  120. package/src/service/__tests__/ast-utils.test.ts +426 -0
  121. package/src/service/__tests__/enhance-scanner.test.ts +200 -0
  122. package/src/service/__tests__/figma/figma.test.ts +652 -0
  123. package/src/service/__tests__/metrics-store.test.ts +409 -0
  124. package/src/service/__tests__/patch-generator.test.ts +186 -0
  125. package/src/service/__tests__/props-extractor.test.ts +365 -0
  126. package/src/service/__tests__/token-registry.test.ts +267 -0
  127. package/src/service/analytics.ts +659 -0
  128. package/src/service/ast-utils.ts +444 -0
  129. package/src/service/browser-pool.ts +339 -0
  130. package/src/service/capture.ts +267 -0
  131. package/src/service/diff.ts +279 -0
  132. package/src/service/enhance/aggregator.ts +489 -0
  133. package/src/service/enhance/cache.ts +275 -0
  134. package/src/service/enhance/codebase-scanner.ts +357 -0
  135. package/src/service/enhance/context-generator.ts +529 -0
  136. package/src/service/enhance/doc-extractor.ts +523 -0
  137. package/src/service/enhance/index.ts +131 -0
  138. package/src/service/enhance/props-extractor.ts +665 -0
  139. package/src/service/enhance/scanner.ts +445 -0
  140. package/src/service/enhance/storybook-parser.ts +552 -0
  141. package/src/service/enhance/types.ts +346 -0
  142. package/src/service/enhance/variant-renderer.ts +479 -0
  143. package/src/service/figma.ts +1008 -0
  144. package/src/service/index.ts +249 -0
  145. package/src/service/metrics-store.ts +333 -0
  146. package/src/service/patch-generator.ts +349 -0
  147. package/src/service/report.ts +854 -0
  148. package/src/service/storage.ts +401 -0
  149. package/src/service/token-fixes.ts +281 -0
  150. package/src/service/token-parser.ts +504 -0
  151. package/src/service/token-registry.ts +721 -0
  152. package/src/service/utils.ts +172 -0
  153. package/src/setup.ts +241 -0
  154. package/src/shared/command-wrapper.ts +81 -0
  155. package/src/shared/dev-server-client.ts +199 -0
  156. package/src/shared/index.ts +8 -0
  157. package/src/shared/segment-loader.ts +59 -0
  158. package/src/shared/types.ts +147 -0
  159. package/src/static-viewer.ts +715 -0
  160. package/src/test/discovery.ts +172 -0
  161. package/src/test/index.ts +281 -0
  162. package/src/test/reporters/console.ts +194 -0
  163. package/src/test/reporters/json.ts +190 -0
  164. package/src/test/reporters/junit.ts +186 -0
  165. package/src/test/runner.ts +598 -0
  166. package/src/test/types.ts +245 -0
  167. package/src/test/watch.ts +200 -0
  168. package/src/validators.ts +152 -0
  169. package/src/viewer/__tests__/jsx-parser.test.ts +502 -0
  170. package/src/viewer/__tests__/render-utils.test.ts +232 -0
  171. package/src/viewer/__tests__/style-utils.test.ts +404 -0
  172. package/src/viewer/bin.ts +86 -0
  173. package/src/viewer/cli/health.ts +256 -0
  174. package/src/viewer/cli/index.ts +33 -0
  175. package/src/viewer/cli/scan.ts +124 -0
  176. package/src/viewer/cli/utils.ts +174 -0
  177. package/src/viewer/components/AccessibilityPanel.tsx +1404 -0
  178. package/src/viewer/components/ActionCapture.tsx +172 -0
  179. package/src/viewer/components/ActionsPanel.tsx +371 -0
  180. package/src/viewer/components/App.tsx +638 -0
  181. package/src/viewer/components/BottomPanel.tsx +224 -0
  182. package/src/viewer/components/CodePanel.tsx +589 -0
  183. package/src/viewer/components/CommandPalette.tsx +336 -0
  184. package/src/viewer/components/ComponentGraph.tsx +394 -0
  185. package/src/viewer/components/ComponentHeader.tsx +85 -0
  186. package/src/viewer/components/ContractPanel.tsx +234 -0
  187. package/src/viewer/components/ErrorBoundary.tsx +85 -0
  188. package/src/viewer/components/FigmaEmbed.tsx +231 -0
  189. package/src/viewer/components/FragmentEditor.tsx +485 -0
  190. package/src/viewer/components/HealthDashboard.tsx +452 -0
  191. package/src/viewer/components/HmrStatusIndicator.tsx +71 -0
  192. package/src/viewer/components/Icons.tsx +417 -0
  193. package/src/viewer/components/InteractionsPanel.tsx +720 -0
  194. package/src/viewer/components/IsolatedPreviewFrame.tsx +321 -0
  195. package/src/viewer/components/IsolatedRender.tsx +111 -0
  196. package/src/viewer/components/KeyboardShortcutsHelp.tsx +89 -0
  197. package/src/viewer/components/LandingPage.tsx +441 -0
  198. package/src/viewer/components/Layout.tsx +22 -0
  199. package/src/viewer/components/LeftSidebar.tsx +391 -0
  200. package/src/viewer/components/MultiViewportPreview.tsx +429 -0
  201. package/src/viewer/components/PreviewArea.tsx +404 -0
  202. package/src/viewer/components/PreviewFrameHost.tsx +310 -0
  203. package/src/viewer/components/PreviewPane.tsx +150 -0
  204. package/src/viewer/components/PreviewToolbar.tsx +176 -0
  205. package/src/viewer/components/PropsEditor.tsx +512 -0
  206. package/src/viewer/components/PropsTable.tsx +98 -0
  207. package/src/viewer/components/RelationsSection.tsx +57 -0
  208. package/src/viewer/components/ResizablePanel.tsx +328 -0
  209. package/src/viewer/components/RightSidebar.tsx +118 -0
  210. package/src/viewer/components/ScreenshotButton.tsx +90 -0
  211. package/src/viewer/components/Sidebar.tsx +169 -0
  212. package/src/viewer/components/SkeletonLoader.tsx +156 -0
  213. package/src/viewer/components/StoryRenderer.tsx +128 -0
  214. package/src/viewer/components/ThemeProvider.tsx +96 -0
  215. package/src/viewer/components/Toast.tsx +67 -0
  216. package/src/viewer/components/TokenStylePanel.tsx +708 -0
  217. package/src/viewer/components/UsageSection.tsx +95 -0
  218. package/src/viewer/components/VariantMatrix.tsx +350 -0
  219. package/src/viewer/components/VariantRenderer.tsx +131 -0
  220. package/src/viewer/components/VariantTabs.tsx +84 -0
  221. package/src/viewer/components/ViewportSelector.tsx +165 -0
  222. package/src/viewer/components/_future/CreatePage.tsx +836 -0
  223. package/src/viewer/composition-renderer.ts +381 -0
  224. package/src/viewer/constants/index.ts +1 -0
  225. package/src/viewer/constants/ui.ts +185 -0
  226. package/src/viewer/entry.tsx +299 -0
  227. package/src/viewer/hooks/index.ts +2 -0
  228. package/src/viewer/hooks/useA11yCache.ts +383 -0
  229. package/src/viewer/hooks/useA11yService.ts +498 -0
  230. package/src/viewer/hooks/useActions.ts +138 -0
  231. package/src/viewer/hooks/useAppState.ts +124 -0
  232. package/src/viewer/hooks/useFigmaIntegration.ts +132 -0
  233. package/src/viewer/hooks/useHmrStatus.ts +109 -0
  234. package/src/viewer/hooks/useKeyboardShortcuts.ts +222 -0
  235. package/src/viewer/hooks/usePreviewBridge.ts +347 -0
  236. package/src/viewer/hooks/useScrollSpy.ts +78 -0
  237. package/src/viewer/hooks/useUrlState.ts +330 -0
  238. package/src/viewer/hooks/useViewSettings.ts +125 -0
  239. package/src/viewer/index.html +28 -0
  240. package/src/viewer/index.ts +14 -0
  241. package/src/viewer/intelligence/healthReport.ts +505 -0
  242. package/src/viewer/intelligence/styleDrift.ts +340 -0
  243. package/src/viewer/intelligence/usageScanner.ts +309 -0
  244. package/src/viewer/jsx-parser.ts +485 -0
  245. package/src/viewer/postcss.config.js +6 -0
  246. package/src/viewer/preview-frame-entry.tsx +25 -0
  247. package/src/viewer/preview-frame.html +109 -0
  248. package/src/viewer/render-template.html +68 -0
  249. package/src/viewer/render-utils.ts +170 -0
  250. package/src/viewer/server.ts +276 -0
  251. package/src/viewer/style-utils.ts +414 -0
  252. package/src/viewer/styles/globals.css +355 -0
  253. package/src/viewer/tailwind.config.js +37 -0
  254. package/src/viewer/types/a11y.ts +197 -0
  255. package/src/viewer/utils/a11y-fixes.ts +471 -0
  256. package/src/viewer/utils/actionExport.ts +372 -0
  257. package/src/viewer/utils/colorSchemes.ts +201 -0
  258. package/src/viewer/utils/detectRelationships.ts +256 -0
  259. package/src/viewer/vite-plugin.ts +2143 -0
@@ -0,0 +1,858 @@
1
+ /**
2
+ * fragments enhance - AI-powered documentation generation
3
+ *
4
+ * Analyzes codebase for component usage patterns and generates
5
+ * when/whenNot documentation using AI.
6
+ *
7
+ * Supports:
8
+ * - Anthropic (Claude) API
9
+ * - OpenAI (GPT) API
10
+ * - Context-only mode for IDE AI (Cursor, Copilot, etc.)
11
+ */
12
+
13
+ import pc from 'picocolors';
14
+ import { BRAND } from '../core/index.js';
15
+ import { loadConfig } from '../core/node.js';
16
+ import { readFile, writeFile } from 'node:fs/promises';
17
+ import { resolve, relative, join } from 'node:path';
18
+ import {
19
+ scanCodebase,
20
+ parseAllStories,
21
+ generateComponentContext,
22
+ generateSystemPrompt,
23
+ generateUserPrompt,
24
+ generatePromptContext,
25
+ getScanStats,
26
+ extractPropsFromFile,
27
+ renderAllComponentVariants,
28
+ checkStorybookRunning,
29
+ shutdownSharedPool,
30
+ type ScanProgress,
31
+ type ComponentContext,
32
+ type UsageAnalysis,
33
+ type ParsedStoryFile,
34
+ type PropsExtractionResult,
35
+ type RenderedVariant,
36
+ } from '../service/index.js';
37
+
38
+ /**
39
+ * Supported AI providers
40
+ */
41
+ export type AIProvider = 'anthropic' | 'openai' | 'none';
42
+
43
+ /**
44
+ * Options for enhance command
45
+ */
46
+ export interface EnhanceOptions {
47
+ /** Path to config file */
48
+ config?: string;
49
+ /** Component name to enhance (or 'all') */
50
+ component?: string;
51
+ /** Skip confirmation prompts */
52
+ yes?: boolean;
53
+ /** Only analyze, don't modify files */
54
+ dryRun?: boolean;
55
+ /** Output format: interactive, json, quiet, context */
56
+ format?: 'interactive' | 'json' | 'quiet' | 'context';
57
+ /** AI provider: anthropic, openai, or none */
58
+ provider?: AIProvider;
59
+ /** API key (or use ANTHROPIC_API_KEY/OPENAI_API_KEY env) */
60
+ apiKey?: string;
61
+ /** Model to use */
62
+ model?: string;
63
+ /** Root directory to scan */
64
+ root?: string;
65
+ /** Output context only (for use with IDE AI like Cursor) */
66
+ contextOnly?: boolean;
67
+ /** Render variants from running Storybook (captures complex stories that fail static extraction) */
68
+ renderVariants?: boolean;
69
+ /** Storybook URL for variant rendering (default: http://localhost:6006) */
70
+ storybookUrl?: string;
71
+ }
72
+
73
+ /**
74
+ * Enhanced segment data
75
+ */
76
+ export interface EnhancedSegment {
77
+ componentName: string;
78
+ added: {
79
+ when: string[];
80
+ whenNot: string[];
81
+ };
82
+ confidence: number;
83
+ tokensUsed: number;
84
+ skipped: boolean;
85
+ reason?: string;
86
+ }
87
+
88
+ /**
89
+ * Result of enhance command
90
+ */
91
+ export interface EnhanceResult {
92
+ success: boolean;
93
+ enhanced: EnhancedSegment[];
94
+ totalTokens: number;
95
+ estimatedCost: number;
96
+ /** Context output for IDE AI mode */
97
+ context?: string;
98
+ }
99
+
100
+ /**
101
+ * Default models for each provider
102
+ */
103
+ const DEFAULT_MODELS: Record<AIProvider, string> = {
104
+ anthropic: 'claude-sonnet-4-20250514',
105
+ openai: 'gpt-4o',
106
+ none: '',
107
+ };
108
+
109
+ /**
110
+ * Run the enhance command
111
+ */
112
+ export async function enhance(options: EnhanceOptions = {}): Promise<EnhanceResult> {
113
+ const {
114
+ config: configPath,
115
+ component,
116
+ yes = false,
117
+ dryRun = false,
118
+ format = 'interactive',
119
+ provider = detectProvider(options),
120
+ apiKey = getApiKey(provider, options.apiKey),
121
+ model = options.model || DEFAULT_MODELS[provider],
122
+ root = process.cwd(),
123
+ contextOnly = false,
124
+ renderVariants = false,
125
+ storybookUrl = 'http://localhost:6006',
126
+ } = options;
127
+
128
+ const isInteractive = format === 'interactive';
129
+ const isQuiet = format === 'quiet';
130
+ const isContextMode = contextOnly || format === 'context' || provider === 'none';
131
+
132
+ // Load config
133
+ const config = await loadConfig(configPath);
134
+ const rootDir = resolve(root);
135
+
136
+ if (isInteractive) {
137
+ console.log(pc.cyan(`\n${BRAND.name} AI Enhancement\n`));
138
+ if (isContextMode) {
139
+ console.log(pc.dim('Running in context-only mode (for use with IDE AI like Cursor)\n'));
140
+ } else {
141
+ console.log(pc.dim(`Using ${provider} API with model: ${model}\n`));
142
+ }
143
+ }
144
+
145
+ // Check for API key (unless context-only mode)
146
+ if (!isContextMode && !apiKey) {
147
+ const envVar = provider === 'openai' ? 'OPENAI_API_KEY' : 'ANTHROPIC_API_KEY';
148
+ const error = `API key required. Set ${envVar} or use --api-key, or use --context-only for IDE AI mode`;
149
+ if (format === 'json') {
150
+ console.log(JSON.stringify({ success: false, error }));
151
+ } else if (!isQuiet) {
152
+ console.error(pc.red('Error:'), error);
153
+ console.log(pc.dim('\nTip: Use --context-only to generate prompts for Cursor/Copilot/etc.'));
154
+ }
155
+ return { success: false, enhanced: [], totalTokens: 0, estimatedCost: 0 };
156
+ }
157
+
158
+ // Phase 1: Scan codebase for usage patterns
159
+ if (isInteractive) {
160
+ console.log(pc.dim('Phase 1: Scanning codebase for usage patterns...'));
161
+ }
162
+
163
+ let usageAnalysis: UsageAnalysis;
164
+ try {
165
+ usageAnalysis = await scanCodebase({
166
+ rootDir,
167
+ useCache: true,
168
+ onProgress: (progress: ScanProgress) => {
169
+ if (isInteractive && progress.phase === 'scanning') {
170
+ const pct = Math.round((progress.current / progress.total) * 100);
171
+ process.stdout.write(`\r Scanning: ${pct}% (${progress.current}/${progress.total})`);
172
+ }
173
+ },
174
+ });
175
+ if (isInteractive) {
176
+ process.stdout.write('\r' + ' '.repeat(60) + '\r');
177
+ const stats = getScanStats(usageAnalysis);
178
+ console.log(pc.green(` Found ${stats.totalUsages} usages across ${stats.totalFiles} files`));
179
+ }
180
+ } catch (error) {
181
+ const msg = error instanceof Error ? error.message : String(error);
182
+ if (format === 'json') {
183
+ console.log(JSON.stringify({ success: false, error: `Scan failed: ${msg}` }));
184
+ } else if (!isQuiet) {
185
+ console.error(pc.red('Scan failed:'), msg);
186
+ }
187
+ return { success: false, enhanced: [], totalTokens: 0, estimatedCost: 0 };
188
+ }
189
+
190
+ // Phase 2: Parse Storybook stories
191
+ if (isInteractive) {
192
+ console.log(pc.dim('Phase 2: Parsing Storybook stories...'));
193
+ }
194
+
195
+ let storyFiles: Map<string, ParsedStoryFile>;
196
+ try {
197
+ storyFiles = await parseAllStories(rootDir);
198
+ if (isInteractive) {
199
+ console.log(pc.green(` Found ${storyFiles.size} story files`));
200
+ }
201
+ } catch {
202
+ storyFiles = new Map();
203
+ if (isInteractive) {
204
+ console.log(pc.yellow(' No Storybook stories found'));
205
+ }
206
+ }
207
+
208
+ // Phase 3: Load fragment files
209
+ if (isInteractive) {
210
+ console.log(pc.dim('Phase 3: Loading fragment files...'));
211
+ }
212
+
213
+ // Search entire root dir, not just src/
214
+ const segmentFiles = await findSegmentFiles(rootDir);
215
+
216
+ if (segmentFiles.length === 0) {
217
+ const msg = 'No fragment files found';
218
+ if (format === 'json') {
219
+ console.log(JSON.stringify({ success: false, error: msg }));
220
+ } else if (!isQuiet) {
221
+ console.log(pc.yellow(msg));
222
+ }
223
+ return { success: false, enhanced: [], totalTokens: 0, estimatedCost: 0 };
224
+ }
225
+
226
+ if (isInteractive) {
227
+ console.log(pc.green(` Found ${segmentFiles.length} fragment files`));
228
+ }
229
+
230
+ // Filter components if specified
231
+ let componentsToEnhance: string[];
232
+ if (component && component !== 'all') {
233
+ componentsToEnhance = [component];
234
+ } else {
235
+ componentsToEnhance = segmentFiles.map(f => extractComponentName(f));
236
+ }
237
+
238
+ // Phase 4: Extract props from TypeScript source files
239
+ if (isInteractive) {
240
+ console.log(pc.dim('Phase 4: Extracting props from TypeScript interfaces...'));
241
+ }
242
+
243
+ const propsExtractions = new Map<string, PropsExtractionResult>();
244
+ for (const compName of componentsToEnhance) {
245
+ const segmentFile = segmentFiles.find(f => extractComponentName(f) === compName);
246
+ if (!segmentFile) continue;
247
+
248
+ // Try to find the component source file relative to the segment file
249
+ const segmentDir = segmentFile.replace(/\.segment\.(tsx?|jsx?)$/, '');
250
+ const possiblePaths = [
251
+ `${segmentDir}.tsx`,
252
+ `${segmentDir}.ts`,
253
+ `${segmentDir}/index.tsx`,
254
+ `${segmentDir}/index.ts`,
255
+ join(rootDir, 'src', 'components', `${compName}.tsx`),
256
+ join(rootDir, 'src', 'components', compName, `${compName}.tsx`),
257
+ join(rootDir, 'src', 'components', compName, 'index.tsx'),
258
+ ];
259
+
260
+ for (const srcPath of possiblePaths) {
261
+ try {
262
+ const fs = await import('node:fs');
263
+ if (fs.existsSync(srcPath)) {
264
+ const extraction = await extractPropsFromFile(srcPath, {
265
+ propsTypeName: `${compName}Props`,
266
+ });
267
+ if (extraction.success) {
268
+ propsExtractions.set(compName, extraction);
269
+ break;
270
+ }
271
+ }
272
+ } catch {
273
+ // Skip files that can't be parsed
274
+ }
275
+ }
276
+ }
277
+
278
+ if (isInteractive) {
279
+ const propsCount = Array.from(propsExtractions.values())
280
+ .reduce((sum, ext) => sum + ext.props.length, 0);
281
+ console.log(pc.green(` Extracted ${propsCount} props from ${propsExtractions.size} component files`));
282
+ }
283
+
284
+ // Phase 5 (optional): Render variants from Storybook
285
+ const renderedVariants = new Map<string, RenderedVariant[]>();
286
+
287
+ if (renderVariants) {
288
+ if (isInteractive) {
289
+ console.log(pc.dim('Phase 5: Rendering variants from Storybook...'));
290
+ }
291
+
292
+ // Check if Storybook is running
293
+ const storybookRunning = await checkStorybookRunning(storybookUrl);
294
+
295
+ if (!storybookRunning) {
296
+ if (isInteractive) {
297
+ console.log(pc.yellow(` Storybook not running at ${storybookUrl}. Skipping variant rendering.`));
298
+ console.log(pc.dim(` Start Storybook with: npm run storybook (or yarn storybook)`));
299
+ }
300
+ } else {
301
+ // Render variants for each component
302
+ let totalVariants = 0;
303
+ let failedVariants = 0;
304
+
305
+ for (const compName of componentsToEnhance) {
306
+ try {
307
+ const result = await renderAllComponentVariants(storybookUrl, compName);
308
+ if (result.variants.length > 0) {
309
+ renderedVariants.set(compName, result.variants);
310
+ totalVariants += result.variants.length;
311
+ }
312
+ failedVariants += result.failed.length;
313
+ } catch (err) {
314
+ if (isInteractive) {
315
+ console.log(pc.dim(` ${compName}: Failed to render (${(err as Error).message})`));
316
+ }
317
+ }
318
+ }
319
+
320
+ if (isInteractive) {
321
+ if (totalVariants > 0) {
322
+ console.log(pc.green(` Rendered ${totalVariants} variants from ${renderedVariants.size} components`));
323
+ }
324
+ if (failedVariants > 0) {
325
+ console.log(pc.yellow(` ${failedVariants} variant(s) failed to render`));
326
+ }
327
+ }
328
+
329
+ // Shutdown browser pool
330
+ await shutdownSharedPool();
331
+ }
332
+ }
333
+
334
+ // Build contexts for all components
335
+ const contexts: Array<{ name: string; context: ComponentContext; segmentFile: string }> = [];
336
+ for (const compName of componentsToEnhance) {
337
+ const analysis = usageAnalysis.components[compName];
338
+ const stories = storyFiles.get(compName);
339
+ const segmentFile = segmentFiles.find(f => extractComponentName(f) === compName);
340
+ const propsExtraction = propsExtractions.get(compName);
341
+
342
+ if (!segmentFile) continue;
343
+
344
+ const context = generateComponentContext(
345
+ compName,
346
+ analysis,
347
+ undefined,
348
+ stories,
349
+ propsExtraction
350
+ );
351
+ contexts.push({ name: compName, context, segmentFile });
352
+ }
353
+
354
+ // Context-only mode: output prompts for IDE AI
355
+ if (isContextMode) {
356
+ return handleContextOnlyMode(contexts, format, isInteractive);
357
+ }
358
+
359
+ // Phase 6: Generate AI enhancements
360
+ if (isInteractive) {
361
+ console.log(pc.dim(`\nPhase 6: Generating AI enhancements for ${componentsToEnhance.length} component(s)...\n`));
362
+ }
363
+
364
+ const enhanced: EnhancedSegment[] = [];
365
+ let totalTokens = 0;
366
+
367
+ // Initialize AI client
368
+ const aiClient = await createAIClient(provider, apiKey!);
369
+
370
+ for (const { name: compName, context, segmentFile } of contexts) {
371
+ // Check if we have enough data
372
+ if (!context.usageAnalysis || context.usageAnalysis.totalUsages < 2) {
373
+ enhanced.push({
374
+ componentName: compName,
375
+ added: { when: [], whenNot: [] },
376
+ confidence: 0,
377
+ tokensUsed: 0,
378
+ skipped: true,
379
+ reason: context.usageAnalysis?.totalUsages === 1 ? 'Only 1 usage found' : 'No usage data found',
380
+ });
381
+ if (isInteractive) {
382
+ console.log(pc.dim(` ${compName}: Skipped (insufficient data)`));
383
+ }
384
+ continue;
385
+ }
386
+
387
+ if (isInteractive) {
388
+ process.stdout.write(` ${compName}: Generating...`);
389
+ }
390
+
391
+ try {
392
+ const result = await generateEnhancement(aiClient, provider, model, context);
393
+
394
+ enhanced.push({
395
+ componentName: compName,
396
+ added: result.suggestions,
397
+ confidence: result.confidence,
398
+ tokensUsed: result.tokensUsed,
399
+ skipped: false,
400
+ });
401
+
402
+ totalTokens += result.tokensUsed;
403
+
404
+ if (isInteractive) {
405
+ process.stdout.write(`\r ${compName}: ${pc.green('Done')} (${result.suggestions.when.length} when, ${result.suggestions.whenNot.length} whenNot)\n`);
406
+ }
407
+ } catch (error) {
408
+ const msg = error instanceof Error ? error.message : String(error);
409
+ enhanced.push({
410
+ componentName: compName,
411
+ added: { when: [], whenNot: [] },
412
+ confidence: 0,
413
+ tokensUsed: 0,
414
+ skipped: true,
415
+ reason: `AI error: ${msg}`,
416
+ });
417
+ if (isInteractive) {
418
+ process.stdout.write(`\r ${compName}: ${pc.red('Failed')} - ${msg}\n`);
419
+ }
420
+ }
421
+ }
422
+
423
+ // Calculate cost
424
+ const estimatedCost = calculateCost(provider, totalTokens);
425
+
426
+ // Phase 7: Apply changes
427
+ if (!dryRun) {
428
+ if (isInteractive) {
429
+ console.log(pc.dim('\nPhase 7: Updating segment files...'));
430
+ }
431
+
432
+ for (const result of enhanced) {
433
+ if (result.skipped || (result.added.when.length === 0 && result.added.whenNot.length === 0)) {
434
+ continue;
435
+ }
436
+
437
+ const segmentFile = segmentFiles.find(f => extractComponentName(f) === result.componentName);
438
+ if (!segmentFile) continue;
439
+
440
+ try {
441
+ await updateSegmentFile(segmentFile, result.added);
442
+ if (isInteractive) {
443
+ console.log(pc.green(` Updated: ${relative(rootDir, segmentFile)}`));
444
+ }
445
+ } catch {
446
+ if (isInteractive) {
447
+ console.log(pc.red(` Failed to update: ${relative(rootDir, segmentFile)}`));
448
+ }
449
+ }
450
+ }
451
+ }
452
+
453
+ // Output results
454
+ const successCount = enhanced.filter(e => !e.skipped).length;
455
+
456
+ if (format === 'json') {
457
+ console.log(JSON.stringify({
458
+ success: true,
459
+ enhanced,
460
+ totalTokens,
461
+ estimatedCost,
462
+ }, null, 2));
463
+ } else if (isInteractive) {
464
+ console.log();
465
+ console.log(pc.bold('Summary:'));
466
+ console.log(` Components processed: ${componentsToEnhance.length}`);
467
+ console.log(` Successfully enhanced: ${successCount}`);
468
+ console.log(` Skipped: ${enhanced.filter(e => e.skipped).length}`);
469
+ console.log(` Total tokens used: ${totalTokens}`);
470
+ console.log(` Estimated cost: $${estimatedCost.toFixed(4)}`);
471
+
472
+ if (dryRun) {
473
+ console.log();
474
+ console.log(pc.yellow('Dry run - no files were modified'));
475
+ }
476
+ console.log();
477
+ }
478
+
479
+ return {
480
+ success: true,
481
+ enhanced,
482
+ totalTokens,
483
+ estimatedCost,
484
+ };
485
+ }
486
+
487
+ /**
488
+ * Handle context-only mode for IDE AI
489
+ */
490
+ function handleContextOnlyMode(
491
+ contexts: Array<{ name: string; context: ComponentContext; segmentFile: string }>,
492
+ format: string,
493
+ isInteractive: boolean
494
+ ): EnhanceResult {
495
+ const systemPrompt = generateSystemPrompt();
496
+ const componentContexts: string[] = [];
497
+
498
+ for (const { name, context } of contexts) {
499
+ if (!context.usageAnalysis || context.usageAnalysis.totalUsages < 2) {
500
+ continue;
501
+ }
502
+ componentContexts.push(generatePromptContext(context));
503
+ }
504
+
505
+ if (componentContexts.length === 0) {
506
+ if (format === 'json') {
507
+ console.log(JSON.stringify({ success: false, error: 'No components with sufficient usage data' }));
508
+ } else if (isInteractive) {
509
+ console.log(pc.yellow('\nNo components with sufficient usage data to analyze.'));
510
+ }
511
+ return { success: false, enhanced: [], totalTokens: 0, estimatedCost: 0 };
512
+ }
513
+
514
+ const fullContext = `${systemPrompt}
515
+
516
+ ---
517
+
518
+ ${componentContexts.join('\n\n---\n\n')}
519
+
520
+ ---
521
+
522
+ Based on the usage analysis above, generate "when" and "whenNot" documentation for each component.
523
+
524
+ For each component, provide your response in JSON format:
525
+ \`\`\`json
526
+ {
527
+ "componentName": "...",
528
+ "when": ["scenario 1", "scenario 2", ...],
529
+ "whenNot": ["anti-pattern 1", ...]
530
+ }
531
+ \`\`\``;
532
+
533
+ if (format === 'json') {
534
+ console.log(JSON.stringify({
535
+ success: true,
536
+ context: fullContext,
537
+ instructions: 'Copy this prompt into your IDE AI (Cursor, Copilot, etc.) to generate suggestions',
538
+ }, null, 2));
539
+ } else {
540
+ console.log(pc.bold('\n📋 AI Context Generated\n'));
541
+ console.log(pc.dim('Copy the following prompt into your IDE AI (Cursor, Copilot, etc.):\n'));
542
+ console.log(pc.dim('─'.repeat(60)));
543
+ console.log(fullContext);
544
+ console.log(pc.dim('─'.repeat(60)));
545
+ console.log();
546
+ console.log(pc.green('Tip: In Cursor, press Cmd+L to open chat and paste this prompt.'));
547
+ console.log(pc.dim('After getting suggestions, manually update your segment files.'));
548
+ console.log();
549
+ }
550
+
551
+ return {
552
+ success: true,
553
+ enhanced: [],
554
+ totalTokens: 0,
555
+ estimatedCost: 0,
556
+ context: fullContext,
557
+ };
558
+ }
559
+
560
+ /**
561
+ * Detect which AI provider to use based on available API keys
562
+ */
563
+ function detectProvider(options: EnhanceOptions): AIProvider {
564
+ if (options.contextOnly) return 'none';
565
+ if (options.provider) return options.provider;
566
+ if (options.apiKey) {
567
+ // Try to guess from key format
568
+ if (options.apiKey.startsWith('sk-ant-')) return 'anthropic';
569
+ if (options.apiKey.startsWith('sk-')) return 'openai';
570
+ }
571
+ // Check environment variables
572
+ if (process.env.ANTHROPIC_API_KEY) return 'anthropic';
573
+ if (process.env.OPENAI_API_KEY) return 'openai';
574
+ return 'none';
575
+ }
576
+
577
+ /**
578
+ * Get API key for the provider
579
+ */
580
+ function getApiKey(provider: AIProvider, explicitKey?: string): string | undefined {
581
+ if (explicitKey) return explicitKey;
582
+ if (provider === 'anthropic') return process.env.ANTHROPIC_API_KEY;
583
+ if (provider === 'openai') return process.env.OPENAI_API_KEY;
584
+ return undefined;
585
+ }
586
+
587
+ /**
588
+ * Create AI client for the provider
589
+ */
590
+ async function createAIClient(provider: AIProvider, apiKey: string): Promise<unknown> {
591
+ if (provider === 'anthropic') {
592
+ const Anthropic = (await import('@anthropic-ai/sdk')).default;
593
+ return new Anthropic({ apiKey });
594
+ }
595
+ if (provider === 'openai') {
596
+ const OpenAI = (await import('openai')).default;
597
+ return new OpenAI({ apiKey });
598
+ }
599
+ throw new Error(`Unknown provider: ${provider}`);
600
+ }
601
+
602
+ /**
603
+ * Generate enhancement using the appropriate AI provider
604
+ */
605
+ async function generateEnhancement(
606
+ client: unknown,
607
+ provider: AIProvider,
608
+ model: string,
609
+ context: ComponentContext
610
+ ): Promise<{
611
+ suggestions: { when: string[]; whenNot: string[] };
612
+ confidence: number;
613
+ tokensUsed: number;
614
+ }> {
615
+ const systemPrompt = generateSystemPrompt();
616
+ const userPrompt = generateUserPrompt(context);
617
+
618
+ if (provider === 'anthropic') {
619
+ return generateWithAnthropic(client, model, systemPrompt, userPrompt, context);
620
+ }
621
+ if (provider === 'openai') {
622
+ return generateWithOpenAI(client, model, systemPrompt, userPrompt, context);
623
+ }
624
+ throw new Error(`Unknown provider: ${provider}`);
625
+ }
626
+
627
+ /**
628
+ * Generate with Anthropic API
629
+ */
630
+ async function generateWithAnthropic(
631
+ client: unknown,
632
+ model: string,
633
+ systemPrompt: string,
634
+ userPrompt: string,
635
+ context: ComponentContext
636
+ ): Promise<{
637
+ suggestions: { when: string[]; whenNot: string[] };
638
+ confidence: number;
639
+ tokensUsed: number;
640
+ }> {
641
+ const anthropic = client as import('@anthropic-ai/sdk').default;
642
+ const response = await anthropic.messages.create({
643
+ model,
644
+ max_tokens: 1024,
645
+ system: systemPrompt,
646
+ messages: [{ role: 'user', content: userPrompt }],
647
+ });
648
+
649
+ const content = response.content[0];
650
+ if (content.type !== 'text') {
651
+ throw new Error('Unexpected response type');
652
+ }
653
+
654
+ const suggestions = parseAIResponse(content.text);
655
+ const tokensUsed = (response.usage?.input_tokens || 0) + (response.usage?.output_tokens || 0);
656
+
657
+ return {
658
+ suggestions,
659
+ confidence: calculateConfidence(context, suggestions),
660
+ tokensUsed,
661
+ };
662
+ }
663
+
664
+ /**
665
+ * Generate with OpenAI API
666
+ */
667
+ async function generateWithOpenAI(
668
+ client: unknown,
669
+ model: string,
670
+ systemPrompt: string,
671
+ userPrompt: string,
672
+ context: ComponentContext
673
+ ): Promise<{
674
+ suggestions: { when: string[]; whenNot: string[] };
675
+ confidence: number;
676
+ tokensUsed: number;
677
+ }> {
678
+ const openai = client as import('openai').default;
679
+ const response = await openai.chat.completions.create({
680
+ model,
681
+ max_tokens: 1024,
682
+ messages: [
683
+ { role: 'system', content: systemPrompt },
684
+ { role: 'user', content: userPrompt },
685
+ ],
686
+ });
687
+
688
+ const content = response.choices[0]?.message?.content;
689
+ if (!content) {
690
+ throw new Error('No response from OpenAI');
691
+ }
692
+
693
+ const suggestions = parseAIResponse(content);
694
+ const tokensUsed = (response.usage?.prompt_tokens || 0) + (response.usage?.completion_tokens || 0);
695
+
696
+ return {
697
+ suggestions,
698
+ confidence: calculateConfidence(context, suggestions),
699
+ tokensUsed,
700
+ };
701
+ }
702
+
703
+ /**
704
+ * Parse AI response to extract suggestions
705
+ */
706
+ function parseAIResponse(text: string): { when: string[]; whenNot: string[] } {
707
+ try {
708
+ // Extract JSON from response (may be wrapped in markdown code block)
709
+ const jsonMatch = text.match(/```json\n?([\s\S]*?)\n?```/) || text.match(/\{[\s\S]*\}/);
710
+ const jsonStr = jsonMatch ? (jsonMatch[1] || jsonMatch[0]) : text;
711
+ const parsed = JSON.parse(jsonStr);
712
+ return {
713
+ when: Array.isArray(parsed.when) ? parsed.when : [],
714
+ whenNot: Array.isArray(parsed.whenNot) ? parsed.whenNot : [],
715
+ };
716
+ } catch {
717
+ return extractSuggestionsFromText(text);
718
+ }
719
+ }
720
+
721
+ /**
722
+ * Extract suggestions from unstructured AI text response
723
+ */
724
+ function extractSuggestionsFromText(text: string): { when: string[]; whenNot: string[] } {
725
+ const when: string[] = [];
726
+ const whenNot: string[] = [];
727
+
728
+ const lines = text.split('\n');
729
+ let currentSection: 'when' | 'whenNot' | null = null;
730
+
731
+ for (const line of lines) {
732
+ const trimmed = line.trim();
733
+ if (/^(when|use when|when to use)/i.test(trimmed)) {
734
+ currentSection = 'when';
735
+ continue;
736
+ }
737
+ if (/^(when not|do not use|avoid|whenNot)/i.test(trimmed)) {
738
+ currentSection = 'whenNot';
739
+ continue;
740
+ }
741
+
742
+ if (currentSection && /^[-*]\s+/.test(trimmed)) {
743
+ const item = trimmed.replace(/^[-*]\s+/, '').trim();
744
+ if (item && item.length > 10) {
745
+ if (currentSection === 'when') {
746
+ when.push(item);
747
+ } else {
748
+ whenNot.push(item);
749
+ }
750
+ }
751
+ }
752
+ }
753
+
754
+ return { when, whenNot };
755
+ }
756
+
757
+ /**
758
+ * Calculate confidence based on data quality and response
759
+ */
760
+ function calculateConfidence(
761
+ context: ComponentContext,
762
+ suggestions: { when: string[]; whenNot: string[] }
763
+ ): number {
764
+ let confidence = 50;
765
+ if (context.usageAnalysis.totalUsages > 10) confidence += 20;
766
+ if (context.storybook && context.storybook.stories.length > 0) confidence += 15;
767
+ if (suggestions.when.length >= 2) confidence += 10;
768
+ if (suggestions.whenNot.length >= 1) confidence += 5;
769
+ return Math.min(confidence, 100);
770
+ }
771
+
772
+ /**
773
+ * Calculate cost based on provider and tokens
774
+ */
775
+ function calculateCost(provider: AIProvider, tokens: number): number {
776
+ // Approximate costs per 1M tokens
777
+ const costsPer1M: Record<AIProvider, number> = {
778
+ anthropic: 3, // Claude Sonnet
779
+ openai: 5, // GPT-4o
780
+ none: 0,
781
+ };
782
+ return (tokens / 1_000_000) * costsPer1M[provider];
783
+ }
784
+
785
+ /**
786
+ * Find all segment files in a directory
787
+ */
788
+ async function findSegmentFiles(dir: string): Promise<string[]> {
789
+ const fg = await import('fast-glob');
790
+ return fg.default(['**/*.segment.tsx', '**/*.segment.ts'], {
791
+ cwd: dir,
792
+ absolute: true,
793
+ ignore: ['**/node_modules/**', '**/dist/**'],
794
+ });
795
+ }
796
+
797
+ /**
798
+ * Extract component name from segment file path
799
+ */
800
+ function extractComponentName(filePath: string): string {
801
+ const match = filePath.match(/([^/\\]+)\.segment\.(tsx?|jsx?)$/);
802
+ return match ? match[1] : '';
803
+ }
804
+
805
+ /**
806
+ * Update a segment file with new when/whenNot suggestions
807
+ */
808
+ async function updateSegmentFile(
809
+ filePath: string,
810
+ suggestions: { when: string[]; whenNot: string[] }
811
+ ): Promise<void> {
812
+ const content = await readFile(filePath, 'utf-8');
813
+ let updated = content;
814
+
815
+ // Add 'when' items
816
+ if (suggestions.when.length > 0) {
817
+ const whenItems = suggestions.when.map(s => ` "${s}"`).join(',\n');
818
+ const whenMatch = updated.match(/when:\s*\[\s*([^\]]*)\]/);
819
+ if (whenMatch) {
820
+ const existingContent = whenMatch[1].trim();
821
+ if (existingContent) {
822
+ updated = updated.replace(
823
+ /when:\s*\[\s*([^\]]*)\]/,
824
+ `when: [\n${existingContent},\n${whenItems}\n ]`
825
+ );
826
+ } else {
827
+ updated = updated.replace(
828
+ /when:\s*\[\s*\]/,
829
+ `when: [\n${whenItems}\n ]`
830
+ );
831
+ }
832
+ }
833
+ }
834
+
835
+ // Add 'whenNot' items
836
+ if (suggestions.whenNot.length > 0) {
837
+ const whenNotItems = suggestions.whenNot.map(s => ` "${s}"`).join(',\n');
838
+ const whenNotMatch = updated.match(/whenNot:\s*\[\s*([^\]]*)\]/);
839
+ if (whenNotMatch) {
840
+ const existingContent = whenNotMatch[1].trim();
841
+ if (existingContent) {
842
+ updated = updated.replace(
843
+ /whenNot:\s*\[\s*([^\]]*)\]/,
844
+ `whenNot: [\n${existingContent},\n${whenNotItems}\n ]`
845
+ );
846
+ } else {
847
+ updated = updated.replace(
848
+ /whenNot:\s*\[\s*\]/,
849
+ `whenNot: [\n${whenNotItems}\n ]`
850
+ );
851
+ }
852
+ }
853
+ }
854
+
855
+ if (updated !== content) {
856
+ await writeFile(filePath, updated, 'utf-8');
857
+ }
858
+ }