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
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Tests for shared-packages module
3
+ * Verifies color scale generation and brand color passthrough
4
+ */
5
+
6
+ import { describe, it, expect } from 'vitest';
7
+ import {
8
+ generateColorScale,
9
+ generateDesignTokensPackage,
10
+ } from '../../src/generators/shared-packages.js';
11
+
12
+ describe('generateColorScale', () => {
13
+ it('generates 10-stop color scale from valid hex', () => {
14
+ const scale = generateColorScale('#2563EB');
15
+
16
+ expect(Object.keys(scale)).toEqual(['50', '100', '200', '300', '400', '500', '600', '700', '800', '900']);
17
+ // Lightest stop should be light, darkest should be dark
18
+ expect(scale['50']).toMatch(/^#[0-9a-f]{6}$/);
19
+ expect(scale['900']).toMatch(/^#[0-9a-f]{6}$/);
20
+ // The stops should vary (not all the same)
21
+ const uniqueColors = new Set(Object.values(scale));
22
+ expect(uniqueColors.size).toBeGreaterThan(5);
23
+ });
24
+
25
+ it('falls back to default sky-blue on invalid hex', () => {
26
+ const scale = generateColorScale('not-a-color');
27
+
28
+ // Should return the default sky-blue palette
29
+ expect(scale['500']).toBe('#0ea5e9');
30
+ expect(scale['600']).toBe('#0284c7');
31
+ });
32
+
33
+ it('handles hex with # prefix correctly', () => {
34
+ const withHash = generateColorScale('#FF0000');
35
+ const withoutHash = generateColorScale('#FF0000');
36
+
37
+ expect(withHash).toEqual(withoutHash);
38
+ });
39
+ });
40
+
41
+ describe('generateDesignTokensPackage', () => {
42
+ it('uses brand color when provided', () => {
43
+ const result = generateDesignTokensPackage('test-project', {
44
+ primaryColor: '#2563EB',
45
+ });
46
+
47
+ // Find the colors.ts file
48
+ const colorsFile = result.files.find((f) => f.path === 'src/colors.ts');
49
+ expect(colorsFile).toBeDefined();
50
+ // Should NOT contain the default sky-blue
51
+ expect(colorsFile!.content).not.toContain('#0ea5e9');
52
+ });
53
+
54
+ it('uses default sky-blue when no brand color provided', () => {
55
+ const result = generateDesignTokensPackage('test-project');
56
+
57
+ const colorsFile = result.files.find((f) => f.path === 'src/colors.ts');
58
+ expect(colorsFile).toBeDefined();
59
+ // Should contain default sky-blue values
60
+ expect(colorsFile!.content).toContain('#0ea5e9');
61
+ });
62
+
63
+ it('generates correct package structure', () => {
64
+ const result = generateDesignTokensPackage('my-project');
65
+
66
+ const paths = result.files.map((f) => f.path);
67
+ expect(paths).toContain('package.json');
68
+ expect(paths).toContain('tsconfig.json');
69
+ expect(paths).toContain('src/index.ts');
70
+ expect(paths).toContain('src/colors.ts');
71
+ expect(paths).toContain('src/typography.ts');
72
+ expect(paths).toContain('src/tailwind-preset.ts');
73
+ });
74
+
75
+ it('package.json contains correct name with scope', () => {
76
+ const result = generateDesignTokensPackage('gateco');
77
+
78
+ const pkgFile = result.files.find((f) => f.path === 'package.json');
79
+ expect(pkgFile).toBeDefined();
80
+ const pkg = JSON.parse(pkgFile!.content);
81
+ expect(pkg.name).toBe('@gateco/design-tokens');
82
+ });
83
+ });
@@ -67,7 +67,7 @@ describe('generateWebsiteHeader', () => {
67
67
  it('renders text fallback when no logo', () => {
68
68
  const header = generateWebsiteHeader('deploy-ai', contextNoLogo, mockStrategy);
69
69
  expect(header).toContain('DeployAI');
70
- expect(header).toContain('font-bold text-primary-600');
70
+ expect(header).toContain('font-bold text-foreground');
71
71
  });
72
72
 
73
73
  it('includes navigation links from strategy', () => {
@@ -0,0 +1,84 @@
1
+ /**
2
+ * Tests for website-config tailwind generation with brand colors
3
+ * Extended to verify full color token set, animations, and CSS vars
4
+ */
5
+
6
+ import { describe, it, expect } from 'vitest';
7
+ import { generateWebsiteTailwindConfig } from '../../src/generators/templates/website-config.js';
8
+
9
+ describe('generateWebsiteTailwindConfig', () => {
10
+ it('generates config with brand primary color', () => {
11
+ const config = generateWebsiteTailwindConfig({
12
+ primaryColor: '#2563EB',
13
+ });
14
+
15
+ // Should contain generated color scale, NOT default sky-blue
16
+ expect(config).not.toContain('#0ea5e9');
17
+ expect(config).not.toContain('#0284c7');
18
+ // Should be valid TypeScript with Config type
19
+ expect(config).toContain("import type { Config } from 'tailwindcss'");
20
+ expect(config).toContain('primary:');
21
+ });
22
+
23
+ it('includes workspace design-tokens preset import', () => {
24
+ const config = generateWebsiteTailwindConfig({
25
+ workspaceMode: true,
26
+ projectName: 'gateco',
27
+ });
28
+
29
+ expect(config).toContain("import designPreset from '@gateco/design-tokens/tailwind'");
30
+ expect(config).toContain('presets: [designPreset]');
31
+ });
32
+
33
+ it('uses default sky-blue when no options provided', () => {
34
+ const config = generateWebsiteTailwindConfig();
35
+
36
+ // Should contain default sky-blue palette
37
+ expect(config).toContain('#0ea5e9');
38
+ expect(config).toContain('#0284c7');
39
+ // Should NOT have preset import
40
+ expect(config).not.toContain('designPreset');
41
+ });
42
+
43
+ it('includes full shadcn-compatible color token set', () => {
44
+ const config = generateWebsiteTailwindConfig();
45
+
46
+ // Background/foreground
47
+ expect(config).toContain("background: 'hsl(var(--background))'");
48
+ expect(config).toContain("foreground: 'hsl(var(--foreground))'");
49
+
50
+ // Muted with sub-tokens
51
+ expect(config).toContain("muted:");
52
+ expect(config).toContain("hsl(var(--muted))");
53
+ expect(config).toContain("hsl(var(--muted-foreground))");
54
+
55
+ // Accent
56
+ expect(config).toContain("accent:");
57
+ expect(config).toContain("hsl(var(--accent))");
58
+
59
+ // Card
60
+ expect(config).toContain("card:");
61
+ expect(config).toContain("hsl(var(--card))");
62
+
63
+ // Border and ring
64
+ expect(config).toContain("border: 'hsl(var(--border))'");
65
+ expect(config).toContain("ring: 'hsl(var(--ring))'");
66
+ });
67
+
68
+ it('includes animation utilities', () => {
69
+ const config = generateWebsiteTailwindConfig();
70
+
71
+ expect(config).toContain('keyframes:');
72
+ expect(config).toContain('fadeIn:');
73
+ expect(config).toContain('slideUp:');
74
+ expect(config).toContain('animation:');
75
+ });
76
+
77
+ it('includes borderColor and borderRadius extensions', () => {
78
+ const config = generateWebsiteTailwindConfig();
79
+
80
+ expect(config).toContain('borderColor:');
81
+ expect(config).toContain('borderRadius:');
82
+ expect(config).toContain('var(--radius)');
83
+ });
84
+ });
@@ -0,0 +1,181 @@
1
+ /**
2
+ * Tests for post-generation website content scanner
3
+ * Verifies detection of placeholder fingerprints in generated files
4
+ */
5
+
6
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
7
+ import { promises as fs } from 'node:fs';
8
+ import path from 'node:path';
9
+ import os from 'node:os';
10
+ import { scanGeneratedContent } from '../../src/generators/website-content-scanner.js';
11
+
12
+ let tmpDir: string;
13
+
14
+ beforeEach(async () => {
15
+ tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'scanner-test-'));
16
+ await fs.mkdir(path.join(tmpDir, 'src', 'app'), { recursive: true });
17
+ await fs.mkdir(path.join(tmpDir, 'src', 'components'), { recursive: true });
18
+ });
19
+
20
+ afterEach(async () => {
21
+ await fs.rm(tmpDir, { recursive: true, force: true });
22
+ });
23
+
24
+ describe('scanGeneratedContent', () => {
25
+ it('produces no issues for clean files', async () => {
26
+ await fs.writeFile(
27
+ path.join(tmpDir, 'src', 'app', 'page.tsx'),
28
+ `export default function Home() {
29
+ return <main><h1>Welcome to Gateco</h1></main>;
30
+ }
31
+ `,
32
+ );
33
+
34
+ const result = await scanGeneratedContent(tmpDir);
35
+
36
+ expect(result.issues).toHaveLength(0);
37
+ expect(result.filesScanned).toBe(1);
38
+ expect(result.score).toBe(100);
39
+ });
40
+
41
+ it('flags TODO block comments as errors', async () => {
42
+ await fs.writeFile(
43
+ path.join(tmpDir, 'src', 'app', 'page.tsx'),
44
+ `export default function Home() {
45
+ return <main>/* TODO: Replace with real content */</main>;
46
+ }
47
+ `,
48
+ );
49
+
50
+ const result = await scanGeneratedContent(tmpDir);
51
+
52
+ expect(result.issues.length).toBeGreaterThan(0);
53
+ const todoIssue = result.issues.find((i) => /TODO/i.test(i.message));
54
+ expect(todoIssue).toBeDefined();
55
+ expect(todoIssue!.severity).toBe('error');
56
+ });
57
+
58
+ it('flags TODO line comments as errors', async () => {
59
+ await fs.writeFile(
60
+ path.join(tmpDir, 'src', 'components', 'Header.tsx'),
61
+ `// TODO: add real navigation
62
+ export function Header() { return <header />; }
63
+ `,
64
+ );
65
+
66
+ const result = await scanGeneratedContent(tmpDir);
67
+
68
+ expect(result.issues.some((i) => /TODO/i.test(i.message))).toBe(true);
69
+ });
70
+
71
+ it('detects default pricing pattern in file content', async () => {
72
+ await fs.writeFile(
73
+ path.join(tmpDir, 'src', 'app', 'page.tsx'),
74
+ `const tiers = [
75
+ { name: 'Starter', price: '$0/mo', features: ['1 user'] },
76
+ { name: 'Pro', price: '$29/mo', features: ['10 users'] },
77
+ { name: 'Enterprise', price: 'Custom', features: ['Unlimited'] },
78
+ ];
79
+ export default function Page() { return <div />; }
80
+ `,
81
+ );
82
+
83
+ const result = await scanGeneratedContent(tmpDir);
84
+
85
+ expect(result.issues.some((i) => /pricing/i.test(i.message))).toBe(true);
86
+ expect(result.issues.some((i) => /\$29/i.test(i.message))).toBe(true);
87
+ });
88
+
89
+ it('detects default tagline text', async () => {
90
+ await fs.writeFile(
91
+ path.join(tmpDir, 'src', 'components', 'Footer.tsx'),
92
+ `export function Footer() { return <p>Build something amazing</p>; }
93
+ `,
94
+ );
95
+
96
+ const result = await scanGeneratedContent(tmpDir);
97
+
98
+ expect(result.issues.some((i) => /tagline/i.test(i.message))).toBe(true);
99
+ });
100
+
101
+ it('detects generic description text', async () => {
102
+ await fs.writeFile(
103
+ path.join(tmpDir, 'src', 'app', 'page.tsx'),
104
+ `export default function Page() { return <p>Your modern web application</p>; }
105
+ `,
106
+ );
107
+
108
+ const result = await scanGeneratedContent(tmpDir);
109
+
110
+ expect(result.issues.some((i) => /generic description/i.test(i.message))).toBe(true);
111
+ });
112
+
113
+ it('detects default How It Works steps', async () => {
114
+ await fs.writeFile(
115
+ path.join(tmpDir, 'src', 'app', 'page.tsx'),
116
+ `const steps = [
117
+ { title: 'Sign Up', desc: 'Create account' },
118
+ { title: 'Configure', desc: 'Set preferences' },
119
+ { title: 'Deploy', desc: 'Go live' },
120
+ ];
121
+ export default function Page() { return <div />; }
122
+ `,
123
+ );
124
+
125
+ const result = await scanGeneratedContent(tmpDir);
126
+
127
+ expect(result.issues.some((i) => /How It Works/i.test(i.message))).toBe(true);
128
+ });
129
+
130
+ it('returns score of 100 when no issues found', async () => {
131
+ await fs.writeFile(
132
+ path.join(tmpDir, 'src', 'app', 'page.tsx'),
133
+ `export default function Page() { return <h1>Gateco</h1>; }
134
+ `,
135
+ );
136
+
137
+ const result = await scanGeneratedContent(tmpDir);
138
+
139
+ expect(result.score).toBe(100);
140
+ });
141
+
142
+ it('decreases score with multiple issues', async () => {
143
+ await fs.writeFile(
144
+ path.join(tmpDir, 'src', 'app', 'page.tsx'),
145
+ `// TODO: fix this
146
+ export default function Page() {
147
+ return <div>
148
+ <p>Build something amazing</p>
149
+ <p>Your modern web application costs $29/mo</p>
150
+ </div>;
151
+ }
152
+ `,
153
+ );
154
+
155
+ const result = await scanGeneratedContent(tmpDir);
156
+
157
+ expect(result.score).toBeLessThan(80);
158
+ expect(result.issues.length).toBeGreaterThanOrEqual(3);
159
+ });
160
+
161
+ it('handles empty src directory gracefully', async () => {
162
+ const result = await scanGeneratedContent(tmpDir);
163
+
164
+ expect(result.issues).toHaveLength(0);
165
+ expect(result.filesScanned).toBe(0);
166
+ expect(result.score).toBe(100);
167
+ });
168
+
169
+ it('skips node_modules directory', async () => {
170
+ await fs.mkdir(path.join(tmpDir, 'src', 'node_modules'), { recursive: true });
171
+ await fs.writeFile(
172
+ path.join(tmpDir, 'src', 'node_modules', 'bad.tsx'),
173
+ '// TODO: should be ignored',
174
+ );
175
+
176
+ const result = await scanGeneratedContent(tmpDir);
177
+
178
+ expect(result.filesScanned).toBe(0);
179
+ expect(result.issues).toHaveLength(0);
180
+ });
181
+ });
@@ -12,6 +12,8 @@ import {
12
12
  readProjectDocs,
13
13
  findBrandAssets,
14
14
  buildWebsiteContext,
15
+ validateWebsiteContextOrThrow,
16
+ type WebsiteContentContext,
15
17
  } from '../../src/generators/website-context.js';
16
18
 
17
19
  describe('discoverProjectDocs', () => {
@@ -219,4 +221,111 @@ describe('buildWebsiteContext', () => {
219
221
  // Primary color is accent-primary (#2563EB), NOT bg-primary (#0F172A)
220
222
  expect(context.brand?.primaryColor).toBe('#2563EB');
221
223
  });
224
+
225
+ it('discovers docs from parent directory via workspace root', async () => {
226
+ // Simulate: tmpDir has docs, tmpDir/project/.popeye/ is the project dir
227
+ const projectDir = path.join(tmpDir, 'project');
228
+ await fs.mkdir(path.join(projectDir, '.popeye'), { recursive: true });
229
+ await fs.writeFile(
230
+ path.join(tmpDir, 'product-spec.md'),
231
+ '# MyProduct — A great product\n## Features\n- Feature 1'
232
+ );
233
+
234
+ const context = await buildWebsiteContext(projectDir, 'project');
235
+
236
+ // Should find docs from parent directory
237
+ expect(context.rawDocs).toContain('product-spec.md');
238
+ expect(context.productName).toBe('MyProduct');
239
+ });
240
+
241
+ it('finds brand assets from parent directory', async () => {
242
+ const projectDir = path.join(tmpDir, 'project');
243
+ await fs.mkdir(projectDir, { recursive: true });
244
+ await fs.writeFile(path.join(tmpDir, 'company-logo.png'), 'fake-png');
245
+
246
+ const result = await findBrandAssets(projectDir);
247
+
248
+ // findBrandAssets now scans parent dirs via workspace root
249
+ // The logo should be found in tmpDir
250
+ expect(result.logoPath).toBeDefined();
251
+ expect(result.logoPath).toContain('company-logo.png');
252
+ });
253
+
254
+ it('enforces per-file cap to prevent single large doc from consuming budget', async () => {
255
+ await fs.writeFile(path.join(tmpDir, 'huge-spec.md'), 'x'.repeat(20000));
256
+ await fs.writeFile(path.join(tmpDir, 'color-scheme.md'), '# Colors\nPrimary: #FF0000');
257
+
258
+ const docs = await discoverProjectDocs(tmpDir);
259
+ const content = await readProjectDocs(docs);
260
+
261
+ // Color-scheme should be present (it's prioritized and small)
262
+ expect(content).toContain('color-scheme.md');
263
+ // The huge spec should be capped at 8000 chars
264
+ const specSection = content.split('--- huge-spec.md ---')[1];
265
+ if (specSection) {
266
+ // Per-file cap is 8000 + "..." suffix
267
+ expect(specSection.length).toBeLessThan(8100);
268
+ }
269
+ });
270
+ });
271
+
272
+ describe('validateWebsiteContextOrThrow', () => {
273
+ const baseContext: WebsiteContentContext = {
274
+ productName: 'Gateco',
275
+ features: [{ title: 'Auth', description: 'Access control' }],
276
+ rawDocs: 'x'.repeat(200),
277
+ strategy: {
278
+ icp: { primaryPersona: 'devs', painPoints: [], goals: [], objections: [] },
279
+ positioning: { category: 'Security', differentiators: [], valueProposition: 'Secure AI', proofPoints: [] },
280
+ messaging: { headline: 'h', subheadline: 's', elevatorPitch: 'e', longDescription: 'l' },
281
+ seoStrategy: { primaryKeywords: [], secondaryKeywords: [], longTailKeywords: [], titleTemplates: {}, metaDescriptions: {} },
282
+ siteArchitecture: { pages: [], navigation: [], footerSections: [] },
283
+ conversionStrategy: { primaryCta: { text: 'Go', href: '/' }, secondaryCta: { text: 'More', href: '/' }, trustSignals: [], socialProof: [], leadCapture: 'none' },
284
+ competitiveContext: { category: 'sec', competitors: [], differentiators: [] },
285
+ },
286
+ };
287
+
288
+ it('passes with valid context', () => {
289
+ const result = validateWebsiteContextOrThrow(baseContext, 'gateco');
290
+ expect(result.passed).toBe(true);
291
+ expect(result.issues).toEqual([]);
292
+ });
293
+
294
+ it('fails when strategy is missing', () => {
295
+ const ctx: WebsiteContentContext = { ...baseContext, strategy: undefined };
296
+ expect(() => validateWebsiteContextOrThrow(ctx, 'gateco')).toThrow('strategy missing');
297
+ });
298
+
299
+ it('fails when features are empty', () => {
300
+ const ctx: WebsiteContentContext = { ...baseContext, features: [] };
301
+ expect(() => validateWebsiteContextOrThrow(ctx, 'gateco')).toThrow('features');
302
+ });
303
+
304
+ it('fails when product name looks like a directory', () => {
305
+ const ctx: WebsiteContentContext = { ...baseContext, productName: 'my-cool-project' };
306
+ expect(() => validateWebsiteContextOrThrow(ctx, 'x')).toThrow('directory name');
307
+ });
308
+
309
+ it('fails when no docs found', () => {
310
+ const ctx: WebsiteContentContext = { ...baseContext, rawDocs: '' };
311
+ expect(() => validateWebsiteContextOrThrow(ctx, 'gateco')).toThrow('documentation');
312
+ });
313
+
314
+ it('fails when brand/color docs exist but no color extracted', () => {
315
+ const ctx: WebsiteContentContext = {
316
+ ...baseContext,
317
+ rawDocs: 'x'.repeat(200) + 'color brand guide',
318
+ brand: undefined,
319
+ };
320
+ expect(() => validateWebsiteContextOrThrow(ctx, 'gateco')).toThrow('primary color');
321
+ });
322
+
323
+ it('fails when logo found but output path not resolved', () => {
324
+ const ctx: WebsiteContentContext = {
325
+ ...baseContext,
326
+ brand: { logoPath: '/some/logo.png' },
327
+ brandAssets: undefined,
328
+ };
329
+ expect(() => validateWebsiteContextOrThrow(ctx, 'gateco')).toThrow('output path not resolved');
330
+ });
222
331
  });
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Tests for website-debug module
3
+ * Verifies trace includes sections + validation info
4
+ */
5
+
6
+ import { describe, it, expect } from 'vitest';
7
+ import { formatDebugTrace, type WebsiteDebugTrace } from '../../src/generators/website-debug.js';
8
+
9
+ function makeTrace(overrides?: Partial<WebsiteDebugTrace>): WebsiteDebugTrace {
10
+ return {
11
+ workspaceRoot: '/tmp/project',
12
+ docsFound: [{ path: '/tmp/project/spec.md', size: 1000 }],
13
+ brandAssets: { logoPath: '/tmp/logo.png', logoOutputPath: 'public/brand/logo.png' },
14
+ productName: { value: 'Gateco', source: 'docs' },
15
+ primaryColor: { value: '#2563EB', source: 'brand-docs' },
16
+ strategyStatus: 'success',
17
+ templateValues: { headline: 'Secure AI', features: 3, pricingTiers: 3 },
18
+ sectionsRendered: [
19
+ { name: 'Hero', dataSource: 'strategy', itemCount: 1 },
20
+ { name: 'PainPoints', dataSource: 'strategy', itemCount: 3 },
21
+ { name: 'Features', dataSource: 'docs', itemCount: 3 },
22
+ { name: 'FAQ', dataSource: 'skipped', itemCount: 0 },
23
+ ],
24
+ validationPassed: true,
25
+ validationIssues: [],
26
+ ...overrides,
27
+ };
28
+ }
29
+
30
+ describe('formatDebugTrace', () => {
31
+ it('formats basic trace fields', () => {
32
+ const output = formatDebugTrace(makeTrace());
33
+ expect(output).toContain('WEBSITE GENERATION DEBUG TRACE');
34
+ expect(output).toContain('Gateco');
35
+ expect(output).toContain('#2563EB');
36
+ expect(output).toContain('success');
37
+ });
38
+
39
+ it('includes sections rendered with data sources', () => {
40
+ const output = formatDebugTrace(makeTrace());
41
+ expect(output).toContain('Sections Rendered (4)');
42
+ expect(output).toContain('Hero: strategy (1 items)');
43
+ expect(output).toContain('PainPoints: strategy (3 items)');
44
+ expect(output).toContain('Features: docs (3 items)');
45
+ expect(output).toContain('FAQ: skipped (0 items)');
46
+ });
47
+
48
+ it('shows validation passed', () => {
49
+ const output = formatDebugTrace(makeTrace());
50
+ expect(output).toContain('Validation: PASSED');
51
+ });
52
+
53
+ it('shows validation failed with issues', () => {
54
+ const output = formatDebugTrace(makeTrace({
55
+ validationPassed: false,
56
+ validationIssues: ['Strategy missing', 'No features found'],
57
+ }));
58
+ expect(output).toContain('Validation: FAILED');
59
+ expect(output).toContain('Strategy missing');
60
+ expect(output).toContain('No features found');
61
+ });
62
+
63
+ it('handles empty sections list', () => {
64
+ const output = formatDebugTrace(makeTrace({ sectionsRendered: [] }));
65
+ expect(output).toContain('Sections Rendered (0)');
66
+ expect(output).toContain('(none)');
67
+ });
68
+
69
+ it('shows strategy error when present', () => {
70
+ const output = formatDebugTrace(makeTrace({
71
+ strategyStatus: 'failed',
72
+ strategyError: 'Rate limit exceeded',
73
+ }));
74
+ expect(output).toContain('Strategy: failed');
75
+ expect(output).toContain('Error: Rate limit exceeded');
76
+ });
77
+ });