popeye-cli 1.5.0 → 1.6.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.
Files changed (161) hide show
  1. package/CHANGELOG.md +54 -0
  2. package/README.md +50 -8
  3. package/dist/cli/commands/create.d.ts.map +1 -1
  4. package/dist/cli/commands/create.js +54 -4
  5. package/dist/cli/commands/create.js.map +1 -1
  6. package/dist/cli/interactive.d.ts +29 -0
  7. package/dist/cli/interactive.d.ts.map +1 -1
  8. package/dist/cli/interactive.js +90 -7
  9. package/dist/cli/interactive.js.map +1 -1
  10. package/dist/generators/all.d.ts +4 -1
  11. package/dist/generators/all.d.ts.map +1 -1
  12. package/dist/generators/all.js +36 -316
  13. package/dist/generators/all.js.map +1 -1
  14. package/dist/generators/doc-parser.d.ts +18 -3
  15. package/dist/generators/doc-parser.d.ts.map +1 -1
  16. package/dist/generators/doc-parser.js +81 -10
  17. package/dist/generators/doc-parser.js.map +1 -1
  18. package/dist/generators/frontend-design-analyzer.d.ts +30 -0
  19. package/dist/generators/frontend-design-analyzer.d.ts.map +1 -0
  20. package/dist/generators/frontend-design-analyzer.js +208 -0
  21. package/dist/generators/frontend-design-analyzer.js.map +1 -0
  22. package/dist/generators/shared-packages.d.ts +45 -0
  23. package/dist/generators/shared-packages.d.ts.map +1 -0
  24. package/dist/generators/shared-packages.js +456 -0
  25. package/dist/generators/shared-packages.js.map +1 -0
  26. package/dist/generators/templates/index.d.ts +4 -0
  27. package/dist/generators/templates/index.d.ts.map +1 -1
  28. package/dist/generators/templates/index.js +4 -0
  29. package/dist/generators/templates/index.js.map +1 -1
  30. package/dist/generators/templates/website-components.d.ts.map +1 -1
  31. package/dist/generators/templates/website-components.js +36 -11
  32. package/dist/generators/templates/website-components.js.map +1 -1
  33. package/dist/generators/templates/website-config.d.ts +15 -1
  34. package/dist/generators/templates/website-config.d.ts.map +1 -1
  35. package/dist/generators/templates/website-config.js +155 -13
  36. package/dist/generators/templates/website-config.js.map +1 -1
  37. package/dist/generators/templates/website-landing.d.ts +24 -0
  38. package/dist/generators/templates/website-landing.d.ts.map +1 -0
  39. package/dist/generators/templates/website-landing.js +276 -0
  40. package/dist/generators/templates/website-landing.js.map +1 -0
  41. package/dist/generators/templates/website-layout.d.ts +42 -0
  42. package/dist/generators/templates/website-layout.d.ts.map +1 -0
  43. package/dist/generators/templates/website-layout.js +408 -0
  44. package/dist/generators/templates/website-layout.js.map +1 -0
  45. package/dist/generators/templates/website-pricing.d.ts +11 -0
  46. package/dist/generators/templates/website-pricing.d.ts.map +1 -0
  47. package/dist/generators/templates/website-pricing.js +313 -0
  48. package/dist/generators/templates/website-pricing.js.map +1 -0
  49. package/dist/generators/templates/website-sections.d.ts +102 -0
  50. package/dist/generators/templates/website-sections.d.ts.map +1 -0
  51. package/dist/generators/templates/website-sections.js +444 -0
  52. package/dist/generators/templates/website-sections.js.map +1 -0
  53. package/dist/generators/templates/website.d.ts +10 -50
  54. package/dist/generators/templates/website.d.ts.map +1 -1
  55. package/dist/generators/templates/website.js +12 -788
  56. package/dist/generators/templates/website.js.map +1 -1
  57. package/dist/generators/website-content-scanner.d.ts +37 -0
  58. package/dist/generators/website-content-scanner.d.ts.map +1 -0
  59. package/dist/generators/website-content-scanner.js +165 -0
  60. package/dist/generators/website-content-scanner.js.map +1 -0
  61. package/dist/generators/website-context.d.ts +38 -2
  62. package/dist/generators/website-context.d.ts.map +1 -1
  63. package/dist/generators/website-context.js +179 -19
  64. package/dist/generators/website-context.js.map +1 -1
  65. package/dist/generators/website-debug.d.ts +68 -0
  66. package/dist/generators/website-debug.d.ts.map +1 -0
  67. package/dist/generators/website-debug.js +93 -0
  68. package/dist/generators/website-debug.js.map +1 -0
  69. package/dist/generators/website.d.ts +2 -0
  70. package/dist/generators/website.d.ts.map +1 -1
  71. package/dist/generators/website.js +66 -4
  72. package/dist/generators/website.js.map +1 -1
  73. package/dist/generators/workspace-root.d.ts +27 -0
  74. package/dist/generators/workspace-root.d.ts.map +1 -0
  75. package/dist/generators/workspace-root.js +100 -0
  76. package/dist/generators/workspace-root.js.map +1 -0
  77. package/dist/state/index.d.ts +8 -0
  78. package/dist/state/index.d.ts.map +1 -1
  79. package/dist/state/index.js +10 -0
  80. package/dist/state/index.js.map +1 -1
  81. package/dist/types/workflow.d.ts +6 -0
  82. package/dist/types/workflow.d.ts.map +1 -1
  83. package/dist/types/workflow.js +2 -0
  84. package/dist/types/workflow.js.map +1 -1
  85. package/dist/upgrade/handlers.d.ts +15 -0
  86. package/dist/upgrade/handlers.d.ts.map +1 -1
  87. package/dist/upgrade/handlers.js +52 -0
  88. package/dist/upgrade/handlers.js.map +1 -1
  89. package/dist/workflow/auto-fix-bundler.d.ts +37 -0
  90. package/dist/workflow/auto-fix-bundler.d.ts.map +1 -0
  91. package/dist/workflow/auto-fix-bundler.js +320 -0
  92. package/dist/workflow/auto-fix-bundler.js.map +1 -0
  93. package/dist/workflow/auto-fix.d.ts.map +1 -1
  94. package/dist/workflow/auto-fix.js +10 -3
  95. package/dist/workflow/auto-fix.js.map +1 -1
  96. package/dist/workflow/index.d.ts +1 -0
  97. package/dist/workflow/index.d.ts.map +1 -1
  98. package/dist/workflow/index.js +12 -0
  99. package/dist/workflow/index.js.map +1 -1
  100. package/dist/workflow/overview.d.ts.map +1 -1
  101. package/dist/workflow/overview.js +4 -0
  102. package/dist/workflow/overview.js.map +1 -1
  103. package/dist/workflow/plan-mode.d.ts +4 -3
  104. package/dist/workflow/plan-mode.d.ts.map +1 -1
  105. package/dist/workflow/plan-mode.js +69 -5
  106. package/dist/workflow/plan-mode.js.map +1 -1
  107. package/dist/workflow/website-strategy.d.ts +9 -0
  108. package/dist/workflow/website-strategy.d.ts.map +1 -1
  109. package/dist/workflow/website-strategy.js +73 -1
  110. package/dist/workflow/website-strategy.js.map +1 -1
  111. package/dist/workflow/website-updater.d.ts.map +1 -1
  112. package/dist/workflow/website-updater.js +15 -4
  113. package/dist/workflow/website-updater.js.map +1 -1
  114. package/package.json +1 -1
  115. package/src/cli/commands/create.ts +58 -4
  116. package/src/cli/interactive.ts +96 -7
  117. package/src/generators/all.ts +44 -332
  118. package/src/generators/doc-parser.ts +87 -10
  119. package/src/generators/frontend-design-analyzer.ts +261 -0
  120. package/src/generators/shared-packages.ts +500 -0
  121. package/src/generators/templates/index.ts +4 -0
  122. package/src/generators/templates/website-components.ts +36 -11
  123. package/src/generators/templates/website-config.ts +166 -13
  124. package/src/generators/templates/website-landing.ts +331 -0
  125. package/src/generators/templates/website-layout.ts +443 -0
  126. package/src/generators/templates/website-pricing.ts +330 -0
  127. package/src/generators/templates/website-sections.ts +541 -0
  128. package/src/generators/templates/website.ts +38 -851
  129. package/src/generators/website-content-scanner.ts +208 -0
  130. package/src/generators/website-context.ts +248 -20
  131. package/src/generators/website-debug.ts +130 -0
  132. package/src/generators/website.ts +71 -3
  133. package/src/generators/workspace-root.ts +113 -0
  134. package/src/state/index.ts +14 -0
  135. package/src/types/workflow.ts +6 -0
  136. package/src/upgrade/handlers.ts +65 -0
  137. package/src/workflow/auto-fix-bundler.ts +392 -0
  138. package/src/workflow/auto-fix.ts +11 -3
  139. package/src/workflow/index.ts +12 -0
  140. package/src/workflow/overview.ts +6 -0
  141. package/src/workflow/plan-mode.ts +81 -7
  142. package/src/workflow/website-strategy.ts +75 -1
  143. package/src/workflow/website-updater.ts +17 -6
  144. package/tests/cli/project-naming.test.ts +136 -0
  145. package/tests/generators/doc-parser.test.ts +121 -0
  146. package/tests/generators/frontend-design-analyzer.test.ts +90 -0
  147. package/tests/generators/quality-gate.test.ts +183 -0
  148. package/tests/generators/shared-packages.test.ts +83 -0
  149. package/tests/generators/website-components.test.ts +1 -1
  150. package/tests/generators/website-config.test.ts +84 -0
  151. package/tests/generators/website-content-scanner.test.ts +181 -0
  152. package/tests/generators/website-context.test.ts +109 -0
  153. package/tests/generators/website-debug.test.ts +77 -0
  154. package/tests/generators/website-landing.test.ts +188 -0
  155. package/tests/generators/website-pricing.test.ts +98 -0
  156. package/tests/generators/website-sections.test.ts +245 -0
  157. package/tests/generators/workspace-root.test.ts +105 -0
  158. package/tests/upgrade/handlers.test.ts +162 -0
  159. package/tests/workflow/auto-fix-bundler.test.ts +242 -0
  160. package/tests/workflow/plan-mode.test.ts +111 -1
  161. package/tests/workflow/website-strategy.test.ts +55 -0
@@ -10,6 +10,12 @@ import type { GenerationResult } from './python.js';
10
10
  import { generateFullstackProject } from './fullstack.js';
11
11
  import { generateWebsiteProject } from './website.js';
12
12
  import type { WebsiteContentContext } from './website-context.js';
13
+ import { buildWebsiteContext, validateWebsiteContext } from './website-context.js';
14
+ import {
15
+ generateDesignTokensPackage as generateDesignTokensPackageImpl,
16
+ generateUiPackage as generateUiPackageImpl,
17
+ } from './shared-packages.js';
18
+ import type { BrandColorOptions } from './shared-packages.js';
13
19
 
14
20
  /**
15
21
  * Options for all project generation
@@ -344,346 +350,25 @@ Generated by [Popeye CLI](https://github.com/popeye-cli/popeye)
344
350
 
345
351
  /**
346
352
  * Generate design tokens package
353
+ * Delegates to shared-packages.ts with optional brand color passthrough
347
354
  */
348
- export function generateDesignTokensPackage(projectName: string): {
355
+ export function generateDesignTokensPackage(
356
+ projectName: string,
357
+ brandColors?: BrandColorOptions
358
+ ): {
349
359
  files: Array<{ path: string; content: string }>;
350
360
  } {
351
- return {
352
- files: [
353
- {
354
- path: 'package.json',
355
- content: JSON.stringify(
356
- {
357
- name: `@${projectName}/design-tokens`,
358
- version: '1.0.0',
359
- type: 'module',
360
- main: './dist/index.js',
361
- types: './dist/index.d.ts',
362
- exports: {
363
- '.': './dist/index.js',
364
- './tailwind': './dist/tailwind-preset.js',
365
- },
366
- scripts: {
367
- build: 'tsc',
368
- dev: 'tsc --watch',
369
- },
370
- devDependencies: {
371
- typescript: '^5.3.3',
372
- },
373
- },
374
- null,
375
- 2
376
- ),
377
- },
378
- {
379
- path: 'tsconfig.json',
380
- content: JSON.stringify(
381
- {
382
- compilerOptions: {
383
- target: 'ES2020',
384
- module: 'ESNext',
385
- moduleResolution: 'bundler',
386
- declaration: true,
387
- outDir: './dist',
388
- strict: true,
389
- esModuleInterop: true,
390
- skipLibCheck: true,
391
- },
392
- include: ['src'],
393
- },
394
- null,
395
- 2
396
- ),
397
- },
398
- {
399
- path: 'src/index.ts',
400
- content: `/**
401
- * Design tokens for ${projectName}
402
- */
403
-
404
- export * from './colors.js';
405
- export * from './typography.js';
406
- `,
407
- },
408
- {
409
- path: 'src/colors.ts',
410
- content: `/**
411
- * Color palette
412
- */
413
-
414
- export const colors = {
415
- primary: {
416
- 50: '#f0f9ff',
417
- 100: '#e0f2fe',
418
- 200: '#bae6fd',
419
- 300: '#7dd3fc',
420
- 400: '#38bdf8',
421
- 500: '#0ea5e9',
422
- 600: '#0284c7',
423
- 700: '#0369a1',
424
- 800: '#075985',
425
- 900: '#0c4a6e',
426
- },
427
- secondary: {
428
- 50: '#f8fafc',
429
- 100: '#f1f5f9',
430
- 200: '#e2e8f0',
431
- 300: '#cbd5e1',
432
- 400: '#94a3b8',
433
- 500: '#64748b',
434
- 600: '#475569',
435
- 700: '#334155',
436
- 800: '#1e293b',
437
- 900: '#0f172a',
438
- },
439
- } as const;
440
-
441
- export type ColorScale = typeof colors.primary;
442
- export type Colors = typeof colors;
443
- `,
444
- },
445
- {
446
- path: 'src/typography.ts',
447
- content: `/**
448
- * Typography settings
449
- */
450
-
451
- export const typography = {
452
- fontFamily: {
453
- sans: ['Inter', 'system-ui', 'sans-serif'],
454
- mono: ['JetBrains Mono', 'Fira Code', 'monospace'],
455
- },
456
- fontSize: {
457
- xs: ['0.75rem', { lineHeight: '1rem' }],
458
- sm: ['0.875rem', { lineHeight: '1.25rem' }],
459
- base: ['1rem', { lineHeight: '1.5rem' }],
460
- lg: ['1.125rem', { lineHeight: '1.75rem' }],
461
- xl: ['1.25rem', { lineHeight: '1.75rem' }],
462
- '2xl': ['1.5rem', { lineHeight: '2rem' }],
463
- '3xl': ['1.875rem', { lineHeight: '2.25rem' }],
464
- '4xl': ['2.25rem', { lineHeight: '2.5rem' }],
465
- '5xl': ['3rem', { lineHeight: '1' }],
466
- '6xl': ['3.75rem', { lineHeight: '1' }],
467
- },
468
- } as const;
469
-
470
- export type Typography = typeof typography;
471
- `,
472
- },
473
- {
474
- path: 'src/tailwind-preset.ts',
475
- content: `/**
476
- * Tailwind CSS preset with design tokens
477
- */
478
-
479
- import { colors } from './colors.js';
480
- import { typography } from './typography.js';
481
-
482
- export const preset = {
483
- theme: {
484
- extend: {
485
- colors,
486
- fontFamily: typography.fontFamily,
487
- fontSize: typography.fontSize,
488
- },
489
- },
490
- };
491
-
492
- export default preset;
493
- `,
494
- },
495
- ],
496
- };
361
+ return generateDesignTokensPackageImpl(projectName, brandColors);
497
362
  }
498
363
 
499
364
  /**
500
365
  * Generate UI components package
366
+ * Delegates to shared-packages.ts
501
367
  */
502
368
  export function generateUiPackage(projectName: string): {
503
369
  files: Array<{ path: string; content: string }>;
504
370
  } {
505
- return {
506
- files: [
507
- {
508
- path: 'package.json',
509
- content: JSON.stringify(
510
- {
511
- name: `@${projectName}/ui`,
512
- version: '1.0.0',
513
- type: 'module',
514
- main: './dist/index.js',
515
- types: './dist/index.d.ts',
516
- exports: {
517
- '.': './dist/index.js',
518
- './button': './dist/button.js',
519
- './card': './dist/card.js',
520
- },
521
- scripts: {
522
- build: 'tsc',
523
- dev: 'tsc --watch',
524
- },
525
- dependencies: {
526
- clsx: '^2.1.0',
527
- 'tailwind-merge': '^2.2.0',
528
- },
529
- peerDependencies: {
530
- react: '>=18.0.0',
531
- 'react-dom': '>=18.0.0',
532
- },
533
- devDependencies: {
534
- '@types/react': '^18.2.0',
535
- '@types/react-dom': '^18.2.0',
536
- typescript: '^5.3.3',
537
- },
538
- },
539
- null,
540
- 2
541
- ),
542
- },
543
- {
544
- path: 'tsconfig.json',
545
- content: JSON.stringify(
546
- {
547
- compilerOptions: {
548
- target: 'ES2020',
549
- module: 'ESNext',
550
- moduleResolution: 'bundler',
551
- declaration: true,
552
- outDir: './dist',
553
- strict: true,
554
- esModuleInterop: true,
555
- skipLibCheck: true,
556
- jsx: 'react-jsx',
557
- },
558
- include: ['src'],
559
- },
560
- null,
561
- 2
562
- ),
563
- },
564
- {
565
- path: 'src/index.ts',
566
- content: `/**
567
- * Shared UI components for ${projectName}
568
- */
569
-
570
- export * from './button.js';
571
- export * from './card.js';
572
- export * from './utils.js';
573
- `,
574
- },
575
- {
576
- path: 'src/utils.ts',
577
- content: `import { clsx, type ClassValue } from 'clsx';
578
- import { twMerge } from 'tailwind-merge';
579
-
580
- export function cn(...inputs: ClassValue[]) {
581
- return twMerge(clsx(inputs));
582
- }
583
- `,
584
- },
585
- {
586
- path: 'src/button.tsx',
587
- content: `import * as React from 'react';
588
- import { cn } from './utils.js';
589
-
590
- export interface ButtonProps
591
- extends React.ButtonHTMLAttributes<HTMLButtonElement> {
592
- variant?: 'primary' | 'secondary' | 'outline' | 'ghost';
593
- size?: 'sm' | 'md' | 'lg';
594
- }
595
-
596
- export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
597
- ({ className, variant = 'primary', size = 'md', ...props }, ref) => {
598
- return (
599
- <button
600
- className={cn(
601
- 'inline-flex items-center justify-center rounded-md font-medium transition-colors',
602
- 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2',
603
- 'disabled:pointer-events-none disabled:opacity-50',
604
- {
605
- // Variants
606
- 'bg-primary-600 text-white hover:bg-primary-500': variant === 'primary',
607
- 'bg-secondary-100 text-secondary-900 hover:bg-secondary-200': variant === 'secondary',
608
- 'border border-secondary-300 bg-transparent hover:bg-secondary-50': variant === 'outline',
609
- 'bg-transparent hover:bg-secondary-100': variant === 'ghost',
610
- // Sizes
611
- 'h-8 px-3 text-sm': size === 'sm',
612
- 'h-10 px-4 text-sm': size === 'md',
613
- 'h-12 px-6 text-base': size === 'lg',
614
- },
615
- className
616
- )}
617
- ref={ref}
618
- {...props}
619
- />
620
- );
621
- }
622
- );
623
-
624
- Button.displayName = 'Button';
625
- `,
626
- },
627
- {
628
- path: 'src/card.tsx',
629
- content: `import * as React from 'react';
630
- import { cn } from './utils.js';
631
-
632
- export interface CardProps extends React.HTMLAttributes<HTMLDivElement> {}
633
-
634
- export const Card = React.forwardRef<HTMLDivElement, CardProps>(
635
- ({ className, ...props }, ref) => (
636
- <div
637
- ref={ref}
638
- className={cn(
639
- 'rounded-lg border border-secondary-200 bg-white shadow-sm',
640
- className
641
- )}
642
- {...props}
643
- />
644
- )
645
- );
646
-
647
- Card.displayName = 'Card';
648
-
649
- export const CardHeader = React.forwardRef<
650
- HTMLDivElement,
651
- React.HTMLAttributes<HTMLDivElement>
652
- >(({ className, ...props }, ref) => (
653
- <div
654
- ref={ref}
655
- className={cn('flex flex-col space-y-1.5 p-6', className)}
656
- {...props}
657
- />
658
- ));
659
-
660
- CardHeader.displayName = 'CardHeader';
661
-
662
- export const CardTitle = React.forwardRef<
663
- HTMLParagraphElement,
664
- React.HTMLAttributes<HTMLHeadingElement>
665
- >(({ className, ...props }, ref) => (
666
- <h3
667
- ref={ref}
668
- className={cn('text-lg font-semibold leading-none tracking-tight', className)}
669
- {...props}
670
- />
671
- ));
672
-
673
- CardTitle.displayName = 'CardTitle';
674
-
675
- export const CardContent = React.forwardRef<
676
- HTMLDivElement,
677
- React.HTMLAttributes<HTMLDivElement>
678
- >(({ className, ...props }, ref) => (
679
- <div ref={ref} className={cn('p-6 pt-0', className)} {...props} />
680
- ));
681
-
682
- CardContent.displayName = 'CardContent';
683
- `,
684
- },
685
- ],
686
- };
371
+ return generateUiPackageImpl(projectName);
687
372
  }
688
373
 
689
374
  /**
@@ -711,6 +396,30 @@ export async function generateAllProject(
711
396
  await ensureDir(path.join(projectDir, 'packages', 'contracts'));
712
397
  await ensureDir(path.join(projectDir, '.popeye'));
713
398
 
399
+ // Auto-build content context if not provided
400
+ let contentContext = options.contentContext;
401
+ let contextWarning: string | undefined;
402
+ if (!contentContext) {
403
+ try {
404
+ contentContext = await buildWebsiteContext(projectDir, projectName);
405
+ } catch (e) {
406
+ contextWarning = e instanceof Error ? e.message : 'Unknown error building website context';
407
+ // Proceed with defaults, but warning is logged below
408
+ }
409
+ }
410
+ if (contextWarning) {
411
+ // Log warning so user sees it, but don't block generation
412
+ console.warn(`[website-context] Warning: ${contextWarning}`);
413
+ }
414
+
415
+ // Soft validation: log quality issues without blocking monorepo generation
416
+ if (contentContext) {
417
+ const validation = validateWebsiteContext(contentContext, projectName);
418
+ for (const issue of [...validation.issues, ...validation.warnings]) {
419
+ console.warn(`[website-context] ${issue}`);
420
+ }
421
+ }
422
+
714
423
  // Generate fullstack first (creates apps/frontend and apps/backend)
715
424
  const fullstackResult = await generateFullstackProject(spec, outputDir);
716
425
  if (!fullstackResult.success) {
@@ -724,7 +433,7 @@ export async function generateAllProject(
724
433
  workspaceMode: true,
725
434
  skipDocker: false, // Website needs its own Dockerfile
726
435
  skipReadme: false,
727
- contentContext: options.contentContext,
436
+ contentContext: contentContext,
728
437
  });
729
438
  if (!websiteResult.success) {
730
439
  return {
@@ -734,8 +443,11 @@ export async function generateAllProject(
734
443
  }
735
444
  filesCreated.push(...websiteResult.filesCreated);
736
445
 
737
- // Generate shared packages
738
- const designTokens = generateDesignTokensPackage(projectName);
446
+ // Generate shared packages (pass brand colors if available)
447
+ const brandColors: BrandColorOptions | undefined = contentContext?.brand?.primaryColor
448
+ ? { primaryColor: contentContext.brand.primaryColor }
449
+ : undefined;
450
+ const designTokens = generateDesignTokensPackage(projectName, brandColors);
739
451
  for (const file of designTokens.files) {
740
452
  const filePath = path.join(projectDir, 'packages', 'design-tokens', file.path);
741
453
  await ensureDir(path.dirname(filePath));
@@ -24,36 +24,86 @@ export function stripCodeFences(text: string): string {
24
24
 
25
25
  /**
26
26
  * Extract the real product name from docs or specification
27
- * Finds all "# Name — tagline" headings and picks the shortest (most likely the product name)
27
+ *
28
+ * Priority chain (first match wins):
29
+ * 1. Parsed docs: "# ProductName -- tagline" heading (picks shortest)
30
+ * 2. Specification: "# ProductName" heading
31
+ * 3. Specification: "Product:" / "Name:" / "App:" label
32
+ * 4. package.json "name" field from workspace root (passed via specification context)
33
+ * 5. undefined (caller falls back to directory name)
28
34
  */
29
- export function extractProductName(docs: string, specification?: string): string | undefined {
30
- // Collect all "# Name — tagline" headings, pick the shortest name
35
+ export function extractProductName(
36
+ docs: string,
37
+ specification?: string,
38
+ packageJsonName?: string
39
+ ): string | undefined {
40
+ // 1. Collect all "# Name -- tagline" headings, pick the shortest name
31
41
  // Reason: sub-documents like "Gateco UI Color System" are longer than "Gateco"
32
- const headingPattern = /^#\s+([A-Z][a-zA-Z0-9]+(?:[ \t]+[A-Z][a-zA-Z0-9]+)*)(?:[ \t]*[—\-–|:][ \t])/gm;
42
+ const headingPattern = /^#\s+([A-Z][a-zA-Z0-9]+(?:[ \t]+[A-Z][a-zA-Z0-9]+)*)(?:[ \t]*(?:--|[—–|:])[ \t])/gm;
33
43
  const candidates: string[] = [];
34
44
  let match;
35
45
  while ((match = headingPattern.exec(docs)) !== null) {
36
46
  candidates.push(match[1].trim());
37
47
  }
38
48
  if (candidates.length > 0) {
39
- // Prefer shortest name (product name is typically 1-2 words)
40
49
  candidates.sort((a, b) => a.length - b.length);
41
50
  return candidates[0];
42
51
  }
43
52
 
44
- // Pattern: "**Project Name**: ProductName" in specification
53
+ // 2. "# ProductName" heading in specification (standalone heading, not sub-doc)
54
+ // Reason: Exclude common section headings like "Overview", "Introduction", "Summary"
55
+ if (specification) {
56
+ const commonHeadings = /^(Overview|Introduction|Summary|Features|Architecture|Requirements|Setup|Installation|Configuration|Specification|Appendix|Conclusion|References)$/i;
57
+ const specHeading = specification.match(/^#\s+([A-Z][a-zA-Z0-9]+(?:\s+[A-Z][a-zA-Z0-9]+)*)\s*$/m);
58
+ if (specHeading && !commonHeadings.test(specHeading[1].trim())) {
59
+ return specHeading[1].trim();
60
+ }
61
+ }
62
+
63
+ // 3. "Product:" / "Name:" / "App:" label in specification
45
64
  if (specification) {
46
65
  const nameMatch = specification.match(/\*\*Project\s+Name\*\*:\s*(.+)/i);
47
66
  if (nameMatch) return nameMatch[1].trim();
67
+
68
+ const labelMatch = specification.match(/(?:^|\n)\s*(?:Product|Name|App)\s*:\s*(.+)/i);
69
+ if (labelMatch) {
70
+ const value = labelMatch[1].trim().replace(/\*\*/g, '');
71
+ if (value.length > 0 && value.length < 60) return value;
72
+ }
48
73
  }
49
74
 
50
- // Pattern: "# ProductName" in spec/product doc sections only
75
+ // 4. "# ProductName" in spec/product doc sections only
51
76
  const sectionHeading = docs.match(/^---\s+\S*(?:spec|product)\S*\s+---\n#\s+([A-Z][a-zA-Z0-9]+)/im);
52
77
  if (sectionHeading) return sectionHeading[1].trim();
53
78
 
79
+ // 5. package.json name (strip @scope/ prefix and convert to title case)
80
+ if (packageJsonName) {
81
+ const cleaned = packageJsonName
82
+ .replace(/^@[^/]+\//, '') // Strip scope
83
+ .replace(/[-_]/g, ' ')
84
+ .replace(/\b\w/g, (c) => c.toUpperCase())
85
+ .trim();
86
+ if (cleaned.length > 0) return cleaned;
87
+ }
88
+
54
89
  return undefined;
55
90
  }
56
91
 
92
+ /**
93
+ * Check if a product name looks like a directory name rather than a real product name
94
+ * Used by the quality gate to flag suspicious names
95
+ *
96
+ * @param name - The product name to check
97
+ * @returns True if the name looks suspicious (likely a directory name)
98
+ */
99
+ export function isSuspiciousProductName(name: string): boolean {
100
+ const suspiciousNames = ['my-app', 'my-project', 'project', 'app', 'website', 'frontend'];
101
+ if (suspiciousNames.includes(name.toLowerCase())) return true;
102
+ // Hyphenated lowercase strings like "read-all-files" are likely directory names
103
+ if (/^[a-z]+-[a-z]+(-[a-z]+)*$/.test(name)) return true;
104
+ return false;
105
+ }
106
+
57
107
  /**
58
108
  * Extract a tagline from docs (text after em-dash in first heading)
59
109
  * When productName is provided, prefer tagline from the heading containing that name
@@ -123,16 +173,37 @@ export function extractDescription(docs: string, specification?: string): string
123
173
  return undefined;
124
174
  }
125
175
 
176
+ /** Regex to filter out dev-task items that aren't real product features */
177
+ const DEV_TASK_VERBS = /^(?:implement|fix|refactor|add tests|upgrade|migrate|configure|setup|install|deploy|debug|create|build|write|update|remove|delete)/i;
178
+
126
179
  /**
127
180
  * Extract features from docs and specification
128
- * Looks for bullet/numbered lists in sections about features, principles, capabilities
181
+ * Docs-first: extracts from docs only; falls back to specification if empty
182
+ * Filters out dev-task items (implement, fix, refactor, etc.)
129
183
  */
130
184
  export function extractFeatures(
131
185
  docs: string,
132
186
  specification?: string
187
+ ): Array<{ title: string; description: string }> {
188
+ // Docs-first: try docs only, then fall back to spec
189
+ // Reason: specification often contains dev tasks, not user-facing features
190
+ const docsFeatures = extractFeaturesFromSource(docs);
191
+ if (docsFeatures.length > 0) return docsFeatures;
192
+
193
+ if (specification) {
194
+ return extractFeaturesFromSource(specification);
195
+ }
196
+
197
+ return [];
198
+ }
199
+
200
+ /**
201
+ * Extract features from a single text source
202
+ */
203
+ function extractFeaturesFromSource(
204
+ source: string
133
205
  ): Array<{ title: string; description: string }> {
134
206
  const features: Array<{ title: string; description: string }> = [];
135
- const source = docs + '\n' + (specification || '');
136
207
 
137
208
  // Split into sections by heading
138
209
  const sectionPattern = /^#{1,3}\s+(.+)$/gm;
@@ -173,13 +244,17 @@ export function extractFeatures(
173
244
  // Try "**bold title** - description" pattern
174
245
  const boldWithDesc = text.match(/^\*\*(.+?)\*\*\s*[-–:]\s*(.+)/);
175
246
  if (boldWithDesc) {
247
+ const title = boldWithDesc[1].trim();
248
+ // Filter out dev-task items
249
+ if (DEV_TASK_VERBS.test(title)) continue;
176
250
  features.push({
177
- title: boldWithDesc[1].trim(),
251
+ title,
178
252
  description: boldWithDesc[2].trim().slice(0, 150),
179
253
  });
180
254
  } else if (/^\*\*.+\*\*/.test(text)) {
181
255
  // Bold title with no trailing description: "**Vector DB agnostic**"
182
256
  const title = text.replace(/\*\*/g, '').trim();
257
+ if (DEV_TASK_VERBS.test(title)) continue;
183
258
  if (title.length > 3 && title.length < 80) {
184
259
  features.push({ title, description: title });
185
260
  }
@@ -187,6 +262,8 @@ export function extractFeatures(
187
262
  const cleaned = text.replace(/\*\*/g, '');
188
263
  // Split on sentence-level delimiters only; keep hyphens in compound words
189
264
  const titlePart = cleaned.split(/[.,:;—–]/)[0].trim();
265
+ // Filter out dev-task items
266
+ if (DEV_TASK_VERBS.test(titlePart)) continue;
190
267
  if (titlePart.length > 3 && titlePart.length < 60) {
191
268
  features.push({
192
269
  title: titlePart,