@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
|
@@ -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
|
+
}
|