@tpitre/story-ui 1.0.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/.env.sample +17 -0
- package/LICENSE +21 -0
- package/README.md +531 -0
- package/dist/cli/index.js +250 -0
- package/dist/cli/setup.js +289 -0
- package/dist/index.js +12 -0
- package/dist/mcp-server/index.js +64 -0
- package/dist/mcp-server/routes/claude.js +30 -0
- package/dist/mcp-server/routes/components.js +26 -0
- package/dist/mcp-server/routes/generateStory.js +289 -0
- package/dist/mcp-server/routes/memoryStories.js +141 -0
- package/dist/mcp-server/routes/storySync.js +147 -0
- package/dist/story-generator/componentDiscovery.js +222 -0
- package/dist/story-generator/configLoader.js +482 -0
- package/dist/story-generator/generateStory.js +19 -0
- package/dist/story-generator/gitignoreManager.js +182 -0
- package/dist/story-generator/inMemoryStoryService.js +128 -0
- package/dist/story-generator/productionGitignoreManager.js +333 -0
- package/dist/story-generator/promptGenerator.js +201 -0
- package/dist/story-generator/storySync.js +201 -0
- package/dist/story-ui.config.js +114 -0
- package/dist/story-ui.config.loader.js +205 -0
- package/package.json +80 -0
- package/templates/README.md +32 -0
- package/templates/StoryUI/StoryUIPanel.stories.tsx +28 -0
- package/templates/StoryUI/StoryUIPanel.tsx +870 -0
- package/templates/StoryUI/index.tsx +2 -0
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-memory story service for production environments
|
|
3
|
+
* Stores generated stories in memory and serves them via API
|
|
4
|
+
*/
|
|
5
|
+
export class InMemoryStoryService {
|
|
6
|
+
constructor(config) {
|
|
7
|
+
this.stories = new Map();
|
|
8
|
+
this.config = config;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Stores a generated story in memory
|
|
12
|
+
*/
|
|
13
|
+
storeStory(story) {
|
|
14
|
+
this.stories.set(story.id, {
|
|
15
|
+
...story,
|
|
16
|
+
createdAt: new Date(),
|
|
17
|
+
lastAccessed: new Date()
|
|
18
|
+
});
|
|
19
|
+
// Clean up old stories to prevent memory leaks
|
|
20
|
+
this.cleanupOldStories();
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Retrieves a story by ID
|
|
24
|
+
*/
|
|
25
|
+
getStory(id) {
|
|
26
|
+
const story = this.stories.get(id);
|
|
27
|
+
if (story) {
|
|
28
|
+
story.lastAccessed = new Date();
|
|
29
|
+
return story;
|
|
30
|
+
}
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Gets all stored stories
|
|
35
|
+
*/
|
|
36
|
+
getAllStories() {
|
|
37
|
+
return Array.from(this.stories.values()).sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Deletes a story by ID
|
|
41
|
+
*/
|
|
42
|
+
deleteStory(id) {
|
|
43
|
+
return this.stories.delete(id);
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Clears all stories
|
|
47
|
+
*/
|
|
48
|
+
clearAllStories() {
|
|
49
|
+
this.stories.clear();
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Gets story content for Storybook integration
|
|
53
|
+
*/
|
|
54
|
+
getStoryContent(id) {
|
|
55
|
+
const story = this.stories.get(id);
|
|
56
|
+
return story ? story.content : null;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Gets story metadata for listing
|
|
60
|
+
*/
|
|
61
|
+
getStoryMetadata() {
|
|
62
|
+
return Array.from(this.stories.values()).map(story => ({
|
|
63
|
+
id: story.id,
|
|
64
|
+
title: story.title,
|
|
65
|
+
description: story.description,
|
|
66
|
+
createdAt: story.createdAt,
|
|
67
|
+
lastAccessed: story.lastAccessed,
|
|
68
|
+
componentCount: this.countComponents(story.content)
|
|
69
|
+
}));
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Cleans up old stories to prevent memory leaks
|
|
73
|
+
*/
|
|
74
|
+
cleanupOldStories() {
|
|
75
|
+
const maxStories = 50; // Keep maximum 50 stories in memory
|
|
76
|
+
const maxAge = 24 * 60 * 60 * 1000; // 24 hours
|
|
77
|
+
const now = Date.now();
|
|
78
|
+
// Remove stories older than maxAge
|
|
79
|
+
for (const [id, story] of this.stories.entries()) {
|
|
80
|
+
if (now - story.lastAccessed.getTime() > maxAge) {
|
|
81
|
+
this.stories.delete(id);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
// If still too many stories, remove oldest ones
|
|
85
|
+
if (this.stories.size > maxStories) {
|
|
86
|
+
const sortedStories = Array.from(this.stories.entries())
|
|
87
|
+
.sort(([, a], [, b]) => a.lastAccessed.getTime() - b.lastAccessed.getTime());
|
|
88
|
+
const toRemove = sortedStories.slice(0, this.stories.size - maxStories);
|
|
89
|
+
for (const [id] of toRemove) {
|
|
90
|
+
this.stories.delete(id);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Counts components used in a story
|
|
96
|
+
*/
|
|
97
|
+
countComponents(content) {
|
|
98
|
+
const componentMatches = content.match(/<[A-Z][A-Za-z0-9]*\s/g);
|
|
99
|
+
return componentMatches ? new Set(componentMatches).size : 0;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Gets memory usage statistics
|
|
103
|
+
*/
|
|
104
|
+
getMemoryStats() {
|
|
105
|
+
const stories = Array.from(this.stories.values());
|
|
106
|
+
const totalSize = stories.reduce((sum, story) => sum + story.content.length, 0);
|
|
107
|
+
return {
|
|
108
|
+
storyCount: this.stories.size,
|
|
109
|
+
totalSizeBytes: totalSize,
|
|
110
|
+
averageSizeBytes: stories.length > 0 ? Math.round(totalSize / stories.length) : 0,
|
|
111
|
+
oldestStory: stories.reduce((oldest, story) => !oldest || story.createdAt < oldest.createdAt ? story : oldest, null)?.createdAt || null,
|
|
112
|
+
newestStory: stories.reduce((newest, story) => !newest || story.createdAt > newest.createdAt ? story : newest, null)?.createdAt || null
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Global in-memory story service instance
|
|
118
|
+
*/
|
|
119
|
+
let globalStoryService = null;
|
|
120
|
+
/**
|
|
121
|
+
* Gets or creates the global story service instance
|
|
122
|
+
*/
|
|
123
|
+
export function getInMemoryStoryService(config) {
|
|
124
|
+
if (!globalStoryService) {
|
|
125
|
+
globalStoryService = new InMemoryStoryService(config);
|
|
126
|
+
}
|
|
127
|
+
return globalStoryService;
|
|
128
|
+
}
|
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
/**
|
|
4
|
+
* Production-ready gitignore manager that handles both development and server environments
|
|
5
|
+
*/
|
|
6
|
+
export class ProductionGitignoreManager {
|
|
7
|
+
constructor(config, projectRoot = process.cwd()) {
|
|
8
|
+
this.config = config;
|
|
9
|
+
this.projectRoot = projectRoot;
|
|
10
|
+
this.isProduction = this.detectProductionEnvironment();
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Detects if we're running in a production/read-only environment
|
|
14
|
+
*/
|
|
15
|
+
detectProductionEnvironment() {
|
|
16
|
+
// Check common production environment indicators
|
|
17
|
+
const prodIndicators = [
|
|
18
|
+
process.env.NODE_ENV === 'production',
|
|
19
|
+
process.env.VERCEL === '1',
|
|
20
|
+
process.env.NETLIFY === 'true',
|
|
21
|
+
process.env.CF_PAGES === '1', // Cloudflare Pages
|
|
22
|
+
process.env.RENDER === 'true',
|
|
23
|
+
process.env.RAILWAY_ENVIRONMENT === 'production',
|
|
24
|
+
// Check if we can't write to project root
|
|
25
|
+
!this.canWriteToProjectRoot()
|
|
26
|
+
];
|
|
27
|
+
return prodIndicators.some(indicator => indicator);
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Tests if we can write to the project root
|
|
31
|
+
*/
|
|
32
|
+
canWriteToProjectRoot() {
|
|
33
|
+
try {
|
|
34
|
+
const testFile = path.join(this.projectRoot, '.story-ui-write-test');
|
|
35
|
+
fs.writeFileSync(testFile, 'test');
|
|
36
|
+
fs.unlinkSync(testFile);
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Main setup method that adapts to environment
|
|
45
|
+
*/
|
|
46
|
+
setupGitignoreIntegration() {
|
|
47
|
+
if (this.isProduction) {
|
|
48
|
+
this.handleProductionEnvironment();
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
this.handleDevelopmentEnvironment();
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Production environment: Use in-memory story generation
|
|
56
|
+
*/
|
|
57
|
+
handleProductionEnvironment() {
|
|
58
|
+
console.log('🌐 Production environment detected - using in-memory story generation');
|
|
59
|
+
// Validate that gitignore is already set up
|
|
60
|
+
this.validateProductionSetup();
|
|
61
|
+
// Set up temporary directory for story generation if needed
|
|
62
|
+
this.setupTemporaryDirectory();
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Development environment: Full gitignore management
|
|
66
|
+
*/
|
|
67
|
+
handleDevelopmentEnvironment() {
|
|
68
|
+
console.log('🔧 Development environment - setting up gitignore integration');
|
|
69
|
+
this.ensureGeneratedDirectoryExists();
|
|
70
|
+
this.ensureGeneratedDirectoryIgnored();
|
|
71
|
+
this.createGeneratedDirectoryReadme();
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Validates that production environment is properly configured
|
|
75
|
+
*/
|
|
76
|
+
validateProductionSetup() {
|
|
77
|
+
const gitignorePath = path.join(this.projectRoot, '.gitignore');
|
|
78
|
+
if (!fs.existsSync(gitignorePath)) {
|
|
79
|
+
console.warn('⚠️ No .gitignore found in production. Stories will be generated in memory only.');
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
const gitignoreContent = fs.readFileSync(gitignorePath, 'utf-8');
|
|
83
|
+
const generatedPath = this.getRelativeGeneratedPath();
|
|
84
|
+
if (generatedPath && !this.isPathIgnored(gitignoreContent, generatedPath)) {
|
|
85
|
+
console.warn(`⚠️ Generated path not in .gitignore: ${generatedPath}`);
|
|
86
|
+
console.warn(' Run "npx story-ui setup-gitignore" in development to fix this.');
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
console.log('✅ Production gitignore configuration validated');
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Sets up temporary directory for production story generation
|
|
94
|
+
*/
|
|
95
|
+
setupTemporaryDirectory() {
|
|
96
|
+
try {
|
|
97
|
+
const tempDir = this.getProductionTempDirectory();
|
|
98
|
+
if (!fs.existsSync(tempDir)) {
|
|
99
|
+
fs.mkdirSync(tempDir, { recursive: true });
|
|
100
|
+
console.log(`✅ Created temporary directory: ${tempDir}`);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
catch (error) {
|
|
104
|
+
console.warn('⚠️ Could not create temporary directory, using in-memory generation only');
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Gets a writable temporary directory for production
|
|
109
|
+
*/
|
|
110
|
+
getProductionTempDirectory() {
|
|
111
|
+
// Try various temporary directory locations
|
|
112
|
+
const tempOptions = [
|
|
113
|
+
process.env.TMPDIR,
|
|
114
|
+
process.env.TMP,
|
|
115
|
+
process.env.TEMP,
|
|
116
|
+
'/tmp',
|
|
117
|
+
path.join(process.cwd(), '.tmp')
|
|
118
|
+
].filter(Boolean);
|
|
119
|
+
for (const tempPath of tempOptions) {
|
|
120
|
+
try {
|
|
121
|
+
const storyTempDir = path.join(tempPath, 'story-ui-generated');
|
|
122
|
+
fs.mkdirSync(storyTempDir, { recursive: true });
|
|
123
|
+
return storyTempDir;
|
|
124
|
+
}
|
|
125
|
+
catch {
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
throw new Error('No writable temporary directory found');
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Creates the generated directory if it doesn't exist (development only)
|
|
133
|
+
*/
|
|
134
|
+
ensureGeneratedDirectoryExists() {
|
|
135
|
+
const generatedDir = this.config.generatedStoriesPath;
|
|
136
|
+
if (!fs.existsSync(generatedDir)) {
|
|
137
|
+
fs.mkdirSync(generatedDir, { recursive: true });
|
|
138
|
+
console.log(`✅ Created generated stories directory: ${generatedDir}`);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Ensures the generated stories directory is added to .gitignore (development only)
|
|
143
|
+
*/
|
|
144
|
+
ensureGeneratedDirectoryIgnored() {
|
|
145
|
+
const gitignorePath = path.join(this.projectRoot, '.gitignore');
|
|
146
|
+
const generatedPath = this.getRelativeGeneratedPath();
|
|
147
|
+
if (!generatedPath) {
|
|
148
|
+
console.warn('Could not determine relative path for generated stories directory');
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
// Create .gitignore if it doesn't exist
|
|
152
|
+
if (!fs.existsSync(gitignorePath)) {
|
|
153
|
+
this.createGitignore(gitignorePath, generatedPath);
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
// Check if the path is already ignored
|
|
157
|
+
const gitignoreContent = fs.readFileSync(gitignorePath, 'utf-8');
|
|
158
|
+
if (this.isPathIgnored(gitignoreContent, generatedPath)) {
|
|
159
|
+
console.log(`✅ Generated stories directory already ignored: ${generatedPath}`);
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
// Add the ignore rule
|
|
163
|
+
this.addIgnoreRule(gitignorePath, generatedPath);
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Gets the relative path from project root to generated stories directory
|
|
167
|
+
*/
|
|
168
|
+
getRelativeGeneratedPath() {
|
|
169
|
+
try {
|
|
170
|
+
const absoluteGeneratedPath = path.resolve(this.config.generatedStoriesPath);
|
|
171
|
+
const absoluteProjectRoot = path.resolve(this.projectRoot);
|
|
172
|
+
let relativePath = path.relative(absoluteProjectRoot, absoluteGeneratedPath);
|
|
173
|
+
relativePath = relativePath.replace(/\\/g, '/');
|
|
174
|
+
if (!relativePath.startsWith('../') && !relativePath.startsWith('/')) {
|
|
175
|
+
relativePath = './' + relativePath;
|
|
176
|
+
}
|
|
177
|
+
return relativePath;
|
|
178
|
+
}
|
|
179
|
+
catch (error) {
|
|
180
|
+
console.error('Error calculating relative path:', error);
|
|
181
|
+
return null;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Creates a new .gitignore file with Story UI section
|
|
186
|
+
*/
|
|
187
|
+
createGitignore(gitignorePath, generatedPath) {
|
|
188
|
+
const content = this.generateGitignoreSection(generatedPath);
|
|
189
|
+
fs.writeFileSync(gitignorePath, content);
|
|
190
|
+
console.log(`✅ Created .gitignore with Story UI generated directory: ${generatedPath}`);
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Checks if the generated path is already ignored
|
|
194
|
+
*/
|
|
195
|
+
isPathIgnored(gitignoreContent, generatedPath) {
|
|
196
|
+
const lines = gitignoreContent.split('\n').map(line => line.trim());
|
|
197
|
+
const pathVariations = [
|
|
198
|
+
generatedPath,
|
|
199
|
+
generatedPath.replace(/^\.\//, ''),
|
|
200
|
+
generatedPath + '/',
|
|
201
|
+
generatedPath.replace(/^\.\//, '') + '/',
|
|
202
|
+
generatedPath + '/**',
|
|
203
|
+
generatedPath.replace(/^\.\//, '') + '/**'
|
|
204
|
+
];
|
|
205
|
+
return pathVariations.some(variation => lines.includes(variation) ||
|
|
206
|
+
lines.includes(variation.replace(/\/$/, '')));
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* Adds ignore rule to existing .gitignore
|
|
210
|
+
*/
|
|
211
|
+
addIgnoreRule(gitignorePath, generatedPath) {
|
|
212
|
+
const existingContent = fs.readFileSync(gitignorePath, 'utf-8');
|
|
213
|
+
const newSection = this.generateGitignoreSection(generatedPath);
|
|
214
|
+
const separator = existingContent.endsWith('\n') ? '\n' : '\n\n';
|
|
215
|
+
const updatedContent = existingContent + separator + newSection;
|
|
216
|
+
fs.writeFileSync(gitignorePath, updatedContent);
|
|
217
|
+
console.log(`✅ Added Story UI generated directory to .gitignore: ${generatedPath}`);
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Generates the gitignore section for Story UI
|
|
221
|
+
*/
|
|
222
|
+
generateGitignoreSection(generatedPath) {
|
|
223
|
+
return `# Story UI - AI Generated Stories (ephemeral, not for version control)
|
|
224
|
+
# These are temporary stories for testing layouts and should not be committed
|
|
225
|
+
${generatedPath}/
|
|
226
|
+
${generatedPath}/**`;
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* Creates a README in the generated directory explaining its purpose (development only)
|
|
230
|
+
*/
|
|
231
|
+
createGeneratedDirectoryReadme() {
|
|
232
|
+
const generatedDir = this.config.generatedStoriesPath;
|
|
233
|
+
const readmePath = path.join(generatedDir, 'README.md');
|
|
234
|
+
if (fs.existsSync(readmePath)) {
|
|
235
|
+
return; // Don't overwrite existing README
|
|
236
|
+
}
|
|
237
|
+
const readmeContent = `# AI Generated Stories
|
|
238
|
+
|
|
239
|
+
This directory contains stories generated by Story UI for testing and iteration purposes.
|
|
240
|
+
|
|
241
|
+
## ⚠️ Important Notes
|
|
242
|
+
|
|
243
|
+
- **These stories are ephemeral** - they are meant for testing layouts and sharing with stakeholders
|
|
244
|
+
- **Do not commit these files** - they are automatically ignored by git
|
|
245
|
+
- **Stories are regenerated** - feel free to delete and regenerate as needed
|
|
246
|
+
|
|
247
|
+
## Purpose
|
|
248
|
+
|
|
249
|
+
These stories are designed for:
|
|
250
|
+
- 🎨 **Layout Testing** - Test different component arrangements
|
|
251
|
+
- 👥 **Stakeholder Review** - Share layouts with product owners, designers, and project managers
|
|
252
|
+
- 🔄 **Rapid Iteration** - Quickly generate and modify layouts
|
|
253
|
+
- 📱 **Design Validation** - Validate designs before implementation
|
|
254
|
+
|
|
255
|
+
## Usage
|
|
256
|
+
|
|
257
|
+
Stories in this directory will appear in Storybook under the "${this.config.storyPrefix}" section.
|
|
258
|
+
|
|
259
|
+
Generated by [Story UI](https://github.com/your-org/story-ui) - AI-powered Storybook story generator.
|
|
260
|
+
`;
|
|
261
|
+
try {
|
|
262
|
+
fs.writeFileSync(readmePath, readmeContent);
|
|
263
|
+
console.log(`✅ Created README in generated directory`);
|
|
264
|
+
}
|
|
265
|
+
catch (error) {
|
|
266
|
+
console.warn('⚠️ Could not create README in generated directory');
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* Cleans up old generated stories (safe for both environments)
|
|
271
|
+
*/
|
|
272
|
+
cleanupOldStories(maxAge = 7 * 24 * 60 * 60 * 1000) {
|
|
273
|
+
const directories = [
|
|
274
|
+
this.config.generatedStoriesPath,
|
|
275
|
+
this.isProduction ? this.getProductionTempDirectory() : null
|
|
276
|
+
].filter(Boolean);
|
|
277
|
+
for (const dir of directories) {
|
|
278
|
+
if (!dir || !fs.existsSync(dir))
|
|
279
|
+
continue;
|
|
280
|
+
try {
|
|
281
|
+
const files = fs.readdirSync(dir);
|
|
282
|
+
const now = Date.now();
|
|
283
|
+
let cleanedCount = 0;
|
|
284
|
+
for (const file of files) {
|
|
285
|
+
if (!file.endsWith('.stories.tsx'))
|
|
286
|
+
continue;
|
|
287
|
+
const filePath = path.join(dir, file);
|
|
288
|
+
const stats = fs.statSync(filePath);
|
|
289
|
+
const age = now - stats.mtime.getTime();
|
|
290
|
+
if (age > maxAge) {
|
|
291
|
+
fs.unlinkSync(filePath);
|
|
292
|
+
cleanedCount++;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
if (cleanedCount > 0) {
|
|
296
|
+
console.log(`🧹 Cleaned up ${cleanedCount} old generated stories from ${dir}`);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
catch (error) {
|
|
300
|
+
console.warn(`⚠️ Could not clean up stories in ${dir}`);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
/**
|
|
305
|
+
* Gets the appropriate directory for story generation based on environment
|
|
306
|
+
*/
|
|
307
|
+
getStoryGenerationPath() {
|
|
308
|
+
if (this.isProduction) {
|
|
309
|
+
try {
|
|
310
|
+
return this.getProductionTempDirectory();
|
|
311
|
+
}
|
|
312
|
+
catch {
|
|
313
|
+
// Fallback to in-memory only
|
|
314
|
+
return '';
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
return this.config.generatedStoriesPath;
|
|
318
|
+
}
|
|
319
|
+
/**
|
|
320
|
+
* Checks if we're in production mode
|
|
321
|
+
*/
|
|
322
|
+
isProductionMode() {
|
|
323
|
+
return this.isProduction;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
/**
|
|
327
|
+
* Convenience function to set up gitignore for Story UI (production-ready)
|
|
328
|
+
*/
|
|
329
|
+
export function setupProductionGitignore(config, projectRoot) {
|
|
330
|
+
const manager = new ProductionGitignoreManager(config, projectRoot);
|
|
331
|
+
manager.setupGitignoreIntegration();
|
|
332
|
+
return manager;
|
|
333
|
+
}
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generates a comprehensive AI prompt based on the configuration and discovered components
|
|
3
|
+
*/
|
|
4
|
+
export function generatePrompt(config, components) {
|
|
5
|
+
const componentReference = generateComponentReference(components, config);
|
|
6
|
+
const layoutInstructions = generateLayoutInstructions(config);
|
|
7
|
+
const examples = generateExamples(config);
|
|
8
|
+
const systemPrompt = generateSystemPrompt(config);
|
|
9
|
+
const sampleStory = config.sampleStory || generateDefaultSampleStory(config, components);
|
|
10
|
+
return {
|
|
11
|
+
systemPrompt,
|
|
12
|
+
componentReference,
|
|
13
|
+
layoutInstructions,
|
|
14
|
+
examples,
|
|
15
|
+
sampleStory
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Generates the system prompt based on configuration
|
|
20
|
+
*/
|
|
21
|
+
function generateSystemPrompt(config) {
|
|
22
|
+
if (config.systemPrompt) {
|
|
23
|
+
return config.systemPrompt;
|
|
24
|
+
}
|
|
25
|
+
const componentSystemName = config.componentPrefix ?
|
|
26
|
+
`${config.componentPrefix.replace(/^[A-Z]+/, '')} design system` :
|
|
27
|
+
'component library';
|
|
28
|
+
return `You are an expert UI developer creating Storybook stories. Use ONLY the React components from the ${componentSystemName} listed below.`;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Generates component reference documentation
|
|
32
|
+
*/
|
|
33
|
+
function generateComponentReference(components, config) {
|
|
34
|
+
let reference = '';
|
|
35
|
+
// Group components by category
|
|
36
|
+
const componentsByCategory = components.reduce((acc, component) => {
|
|
37
|
+
if (!acc[component.category]) {
|
|
38
|
+
acc[component.category] = [];
|
|
39
|
+
}
|
|
40
|
+
acc[component.category].push(component);
|
|
41
|
+
return acc;
|
|
42
|
+
}, {});
|
|
43
|
+
// Layout components first (most important for multi-column layouts)
|
|
44
|
+
if (componentsByCategory.layout) {
|
|
45
|
+
reference += 'LAYOUT COMPONENTS (for multi-column layouts, grids, etc.):\n';
|
|
46
|
+
for (const component of componentsByCategory.layout) {
|
|
47
|
+
reference += formatComponentReference(component, config);
|
|
48
|
+
}
|
|
49
|
+
reference += '\n';
|
|
50
|
+
}
|
|
51
|
+
// Other categories
|
|
52
|
+
const categoryOrder = ['content', 'form', 'navigation', 'feedback', 'other'];
|
|
53
|
+
for (const category of categoryOrder) {
|
|
54
|
+
if (componentsByCategory[category] && componentsByCategory[category].length > 0) {
|
|
55
|
+
reference += `${category.toUpperCase()} COMPONENTS:\n`;
|
|
56
|
+
for (const component of componentsByCategory[category]) {
|
|
57
|
+
reference += formatComponentReference(component, config);
|
|
58
|
+
}
|
|
59
|
+
reference += '\n';
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return reference;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Formats a single component reference
|
|
66
|
+
*/
|
|
67
|
+
function formatComponentReference(component, config) {
|
|
68
|
+
let reference = `- ${component.name}`;
|
|
69
|
+
if (component.props.length > 0) {
|
|
70
|
+
reference += `: Props: ${component.props.join(', ')}`;
|
|
71
|
+
}
|
|
72
|
+
if (component.slots && component.slots.length > 0) {
|
|
73
|
+
reference += `. Slots: ${component.slots.join(', ')}`;
|
|
74
|
+
}
|
|
75
|
+
if (component.description && component.description !== `${component.name} component`) {
|
|
76
|
+
reference += ` - ${component.description}`;
|
|
77
|
+
}
|
|
78
|
+
// Add specific usage notes for layout components
|
|
79
|
+
if (component.category === 'layout') {
|
|
80
|
+
if (component.name.toLowerCase().includes('layout') && !component.name.toLowerCase().includes('section')) {
|
|
81
|
+
reference += ' - Use as main wrapper for multi-column layouts';
|
|
82
|
+
}
|
|
83
|
+
else if (component.name.toLowerCase().includes('section')) {
|
|
84
|
+
reference += ' - Use inside layout wrapper for individual columns';
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
reference += '\n';
|
|
88
|
+
return reference;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Generates layout-specific instructions
|
|
92
|
+
*/
|
|
93
|
+
function generateLayoutInstructions(config) {
|
|
94
|
+
const instructions = [];
|
|
95
|
+
const layoutRules = config.layoutRules;
|
|
96
|
+
if (layoutRules.multiColumnWrapper && layoutRules.columnComponent) {
|
|
97
|
+
instructions.push('CRITICAL LAYOUT RULES:');
|
|
98
|
+
instructions.push(`- For ANY multi-column layout (2, 3, or more columns), use CSS Grid with ${layoutRules.multiColumnWrapper} elements`);
|
|
99
|
+
instructions.push(`- Each column must be wrapped in its own ${layoutRules.columnComponent} element`);
|
|
100
|
+
instructions.push(`- Structure: <${layoutRules.multiColumnWrapper} style={{display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '1rem'}}><${layoutRules.columnComponent}>column 1</${layoutRules.columnComponent}><${layoutRules.columnComponent}>column 2</${layoutRules.columnComponent}></${layoutRules.multiColumnWrapper}>`);
|
|
101
|
+
instructions.push(`- Use inline styles for CSS Grid layouts since the design system lacks proper multi-column layout components`);
|
|
102
|
+
instructions.push(`- The grid container ${layoutRules.multiColumnWrapper} should be the main component in your story, not individual cards`);
|
|
103
|
+
}
|
|
104
|
+
if (layoutRules.prohibitedElements && layoutRules.prohibitedElements.length > 0) {
|
|
105
|
+
instructions.push(`- Do NOT use plain HTML ${layoutRules.prohibitedElements.join(', ')} elements for layout - use the provided layout components`);
|
|
106
|
+
}
|
|
107
|
+
return instructions;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Generates layout examples
|
|
111
|
+
*/
|
|
112
|
+
function generateExamples(config) {
|
|
113
|
+
const examples = [];
|
|
114
|
+
const layoutExamples = config.layoutRules.layoutExamples;
|
|
115
|
+
if (layoutExamples) {
|
|
116
|
+
examples.push('EXAMPLES:');
|
|
117
|
+
examples.push('');
|
|
118
|
+
if (layoutExamples.twoColumn) {
|
|
119
|
+
examples.push('Two-column layout:');
|
|
120
|
+
examples.push(layoutExamples.twoColumn);
|
|
121
|
+
examples.push('');
|
|
122
|
+
}
|
|
123
|
+
if (layoutExamples.threeColumn) {
|
|
124
|
+
examples.push('Three-column layout:');
|
|
125
|
+
examples.push(layoutExamples.threeColumn);
|
|
126
|
+
examples.push('');
|
|
127
|
+
}
|
|
128
|
+
if (layoutExamples.grid) {
|
|
129
|
+
examples.push('Grid layout:');
|
|
130
|
+
examples.push(layoutExamples.grid);
|
|
131
|
+
examples.push('');
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return examples;
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Generates a default sample story if none provided
|
|
138
|
+
*/
|
|
139
|
+
function generateDefaultSampleStory(config, components) {
|
|
140
|
+
const layoutComponent = components.find(c => c.category === 'layout' && !c.name.toLowerCase().includes('section'));
|
|
141
|
+
const sectionComponent = components.find(c => c.category === 'layout' && c.name.toLowerCase().includes('section'));
|
|
142
|
+
const contentComponent = components.find(c => c.category === 'content');
|
|
143
|
+
const mainComponent = layoutComponent?.name || contentComponent?.name || components[0]?.name || 'div';
|
|
144
|
+
const imports = [mainComponent];
|
|
145
|
+
if (sectionComponent)
|
|
146
|
+
imports.push(sectionComponent.name);
|
|
147
|
+
if (contentComponent && contentComponent.name !== mainComponent)
|
|
148
|
+
imports.push(contentComponent.name);
|
|
149
|
+
const importStatement = `import { ${imports.join(', ')} } from '${config.importPath}';`;
|
|
150
|
+
let children = '';
|
|
151
|
+
if (layoutComponent && sectionComponent) {
|
|
152
|
+
children = `
|
|
153
|
+
<${layoutComponent.name}>
|
|
154
|
+
<${sectionComponent.name}>
|
|
155
|
+
${contentComponent ? `<${contentComponent.name}>Sample content</${contentComponent.name}>` : 'Sample content'}
|
|
156
|
+
</${sectionComponent.name}>
|
|
157
|
+
</${layoutComponent.name}>`;
|
|
158
|
+
}
|
|
159
|
+
else if (contentComponent) {
|
|
160
|
+
children = `<${contentComponent.name}>Sample content</${contentComponent.name}>`;
|
|
161
|
+
}
|
|
162
|
+
else {
|
|
163
|
+
children = '<div>Sample content</div>';
|
|
164
|
+
}
|
|
165
|
+
return `import type { StoryObj } from '@storybook/react-webpack5';
|
|
166
|
+
${importStatement}
|
|
167
|
+
|
|
168
|
+
export default {
|
|
169
|
+
title: 'Layouts/Sample Layout',
|
|
170
|
+
component: ${mainComponent},
|
|
171
|
+
subcomponents: { ${imports.slice(1).join(', ')} },
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
export const Default: StoryObj<typeof ${mainComponent}> = {
|
|
175
|
+
args: {
|
|
176
|
+
children: (${children}
|
|
177
|
+
)
|
|
178
|
+
}
|
|
179
|
+
};`;
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Builds the complete Claude prompt
|
|
183
|
+
*/
|
|
184
|
+
export function buildClaudePrompt(userPrompt, config, components) {
|
|
185
|
+
const generated = generatePrompt(config, components);
|
|
186
|
+
const promptParts = [
|
|
187
|
+
generated.systemPrompt,
|
|
188
|
+
'',
|
|
189
|
+
...generated.layoutInstructions,
|
|
190
|
+
'',
|
|
191
|
+
'Available components:',
|
|
192
|
+
generated.componentReference,
|
|
193
|
+
...generated.examples,
|
|
194
|
+
];
|
|
195
|
+
// Add critical structure instructions for multi-column layouts
|
|
196
|
+
if (config.layoutRules.multiColumnWrapper && config.layoutRules.columnComponent) {
|
|
197
|
+
promptParts.push(`CRITICAL: For multi-column layouts, the children prop must contain a SINGLE ${config.layoutRules.multiColumnWrapper} with CSS Grid styling wrapping all ${config.layoutRules.columnComponent} components.`, `WRONG: children: (<><${config.layoutRules.columnComponent}>...</${config.layoutRules.columnComponent}><${config.layoutRules.columnComponent}>...</${config.layoutRules.columnComponent}></>)`, `CORRECT: children: (<${config.layoutRules.multiColumnWrapper} style={{display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '1rem'}}><${config.layoutRules.columnComponent}>...</${config.layoutRules.columnComponent}><${config.layoutRules.columnComponent}>...</${config.layoutRules.columnComponent}></${config.layoutRules.multiColumnWrapper}>)`, '');
|
|
198
|
+
}
|
|
199
|
+
promptParts.push(`Output a complete Storybook story file in TypeScript. Import components from "${config.importPath}". Use the following sample as a template. Respond ONLY with a single code block containing the full file, and nothing else.`, '', 'Sample story format:', generated.sampleStory, '', 'User request:', userPrompt);
|
|
200
|
+
return promptParts.join('\n');
|
|
201
|
+
}
|