@tpitre/story-ui 3.7.0 → 3.9.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/dist/cli/index.js CHANGED
@@ -6,6 +6,7 @@ import path from 'path';
6
6
  import { fileURLToPath } from 'url';
7
7
  import { setupCommand, cleanupDefaultStorybookComponents } from './setup.js';
8
8
  import { deployCommand } from './deploy.js';
9
+ import { updateCommand, statusCommand } from './update.js';
9
10
  import net from 'net';
10
11
  const __filename = fileURLToPath(import.meta.url);
11
12
  const __dirname = path.dirname(__filename);
@@ -279,4 +280,25 @@ program
279
280
  mcpServer.kill('SIGINT');
280
281
  });
281
282
  });
283
+ program
284
+ .command('update')
285
+ .description('Update Story UI managed files to the latest version')
286
+ .option('-f, --force', 'Skip confirmation prompts')
287
+ .option('--no-backup', 'Skip creating backups of existing files')
288
+ .option('-n, --dry-run', 'Show what would be updated without making changes')
289
+ .option('-v, --verbose', 'Show detailed output')
290
+ .action(async (options) => {
291
+ await updateCommand({
292
+ force: options.force,
293
+ backup: options.backup,
294
+ dryRun: options.dryRun,
295
+ verbose: options.verbose
296
+ });
297
+ });
298
+ program
299
+ .command('status')
300
+ .description('Show Story UI installation status and version info')
301
+ .action(() => {
302
+ statusCommand();
303
+ });
282
304
  program.parse(process.argv);
@@ -1 +1 @@
1
- {"version":3,"file":"setup.d.ts","sourceRoot":"","sources":["../../cli/setup.ts"],"names":[],"mappings":"AAkCA;;GAEG;AACH,wBAAgB,iCAAiC,SA8ChD;AA2VD,MAAM,WAAW,YAAY;IAC3B,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,WAAW,CAAC,EAAE,QAAQ,GAAG,QAAQ,GAAG,QAAQ,CAAC;IAC7C,GAAG,CAAC,EAAE,OAAO,CAAC;IACd,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB;AAED,wBAAsB,YAAY,CAAC,OAAO,GAAE,YAAiB,iBAwsB5D"}
1
+ {"version":3,"file":"setup.d.ts","sourceRoot":"","sources":["../../cli/setup.ts"],"names":[],"mappings":"AAmDA;;GAEG;AACH,wBAAgB,iCAAiC,SA8ChD;AAiWD,MAAM,WAAW,YAAY;IAC3B,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,WAAW,CAAC,EAAE,QAAQ,GAAG,QAAQ,GAAG,QAAQ,CAAC;IAC7C,GAAG,CAAC,EAAE,OAAO,CAAC;IACd,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB;AAED,wBAAsB,YAAY,CAAC,OAAO,GAAE,YAAiB,iBA2sB5D"}
package/dist/cli/setup.js CHANGED
@@ -8,6 +8,23 @@ import net from 'net';
8
8
  import { execSync } from 'child_process';
9
9
  const __filename = fileURLToPath(import.meta.url);
10
10
  const __dirname = path.dirname(__filename);
11
+ /**
12
+ * Get the Story UI package version for version tracking
13
+ */
14
+ function getStoryUIVersion() {
15
+ try {
16
+ const pkgRoot = path.resolve(__dirname, '..');
17
+ const packageJsonPath = path.join(pkgRoot, 'package.json');
18
+ if (fs.existsSync(packageJsonPath)) {
19
+ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
20
+ return packageJson.version || 'unknown';
21
+ }
22
+ }
23
+ catch (error) {
24
+ // Fallback
25
+ }
26
+ return 'unknown';
27
+ }
11
28
  // FIRST_EDIT: helper functions to check for free ports
12
29
  async function isPortAvailable(port) {
13
30
  return new Promise((resolve) => {
@@ -244,10 +261,16 @@ const DESIGN_SYSTEM_CONFIGS = {
244
261
  framework: 'vue'
245
262
  },
246
263
  vuetify: {
247
- packages: ['vuetify'],
264
+ packages: ['vuetify', '@mdi/font', '@fontsource/roboto'],
248
265
  name: 'Vuetify',
249
266
  importPath: 'vuetify',
250
- additionalSetup: 'import "vuetify/styles";',
267
+ additionalSetup: `import "vuetify/styles";
268
+ import "@mdi/font/css/materialdesignicons.css";
269
+ // Roboto font required for proper Vuetify typography
270
+ import "@fontsource/roboto/300.css";
271
+ import "@fontsource/roboto/400.css";
272
+ import "@fontsource/roboto/500.css";
273
+ import "@fontsource/roboto/700.css";`,
251
274
  framework: 'vue'
252
275
  },
253
276
  'element-plus': {
@@ -835,6 +858,9 @@ Material UI (MUI) is a React component library implementing Material Design.
835
858
  config.componentFramework = componentFramework; // react, angular, vue, svelte, or web-components
836
859
  config.storybookFramework = storybookFramework; // e.g., @storybook/react-vite, @storybook/angular
837
860
  config.llmProvider = answers.llmProvider || 'claude'; // claude, openai, or gemini
861
+ // Add version tracking for update command
862
+ config._storyUIVersion = getStoryUIVersion();
863
+ config._lastUpdated = new Date().toISOString();
838
864
  // Create configuration file
839
865
  const configContent = `module.exports = ${JSON.stringify(config, null, 2)};`;
840
866
  const configPath = path.join(process.cwd(), 'story-ui.config.js');
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Story UI Update Command
3
+ *
4
+ * Refreshes managed Story UI files (StoryUIPanel.tsx, StoryUIPanel.mdx, index.tsx)
5
+ * while preserving user configuration files (story-ui.config.js, .env, etc.)
6
+ */
7
+ export interface UpdateOptions {
8
+ force?: boolean;
9
+ backup?: boolean;
10
+ dryRun?: boolean;
11
+ verbose?: boolean;
12
+ }
13
+ export interface UpdateResult {
14
+ success: boolean;
15
+ filesUpdated: string[];
16
+ filesBackedUp: string[];
17
+ errors: string[];
18
+ currentVersion: string;
19
+ newVersion: string;
20
+ }
21
+ /**
22
+ * Main update command
23
+ */
24
+ export declare function updateCommand(options?: UpdateOptions): Promise<UpdateResult>;
25
+ /**
26
+ * Show current Story UI installation status
27
+ */
28
+ export declare function statusCommand(): void;
29
+ //# sourceMappingURL=update.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"update.d.ts","sourceRoot":"","sources":["../../cli/update.ts"],"names":[],"mappings":"AASA;;;;;GAKG;AAEH,MAAM,WAAW,aAAa;IAC5B,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED,MAAM,WAAW,YAAY;IAC3B,OAAO,EAAE,OAAO,CAAC;IACjB,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,aAAa,EAAE,MAAM,EAAE,CAAC;IACxB,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,cAAc,EAAE,MAAM,CAAC;IACvB,UAAU,EAAE,MAAM,CAAC;CACpB;AA2SD;;GAEG;AACH,wBAAsB,aAAa,CAAC,OAAO,GAAE,aAAkB,GAAG,OAAO,CAAC,YAAY,CAAC,CAkItF;AAED;;GAEG;AACH,wBAAgB,aAAa,IAAI,IAAI,CA+BpC"}
@@ -0,0 +1,398 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import chalk from 'chalk';
4
+ import { fileURLToPath } from 'url';
5
+ import inquirer from 'inquirer';
6
+ const __filename = fileURLToPath(import.meta.url);
7
+ const __dirname = path.dirname(__filename);
8
+ // Files managed by Story UI that can be safely overwritten
9
+ const MANAGED_FILES = [
10
+ {
11
+ source: 'templates/StoryUI/StoryUIPanel.tsx',
12
+ target: 'src/stories/StoryUI/StoryUIPanel.tsx',
13
+ description: 'Main chat panel component'
14
+ },
15
+ {
16
+ source: 'templates/StoryUI/StoryUIPanel.mdx',
17
+ target: 'src/stories/StoryUI/StoryUIPanel.mdx',
18
+ description: 'Cross-framework MDX wrapper'
19
+ },
20
+ {
21
+ source: 'templates/StoryUI/index.tsx',
22
+ target: 'src/stories/StoryUI/index.tsx',
23
+ description: 'Panel registration'
24
+ }
25
+ ];
26
+ // Files that should NEVER be modified by update
27
+ const USER_CONFIG_FILES = [
28
+ 'story-ui.config.js',
29
+ 'story-ui.config.mjs',
30
+ 'story-ui.config.cjs',
31
+ '.env',
32
+ 'story-ui-considerations.md',
33
+ 'story-ui-docs/'
34
+ ];
35
+ // Directories that should NEVER be touched
36
+ const PROTECTED_DIRECTORIES = [
37
+ 'src/stories/generated/'
38
+ ];
39
+ /**
40
+ * Get the Story UI package version
41
+ */
42
+ function getPackageVersion() {
43
+ try {
44
+ // Try multiple paths to find package.json
45
+ // When running from dist/cli/index.js, we need to go up 2 levels
46
+ const possiblePaths = [
47
+ path.resolve(__dirname, '..', 'package.json'), // From dist/cli
48
+ path.resolve(__dirname, '..', '..', 'package.json'), // From src/cli
49
+ ];
50
+ for (const packageJsonPath of possiblePaths) {
51
+ if (fs.existsSync(packageJsonPath)) {
52
+ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
53
+ if (packageJson.name === '@tpitre/story-ui' && packageJson.version) {
54
+ return packageJson.version;
55
+ }
56
+ }
57
+ }
58
+ }
59
+ catch (error) {
60
+ // Fallback
61
+ }
62
+ return 'unknown';
63
+ }
64
+ /**
65
+ * Detect if Story UI is initialized in the current directory
66
+ */
67
+ function detectStoryUIInstallation() {
68
+ const cwd = process.cwd();
69
+ // Check for Story UI directory
70
+ const possibleStoryUIDirs = [
71
+ path.join(cwd, 'src', 'stories', 'StoryUI'),
72
+ path.join(cwd, 'stories', 'StoryUI')
73
+ ];
74
+ let storyUIDir;
75
+ for (const dir of possibleStoryUIDirs) {
76
+ if (fs.existsSync(dir)) {
77
+ storyUIDir = dir;
78
+ break;
79
+ }
80
+ }
81
+ // Check for config file
82
+ const configFiles = [
83
+ 'story-ui.config.js',
84
+ 'story-ui.config.mjs',
85
+ 'story-ui.config.cjs'
86
+ ];
87
+ let configPath;
88
+ for (const configFile of configFiles) {
89
+ const fullPath = path.join(cwd, configFile);
90
+ if (fs.existsSync(fullPath)) {
91
+ configPath = fullPath;
92
+ break;
93
+ }
94
+ }
95
+ // Try to read installed version from config
96
+ let installedVersion;
97
+ if (configPath) {
98
+ try {
99
+ const configContent = fs.readFileSync(configPath, 'utf-8');
100
+ const versionMatch = configContent.match(/_storyUIVersion:\s*['"]([^'"]+)['"]/);
101
+ if (versionMatch) {
102
+ installedVersion = versionMatch[1];
103
+ }
104
+ }
105
+ catch (error) {
106
+ // Ignore read errors
107
+ }
108
+ }
109
+ return {
110
+ isInstalled: !!(storyUIDir || configPath),
111
+ storyUIDir,
112
+ configPath,
113
+ installedVersion
114
+ };
115
+ }
116
+ /**
117
+ * Create a backup of a file
118
+ */
119
+ function createBackup(filePath) {
120
+ if (!fs.existsSync(filePath)) {
121
+ return null;
122
+ }
123
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
124
+ const backupPath = `${filePath}.backup-${timestamp}`;
125
+ try {
126
+ fs.copyFileSync(filePath, backupPath);
127
+ return backupPath;
128
+ }
129
+ catch (error) {
130
+ return null;
131
+ }
132
+ }
133
+ /**
134
+ * Get the source path for a template file
135
+ */
136
+ function getSourcePath(relativePath) {
137
+ // First try the dist directory (when installed as package)
138
+ const pkgRoot = path.resolve(__dirname, '..');
139
+ const distPath = path.join(pkgRoot, relativePath);
140
+ if (fs.existsSync(distPath)) {
141
+ return distPath;
142
+ }
143
+ // Fall back to project root (when running in development)
144
+ const projectRoot = path.resolve(__dirname, '..', '..');
145
+ const projectPath = path.join(projectRoot, relativePath);
146
+ if (fs.existsSync(projectPath)) {
147
+ return projectPath;
148
+ }
149
+ throw new Error(`Template file not found: ${relativePath}`);
150
+ }
151
+ /**
152
+ * Compare file contents to check if update is needed
153
+ */
154
+ function filesAreDifferent(sourcePath, targetPath) {
155
+ if (!fs.existsSync(targetPath)) {
156
+ return true;
157
+ }
158
+ try {
159
+ const sourceContent = fs.readFileSync(sourcePath, 'utf-8');
160
+ const targetContent = fs.readFileSync(targetPath, 'utf-8');
161
+ return sourceContent !== targetContent;
162
+ }
163
+ catch (error) {
164
+ return true;
165
+ }
166
+ }
167
+ /**
168
+ * Update a single managed file
169
+ */
170
+ function updateManagedFile(sourceRelative, targetRelative, options) {
171
+ const cwd = process.cwd();
172
+ const targetPath = path.join(cwd, targetRelative);
173
+ try {
174
+ const sourcePath = getSourcePath(sourceRelative);
175
+ // Check if update is needed
176
+ if (!filesAreDifferent(sourcePath, targetPath)) {
177
+ if (options.verbose) {
178
+ console.log(chalk.gray(` ⏭️ ${targetRelative} (already up to date)`));
179
+ }
180
+ return { updated: false };
181
+ }
182
+ if (options.dryRun) {
183
+ console.log(chalk.cyan(` 📋 Would update: ${targetRelative}`));
184
+ return { updated: true };
185
+ }
186
+ // Create backup if enabled and file exists
187
+ let backupPath;
188
+ if (options.backup !== false && fs.existsSync(targetPath)) {
189
+ const backup = createBackup(targetPath);
190
+ if (backup) {
191
+ backupPath = backup;
192
+ if (options.verbose) {
193
+ console.log(chalk.gray(` 💾 Backed up: ${path.basename(backup)}`));
194
+ }
195
+ }
196
+ }
197
+ // Ensure target directory exists
198
+ const targetDir = path.dirname(targetPath);
199
+ if (!fs.existsSync(targetDir)) {
200
+ fs.mkdirSync(targetDir, { recursive: true });
201
+ }
202
+ // Copy the new file
203
+ fs.copyFileSync(sourcePath, targetPath);
204
+ console.log(chalk.green(` ✅ Updated: ${targetRelative}`));
205
+ return { updated: true, backupPath };
206
+ }
207
+ catch (error) {
208
+ return { updated: false, error: error.message };
209
+ }
210
+ }
211
+ /**
212
+ * Update the config file with version tracking
213
+ */
214
+ function updateConfigVersion(configPath, version) {
215
+ try {
216
+ let content = fs.readFileSync(configPath, 'utf-8');
217
+ // Check if version tracking already exists
218
+ const hasVersion = /_storyUIVersion/.test(content);
219
+ const hasLastUpdated = /_lastUpdated/.test(content);
220
+ const timestamp = new Date().toISOString();
221
+ if (hasVersion) {
222
+ // Update existing version
223
+ content = content.replace(/_storyUIVersion:\s*['"][^'"]*['"]/, `_storyUIVersion: '${version}'`);
224
+ }
225
+ if (hasLastUpdated) {
226
+ // Update existing timestamp
227
+ content = content.replace(/_lastUpdated:\s*['"][^'"]*['"]/, `_lastUpdated: '${timestamp}'`);
228
+ }
229
+ // If neither exists, add them before the closing brace
230
+ if (!hasVersion && !hasLastUpdated) {
231
+ // Find the last property and add version tracking
232
+ const insertPosition = content.lastIndexOf('}');
233
+ if (insertPosition !== -1) {
234
+ const versionFields = `
235
+ // Story UI version tracking (auto-generated)
236
+ _storyUIVersion: '${version}',
237
+ _lastUpdated: '${timestamp}',
238
+ `;
239
+ // Check if there's a trailing comma needed
240
+ const beforeInsert = content.substring(0, insertPosition).trim();
241
+ const needsComma = beforeInsert.endsWith(',') || beforeInsert.endsWith('{') ? '' : ',';
242
+ content = content.substring(0, insertPosition - 1) +
243
+ needsComma +
244
+ versionFields +
245
+ '};' +
246
+ content.substring(insertPosition + 1);
247
+ }
248
+ }
249
+ fs.writeFileSync(configPath, content);
250
+ return true;
251
+ }
252
+ catch (error) {
253
+ return false;
254
+ }
255
+ }
256
+ /**
257
+ * Main update command
258
+ */
259
+ export async function updateCommand(options = {}) {
260
+ const result = {
261
+ success: false,
262
+ filesUpdated: [],
263
+ filesBackedUp: [],
264
+ errors: [],
265
+ currentVersion: 'unknown',
266
+ newVersion: getPackageVersion()
267
+ };
268
+ console.log(chalk.bold('\n🔄 Story UI Update\n'));
269
+ // Step 1: Detect installation
270
+ const installation = detectStoryUIInstallation();
271
+ if (!installation.isInstalled) {
272
+ console.log(chalk.red('❌ Story UI is not initialized in this directory.'));
273
+ console.log(chalk.yellow(' Run "npx story-ui init" first to set up Story UI.'));
274
+ result.errors.push('Story UI not initialized');
275
+ return result;
276
+ }
277
+ result.currentVersion = installation.installedVersion || 'unknown';
278
+ console.log(chalk.gray(` Current version: ${result.currentVersion}`));
279
+ console.log(chalk.gray(` New version: ${result.newVersion}`));
280
+ // Step 2: Show what will be updated
281
+ console.log(chalk.bold('\n📦 Managed files to update:'));
282
+ const filesToUpdate = [];
283
+ for (const file of MANAGED_FILES) {
284
+ try {
285
+ const sourcePath = getSourcePath(file.source);
286
+ const targetPath = path.join(process.cwd(), file.target);
287
+ const needsUpdate = filesAreDifferent(sourcePath, targetPath);
288
+ if (needsUpdate) {
289
+ filesToUpdate.push(file);
290
+ console.log(chalk.cyan(` • ${file.target}`));
291
+ console.log(chalk.gray(` ${file.description}`));
292
+ }
293
+ else if (options.verbose) {
294
+ console.log(chalk.gray(` ⏭️ ${file.target} (up to date)`));
295
+ }
296
+ }
297
+ catch (error) {
298
+ console.log(chalk.red(` ❌ ${file.target}: ${error.message}`));
299
+ result.errors.push(`${file.target}: ${error.message}`);
300
+ }
301
+ }
302
+ if (filesToUpdate.length === 0) {
303
+ console.log(chalk.green('\n✅ All files are already up to date!'));
304
+ result.success = true;
305
+ return result;
306
+ }
307
+ // Step 3: Confirm update (unless --force or --dry-run)
308
+ if (!options.force && !options.dryRun) {
309
+ console.log(chalk.yellow('\n⚠️ The following will NOT be modified:'));
310
+ console.log(chalk.gray(' • story-ui.config.js (your configuration)'));
311
+ console.log(chalk.gray(' • .env (your API keys)'));
312
+ console.log(chalk.gray(' • story-ui-docs/ (your documentation)'));
313
+ console.log(chalk.gray(' • src/stories/generated/ (your generated stories)'));
314
+ const { confirm } = await inquirer.prompt([{
315
+ type: 'confirm',
316
+ name: 'confirm',
317
+ message: `Update ${filesToUpdate.length} file(s)?`,
318
+ default: true
319
+ }]);
320
+ if (!confirm) {
321
+ console.log(chalk.yellow('\n⏹️ Update cancelled.'));
322
+ return result;
323
+ }
324
+ }
325
+ // Step 4: Perform updates
326
+ if (options.dryRun) {
327
+ console.log(chalk.bold('\n📋 Dry run - no changes made:'));
328
+ }
329
+ else {
330
+ console.log(chalk.bold('\n🔧 Updating files...'));
331
+ }
332
+ for (const file of filesToUpdate) {
333
+ const updateResult = updateManagedFile(file.source, file.target, options);
334
+ if (updateResult.updated) {
335
+ result.filesUpdated.push(file.target);
336
+ }
337
+ if (updateResult.backupPath) {
338
+ result.filesBackedUp.push(updateResult.backupPath);
339
+ }
340
+ if (updateResult.error) {
341
+ result.errors.push(`${file.target}: ${updateResult.error}`);
342
+ }
343
+ }
344
+ // Step 5: Update config version tracking
345
+ if (!options.dryRun && installation.configPath) {
346
+ if (updateConfigVersion(installation.configPath, result.newVersion)) {
347
+ console.log(chalk.gray(`\n Updated version tracking in ${path.basename(installation.configPath)}`));
348
+ }
349
+ }
350
+ // Step 6: Summary
351
+ console.log(chalk.bold('\n📊 Update Summary:'));
352
+ console.log(chalk.green(` ✅ Files updated: ${result.filesUpdated.length}`));
353
+ if (result.filesBackedUp.length > 0) {
354
+ console.log(chalk.gray(` 💾 Backups created: ${result.filesBackedUp.length}`));
355
+ }
356
+ if (result.errors.length > 0) {
357
+ console.log(chalk.red(` ❌ Errors: ${result.errors.length}`));
358
+ for (const error of result.errors) {
359
+ console.log(chalk.red(` • ${error}`));
360
+ }
361
+ }
362
+ result.success = result.errors.length === 0;
363
+ if (result.success && !options.dryRun) {
364
+ console.log(chalk.green('\n✅ Story UI updated successfully!'));
365
+ console.log(chalk.gray(' Restart Storybook to see the changes.'));
366
+ }
367
+ return result;
368
+ }
369
+ /**
370
+ * Show current Story UI installation status
371
+ */
372
+ export function statusCommand() {
373
+ console.log(chalk.bold('\n📊 Story UI Status\n'));
374
+ const installation = detectStoryUIInstallation();
375
+ const packageVersion = getPackageVersion();
376
+ if (!installation.isInstalled) {
377
+ console.log(chalk.red('❌ Story UI is not initialized in this directory.'));
378
+ console.log(chalk.gray(' Run "npx story-ui init" to set up Story UI.'));
379
+ return;
380
+ }
381
+ console.log(chalk.green('✅ Story UI is installed'));
382
+ console.log(chalk.gray(` Package version: ${packageVersion}`));
383
+ console.log(chalk.gray(` Installed version: ${installation.installedVersion || 'unknown'}`));
384
+ if (installation.configPath) {
385
+ console.log(chalk.gray(` Config: ${path.basename(installation.configPath)}`));
386
+ }
387
+ if (installation.storyUIDir) {
388
+ console.log(chalk.gray(` Panel directory: ${installation.storyUIDir}`));
389
+ }
390
+ // Check for updates
391
+ if (installation.installedVersion && installation.installedVersion !== packageVersion) {
392
+ console.log(chalk.yellow(`\n⚡ Update available: ${installation.installedVersion} → ${packageVersion}`));
393
+ console.log(chalk.gray(' Run "npx story-ui update" to update.'));
394
+ }
395
+ else if (installation.installedVersion === packageVersion) {
396
+ console.log(chalk.green('\n✅ Up to date!'));
397
+ }
398
+ }
@@ -263,6 +263,87 @@ app.post('/story-ui/delete', async (req, res) => {
263
263
  res.status(500).json({ error: 'Failed to delete story' });
264
264
  }
265
265
  });
266
+ // Bulk delete stories
267
+ app.post('/story-ui/stories/delete-bulk', async (req, res) => {
268
+ try {
269
+ const { ids } = req.body;
270
+ if (!ids || !Array.isArray(ids) || ids.length === 0) {
271
+ return res.status(400).json({ error: 'ids array is required' });
272
+ }
273
+ console.log(`🗑️ Bulk deleting ${ids.length} stories`);
274
+ const storiesPath = config.generatedStoriesPath;
275
+ const deleted = [];
276
+ const notFound = [];
277
+ const errors = [];
278
+ for (const id of ids) {
279
+ try {
280
+ const fileName = id.endsWith('.stories.tsx') ? id : `${id}.stories.tsx`;
281
+ const filePath = path.join(storiesPath, fileName);
282
+ if (fs.existsSync(filePath)) {
283
+ fs.unlinkSync(filePath);
284
+ deleted.push(id);
285
+ console.log(`✅ Deleted: ${fileName}`);
286
+ }
287
+ else {
288
+ notFound.push(id);
289
+ }
290
+ }
291
+ catch (err) {
292
+ errors.push(id);
293
+ console.error(`❌ Error deleting ${id}:`, err);
294
+ }
295
+ }
296
+ console.log(`📊 Bulk delete complete: ${deleted.length} deleted, ${notFound.length} not found, ${errors.length} errors`);
297
+ return res.json({
298
+ success: true,
299
+ deleted,
300
+ notFound,
301
+ errors,
302
+ summary: {
303
+ requested: ids.length,
304
+ deleted: deleted.length,
305
+ notFound: notFound.length,
306
+ errors: errors.length
307
+ }
308
+ });
309
+ }
310
+ catch (error) {
311
+ console.error('Error in bulk delete:', error);
312
+ return res.status(500).json({ error: 'Failed to bulk delete stories' });
313
+ }
314
+ });
315
+ // Clear all generated stories
316
+ app.delete('/story-ui/stories', async (req, res) => {
317
+ try {
318
+ const storiesPath = config.generatedStoriesPath;
319
+ console.log(`🗑️ Clearing all stories from: ${storiesPath}`);
320
+ if (!fs.existsSync(storiesPath)) {
321
+ return res.json({ success: true, deleted: 0, message: 'No stories directory found' });
322
+ }
323
+ const files = fs.readdirSync(storiesPath);
324
+ const storyFiles = files.filter(file => file.endsWith('.stories.tsx'));
325
+ let deleted = 0;
326
+ for (const file of storyFiles) {
327
+ try {
328
+ fs.unlinkSync(path.join(storiesPath, file));
329
+ deleted++;
330
+ }
331
+ catch (err) {
332
+ console.error(`Error deleting ${file}:`, err);
333
+ }
334
+ }
335
+ console.log(`✅ Cleared ${deleted} stories`);
336
+ return res.json({
337
+ success: true,
338
+ deleted,
339
+ message: `Cleared ${deleted} stories`
340
+ });
341
+ }
342
+ catch (error) {
343
+ console.error('Error clearing stories:', error);
344
+ return res.status(500).json({ error: 'Failed to clear stories' });
345
+ }
346
+ });
266
347
  // MCP Remote HTTP transport routes (for Claude Desktop remote connections)
267
348
  // Provides Streamable HTTP and legacy SSE endpoints for remote MCP access
268
349
  app.use('/mcp-remote', mcpRemoteRouter);
@@ -1 +1 @@
1
- {"version":3,"file":"generateStory.d.ts","sourceRoot":"","sources":["../../../mcp-server/routes/generateStory.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AA2b5C,wBAAsB,uBAAuB,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,2DA6dxE"}
1
+ {"version":3,"file":"generateStory.d.ts","sourceRoot":"","sources":["../../../mcp-server/routes/generateStory.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAgc5C,wBAAsB,uBAAuB,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,2DAkgBxE"}
@@ -18,6 +18,7 @@ import { chatCompletion, generateTitle as llmGenerateTitle, isProviderConfigured
18
18
  import { processImageInputs } from '../../story-generator/imageProcessor.js';
19
19
  import { buildVisionAwarePrompt } from '../../story-generator/visionPrompts.js';
20
20
  import { aggregateValidationErrors, shouldContinueRetrying, buildSelfHealingPrompt, hasNoErrors, getTotalErrorCount, createEmptyErrors, formatErrorsForLog, selectBestAttempt, } from '../../story-generator/selfHealingLoop.js';
21
+ import { validateStoryRuntime, formatRuntimeErrorForHealing, isRuntimeValidationEnabled, } from '../../story-generator/runtimeValidator.js';
21
22
  // Build suggestion using the user's actual discovered components (design-system agnostic)
22
23
  function buildComponentSuggestion(components) {
23
24
  if (!components?.length) {
@@ -730,6 +731,33 @@ export async function generateStoryFromPrompt(req, res) {
730
731
  // Save to history
731
732
  historyManager.addVersion(finalFileName, prompt, fixedFileContents, parentVersionId);
732
733
  logger.log(`Story ${isActualUpdate ? 'updated' : 'written'} to:`, outPath);
734
+ // --- RUNTIME VALIDATION: Check if story loads in Storybook ---
735
+ // This catches errors that static validation cannot detect, like CSF module loader errors
736
+ let runtimeValidationResult = { success: true, storyExists: true };
737
+ let hasRuntimeError = false;
738
+ if (isRuntimeValidationEnabled()) {
739
+ logger.info('🔍 Running runtime validation...');
740
+ try {
741
+ runtimeValidationResult = await validateStoryRuntime(fixedFileContents, aiTitle, config.storyPrefix);
742
+ if (!runtimeValidationResult.success) {
743
+ hasRuntimeError = true;
744
+ hasValidationWarnings = true;
745
+ logger.error(`❌ Runtime validation failed: ${runtimeValidationResult.renderError}`);
746
+ logger.error(` Error type: ${runtimeValidationResult.errorType}`);
747
+ // Format the error for potential future self-healing
748
+ const runtimeErrorMessage = formatRuntimeErrorForHealing(runtimeValidationResult);
749
+ logger.debug('Runtime error details:', runtimeErrorMessage);
750
+ }
751
+ else {
752
+ logger.info('✅ Runtime validation passed - story loads correctly in Storybook');
753
+ }
754
+ }
755
+ catch (runtimeErr) {
756
+ // Don't fail the request if runtime validation itself fails (e.g., Storybook not running)
757
+ logger.warn(`⚠️ Runtime validation could not complete: ${runtimeErr.message}`);
758
+ logger.warn(' Story was saved but runtime status is unknown');
759
+ }
760
+ }
733
761
  // Track URL redirect if this is an update and the title changed
734
762
  if (isActualUpdate && oldTitle && oldStoryUrl) {
735
763
  const newTitleMatch = fixedFileContents.match(/title:\s*["']([^"']+)['"]/);
@@ -759,6 +787,14 @@ export async function generateStoryFromPrompt(req, res) {
759
787
  warnings: [],
760
788
  selfHealingUsed,
761
789
  attempts
790
+ },
791
+ runtimeValidation: {
792
+ enabled: isRuntimeValidationEnabled(),
793
+ success: runtimeValidationResult.success,
794
+ storyExists: runtimeValidationResult.storyExists,
795
+ error: runtimeValidationResult.renderError,
796
+ errorType: runtimeValidationResult.errorType,
797
+ details: runtimeValidationResult.details
762
798
  }
763
799
  });
764
800
  }