@tpitre/story-ui 2.1.4 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +41 -4
- package/dist/cli/index.js +29 -0
- package/dist/mcp-server/index.js +18 -4
- package/dist/mcp-server/mcp-stdio-server.js +631 -0
- package/dist/mcp-server/routes/generateStory.js +115 -40
- package/dist/mcp-server/routes/hybridStories.js +214 -0
- package/dist/mcp-server/routes/memoryStories.js +13 -7
- package/dist/mcp-server/sessionManager.js +125 -0
- package/dist/story-generator/componentBlacklist.js +4 -0
- package/dist/story-generator/configLoader.js +8 -1
- package/dist/story-generator/considerationsLoader.js +2 -1
- package/dist/story-generator/documentationLoader.js +4 -3
- package/dist/story-generator/dynamicPackageDiscovery.js +31 -22
- package/dist/story-generator/enhancedComponentDiscovery.js +53 -12
- package/dist/story-generator/gitignoreManager.js +7 -6
- package/dist/story-generator/logger.js +52 -0
- package/dist/story-generator/postProcessStory.js +8 -7
- package/dist/story-generator/productionGitignoreManager.js +11 -10
- package/dist/story-generator/storyTracker.js +2 -1
- package/dist/story-generator/universalDesignSystemAdapter.js +3 -2
- package/dist/story-generator/urlRedirectService.js +140 -0
- package/package.json +19 -2
- package/templates/StoryUI/StoryUIPanel.tsx +35 -8
- package/templates/mcp-config-claude.json +11 -0
- package/templates/mcp-example.md +76 -0
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
export class SessionManager {
|
|
2
|
+
constructor() {
|
|
3
|
+
this.sessions = new Map();
|
|
4
|
+
this.currentStoryId = null;
|
|
5
|
+
}
|
|
6
|
+
static getInstance() {
|
|
7
|
+
if (!SessionManager.instance) {
|
|
8
|
+
SessionManager.instance = new SessionManager();
|
|
9
|
+
}
|
|
10
|
+
return SessionManager.instance;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Track a newly generated story in the current session
|
|
14
|
+
*/
|
|
15
|
+
trackStory(sessionId, story) {
|
|
16
|
+
const sessionStories = this.sessions.get(sessionId) || [];
|
|
17
|
+
// Extract keywords from prompt and title for smart matching
|
|
18
|
+
const keywords = this.extractKeywords(story.prompt + ' ' + story.title);
|
|
19
|
+
const sessionStory = {
|
|
20
|
+
...story,
|
|
21
|
+
timestamp: new Date(),
|
|
22
|
+
keywords
|
|
23
|
+
};
|
|
24
|
+
sessionStories.push(sessionStory);
|
|
25
|
+
this.sessions.set(sessionId, sessionStories);
|
|
26
|
+
this.currentStoryId = story.id;
|
|
27
|
+
console.error(`[SessionManager] Tracked story: ${story.title} (${story.id}) for session ${sessionId}`);
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Get the current story being discussed
|
|
31
|
+
*/
|
|
32
|
+
getCurrentStory(sessionId) {
|
|
33
|
+
const stories = this.sessions.get(sessionId) || [];
|
|
34
|
+
if (this.currentStoryId) {
|
|
35
|
+
return stories.find(s => s.id === this.currentStoryId) || null;
|
|
36
|
+
}
|
|
37
|
+
// Return the most recent story if no current story is set
|
|
38
|
+
return stories[stories.length - 1] || null;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Find a story by context (keywords in the user's message)
|
|
42
|
+
*/
|
|
43
|
+
findStoryByContext(sessionId, userMessage) {
|
|
44
|
+
const stories = this.sessions.get(sessionId) || [];
|
|
45
|
+
if (stories.length === 0)
|
|
46
|
+
return null;
|
|
47
|
+
const messageKeywords = this.extractKeywords(userMessage.toLowerCase());
|
|
48
|
+
// Score each story based on keyword matches
|
|
49
|
+
const scoredStories = stories.map(story => {
|
|
50
|
+
const score = story.keywords.filter(keyword => messageKeywords.some(msgKeyword => msgKeyword.includes(keyword) || keyword.includes(msgKeyword))).length;
|
|
51
|
+
return { story, score };
|
|
52
|
+
});
|
|
53
|
+
// Sort by score and recency
|
|
54
|
+
scoredStories.sort((a, b) => {
|
|
55
|
+
if (a.score !== b.score)
|
|
56
|
+
return b.score - a.score;
|
|
57
|
+
return b.story.timestamp.getTime() - a.story.timestamp.getTime();
|
|
58
|
+
});
|
|
59
|
+
// Return the best match if it has any keyword matches
|
|
60
|
+
if (scoredStories[0].score > 0) {
|
|
61
|
+
this.currentStoryId = scoredStories[0].story.id;
|
|
62
|
+
console.error(`[SessionManager] Found story by context: ${scoredStories[0].story.title} (score: ${scoredStories[0].score})`);
|
|
63
|
+
return scoredStories[0].story;
|
|
64
|
+
}
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Get all stories in the current session
|
|
69
|
+
*/
|
|
70
|
+
getSessionStories(sessionId) {
|
|
71
|
+
return this.sessions.get(sessionId) || [];
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Set the current story being discussed
|
|
75
|
+
*/
|
|
76
|
+
setCurrentStory(sessionId, storyId) {
|
|
77
|
+
const stories = this.sessions.get(sessionId) || [];
|
|
78
|
+
if (stories.some(s => s.id === storyId)) {
|
|
79
|
+
this.currentStoryId = storyId;
|
|
80
|
+
console.error(`[SessionManager] Set current story to: ${storyId}`);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Clear session data (for cleanup)
|
|
85
|
+
*/
|
|
86
|
+
clearSession(sessionId) {
|
|
87
|
+
this.sessions.delete(sessionId);
|
|
88
|
+
if (this.currentStoryId) {
|
|
89
|
+
this.currentStoryId = null;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Extract meaningful keywords from text
|
|
94
|
+
*/
|
|
95
|
+
extractKeywords(text) {
|
|
96
|
+
// Remove common words and extract meaningful terms
|
|
97
|
+
const commonWords = new Set([
|
|
98
|
+
'a', 'an', 'the', 'and', 'or', 'but', 'in', 'on', 'at', 'to', 'for',
|
|
99
|
+
'of', 'with', 'by', 'from', 'up', 'about', 'into', 'through', 'during',
|
|
100
|
+
'before', 'after', 'above', 'below', 'between', 'under', 'again', 'further',
|
|
101
|
+
'then', 'once', 'create', 'make', 'generate', 'build', 'add', 'update',
|
|
102
|
+
'component', 'story', 'please', 'should', 'would', 'could'
|
|
103
|
+
]);
|
|
104
|
+
const words = text.toLowerCase()
|
|
105
|
+
.replace(/[^a-z0-9\s]/g, ' ')
|
|
106
|
+
.split(/\s+/)
|
|
107
|
+
.filter(word => word.length > 2 && !commonWords.has(word));
|
|
108
|
+
// Also extract compound terms (like "toast notification")
|
|
109
|
+
const phrases = [];
|
|
110
|
+
const importantPhrases = [
|
|
111
|
+
'toast notification', 'banner notification', 'success message',
|
|
112
|
+
'error message', 'warning message', 'info message',
|
|
113
|
+
'dark mode', 'light mode', 'switch', 'toggle', 'button',
|
|
114
|
+
'card', 'alert', 'modal', 'dialog', 'form', 'input',
|
|
115
|
+
'table', 'list', 'grid', 'layout', 'navigation', 'menu'
|
|
116
|
+
];
|
|
117
|
+
const lowerText = text.toLowerCase();
|
|
118
|
+
for (const phrase of importantPhrases) {
|
|
119
|
+
if (lowerText.includes(phrase)) {
|
|
120
|
+
phrases.push(phrase);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return [...new Set([...words, ...phrases])];
|
|
124
|
+
}
|
|
125
|
+
}
|
|
@@ -80,6 +80,10 @@ export const ICON_CORRECTIONS = {
|
|
|
80
80
|
'ForkIcon': 'RepoForkedIcon',
|
|
81
81
|
'CloseIcon': 'XIcon',
|
|
82
82
|
'CheckmarkIcon': 'CheckIcon',
|
|
83
|
+
// Ant Design icon corrections
|
|
84
|
+
'ExclamationTriangleOutlined': 'ExclamationCircleOutlined',
|
|
85
|
+
'WarningTriangleOutlined': 'WarningOutlined',
|
|
86
|
+
'ErrorCircleOutlined': 'CloseCircleOutlined',
|
|
83
87
|
'CrossIcon': 'XIcon',
|
|
84
88
|
'EditIcon': 'PencilIcon',
|
|
85
89
|
'DeleteIcon': 'TrashIcon',
|
|
@@ -25,7 +25,14 @@ export function loadUserConfig() {
|
|
|
25
25
|
for (const configPath of configPaths) {
|
|
26
26
|
if (fs.existsSync(configPath)) {
|
|
27
27
|
try {
|
|
28
|
-
|
|
28
|
+
// Only log to stderr when in MCP mode to avoid corrupting JSON-RPC communication
|
|
29
|
+
const isMcpMode = process.argv.includes('mcp') || process.env.STORY_UI_MCP_MODE === 'true';
|
|
30
|
+
if (isMcpMode) {
|
|
31
|
+
console.error(`Loading Story UI config from: ${configPath}`);
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
console.log(`Loading Story UI config from: ${configPath}`);
|
|
35
|
+
}
|
|
29
36
|
// Read and evaluate the config file
|
|
30
37
|
const configContent = fs.readFileSync(configPath, 'utf-8');
|
|
31
38
|
// Handle both CommonJS and ES modules
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
2
|
import path from 'path';
|
|
3
|
+
import { logger } from './logger.js';
|
|
3
4
|
/**
|
|
4
5
|
* Loads AI considerations from a markdown or JSON file
|
|
5
6
|
*/
|
|
@@ -15,7 +16,7 @@ export function loadConsiderations(considerationsPath) {
|
|
|
15
16
|
for (const possiblePath of possiblePaths) {
|
|
16
17
|
if (fs.existsSync(possiblePath)) {
|
|
17
18
|
considerationsPath = possiblePath;
|
|
18
|
-
|
|
19
|
+
logger.log(`Found considerations file at: ${considerationsPath}`);
|
|
19
20
|
break;
|
|
20
21
|
}
|
|
21
22
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import { glob } from 'glob';
|
|
4
|
+
import { logger } from './logger.js';
|
|
4
5
|
export class DocumentationLoader {
|
|
5
6
|
constructor(projectRoot) {
|
|
6
7
|
this.cache = null;
|
|
@@ -32,7 +33,7 @@ export class DocumentationLoader {
|
|
|
32
33
|
if (this.cache && stats.mtimeMs <= this.lastModified) {
|
|
33
34
|
return this.cache;
|
|
34
35
|
}
|
|
35
|
-
|
|
36
|
+
logger.log(`📚 Loading documentation from ${this.docsDir}`);
|
|
36
37
|
const documentation = {
|
|
37
38
|
sources: [],
|
|
38
39
|
guidelines: [],
|
|
@@ -58,7 +59,7 @@ export class DocumentationLoader {
|
|
|
58
59
|
allFiles.push(...matches);
|
|
59
60
|
}
|
|
60
61
|
const files = [...new Set(allFiles)]; // Remove duplicates
|
|
61
|
-
|
|
62
|
+
logger.log(`📄 Found ${files.length} documentation files`);
|
|
62
63
|
// Process each file
|
|
63
64
|
for (const file of files) {
|
|
64
65
|
// Skip README files as they are instructional, not design system documentation
|
|
@@ -82,7 +83,7 @@ export class DocumentationLoader {
|
|
|
82
83
|
// Cache the results
|
|
83
84
|
this.cache = documentation;
|
|
84
85
|
this.lastModified = stats.mtimeMs;
|
|
85
|
-
|
|
86
|
+
logger.log(`✅ Loaded documentation with ${documentation.guidelines.length} guidelines, ${Object.keys(documentation.tokens).length} token categories, ${Object.keys(documentation.patterns).length} patterns`);
|
|
86
87
|
return documentation;
|
|
87
88
|
}
|
|
88
89
|
/**
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import { createRequire } from 'module';
|
|
4
|
+
import { logger } from './logger.js';
|
|
4
5
|
/**
|
|
5
6
|
* Dynamically discovers what components are actually available in an installed package
|
|
6
7
|
*/
|
|
@@ -29,7 +30,7 @@ export class DynamicPackageDiscovery {
|
|
|
29
30
|
// Try to require the package and inspect its exports
|
|
30
31
|
const packageExports = await this.requirePackage(this.packageName);
|
|
31
32
|
if (!packageExports) {
|
|
32
|
-
|
|
33
|
+
logger.log(`🔄 Could not directly import ${this.packageName}, falling back to structure analysis`);
|
|
33
34
|
// Don't return null here - fall back to structure discovery
|
|
34
35
|
}
|
|
35
36
|
const components = [];
|
|
@@ -49,14 +50,14 @@ export class DynamicPackageDiscovery {
|
|
|
49
50
|
}
|
|
50
51
|
// Check if we found any actual components
|
|
51
52
|
const componentCount = components.filter(c => c.isComponent).length;
|
|
52
|
-
|
|
53
|
+
logger.log(`📋 Found ${componentCount} components in main ${this.packageName} export`);
|
|
53
54
|
// If no components found in main export, fall back to structure analysis
|
|
54
55
|
if (componentCount === 0) {
|
|
55
|
-
|
|
56
|
+
logger.log(`🔄 No components in main export, falling back to structure analysis for ${this.packageName}...`);
|
|
56
57
|
const structureExports = this.discoverFromPackageStructure();
|
|
57
58
|
if (structureExports) {
|
|
58
59
|
const structureComponentNames = Object.keys(structureExports);
|
|
59
|
-
|
|
60
|
+
logger.log(`📁 Structure analysis found ${structureComponentNames.length} components`);
|
|
60
61
|
// Replace with structure-discovered components
|
|
61
62
|
allExports = structureComponentNames;
|
|
62
63
|
components.length = 0; // Clear the array
|
|
@@ -75,7 +76,7 @@ export class DynamicPackageDiscovery {
|
|
|
75
76
|
}
|
|
76
77
|
else {
|
|
77
78
|
// Failed to import - fall back to structure analysis
|
|
78
|
-
|
|
79
|
+
logger.log(`📁 Import failed, analyzing package structure for ${this.packageName}...`);
|
|
79
80
|
const structureExports = this.discoverFromPackageStructure();
|
|
80
81
|
if (structureExports) {
|
|
81
82
|
allExports = Object.keys(structureExports);
|
|
@@ -91,7 +92,7 @@ export class DynamicPackageDiscovery {
|
|
|
91
92
|
}
|
|
92
93
|
}
|
|
93
94
|
}
|
|
94
|
-
|
|
95
|
+
logger.log(`✅ Discovered ${components.filter(c => c.isComponent).length} components from ${this.packageName} v${packageVersion}`);
|
|
95
96
|
return {
|
|
96
97
|
components,
|
|
97
98
|
allExports,
|
|
@@ -118,7 +119,11 @@ export class DynamicPackageDiscovery {
|
|
|
118
119
|
// Check if this is a CSS import error (common with compiled design systems)
|
|
119
120
|
const errorMessage = importError?.message || String(importError);
|
|
120
121
|
if (errorMessage.includes('.css:') || errorMessage.includes('Unexpected token')) {
|
|
121
|
-
|
|
122
|
+
logger.log(`🔄 ${packageName}: CSS detected, using static analysis (normal for design systems)`);
|
|
123
|
+
return this.discoverFromPackageStructure();
|
|
124
|
+
}
|
|
125
|
+
if (errorMessage.includes('Invalid hook call') || errorMessage.includes('Hooks can only be called')) {
|
|
126
|
+
logger.log(`🔄 ${packageName}: React hooks detected outside component context, using static analysis`);
|
|
122
127
|
return this.discoverFromPackageStructure();
|
|
123
128
|
}
|
|
124
129
|
// Fall back to require (for CommonJS)
|
|
@@ -129,17 +134,21 @@ export class DynamicPackageDiscovery {
|
|
|
129
134
|
}
|
|
130
135
|
}
|
|
131
136
|
catch (error) {
|
|
132
|
-
// Check if this is a CSS import error
|
|
137
|
+
// Check if this is a CSS import error
|
|
133
138
|
const errorMessage = error?.message || String(error);
|
|
134
139
|
if (errorMessage.includes('.css:') || errorMessage.includes('Unexpected token')) {
|
|
135
|
-
|
|
140
|
+
logger.log(`🔄 ${packageName}: CSS detected, using static analysis (normal for design systems)`);
|
|
136
141
|
return this.discoverFromPackageStructure();
|
|
137
142
|
}
|
|
138
143
|
if (errorMessage.includes('window is not defined')) {
|
|
139
|
-
|
|
144
|
+
logger.log(`🔄 ${packageName}: Browser-only component, using static analysis`);
|
|
145
|
+
return this.discoverFromPackageStructure();
|
|
146
|
+
}
|
|
147
|
+
if (errorMessage.includes('Invalid hook call') || errorMessage.includes('Hooks can only be called')) {
|
|
148
|
+
logger.log(`🔄 ${packageName}: React hooks detected outside component context, using static analysis`);
|
|
140
149
|
return this.discoverFromPackageStructure();
|
|
141
150
|
}
|
|
142
|
-
|
|
151
|
+
logger.log(`📋 ${packageName}: Dynamic import failed, using static analysis`);
|
|
143
152
|
return this.discoverFromPackageStructure();
|
|
144
153
|
}
|
|
145
154
|
}
|
|
@@ -335,26 +344,26 @@ export class DynamicPackageDiscovery {
|
|
|
335
344
|
const packagePath = path.join(this.projectRoot, 'node_modules', this.packageName);
|
|
336
345
|
const packageJsonPath = path.join(packagePath, 'package.json');
|
|
337
346
|
if (!fs.existsSync(packageJsonPath)) {
|
|
338
|
-
|
|
347
|
+
logger.log(`📦 No package.json found for ${this.packageName}`);
|
|
339
348
|
return null;
|
|
340
349
|
}
|
|
341
350
|
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
|
|
342
351
|
const exports = {};
|
|
343
352
|
// Method 1: Analyze package.json exports field
|
|
344
353
|
if (packageJson.exports) {
|
|
345
|
-
|
|
354
|
+
logger.log(`📋 Analyzing exports field in ${this.packageName}/package.json`);
|
|
346
355
|
this.extractExportsFromPackageJson(packageJson.exports, exports);
|
|
347
356
|
}
|
|
348
357
|
// Method 2: Look for index.d.ts or main TypeScript declarations
|
|
349
358
|
const typingsPath = packageJson.types || packageJson.typings || './dist/types/index.d.ts';
|
|
350
359
|
const fullTypingsPath = path.join(packagePath, typingsPath);
|
|
351
360
|
if (fs.existsSync(fullTypingsPath)) {
|
|
352
|
-
|
|
361
|
+
logger.log(`📋 Analyzing TypeScript declarations for ${this.packageName}`);
|
|
353
362
|
this.extractExportsFromTypeDefinitions(fullTypingsPath, exports);
|
|
354
363
|
}
|
|
355
364
|
// Method 3: Scan for component subdirectories (for packages like Base Web)
|
|
356
365
|
if (Object.keys(exports).length === 0) {
|
|
357
|
-
|
|
366
|
+
logger.log(`📁 Scanning subdirectories for ${this.packageName} components...`);
|
|
358
367
|
this.scanComponentSubdirectories(packagePath, exports);
|
|
359
368
|
}
|
|
360
369
|
return Object.keys(exports).length > 0 ? exports : null;
|
|
@@ -369,9 +378,9 @@ export class DynamicPackageDiscovery {
|
|
|
369
378
|
*/
|
|
370
379
|
scanComponentSubdirectories(packagePath, result) {
|
|
371
380
|
try {
|
|
372
|
-
|
|
381
|
+
logger.log(`🔍 Scanning ${packagePath} for component subdirectories...`);
|
|
373
382
|
const entries = fs.readdirSync(packagePath, { withFileTypes: true });
|
|
374
|
-
|
|
383
|
+
logger.log(`📁 Found ${entries.length} entries in ${packagePath}`);
|
|
375
384
|
let componentDirsFound = 0;
|
|
376
385
|
for (const entry of entries) {
|
|
377
386
|
if (!entry.isDirectory())
|
|
@@ -388,7 +397,7 @@ export class DynamicPackageDiscovery {
|
|
|
388
397
|
// Look for component exports (functions/classes starting with uppercase)
|
|
389
398
|
const componentExports = this.extractComponentsFromTypings(typingsContent);
|
|
390
399
|
if (componentExports.length > 0) {
|
|
391
|
-
|
|
400
|
+
logger.log(`📦 Found ${componentExports.length} components in ${entry.name}/`);
|
|
392
401
|
// Add each component to the result
|
|
393
402
|
for (const componentName of componentExports) {
|
|
394
403
|
// Create a mock export function for this component
|
|
@@ -404,8 +413,8 @@ export class DynamicPackageDiscovery {
|
|
|
404
413
|
}
|
|
405
414
|
}
|
|
406
415
|
}
|
|
407
|
-
|
|
408
|
-
|
|
416
|
+
logger.log(`✅ Scanned ${componentDirsFound} component directories for ${this.packageName}`);
|
|
417
|
+
logger.log(`📦 Total components found in subdirectories: ${Object.keys(result).length}`);
|
|
409
418
|
}
|
|
410
419
|
catch (error) {
|
|
411
420
|
console.warn(`Failed to scan subdirectories for ${this.packageName}:`, error);
|
|
@@ -484,7 +493,7 @@ export class DynamicPackageDiscovery {
|
|
|
484
493
|
const componentName = key.replace('./', '').split('/').pop();
|
|
485
494
|
if (componentName && /^[A-Z]/.test(componentName)) {
|
|
486
495
|
result[componentName] = `Component_${componentName}`;
|
|
487
|
-
|
|
496
|
+
logger.log(`📍 Found component export: ${componentName}`);
|
|
488
497
|
}
|
|
489
498
|
}
|
|
490
499
|
}
|
|
@@ -512,7 +521,7 @@ export class DynamicPackageDiscovery {
|
|
|
512
521
|
const componentName = match[1];
|
|
513
522
|
if (componentName && /^[A-Z]/.test(componentName)) {
|
|
514
523
|
result[componentName] = `Component_${componentName}`;
|
|
515
|
-
|
|
524
|
+
logger.log(`📍 Found component in .d.ts: ${componentName}`);
|
|
516
525
|
}
|
|
517
526
|
}
|
|
518
527
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import { DynamicPackageDiscovery } from './dynamicPackageDiscovery.js';
|
|
4
|
+
import { logger } from './logger.js';
|
|
4
5
|
export class EnhancedComponentDiscovery {
|
|
5
6
|
constructor(config) {
|
|
6
7
|
this.discoveredComponents = new Map();
|
|
@@ -12,10 +13,10 @@ export class EnhancedComponentDiscovery {
|
|
|
12
13
|
* Priority: 1. Dynamic Discovery 2. Static Lists 3. Manual Config
|
|
13
14
|
*/
|
|
14
15
|
async discoverAll() {
|
|
15
|
-
|
|
16
|
+
logger.log('🔍 Starting comprehensive component discovery...');
|
|
16
17
|
// Step 1: Discover from all sources
|
|
17
18
|
const sources = this.identifySources();
|
|
18
|
-
|
|
19
|
+
logger.log(`📁 Found ${sources.length} discovery sources:`, sources.map(s => `${s.type}:${s.path}`));
|
|
19
20
|
for (const source of sources) {
|
|
20
21
|
try {
|
|
21
22
|
switch (source.type) {
|
|
@@ -42,7 +43,7 @@ export class EnhancedComponentDiscovery {
|
|
|
42
43
|
// Step 3: Resolve component conflicts and apply prioritization
|
|
43
44
|
this.resolveComponentConflicts();
|
|
44
45
|
const finalComponents = Array.from(this.discoveredComponents.values());
|
|
45
|
-
|
|
46
|
+
logger.log(`✅ Discovery complete: ${finalComponents.length} components found`);
|
|
46
47
|
// Log summary by source type
|
|
47
48
|
this.logDiscoverySummary(finalComponents);
|
|
48
49
|
return finalComponents;
|
|
@@ -64,7 +65,7 @@ export class EnhancedComponentDiscovery {
|
|
|
64
65
|
// Resolve conflicts using priority system
|
|
65
66
|
for (const [name, componentList] of conflicts) {
|
|
66
67
|
if (componentList.length > 1) {
|
|
67
|
-
|
|
68
|
+
logger.log(`⚠️ Resolving conflict for component "${name}" (${componentList.length} versions found)`);
|
|
68
69
|
// Priority order: local > manual config > npm
|
|
69
70
|
const prioritized = componentList.sort((a, b) => {
|
|
70
71
|
const getPriority = (comp) => {
|
|
@@ -82,7 +83,7 @@ export class EnhancedComponentDiscovery {
|
|
|
82
83
|
for (const loser of losers) {
|
|
83
84
|
this.discoveredComponents.delete(loser.name);
|
|
84
85
|
}
|
|
85
|
-
|
|
86
|
+
logger.log(`✅ Kept ${winner.source.type} version of "${name}" from ${winner.source.path}`);
|
|
86
87
|
}
|
|
87
88
|
}
|
|
88
89
|
}
|
|
@@ -97,7 +98,7 @@ export class EnhancedComponentDiscovery {
|
|
|
97
98
|
acc[sourceType]++;
|
|
98
99
|
return acc;
|
|
99
100
|
}, {});
|
|
100
|
-
|
|
101
|
+
logger.log('📊 Component discovery summary:', summary);
|
|
101
102
|
}
|
|
102
103
|
/**
|
|
103
104
|
* Get the project root directory from the config
|
|
@@ -263,12 +264,12 @@ export class EnhancedComponentDiscovery {
|
|
|
263
264
|
console.warn(`Package ${source.path} not found in node_modules at ${packagePath}`);
|
|
264
265
|
return;
|
|
265
266
|
}
|
|
266
|
-
|
|
267
|
+
logger.log(`🔍 Dynamically discovering components from ${source.path}...`);
|
|
267
268
|
// Use dynamic discovery to get real exports
|
|
268
269
|
const dynamicDiscovery = new DynamicPackageDiscovery(source.path, projectRoot);
|
|
269
270
|
const packageExports = await dynamicDiscovery.getRealPackageExports();
|
|
270
271
|
if (!packageExports) {
|
|
271
|
-
|
|
272
|
+
logger.log(`📋 ${source.path}: Using static component list (design system detected)`);
|
|
272
273
|
// Fallback to predefined components if dynamic discovery fails
|
|
273
274
|
const knownComponents = this.getKnownDesignSystemComponents(source.path);
|
|
274
275
|
if (knownComponents.length > 0) {
|
|
@@ -286,7 +287,7 @@ export class EnhancedComponentDiscovery {
|
|
|
286
287
|
// Process the real components found in the package
|
|
287
288
|
const realComponents = packageExports.components.filter(comp => comp.isComponent);
|
|
288
289
|
console.log(`✅ Found ${realComponents.length} real components in ${source.path} v${packageExports.packageVersion}`);
|
|
289
|
-
|
|
290
|
+
logger.log(`📦 Available components: ${realComponents.map(c => c.name).join(', ')}`);
|
|
290
291
|
for (const realComp of realComponents) {
|
|
291
292
|
// Get enhanced metadata from predefined list if available
|
|
292
293
|
const knownComponents = this.getKnownDesignSystemComponents(source.path);
|
|
@@ -309,11 +310,51 @@ export class EnhancedComponentDiscovery {
|
|
|
309
310
|
}
|
|
310
311
|
/**
|
|
311
312
|
* Get known components for popular design systems
|
|
312
|
-
* Returns
|
|
313
|
+
* Returns a fallback list when dynamic discovery fails
|
|
313
314
|
*/
|
|
314
315
|
getKnownDesignSystemComponents(packageName) {
|
|
315
|
-
//
|
|
316
|
-
|
|
316
|
+
// Chakra UI fallback components
|
|
317
|
+
if (packageName === '@chakra-ui/react') {
|
|
318
|
+
const basicComponents = [
|
|
319
|
+
// Layout
|
|
320
|
+
'Box', 'Flex', 'Grid', 'Stack', 'HStack', 'VStack', 'Container', 'Center', 'Square', 'Circle',
|
|
321
|
+
'SimpleGrid', 'Wrap', 'WrapItem', 'AspectRatio', 'Spacer', 'Divider',
|
|
322
|
+
// Typography
|
|
323
|
+
'Text', 'Heading', 'Badge', 'Code', 'Kbd', 'Mark',
|
|
324
|
+
// Forms
|
|
325
|
+
'Button', 'IconButton', 'Input', 'InputGroup', 'InputLeftElement', 'InputRightElement',
|
|
326
|
+
'Textarea', 'Select', 'Checkbox', 'Radio', 'RadioGroup', 'Switch', 'Slider',
|
|
327
|
+
'FormControl', 'FormLabel', 'FormHelperText', 'FormErrorMessage',
|
|
328
|
+
// Feedback
|
|
329
|
+
'Alert', 'AlertIcon', 'AlertTitle', 'AlertDescription', 'Progress', 'Skeleton', 'Spinner',
|
|
330
|
+
'Toast', 'useToast', 'CircularProgress', 'CircularProgressLabel',
|
|
331
|
+
// Data Display
|
|
332
|
+
'Avatar', 'AvatarGroup', 'Card', 'CardHeader', 'CardBody', 'CardFooter',
|
|
333
|
+
'Image', 'Badge', 'Stat', 'StatLabel', 'StatNumber', 'StatHelpText', 'StatArrow',
|
|
334
|
+
'Table', 'Thead', 'Tbody', 'Tfoot', 'Tr', 'Th', 'Td', 'TableCaption',
|
|
335
|
+
// Navigation
|
|
336
|
+
'Breadcrumb', 'BreadcrumbItem', 'BreadcrumbLink', 'Link', 'LinkBox', 'LinkOverlay',
|
|
337
|
+
'Tabs', 'TabList', 'TabPanels', 'Tab', 'TabPanel',
|
|
338
|
+
// Overlay
|
|
339
|
+
'Modal', 'ModalOverlay', 'ModalContent', 'ModalHeader', 'ModalFooter', 'ModalBody', 'ModalCloseButton',
|
|
340
|
+
'Drawer', 'DrawerBody', 'DrawerFooter', 'DrawerHeader', 'DrawerOverlay', 'DrawerContent', 'DrawerCloseButton',
|
|
341
|
+
'Menu', 'MenuButton', 'MenuList', 'MenuItem', 'MenuItemOption', 'MenuGroup', 'MenuOptionGroup', 'MenuDivider',
|
|
342
|
+
'Popover', 'PopoverTrigger', 'PopoverContent', 'PopoverHeader', 'PopoverBody', 'PopoverFooter', 'PopoverArrow', 'PopoverCloseButton',
|
|
343
|
+
'Tooltip', 'AlertDialog', 'AlertDialogBody', 'AlertDialogFooter', 'AlertDialogHeader', 'AlertDialogContent', 'AlertDialogOverlay',
|
|
344
|
+
// Disclosure
|
|
345
|
+
'Accordion', 'AccordionItem', 'AccordionButton', 'AccordionPanel', 'AccordionIcon',
|
|
346
|
+
'VisuallyHidden', 'Show', 'Hide', 'Collapse',
|
|
347
|
+
// Media
|
|
348
|
+
'Icon', 'CloseButton'
|
|
349
|
+
];
|
|
350
|
+
return basicComponents.map(name => ({
|
|
351
|
+
name,
|
|
352
|
+
description: `${name} component from Chakra UI`,
|
|
353
|
+
category: this.categorizeComponent(name, '')
|
|
354
|
+
}));
|
|
355
|
+
}
|
|
356
|
+
// Add other design systems here as needed
|
|
357
|
+
// Default: return empty array
|
|
317
358
|
return [];
|
|
318
359
|
}
|
|
319
360
|
/**
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
2
|
import path from 'path';
|
|
3
|
+
import { logger } from './logger.js';
|
|
3
4
|
/**
|
|
4
5
|
* Manages .gitignore entries for Story UI generated content
|
|
5
6
|
*/
|
|
@@ -26,7 +27,7 @@ export class GitignoreManager {
|
|
|
26
27
|
// Check if the path is already ignored
|
|
27
28
|
const gitignoreContent = fs.readFileSync(gitignorePath, 'utf-8');
|
|
28
29
|
if (this.isPathIgnored(gitignoreContent, generatedPath)) {
|
|
29
|
-
|
|
30
|
+
logger.log(`✅ Generated stories directory already ignored: ${generatedPath}`);
|
|
30
31
|
return;
|
|
31
32
|
}
|
|
32
33
|
// Add the ignore rule
|
|
@@ -60,7 +61,7 @@ export class GitignoreManager {
|
|
|
60
61
|
createGitignore(gitignorePath, generatedPath) {
|
|
61
62
|
const content = this.generateGitignoreSection(generatedPath);
|
|
62
63
|
fs.writeFileSync(gitignorePath, content);
|
|
63
|
-
|
|
64
|
+
logger.log(`✅ Created .gitignore with Story UI generated directory: ${generatedPath}`);
|
|
64
65
|
}
|
|
65
66
|
/**
|
|
66
67
|
* Checks if the generated path is already ignored
|
|
@@ -89,7 +90,7 @@ export class GitignoreManager {
|
|
|
89
90
|
const separator = existingContent.endsWith('\n') ? '\n' : '\n\n';
|
|
90
91
|
const updatedContent = existingContent + separator + newSection;
|
|
91
92
|
fs.writeFileSync(gitignorePath, updatedContent);
|
|
92
|
-
|
|
93
|
+
logger.log(`✅ Added Story UI generated directory to .gitignore: ${generatedPath}`);
|
|
93
94
|
}
|
|
94
95
|
/**
|
|
95
96
|
* Generates the gitignore section for Story UI
|
|
@@ -107,7 +108,7 @@ ${generatedPath}/**`;
|
|
|
107
108
|
const generatedDir = this.config.generatedStoriesPath;
|
|
108
109
|
if (!fs.existsSync(generatedDir)) {
|
|
109
110
|
fs.mkdirSync(generatedDir, { recursive: true });
|
|
110
|
-
|
|
111
|
+
logger.log(`✅ Created generated stories directory: ${generatedDir}`);
|
|
111
112
|
// Create a README to explain the purpose
|
|
112
113
|
this.createGeneratedDirectoryReadme(generatedDir);
|
|
113
114
|
}
|
|
@@ -142,7 +143,7 @@ Stories in this directory will appear in Storybook under the "${this.config.stor
|
|
|
142
143
|
Generated by [Story UI](https://github.com/your-org/story-ui) - AI-powered Storybook story generator.
|
|
143
144
|
`;
|
|
144
145
|
fs.writeFileSync(readmePath, readmeContent);
|
|
145
|
-
|
|
146
|
+
logger.log(`✅ Created README in generated directory`);
|
|
146
147
|
}
|
|
147
148
|
/**
|
|
148
149
|
* Cleans up old generated stories (optional utility)
|
|
@@ -168,7 +169,7 @@ Generated by [Story UI](https://github.com/your-org/story-ui) - AI-powered Story
|
|
|
168
169
|
}
|
|
169
170
|
}
|
|
170
171
|
if (cleanedCount > 0) {
|
|
171
|
-
|
|
172
|
+
logger.log(`🧹 Cleaned up ${cleanedCount} old generated stories`);
|
|
172
173
|
}
|
|
173
174
|
}
|
|
174
175
|
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// Utility for safe logging that doesn't corrupt MCP stdio communication
|
|
2
|
+
const isMcpMode = () => {
|
|
3
|
+
return process.argv.includes('mcp') || process.env.STORY_UI_MCP_MODE === 'true';
|
|
4
|
+
};
|
|
5
|
+
// Remove emojis from strings to prevent JSON parsing errors in MCP
|
|
6
|
+
const stripEmojis = (str) => {
|
|
7
|
+
// Remove common emojis and unicode symbols
|
|
8
|
+
return str.replace(/[\u{1F300}-\u{1F9FF}]|[\u{2600}-\u{26FF}]|[\u{2700}-\u{27BF}]|[\u{1F000}-\u{1F02F}]|[\u{1F0A0}-\u{1F0FF}]|[\u{1F100}-\u{1F64F}]|[\u{1F680}-\u{1F6FF}]|[\u{1F900}-\u{1F9FF}]|[\u{2190}-\u{21FF}]|[\u{2300}-\u{23FF}]|[\u{25A0}-\u{25FF}]|[\u{2B00}-\u{2BFF}]|[\u{3000}-\u{303F}]|✅|❌|⚠️|🔍|📦|📋|🔄|📊|⭐|🚀|💡|🎯|🔧|📌|🏃|🎨|💪|🌟|🎉|🎊|👍|👎|📝|📄|🗑️|🗂️|📁|🖥️|💻|📱|🌐|🔒|🔓|🔑|🔨|⚡|🔥|💧|🌈|☀️|🌙|⭐|✨|💫|☁️|🌧️|⛈️|❄️|☃️|⛄|🌬️|💨|🌪️|🌫️|🌊|🎯/gu, '');
|
|
9
|
+
};
|
|
10
|
+
const processArgs = (args) => {
|
|
11
|
+
if (!isMcpMode())
|
|
12
|
+
return args;
|
|
13
|
+
return args.map(arg => {
|
|
14
|
+
if (typeof arg === 'string') {
|
|
15
|
+
return stripEmojis(arg);
|
|
16
|
+
}
|
|
17
|
+
return arg;
|
|
18
|
+
});
|
|
19
|
+
};
|
|
20
|
+
export const logger = {
|
|
21
|
+
log: (...args) => {
|
|
22
|
+
const processedArgs = processArgs(args);
|
|
23
|
+
if (isMcpMode()) {
|
|
24
|
+
console.error(...processedArgs);
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
console.log(...args);
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
error: (...args) => {
|
|
31
|
+
const processedArgs = processArgs(args);
|
|
32
|
+
console.error(...processedArgs);
|
|
33
|
+
},
|
|
34
|
+
warn: (...args) => {
|
|
35
|
+
const processedArgs = processArgs(args);
|
|
36
|
+
if (isMcpMode()) {
|
|
37
|
+
console.error(...processedArgs);
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
console.warn(...args);
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
info: (...args) => {
|
|
44
|
+
const processedArgs = processArgs(args);
|
|
45
|
+
if (isMcpMode()) {
|
|
46
|
+
console.error(...processedArgs);
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
console.info(...args);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
};
|