@tpitre/story-ui 2.1.5 → 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 +14 -5
- package/templates/mcp-config-claude.json +11 -0
- package/templates/mcp-example.md +76 -0
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fetch from 'node-fetch';
|
|
2
2
|
import { generateStory } from '../../story-generator/generateStory.js';
|
|
3
3
|
import * as crypto from 'crypto';
|
|
4
|
+
import * as path from 'path';
|
|
4
5
|
import { buildClaudePrompt as buildFlexiblePrompt } from '../../story-generator/promptGenerator.js';
|
|
5
6
|
import { loadUserConfig, validateConfig } from '../../story-generator/configLoader.js';
|
|
6
7
|
import { setupProductionGitignore } from '../../story-generator/productionGitignoreManager.js';
|
|
@@ -13,6 +14,8 @@ import { getDocumentation } from '../../story-generator/documentation-sources.js
|
|
|
13
14
|
import { postProcessStory } from '../../story-generator/postProcessStory.js';
|
|
14
15
|
import { validateStory } from '../../story-generator/storyValidator.js';
|
|
15
16
|
import { StoryHistoryManager } from '../../story-generator/storyHistory.js';
|
|
17
|
+
import { logger } from '../../story-generator/logger.js';
|
|
18
|
+
import { UrlRedirectService } from '../../story-generator/urlRedirectService.js';
|
|
16
19
|
const CLAUDE_API_URL = 'https://api.anthropic.com/v1/messages';
|
|
17
20
|
const CLAUDE_MODEL = process.env.CLAUDE_MODEL || 'claude-sonnet-4-20250514';
|
|
18
21
|
// Legacy constants - now using dynamic discovery
|
|
@@ -32,13 +35,13 @@ async function buildClaudePromptWithContext(userPrompt, config, conversation, pr
|
|
|
32
35
|
const discovery = new EnhancedComponentDiscovery(config);
|
|
33
36
|
const components = await discovery.discoverAll();
|
|
34
37
|
// Always start with component discovery as the authoritative source
|
|
35
|
-
|
|
38
|
+
logger.log(`📦 Discovered ${components.length} components from ${config.importPath}`);
|
|
36
39
|
const availableComponents = components.map(c => c.name).join(', ');
|
|
37
|
-
|
|
40
|
+
logger.log(`✅ Available components: ${availableComponents}`);
|
|
38
41
|
// Build base prompt with discovered components (always required)
|
|
39
42
|
let prompt = await buildFlexiblePrompt(userPrompt, config, components);
|
|
40
43
|
// Try to enhance with bundled documentation for usage patterns and design tokens
|
|
41
|
-
|
|
44
|
+
logger.log('📋 Using bundled documentation for enhancement');
|
|
42
45
|
const documentation = getDocumentation(config.importPath);
|
|
43
46
|
if (documentation) {
|
|
44
47
|
const bundledEnhancement = `
|
|
@@ -313,7 +316,7 @@ function fileNameFromTitle(title, hash) {
|
|
|
313
316
|
return `${base}-${hash}.stories.tsx`;
|
|
314
317
|
}
|
|
315
318
|
export async function generateStoryFromPrompt(req, res) {
|
|
316
|
-
const { prompt, fileName, conversation } = req.body;
|
|
319
|
+
const { prompt, fileName, conversation, isUpdate, originalTitle, storyId: providedStoryId } = req.body;
|
|
317
320
|
if (!prompt)
|
|
318
321
|
return res.status(400).json({ error: 'Missing prompt' });
|
|
319
322
|
try {
|
|
@@ -334,17 +337,36 @@ export async function generateStoryFromPrompt(req, res) {
|
|
|
334
337
|
const storyTracker = new StoryTracker(config);
|
|
335
338
|
// Initialize history manager - use the current working directory
|
|
336
339
|
const historyManager = new StoryHistoryManager(process.cwd());
|
|
340
|
+
// Initialize URL redirect service
|
|
341
|
+
// Use the same directory as the stories to ensure consistency
|
|
342
|
+
const redirectDir = isProduction ? process.cwd() : path.dirname(config.generatedStoriesPath);
|
|
343
|
+
const redirectService = new UrlRedirectService(redirectDir);
|
|
337
344
|
// Check if this is an update to an existing story
|
|
338
|
-
|
|
345
|
+
// Use the explicit isUpdate flag from request, or fallback to old logic
|
|
346
|
+
const isActualUpdate = req.body.isUpdate || (fileName && conversation && conversation.length > 2);
|
|
339
347
|
// Get previous code if this is an update
|
|
340
348
|
let previousCode;
|
|
341
349
|
let parentVersionId;
|
|
342
|
-
|
|
350
|
+
let oldTitle;
|
|
351
|
+
let oldStoryUrl;
|
|
352
|
+
if (isActualUpdate && fileName) {
|
|
343
353
|
const currentVersion = historyManager.getCurrentVersion(fileName);
|
|
344
354
|
if (currentVersion) {
|
|
345
355
|
previousCode = currentVersion.code;
|
|
346
356
|
parentVersionId = currentVersion.id;
|
|
347
|
-
|
|
357
|
+
logger.log('🔄 Found previous version for iteration');
|
|
358
|
+
// Extract the old title from previous code
|
|
359
|
+
const titleMatch = previousCode.match(/title:\s*["']([^"']+)['"]/);
|
|
360
|
+
if (titleMatch) {
|
|
361
|
+
oldTitle = titleMatch[1];
|
|
362
|
+
// Remove the prefix to get clean title for URL generation
|
|
363
|
+
const cleanOldTitle = oldTitle.replace(config.storyPrefix || 'Generated/', '');
|
|
364
|
+
// Convert title to Storybook URL format
|
|
365
|
+
oldStoryUrl = `/story/${cleanOldTitle.toLowerCase().replace(/[^a-z0-9]+/g, '-')}--primary`;
|
|
366
|
+
logger.log('📌 Previous title:', oldTitle);
|
|
367
|
+
logger.log('📌 Clean title for URL:', cleanOldTitle);
|
|
368
|
+
logger.log('📌 Previous URL:', oldStoryUrl);
|
|
369
|
+
}
|
|
348
370
|
}
|
|
349
371
|
}
|
|
350
372
|
// --- Start of Validation and Retry Loop ---
|
|
@@ -356,13 +378,13 @@ export async function generateStoryFromPrompt(req, res) {
|
|
|
356
378
|
const messages = [{ role: 'user', content: initialPrompt }];
|
|
357
379
|
while (attempts < maxRetries) {
|
|
358
380
|
attempts++;
|
|
359
|
-
|
|
381
|
+
logger.log(`--- Story Generation Attempt ${attempts} ---`);
|
|
360
382
|
const claudeResponse = await callClaude(messages);
|
|
361
383
|
const extractedCode = extractCodeBlock(claudeResponse);
|
|
362
384
|
if (!extractedCode) {
|
|
363
385
|
aiText = claudeResponse; // Use raw response if no code block
|
|
364
386
|
if (attempts < maxRetries) {
|
|
365
|
-
|
|
387
|
+
logger.log('No code block found, retrying...');
|
|
366
388
|
messages.push({ role: 'assistant', content: aiText });
|
|
367
389
|
messages.push({ role: 'user', content: 'You did not provide a code block. Please provide the complete story in a single `tsx` code block.' });
|
|
368
390
|
continue;
|
|
@@ -377,11 +399,11 @@ export async function generateStoryFromPrompt(req, res) {
|
|
|
377
399
|
}
|
|
378
400
|
validationErrors = validateStory(aiText);
|
|
379
401
|
if (validationErrors.length === 0) {
|
|
380
|
-
|
|
402
|
+
logger.log('✅ Validation successful!');
|
|
381
403
|
break; // Exit loop on success
|
|
382
404
|
}
|
|
383
|
-
|
|
384
|
-
validationErrors.forEach(err =>
|
|
405
|
+
logger.log(`❌ Validation failed with ${validationErrors.length} errors:`);
|
|
406
|
+
validationErrors.forEach(err => logger.log(` - Line ${err.line}: ${err.message}`));
|
|
385
407
|
if (attempts < maxRetries) {
|
|
386
408
|
const errorFeedback = validationErrors
|
|
387
409
|
.map(err => `- Line ${err.line}: ${err.message}`)
|
|
@@ -397,7 +419,7 @@ export async function generateStoryFromPrompt(req, res) {
|
|
|
397
419
|
// For now, we'll proceed with the last attempt and let the user see the result
|
|
398
420
|
}
|
|
399
421
|
// --- End of Validation and Retry Loop ---
|
|
400
|
-
|
|
422
|
+
logger.log('Claude final response:', aiText);
|
|
401
423
|
// Create enhanced component discovery for validation
|
|
402
424
|
const discovery = new EnhancedComponentDiscovery(config);
|
|
403
425
|
await discovery.discoverAll();
|
|
@@ -416,7 +438,7 @@ export async function generateStoryFromPrompt(req, res) {
|
|
|
416
438
|
const validationResult = extractAndValidateCodeBlock(aiText, config);
|
|
417
439
|
let fileContents;
|
|
418
440
|
let hasValidationWarnings = false;
|
|
419
|
-
|
|
441
|
+
logger.log('Validation result:', {
|
|
420
442
|
isValid: validationResult.isValid,
|
|
421
443
|
errors: validationResult.errors,
|
|
422
444
|
warnings: validationResult.warnings,
|
|
@@ -425,7 +447,7 @@ export async function generateStoryFromPrompt(req, res) {
|
|
|
425
447
|
if (!validationResult.isValid && !validationResult.fixedCode) {
|
|
426
448
|
console.error('Generated code validation failed:', validationResult.errors);
|
|
427
449
|
// Create fallback story only if we can't fix the code
|
|
428
|
-
|
|
450
|
+
logger.log('Creating fallback story due to validation failure');
|
|
429
451
|
fileContents = createFallbackStory(prompt, config);
|
|
430
452
|
hasValidationWarnings = true;
|
|
431
453
|
}
|
|
@@ -434,7 +456,7 @@ export async function generateStoryFromPrompt(req, res) {
|
|
|
434
456
|
if (validationResult.fixedCode) {
|
|
435
457
|
fileContents = validationResult.fixedCode;
|
|
436
458
|
hasValidationWarnings = true;
|
|
437
|
-
|
|
459
|
+
logger.log('Using auto-fixed code');
|
|
438
460
|
}
|
|
439
461
|
else {
|
|
440
462
|
// Extract the validated code
|
|
@@ -455,7 +477,7 @@ export async function generateStoryFromPrompt(req, res) {
|
|
|
455
477
|
}
|
|
456
478
|
if (validationResult.warnings && validationResult.warnings.length > 0) {
|
|
457
479
|
hasValidationWarnings = true;
|
|
458
|
-
|
|
480
|
+
logger.log('Validation warnings:', validationResult.warnings);
|
|
459
481
|
}
|
|
460
482
|
}
|
|
461
483
|
if (!fileContents) {
|
|
@@ -470,12 +492,18 @@ export async function generateStoryFromPrompt(req, res) {
|
|
|
470
492
|
let fixedFileContents = postProcessStory(fileContents, config.importPath);
|
|
471
493
|
// Generate title based on conversation context
|
|
472
494
|
let aiTitle;
|
|
473
|
-
if (
|
|
474
|
-
// For updates,
|
|
495
|
+
if (isActualUpdate && originalTitle) {
|
|
496
|
+
// For updates, preserve the original title
|
|
497
|
+
aiTitle = originalTitle;
|
|
498
|
+
logger.log('📝 Preserving original title for update:', aiTitle);
|
|
499
|
+
}
|
|
500
|
+
else if (isActualUpdate) {
|
|
501
|
+
// For updates without original title, try to keep the original title or modify it slightly
|
|
475
502
|
const originalPrompt = conversation.find((msg) => msg.role === 'user')?.content || prompt;
|
|
476
503
|
aiTitle = await getClaudeTitle(originalPrompt);
|
|
477
504
|
}
|
|
478
505
|
else {
|
|
506
|
+
// For new stories, generate a new title
|
|
479
507
|
aiTitle = await getClaudeTitle(prompt);
|
|
480
508
|
}
|
|
481
509
|
if (!aiTitle || aiTitle.length < 2) {
|
|
@@ -486,20 +514,26 @@ export async function generateStoryFromPrompt(req, res) {
|
|
|
486
514
|
const prettyPrompt = escapeTitleForTS(aiTitle);
|
|
487
515
|
// Fix title with storyPrefix - handle both single-line and multi-line formats
|
|
488
516
|
fixedFileContents = fixedFileContents.replace(/(const\s+meta\s*=\s*\{[\s\S]*?title:\s*["'])([^"']+)(["'])/, (match, p1, oldTitle, p3) => {
|
|
489
|
-
|
|
490
|
-
|
|
517
|
+
// Check if the title already has the prefix to avoid double prefixing
|
|
518
|
+
const titleToUse = prettyPrompt.startsWith(config.storyPrefix)
|
|
519
|
+
? prettyPrompt
|
|
520
|
+
: config.storyPrefix + prettyPrompt;
|
|
521
|
+
return p1 + titleToUse + p3;
|
|
491
522
|
});
|
|
492
523
|
// Fallback: export default { title: "..." } format
|
|
493
524
|
if (!fixedFileContents.includes(config.storyPrefix)) {
|
|
494
525
|
fixedFileContents = fixedFileContents.replace(/(export\s+default\s*\{[\s\S]*?title:\s*["'])([^"']+)(["'])/, (match, p1, oldTitle, p3) => {
|
|
495
|
-
|
|
496
|
-
|
|
526
|
+
// Check if the title already has the prefix to avoid double prefixing
|
|
527
|
+
const titleToUse = prettyPrompt.startsWith(config.storyPrefix)
|
|
528
|
+
? prettyPrompt
|
|
529
|
+
: config.storyPrefix + prettyPrompt;
|
|
530
|
+
return p1 + titleToUse + p3;
|
|
497
531
|
});
|
|
498
532
|
}
|
|
499
533
|
// Check if this is an update to an existing story
|
|
500
534
|
// ONLY consider it an update if we're in the same conversation context
|
|
501
535
|
let existingStory = null;
|
|
502
|
-
if (
|
|
536
|
+
if (isActualUpdate && fileName) {
|
|
503
537
|
// When updating within a conversation, look for the story by fileName
|
|
504
538
|
existingStory = storyTracker.findByTitle(aiTitle);
|
|
505
539
|
if (existingStory && existingStory.fileName !== fileName) {
|
|
@@ -510,15 +544,23 @@ export async function generateStoryFromPrompt(req, res) {
|
|
|
510
544
|
// Remove the automatic "find by prompt" logic that was preventing duplicates
|
|
511
545
|
// Generate unique ID and filename
|
|
512
546
|
let hash, finalFileName, storyId;
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
// For conversation-based updates, use existing fileName and ID
|
|
547
|
+
if (isActualUpdate && (fileName || providedStoryId)) {
|
|
548
|
+
// For updates, preserve the existing fileName and ID
|
|
516
549
|
finalFileName = fileName;
|
|
517
|
-
//
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
550
|
+
// Use provided storyId or extract from fileName
|
|
551
|
+
if (providedStoryId) {
|
|
552
|
+
storyId = providedStoryId;
|
|
553
|
+
// Extract hash from storyId
|
|
554
|
+
const hashMatch = providedStoryId.match(/^story-([a-f0-9]{8})$/);
|
|
555
|
+
hash = hashMatch ? hashMatch[1] : crypto.createHash('sha1').update(prompt).digest('hex').slice(0, 8);
|
|
556
|
+
}
|
|
557
|
+
else {
|
|
558
|
+
// Extract hash from existing fileName if possible
|
|
559
|
+
const hashMatch = fileName.match(/-([a-f0-9]{8})(?:\.stories\.tsx)?$/);
|
|
560
|
+
hash = hashMatch ? hashMatch[1] : crypto.createHash('sha1').update(prompt).digest('hex').slice(0, 8);
|
|
561
|
+
storyId = `story-${hash}`;
|
|
562
|
+
}
|
|
563
|
+
logger.log('📌 Preserving story identity for update:', { storyId, fileName: finalFileName });
|
|
522
564
|
}
|
|
523
565
|
else {
|
|
524
566
|
// For new stories, ALWAYS generate new IDs with timestamp to ensure uniqueness
|
|
@@ -526,18 +568,18 @@ export async function generateStoryFromPrompt(req, res) {
|
|
|
526
568
|
hash = crypto.createHash('sha1').update(prompt + timestamp).digest('hex').slice(0, 8);
|
|
527
569
|
finalFileName = fileName || fileNameFromTitle(aiTitle, hash);
|
|
528
570
|
storyId = `story-${hash}`;
|
|
529
|
-
|
|
571
|
+
logger.log('🆕 Creating new story:', { storyId, fileName: finalFileName });
|
|
530
572
|
}
|
|
531
573
|
if (isProduction) {
|
|
532
574
|
// Production: Store in memory
|
|
533
575
|
const generatedStory = {
|
|
534
576
|
id: storyId,
|
|
535
577
|
title: aiTitle,
|
|
536
|
-
description:
|
|
578
|
+
description: isActualUpdate ? `Updated: ${prompt}` : prompt,
|
|
537
579
|
content: fixedFileContents,
|
|
538
|
-
createdAt:
|
|
580
|
+
createdAt: isActualUpdate ? (new Date()) : new Date(),
|
|
539
581
|
lastAccessed: new Date(),
|
|
540
|
-
prompt:
|
|
582
|
+
prompt: isActualUpdate ? conversation.map((msg) => `${msg.role}: ${msg.content}`).join('\n\n') : prompt,
|
|
541
583
|
components: extractComponentsFromContent(fixedFileContents)
|
|
542
584
|
};
|
|
543
585
|
storyService.storeStory(generatedStory);
|
|
@@ -554,7 +596,23 @@ export async function generateStoryFromPrompt(req, res) {
|
|
|
554
596
|
storyTracker.registerStory(mapping);
|
|
555
597
|
// Save to history
|
|
556
598
|
historyManager.addVersion(finalFileName, prompt, fixedFileContents, parentVersionId);
|
|
557
|
-
|
|
599
|
+
logger.log(`Story ${isActualUpdate ? 'updated' : 'stored'} in memory: ${storyId}`);
|
|
600
|
+
// Track URL redirect if this is an update and the title changed
|
|
601
|
+
if (isActualUpdate && oldTitle && oldStoryUrl) {
|
|
602
|
+
// Extract the new title from the fixed content
|
|
603
|
+
const newTitleMatch = fixedFileContents.match(/title:\s*["']([^"']+)['"]/);
|
|
604
|
+
if (newTitleMatch) {
|
|
605
|
+
const newTitle = newTitleMatch[1];
|
|
606
|
+
// Remove the prefix to get clean title for URL
|
|
607
|
+
const cleanNewTitle = newTitle.replace(config.storyPrefix, '');
|
|
608
|
+
const cleanOldTitle = oldTitle.replace(config.storyPrefix, '');
|
|
609
|
+
const newStoryUrl = `/story/${cleanNewTitle.toLowerCase().replace(/[^a-z0-9]+/g, '-')}--primary`;
|
|
610
|
+
if (oldStoryUrl !== newStoryUrl) {
|
|
611
|
+
redirectService.addRedirect(oldStoryUrl, newStoryUrl, cleanOldTitle, cleanNewTitle, storyId);
|
|
612
|
+
logger.log(`🔀 Added redirect: ${oldStoryUrl} → ${newStoryUrl}`);
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
}
|
|
558
616
|
res.json({
|
|
559
617
|
success: true,
|
|
560
618
|
fileName: finalFileName,
|
|
@@ -563,7 +621,7 @@ export async function generateStoryFromPrompt(req, res) {
|
|
|
563
621
|
story: fileContents,
|
|
564
622
|
environment: 'production',
|
|
565
623
|
storage: 'in-memory',
|
|
566
|
-
isUpdate:
|
|
624
|
+
isUpdate: isActualUpdate,
|
|
567
625
|
validation: {
|
|
568
626
|
hasWarnings: hasValidationWarnings,
|
|
569
627
|
errors: validationResult?.errors || [],
|
|
@@ -591,16 +649,33 @@ export async function generateStoryFromPrompt(req, res) {
|
|
|
591
649
|
storyTracker.registerStory(mapping);
|
|
592
650
|
// Save to history
|
|
593
651
|
historyManager.addVersion(finalFileName, prompt, fixedFileContents, parentVersionId);
|
|
594
|
-
|
|
652
|
+
logger.log(`Story ${isActualUpdate ? 'updated' : 'written'} to:`, outPath);
|
|
653
|
+
// Track URL redirect if this is an update and the title changed
|
|
654
|
+
if (isActualUpdate && oldTitle && oldStoryUrl) {
|
|
655
|
+
// Extract the new title from the fixed content
|
|
656
|
+
const newTitleMatch = fixedFileContents.match(/title:\s*["']([^"']+)['"]/);
|
|
657
|
+
if (newTitleMatch) {
|
|
658
|
+
const newTitle = newTitleMatch[1];
|
|
659
|
+
// Remove the prefix to get clean title for URL
|
|
660
|
+
const cleanNewTitle = newTitle.replace(config.storyPrefix, '');
|
|
661
|
+
const cleanOldTitle = oldTitle.replace(config.storyPrefix, '');
|
|
662
|
+
const newStoryUrl = `/story/${cleanNewTitle.toLowerCase().replace(/[^a-z0-9]+/g, '-')}--primary`;
|
|
663
|
+
if (oldStoryUrl !== newStoryUrl) {
|
|
664
|
+
redirectService.addRedirect(oldStoryUrl, newStoryUrl, cleanOldTitle, cleanNewTitle, storyId);
|
|
665
|
+
logger.log(`🔀 Added redirect: ${oldStoryUrl} → ${newStoryUrl}`);
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
}
|
|
595
669
|
res.json({
|
|
596
670
|
success: true,
|
|
597
671
|
fileName: finalFileName,
|
|
672
|
+
storyId,
|
|
598
673
|
outPath,
|
|
599
674
|
title: aiTitle,
|
|
600
675
|
story: fileContents,
|
|
601
676
|
environment: 'development',
|
|
602
677
|
storage: 'file-system',
|
|
603
|
-
isUpdate:
|
|
678
|
+
isUpdate: isActualUpdate,
|
|
604
679
|
validation: {
|
|
605
680
|
hasWarnings: hasValidationWarnings,
|
|
606
681
|
errors: validationResult?.errors || [],
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { getInMemoryStoryService } from '../../story-generator/inMemoryStoryService.js';
|
|
4
|
+
import { loadUserConfig } from '../../story-generator/configLoader.js';
|
|
5
|
+
import { setupProductionGitignore } from '../../story-generator/productionGitignoreManager.js';
|
|
6
|
+
/**
|
|
7
|
+
* Get all stories from both memory and file system
|
|
8
|
+
*/
|
|
9
|
+
export function getAllStories(req, res) {
|
|
10
|
+
try {
|
|
11
|
+
const config = loadUserConfig();
|
|
12
|
+
const gitignoreManager = setupProductionGitignore(config);
|
|
13
|
+
const storyService = getInMemoryStoryService(config);
|
|
14
|
+
let allStories = [];
|
|
15
|
+
// Get stories from memory
|
|
16
|
+
const memoryStories = storyService.getStoryMetadata();
|
|
17
|
+
allStories = [...memoryStories];
|
|
18
|
+
// In development mode, also check file system
|
|
19
|
+
if (!gitignoreManager.isProductionMode() && config.generatedStoriesPath) {
|
|
20
|
+
try {
|
|
21
|
+
if (fs.existsSync(config.generatedStoriesPath)) {
|
|
22
|
+
const files = fs.readdirSync(config.generatedStoriesPath);
|
|
23
|
+
const fileStories = files
|
|
24
|
+
.filter(file => file.endsWith('.stories.tsx'))
|
|
25
|
+
.map(file => {
|
|
26
|
+
const fileName = file;
|
|
27
|
+
const hash = file.match(/-([a-f0-9]{8})\.stories\.tsx$/)?.[1] || '';
|
|
28
|
+
const storyId = hash ? `story-${hash}` : file.replace('.stories.tsx', '');
|
|
29
|
+
// Try to read the file to get the title
|
|
30
|
+
let title = file.replace('.stories.tsx', '').replace(/-/g, ' ');
|
|
31
|
+
try {
|
|
32
|
+
const filePath = path.join(config.generatedStoriesPath, file);
|
|
33
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
34
|
+
const titleMatch = content.match(/title:\s*['"]([^'"]+)['"]/);
|
|
35
|
+
if (titleMatch) {
|
|
36
|
+
title = titleMatch[1].replace('Generated/', '');
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
catch (e) {
|
|
40
|
+
// Use filename as fallback
|
|
41
|
+
}
|
|
42
|
+
return {
|
|
43
|
+
id: storyId,
|
|
44
|
+
fileName,
|
|
45
|
+
title,
|
|
46
|
+
createdAt: fs.statSync(path.join(config.generatedStoriesPath, file)).birthtime,
|
|
47
|
+
storage: 'file-system'
|
|
48
|
+
};
|
|
49
|
+
});
|
|
50
|
+
// Merge, avoiding duplicates
|
|
51
|
+
const memoryIds = new Set(memoryStories.map(s => s.id));
|
|
52
|
+
const uniqueFileStories = fileStories.filter(s => !memoryIds.has(s.id));
|
|
53
|
+
allStories = [...allStories, ...uniqueFileStories];
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
console.error('Error reading file system stories:', error);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
res.json(allStories);
|
|
61
|
+
}
|
|
62
|
+
catch (error) {
|
|
63
|
+
console.error('Error in getAllStories:', error);
|
|
64
|
+
res.status(500).json({ error: 'Failed to retrieve stories' });
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Get a specific story by ID from memory or file system
|
|
69
|
+
*/
|
|
70
|
+
export function getStoryById(req, res) {
|
|
71
|
+
try {
|
|
72
|
+
const { id } = req.params;
|
|
73
|
+
const config = loadUserConfig();
|
|
74
|
+
const gitignoreManager = setupProductionGitignore(config);
|
|
75
|
+
const storyService = getInMemoryStoryService(config);
|
|
76
|
+
// First try memory
|
|
77
|
+
const memoryStory = storyService.getStory(id);
|
|
78
|
+
if (memoryStory) {
|
|
79
|
+
return res.json(memoryStory);
|
|
80
|
+
}
|
|
81
|
+
// In development, try file system
|
|
82
|
+
if (!gitignoreManager.isProductionMode() && config.generatedStoriesPath) {
|
|
83
|
+
// Try to find by story ID pattern
|
|
84
|
+
const files = fs.readdirSync(config.generatedStoriesPath);
|
|
85
|
+
// Extract hash from story ID (e.g., "story-abc123" -> "abc123")
|
|
86
|
+
const hashMatch = id.match(/^story-([a-f0-9]{8})$/);
|
|
87
|
+
const hash = hashMatch ? hashMatch[1] : null;
|
|
88
|
+
// Find file by hash or exact match
|
|
89
|
+
const matchingFile = files.find(file => {
|
|
90
|
+
if (hash && file.includes(`-${hash}.stories.tsx`))
|
|
91
|
+
return true;
|
|
92
|
+
if (file === `${id}.stories.tsx`)
|
|
93
|
+
return true;
|
|
94
|
+
if (file === id)
|
|
95
|
+
return true;
|
|
96
|
+
return false;
|
|
97
|
+
});
|
|
98
|
+
if (matchingFile) {
|
|
99
|
+
const filePath = path.join(config.generatedStoriesPath, matchingFile);
|
|
100
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
101
|
+
const stats = fs.statSync(filePath);
|
|
102
|
+
// Extract title from content
|
|
103
|
+
let title = matchingFile.replace('.stories.tsx', '').replace(/-/g, ' ');
|
|
104
|
+
const titleMatch = content.match(/title:\s*['"]([^'"]+)['"]/);
|
|
105
|
+
if (titleMatch) {
|
|
106
|
+
title = titleMatch[1].replace('Generated/', '');
|
|
107
|
+
}
|
|
108
|
+
return res.json({
|
|
109
|
+
id,
|
|
110
|
+
fileName: matchingFile,
|
|
111
|
+
title,
|
|
112
|
+
content,
|
|
113
|
+
createdAt: stats.birthtime,
|
|
114
|
+
storage: 'file-system'
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
res.status(404).json({ error: 'Story not found' });
|
|
119
|
+
}
|
|
120
|
+
catch (error) {
|
|
121
|
+
console.error('Error in getStoryById:', error);
|
|
122
|
+
res.status(500).json({ error: 'Failed to retrieve story' });
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Get story content by ID
|
|
127
|
+
*/
|
|
128
|
+
export function getStoryContent(req, res) {
|
|
129
|
+
try {
|
|
130
|
+
const { id } = req.params;
|
|
131
|
+
const config = loadUserConfig();
|
|
132
|
+
const gitignoreManager = setupProductionGitignore(config);
|
|
133
|
+
const storyService = getInMemoryStoryService(config);
|
|
134
|
+
// First try memory
|
|
135
|
+
const content = storyService.getStoryContent(id);
|
|
136
|
+
if (content) {
|
|
137
|
+
res.setHeader('Content-Type', 'text/plain');
|
|
138
|
+
return res.send(content);
|
|
139
|
+
}
|
|
140
|
+
// In development, try file system
|
|
141
|
+
if (!gitignoreManager.isProductionMode() && config.generatedStoriesPath) {
|
|
142
|
+
const files = fs.readdirSync(config.generatedStoriesPath);
|
|
143
|
+
// Extract hash from story ID
|
|
144
|
+
const hashMatch = id.match(/^story-([a-f0-9]{8})$/);
|
|
145
|
+
const hash = hashMatch ? hashMatch[1] : null;
|
|
146
|
+
const matchingFile = files.find(file => {
|
|
147
|
+
if (hash && file.includes(`-${hash}.stories.tsx`))
|
|
148
|
+
return true;
|
|
149
|
+
if (file === `${id}.stories.tsx`)
|
|
150
|
+
return true;
|
|
151
|
+
if (file === id)
|
|
152
|
+
return true;
|
|
153
|
+
return false;
|
|
154
|
+
});
|
|
155
|
+
if (matchingFile) {
|
|
156
|
+
const filePath = path.join(config.generatedStoriesPath, matchingFile);
|
|
157
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
158
|
+
res.setHeader('Content-Type', 'text/plain');
|
|
159
|
+
return res.send(content);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
res.status(404).json({ error: 'Story content not found' });
|
|
163
|
+
}
|
|
164
|
+
catch (error) {
|
|
165
|
+
console.error('Error in getStoryContent:', error);
|
|
166
|
+
res.status(500).json({ error: 'Failed to retrieve story content' });
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Delete a story by ID
|
|
171
|
+
*/
|
|
172
|
+
export function deleteStory(req, res) {
|
|
173
|
+
try {
|
|
174
|
+
const { id } = req.params;
|
|
175
|
+
const config = loadUserConfig();
|
|
176
|
+
const gitignoreManager = setupProductionGitignore(config);
|
|
177
|
+
const storyService = getInMemoryStoryService(config);
|
|
178
|
+
console.log(`🗑️ Attempting to delete story: ${id}`);
|
|
179
|
+
// First try memory
|
|
180
|
+
const memoryDeleted = storyService.deleteStory(id);
|
|
181
|
+
if (memoryDeleted) {
|
|
182
|
+
console.log(`✅ Deleted story from memory: ${id}`);
|
|
183
|
+
return res.json({ success: true, message: 'Story deleted from memory' });
|
|
184
|
+
}
|
|
185
|
+
// In development, try file system
|
|
186
|
+
if (!gitignoreManager.isProductionMode() && config.generatedStoriesPath) {
|
|
187
|
+
const files = fs.readdirSync(config.generatedStoriesPath);
|
|
188
|
+
// Extract hash from story ID
|
|
189
|
+
const hashMatch = id.match(/^story-([a-f0-9]{8})$/);
|
|
190
|
+
const hash = hashMatch ? hashMatch[1] : null;
|
|
191
|
+
const matchingFile = files.find(file => {
|
|
192
|
+
if (hash && file.includes(`-${hash}.stories.tsx`))
|
|
193
|
+
return true;
|
|
194
|
+
if (file === `${id}.stories.tsx`)
|
|
195
|
+
return true;
|
|
196
|
+
if (file === id)
|
|
197
|
+
return true;
|
|
198
|
+
return false;
|
|
199
|
+
});
|
|
200
|
+
if (matchingFile) {
|
|
201
|
+
const filePath = path.join(config.generatedStoriesPath, matchingFile);
|
|
202
|
+
fs.unlinkSync(filePath);
|
|
203
|
+
console.log(`✅ Deleted story file: ${filePath}`);
|
|
204
|
+
return res.json({ success: true, message: 'Story deleted from file system' });
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
console.log(`❌ Story not found: ${id}`);
|
|
208
|
+
res.status(404).json({ error: 'Story not found' });
|
|
209
|
+
}
|
|
210
|
+
catch (error) {
|
|
211
|
+
console.error('Error in deleteStory:', error);
|
|
212
|
+
res.status(500).json({ error: 'Failed to delete story' });
|
|
213
|
+
}
|
|
214
|
+
}
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { getInMemoryStoryService } from '../../story-generator/inMemoryStoryService.js';
|
|
2
|
-
import {
|
|
2
|
+
import { loadUserConfig } from '../../story-generator/configLoader.js';
|
|
3
3
|
/**
|
|
4
4
|
* Get all stories metadata
|
|
5
5
|
*/
|
|
6
6
|
export function getStoriesMetadata(req, res) {
|
|
7
7
|
try {
|
|
8
|
-
const
|
|
8
|
+
const config = loadUserConfig();
|
|
9
|
+
const storyService = getInMemoryStoryService(config);
|
|
9
10
|
const metadata = storyService.getStoryMetadata();
|
|
10
11
|
res.json({
|
|
11
12
|
success: true,
|
|
@@ -26,7 +27,8 @@ export function getStoriesMetadata(req, res) {
|
|
|
26
27
|
export function getStoryById(req, res) {
|
|
27
28
|
try {
|
|
28
29
|
const { id } = req.params;
|
|
29
|
-
const
|
|
30
|
+
const config = loadUserConfig();
|
|
31
|
+
const storyService = getInMemoryStoryService(config);
|
|
30
32
|
const story = storyService.getStory(id);
|
|
31
33
|
if (!story) {
|
|
32
34
|
return res.status(404).json({
|
|
@@ -52,7 +54,8 @@ export function getStoryById(req, res) {
|
|
|
52
54
|
export function getStoryContent(req, res) {
|
|
53
55
|
try {
|
|
54
56
|
const { id } = req.params;
|
|
55
|
-
const
|
|
57
|
+
const config = loadUserConfig();
|
|
58
|
+
const storyService = getInMemoryStoryService(config);
|
|
56
59
|
const content = storyService.getStoryContent(id);
|
|
57
60
|
if (!content) {
|
|
58
61
|
return res.status(404).json({
|
|
@@ -77,7 +80,8 @@ export function getStoryContent(req, res) {
|
|
|
77
80
|
export function deleteStory(req, res) {
|
|
78
81
|
try {
|
|
79
82
|
const { id } = req.params;
|
|
80
|
-
const
|
|
83
|
+
const config = loadUserConfig();
|
|
84
|
+
const storyService = getInMemoryStoryService(config);
|
|
81
85
|
const deleted = storyService.deleteStory(id);
|
|
82
86
|
if (!deleted) {
|
|
83
87
|
return res.status(404).json({
|
|
@@ -102,7 +106,8 @@ export function deleteStory(req, res) {
|
|
|
102
106
|
*/
|
|
103
107
|
export function clearAllStories(req, res) {
|
|
104
108
|
try {
|
|
105
|
-
const
|
|
109
|
+
const config = loadUserConfig();
|
|
110
|
+
const storyService = getInMemoryStoryService(config);
|
|
106
111
|
storyService.clearAllStories();
|
|
107
112
|
res.json({
|
|
108
113
|
success: true,
|
|
@@ -121,7 +126,8 @@ export function clearAllStories(req, res) {
|
|
|
121
126
|
*/
|
|
122
127
|
export function getMemoryStats(req, res) {
|
|
123
128
|
try {
|
|
124
|
-
const
|
|
129
|
+
const config = loadUserConfig();
|
|
130
|
+
const storyService = getInMemoryStoryService(config);
|
|
125
131
|
const stats = storyService.getMemoryStats();
|
|
126
132
|
res.json({
|
|
127
133
|
success: true,
|