popeye-cli 1.4.7 → 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 (214) hide show
  1. package/CHANGELOG.md +54 -0
  2. package/README.md +264 -63
  3. package/dist/adapters/gemini.d.ts +1 -0
  4. package/dist/adapters/gemini.d.ts.map +1 -1
  5. package/dist/adapters/gemini.js +9 -4
  6. package/dist/adapters/gemini.js.map +1 -1
  7. package/dist/adapters/grok.d.ts +1 -0
  8. package/dist/adapters/grok.d.ts.map +1 -1
  9. package/dist/adapters/grok.js +9 -4
  10. package/dist/adapters/grok.js.map +1 -1
  11. package/dist/adapters/openai.d.ts +1 -1
  12. package/dist/adapters/openai.d.ts.map +1 -1
  13. package/dist/adapters/openai.js +35 -9
  14. package/dist/adapters/openai.js.map +1 -1
  15. package/dist/cli/commands/create.d.ts.map +1 -1
  16. package/dist/cli/commands/create.js +54 -4
  17. package/dist/cli/commands/create.js.map +1 -1
  18. package/dist/cli/interactive.d.ts +29 -0
  19. package/dist/cli/interactive.d.ts.map +1 -1
  20. package/dist/cli/interactive.js +132 -7
  21. package/dist/cli/interactive.js.map +1 -1
  22. package/dist/generators/all.d.ts +8 -2
  23. package/dist/generators/all.d.ts.map +1 -1
  24. package/dist/generators/all.js +37 -316
  25. package/dist/generators/all.js.map +1 -1
  26. package/dist/generators/doc-parser.d.ts +64 -0
  27. package/dist/generators/doc-parser.d.ts.map +1 -0
  28. package/dist/generators/doc-parser.js +407 -0
  29. package/dist/generators/doc-parser.js.map +1 -0
  30. package/dist/generators/frontend-design-analyzer.d.ts +30 -0
  31. package/dist/generators/frontend-design-analyzer.d.ts.map +1 -0
  32. package/dist/generators/frontend-design-analyzer.js +208 -0
  33. package/dist/generators/frontend-design-analyzer.js.map +1 -0
  34. package/dist/generators/shared-packages.d.ts +45 -0
  35. package/dist/generators/shared-packages.d.ts.map +1 -0
  36. package/dist/generators/shared-packages.js +456 -0
  37. package/dist/generators/shared-packages.js.map +1 -0
  38. package/dist/generators/templates/index.d.ts +8 -0
  39. package/dist/generators/templates/index.d.ts.map +1 -1
  40. package/dist/generators/templates/index.js +8 -0
  41. package/dist/generators/templates/index.js.map +1 -1
  42. package/dist/generators/templates/website-components.d.ts +33 -0
  43. package/dist/generators/templates/website-components.d.ts.map +1 -0
  44. package/dist/generators/templates/website-components.js +303 -0
  45. package/dist/generators/templates/website-components.js.map +1 -0
  46. package/dist/generators/templates/website-config.d.ts +55 -0
  47. package/dist/generators/templates/website-config.d.ts.map +1 -0
  48. package/dist/generators/templates/website-config.js +425 -0
  49. package/dist/generators/templates/website-config.js.map +1 -0
  50. package/dist/generators/templates/website-conversion.d.ts +27 -0
  51. package/dist/generators/templates/website-conversion.d.ts.map +1 -0
  52. package/dist/generators/templates/website-conversion.js +326 -0
  53. package/dist/generators/templates/website-conversion.js.map +1 -0
  54. package/dist/generators/templates/website-landing.d.ts +24 -0
  55. package/dist/generators/templates/website-landing.d.ts.map +1 -0
  56. package/dist/generators/templates/website-landing.js +276 -0
  57. package/dist/generators/templates/website-landing.js.map +1 -0
  58. package/dist/generators/templates/website-layout.d.ts +42 -0
  59. package/dist/generators/templates/website-layout.d.ts.map +1 -0
  60. package/dist/generators/templates/website-layout.js +408 -0
  61. package/dist/generators/templates/website-layout.js.map +1 -0
  62. package/dist/generators/templates/website-pricing.d.ts +11 -0
  63. package/dist/generators/templates/website-pricing.d.ts.map +1 -0
  64. package/dist/generators/templates/website-pricing.js +313 -0
  65. package/dist/generators/templates/website-pricing.js.map +1 -0
  66. package/dist/generators/templates/website-sections.d.ts +102 -0
  67. package/dist/generators/templates/website-sections.d.ts.map +1 -0
  68. package/dist/generators/templates/website-sections.js +444 -0
  69. package/dist/generators/templates/website-sections.js.map +1 -0
  70. package/dist/generators/templates/website-seo.d.ts +76 -0
  71. package/dist/generators/templates/website-seo.d.ts.map +1 -0
  72. package/dist/generators/templates/website-seo.js +326 -0
  73. package/dist/generators/templates/website-seo.js.map +1 -0
  74. package/dist/generators/templates/website.d.ts +10 -83
  75. package/dist/generators/templates/website.d.ts.map +1 -1
  76. package/dist/generators/templates/website.js +12 -875
  77. package/dist/generators/templates/website.js.map +1 -1
  78. package/dist/generators/website-content-scanner.d.ts +37 -0
  79. package/dist/generators/website-content-scanner.d.ts.map +1 -0
  80. package/dist/generators/website-content-scanner.js +165 -0
  81. package/dist/generators/website-content-scanner.js.map +1 -0
  82. package/dist/generators/website-context.d.ts +119 -0
  83. package/dist/generators/website-context.d.ts.map +1 -0
  84. package/dist/generators/website-context.js +350 -0
  85. package/dist/generators/website-context.js.map +1 -0
  86. package/dist/generators/website-debug.d.ts +68 -0
  87. package/dist/generators/website-debug.d.ts.map +1 -0
  88. package/dist/generators/website-debug.js +93 -0
  89. package/dist/generators/website-debug.js.map +1 -0
  90. package/dist/generators/website.d.ts +5 -0
  91. package/dist/generators/website.d.ts.map +1 -1
  92. package/dist/generators/website.js +136 -11
  93. package/dist/generators/website.js.map +1 -1
  94. package/dist/generators/workspace-root.d.ts +27 -0
  95. package/dist/generators/workspace-root.d.ts.map +1 -0
  96. package/dist/generators/workspace-root.js +100 -0
  97. package/dist/generators/workspace-root.js.map +1 -0
  98. package/dist/state/index.d.ts +35 -0
  99. package/dist/state/index.d.ts.map +1 -1
  100. package/dist/state/index.js +40 -0
  101. package/dist/state/index.js.map +1 -1
  102. package/dist/types/consensus.d.ts +3 -0
  103. package/dist/types/consensus.d.ts.map +1 -1
  104. package/dist/types/consensus.js +1 -0
  105. package/dist/types/consensus.js.map +1 -1
  106. package/dist/types/website-strategy.d.ts +263 -0
  107. package/dist/types/website-strategy.d.ts.map +1 -0
  108. package/dist/types/website-strategy.js +105 -0
  109. package/dist/types/website-strategy.js.map +1 -0
  110. package/dist/types/workflow.d.ts +21 -0
  111. package/dist/types/workflow.d.ts.map +1 -1
  112. package/dist/types/workflow.js +8 -0
  113. package/dist/types/workflow.js.map +1 -1
  114. package/dist/upgrade/handlers.d.ts +15 -0
  115. package/dist/upgrade/handlers.d.ts.map +1 -1
  116. package/dist/upgrade/handlers.js +52 -0
  117. package/dist/upgrade/handlers.js.map +1 -1
  118. package/dist/workflow/auto-fix-bundler.d.ts +37 -0
  119. package/dist/workflow/auto-fix-bundler.d.ts.map +1 -0
  120. package/dist/workflow/auto-fix-bundler.js +320 -0
  121. package/dist/workflow/auto-fix-bundler.js.map +1 -0
  122. package/dist/workflow/auto-fix.d.ts.map +1 -1
  123. package/dist/workflow/auto-fix.js +10 -3
  124. package/dist/workflow/auto-fix.js.map +1 -1
  125. package/dist/workflow/consensus.d.ts.map +1 -1
  126. package/dist/workflow/consensus.js +2 -0
  127. package/dist/workflow/consensus.js.map +1 -1
  128. package/dist/workflow/execution-mode.d.ts.map +1 -1
  129. package/dist/workflow/execution-mode.js +18 -0
  130. package/dist/workflow/execution-mode.js.map +1 -1
  131. package/dist/workflow/index.d.ts +4 -0
  132. package/dist/workflow/index.d.ts.map +1 -1
  133. package/dist/workflow/index.js +37 -0
  134. package/dist/workflow/index.js.map +1 -1
  135. package/dist/workflow/overview.d.ts +89 -0
  136. package/dist/workflow/overview.d.ts.map +1 -0
  137. package/dist/workflow/overview.js +358 -0
  138. package/dist/workflow/overview.js.map +1 -0
  139. package/dist/workflow/plan-mode.d.ts +6 -4
  140. package/dist/workflow/plan-mode.d.ts.map +1 -1
  141. package/dist/workflow/plan-mode.js +148 -6
  142. package/dist/workflow/plan-mode.js.map +1 -1
  143. package/dist/workflow/website-strategy.d.ts +79 -0
  144. package/dist/workflow/website-strategy.d.ts.map +1 -0
  145. package/dist/workflow/website-strategy.js +310 -0
  146. package/dist/workflow/website-strategy.js.map +1 -0
  147. package/dist/workflow/website-updater.d.ts +17 -0
  148. package/dist/workflow/website-updater.d.ts.map +1 -0
  149. package/dist/workflow/website-updater.js +116 -0
  150. package/dist/workflow/website-updater.js.map +1 -0
  151. package/dist/workflow/workflow-logger.d.ts +1 -1
  152. package/dist/workflow/workflow-logger.d.ts.map +1 -1
  153. package/dist/workflow/workflow-logger.js.map +1 -1
  154. package/package.json +1 -1
  155. package/src/adapters/gemini.ts +10 -4
  156. package/src/adapters/grok.ts +10 -4
  157. package/src/adapters/openai.ts +38 -6
  158. package/src/cli/commands/create.ts +58 -4
  159. package/src/cli/interactive.ts +143 -7
  160. package/src/generators/all.ts +49 -332
  161. package/src/generators/doc-parser.ts +449 -0
  162. package/src/generators/frontend-design-analyzer.ts +261 -0
  163. package/src/generators/shared-packages.ts +500 -0
  164. package/src/generators/templates/index.ts +8 -0
  165. package/src/generators/templates/website-components.ts +330 -0
  166. package/src/generators/templates/website-config.ts +444 -0
  167. package/src/generators/templates/website-conversion.ts +341 -0
  168. package/src/generators/templates/website-landing.ts +331 -0
  169. package/src/generators/templates/website-layout.ts +443 -0
  170. package/src/generators/templates/website-pricing.ts +330 -0
  171. package/src/generators/templates/website-sections.ts +541 -0
  172. package/src/generators/templates/website-seo.ts +370 -0
  173. package/src/generators/templates/website.ts +38 -905
  174. package/src/generators/website-content-scanner.ts +208 -0
  175. package/src/generators/website-context.ts +493 -0
  176. package/src/generators/website-debug.ts +130 -0
  177. package/src/generators/website.ts +178 -20
  178. package/src/generators/workspace-root.ts +113 -0
  179. package/src/state/index.ts +56 -0
  180. package/src/types/consensus.ts +3 -0
  181. package/src/types/website-strategy.ts +243 -0
  182. package/src/types/workflow.ts +21 -0
  183. package/src/upgrade/handlers.ts +65 -0
  184. package/src/workflow/auto-fix-bundler.ts +392 -0
  185. package/src/workflow/auto-fix.ts +11 -3
  186. package/src/workflow/consensus.ts +2 -0
  187. package/src/workflow/execution-mode.ts +21 -0
  188. package/src/workflow/index.ts +37 -0
  189. package/src/workflow/overview.ts +475 -0
  190. package/src/workflow/plan-mode.ts +193 -8
  191. package/src/workflow/website-strategy.ts +379 -0
  192. package/src/workflow/website-updater.ts +142 -0
  193. package/src/workflow/workflow-logger.ts +1 -0
  194. package/tests/adapters/persona-switching.test.ts +63 -0
  195. package/tests/cli/project-naming.test.ts +136 -0
  196. package/tests/generators/doc-parser.test.ts +121 -0
  197. package/tests/generators/frontend-design-analyzer.test.ts +90 -0
  198. package/tests/generators/quality-gate.test.ts +183 -0
  199. package/tests/generators/shared-packages.test.ts +83 -0
  200. package/tests/generators/website-components.test.ts +159 -0
  201. package/tests/generators/website-config.test.ts +84 -0
  202. package/tests/generators/website-content-scanner.test.ts +181 -0
  203. package/tests/generators/website-context.test.ts +331 -0
  204. package/tests/generators/website-debug.test.ts +77 -0
  205. package/tests/generators/website-landing.test.ts +188 -0
  206. package/tests/generators/website-pricing.test.ts +98 -0
  207. package/tests/generators/website-sections.test.ts +245 -0
  208. package/tests/generators/website-seo-quality.test.ts +246 -0
  209. package/tests/generators/workspace-root.test.ts +105 -0
  210. package/tests/upgrade/handlers.test.ts +162 -0
  211. package/tests/workflow/auto-fix-bundler.test.ts +242 -0
  212. package/tests/workflow/overview.test.ts +392 -0
  213. package/tests/workflow/plan-mode.test.ts +111 -1
  214. package/tests/workflow/website-strategy.test.ts +246 -0
@@ -0,0 +1,136 @@
1
+ /**
2
+ * Tests for project naming logic
3
+ * Verifies CWD-aware naming, doc-derived names, and fallback behavior
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 {
11
+ generateProjectName,
12
+ generateProjectNameFromIdea,
13
+ extractNameFromDocs,
14
+ } from '../../src/cli/interactive.js';
15
+
16
+ let tmpDir: string;
17
+
18
+ beforeEach(async () => {
19
+ tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'popeye-naming-'));
20
+ });
21
+
22
+ afterEach(async () => {
23
+ await fs.rm(tmpDir, { recursive: true, force: true });
24
+ });
25
+
26
+ describe('generateProjectNameFromIdea', () => {
27
+ it('should extract explicit project name patterns', () => {
28
+ // "named" pattern extracts the word after it
29
+ expect(generateProjectNameFromIdea("Create an app named 'Gateco'")).toBe('gateco');
30
+ // "called" pattern extracts the word after it
31
+ expect(generateProjectNameFromIdea('Build something called TodoApp')).toBe('todo-app');
32
+ });
33
+
34
+ it('should extract CamelCase project names', () => {
35
+ // Without "for" keyword that triggers the explicit pattern, CamelCase is detected
36
+ expect(generateProjectNameFromIdea('Build TodoMaster with style')).toBe('todo-master');
37
+ });
38
+
39
+ it('should filter out action words from prompt text', () => {
40
+ // "read all files" should NOT produce "read-all-files" since those are stop words
41
+ const result = generateProjectNameFromIdea('read all files');
42
+ expect(result).not.toBe('read-all-files');
43
+ // Meaningful word extraction filters all three, but the fallback takes first 2 raw words
44
+ expect(result).toBe('read-all');
45
+ });
46
+
47
+ it('should extract meaningful words when no explicit name found', () => {
48
+ const result = generateProjectNameFromIdea('secure enterprise authentication system');
49
+ expect(result).toBe('secure-enterprise-authentication');
50
+ });
51
+
52
+ it('should return my-project as last resort', () => {
53
+ expect(generateProjectNameFromIdea('')).toBe('my-project');
54
+ });
55
+ });
56
+
57
+ describe('extractNameFromDocs', () => {
58
+ it('should extract product name from markdown heading', async () => {
59
+ const docPath = path.join(tmpDir, 'Gateco-spec.md');
60
+ await fs.writeFile(docPath, '# Gateco\n\nSome description here.');
61
+
62
+ const name = await extractNameFromDocs(tmpDir);
63
+ expect(name).toBe('Gateco');
64
+ });
65
+
66
+ it('should skip README files', async () => {
67
+ await fs.writeFile(path.join(tmpDir, 'README.md'), '# MyProject\n\nDescription.');
68
+
69
+ const name = await extractNameFromDocs(tmpDir);
70
+ expect(name).toBeNull();
71
+ });
72
+
73
+ it('should return null when no docs found', async () => {
74
+ const name = await extractNameFromDocs(tmpDir);
75
+ expect(name).toBeNull();
76
+ });
77
+
78
+ it('should return null for non-existent directory', async () => {
79
+ const name = await extractNameFromDocs('/nonexistent/path/123456');
80
+ expect(name).toBeNull();
81
+ });
82
+
83
+ it('should ignore generic headings', async () => {
84
+ // "Home" is too short, but length >= 3 so it would pass... let's test with a generic dir name
85
+ await fs.writeFile(path.join(tmpDir, 'spec.md'), '# Src\n\nDescription.');
86
+
87
+ const name = await extractNameFromDocs(tmpDir);
88
+ // "Src" is in GENERIC_DIR_NAMES
89
+ expect(name).toBeNull();
90
+ });
91
+ });
92
+
93
+ describe('generateProjectName (CWD-aware)', () => {
94
+ it('should prefer doc-derived name over CWD basename', async () => {
95
+ const projectDir = path.join(tmpDir, 'SomeDir');
96
+ await fs.mkdir(projectDir, { recursive: true });
97
+ await fs.writeFile(
98
+ path.join(projectDir, 'product-spec.md'),
99
+ '# Gateco\n\nA security layer for AI.'
100
+ );
101
+
102
+ const name = await generateProjectName('read all files', projectDir);
103
+ expect(name).toBe('gateco');
104
+ });
105
+
106
+ it('should use CWD basename when no docs found', async () => {
107
+ const projectDir = path.join(tmpDir, 'MyAwesomeApp');
108
+ await fs.mkdir(projectDir, { recursive: true });
109
+
110
+ const name = await generateProjectName('read all files', projectDir);
111
+ expect(name).toBe('my-awesome-app');
112
+ });
113
+
114
+ it('should skip generic CWD names and fall back to idea', async () => {
115
+ const projectDir = path.join(tmpDir, 'Projects');
116
+ await fs.mkdir(projectDir, { recursive: true });
117
+
118
+ // "Projects" is a generic dir name, so falls back to idea extraction
119
+ // The idea contains "named SuperApp" pattern -> extracts "SuperApp"
120
+ const name = await generateProjectName('Build an app named SuperApp', projectDir);
121
+ expect(name).toBe('super-app');
122
+ });
123
+
124
+ it('should fall back to idea extraction when no CWD provided', async () => {
125
+ const name = await generateProjectName('Build an app called Gateco');
126
+ expect(name).toBe('gateco');
127
+ });
128
+
129
+ it('should handle CamelCase CWD basenames', async () => {
130
+ const projectDir = path.join(tmpDir, 'MyGateco');
131
+ await fs.mkdir(projectDir, { recursive: true });
132
+
133
+ const name = await generateProjectName('some idea', projectDir);
134
+ expect(name).toBe('my-gateco');
135
+ });
136
+ });
@@ -0,0 +1,121 @@
1
+ /**
2
+ * Tests for doc-parser module
3
+ * Verifies feature extraction, dev-task filtering, docs-first priority
4
+ */
5
+
6
+ import { describe, it, expect } from 'vitest';
7
+ import {
8
+ extractFeatures,
9
+ extractProductName,
10
+ extractTagline,
11
+ extractDescription,
12
+ extractPrimaryColor,
13
+ extractPricing,
14
+ isSuspiciousProductName,
15
+ } from '../../src/generators/doc-parser.js';
16
+
17
+ describe('extractFeatures', () => {
18
+ it('extracts features from docs bullet list', () => {
19
+ const docs = `# Product\n## Features\n- **Fast Search** - Lightning fast search\n- **Auth** - Built-in authentication`;
20
+ const features = extractFeatures(docs);
21
+ expect(features.length).toBeGreaterThanOrEqual(2);
22
+ expect(features[0].title).toBe('Fast Search');
23
+ });
24
+
25
+ it('extracts from docs before specification', () => {
26
+ const docs = `# Product\n## Features\n- **Real Feature** - From docs`;
27
+ const spec = `## Features\n- Implement login page\n- Fix CSS bug`;
28
+ const features = extractFeatures(docs, spec);
29
+ // Should use docs features, not spec dev tasks
30
+ expect(features[0].title).toBe('Real Feature');
31
+ });
32
+
33
+ it('falls back to specification when docs have no features', () => {
34
+ const docs = `# Product\n## About\nJust a description.`;
35
+ const spec = `## Core Design Principles\n1. **Vector DB agnostic**\n2. **Embedding agnostic**`;
36
+ const features = extractFeatures(docs, spec);
37
+ expect(features.length).toBeGreaterThanOrEqual(2);
38
+ expect(features[0].title).toContain('Vector DB');
39
+ });
40
+
41
+ it('filters out dev-task verbs', () => {
42
+ const docs = `# Tasks\n## Features\n- Implement user auth\n- Fix broken login\n- **Real Feature** - Does real stuff\n- Refactor database layer`;
43
+ const features = extractFeatures(docs);
44
+ // Dev tasks should be filtered out
45
+ expect(features.some(f => f.title.startsWith('Implement'))).toBe(false);
46
+ expect(features.some(f => f.title.startsWith('Fix'))).toBe(false);
47
+ expect(features.some(f => f.title.startsWith('Refactor'))).toBe(false);
48
+ // Real feature should remain
49
+ expect(features.some(f => f.title === 'Real Feature')).toBe(true);
50
+ });
51
+
52
+ it('returns empty when no feature sections exist', () => {
53
+ const docs = `# README\nJust a readme file.`;
54
+ const features = extractFeatures(docs);
55
+ expect(features).toEqual([]);
56
+ });
57
+
58
+ it('limits features to 6', () => {
59
+ const items = Array.from({ length: 10 }, (_, i) => `- **Feature ${i + 1}** - Description ${i + 1}`).join('\n');
60
+ const docs = `## Features\n${items}`;
61
+ const features = extractFeatures(docs);
62
+ expect(features.length).toBeLessThanOrEqual(6);
63
+ });
64
+ });
65
+
66
+ describe('extractProductName', () => {
67
+ it('extracts from "# Name -- tagline" heading', () => {
68
+ const docs = '# Gateco -- Permission-Aware Retrieval';
69
+ expect(extractProductName(docs)).toBe('Gateco');
70
+ });
71
+
72
+ it('picks shortest name when multiple headings', () => {
73
+ const docs = '# Gateco -- main product\n# Gateco UI Color System -- colors';
74
+ expect(extractProductName(docs)).toBe('Gateco');
75
+ });
76
+
77
+ it('returns undefined when no match', () => {
78
+ expect(extractProductName('Just some text')).toBeUndefined();
79
+ });
80
+ });
81
+
82
+ describe('extractPrimaryColor', () => {
83
+ it('extracts accent-primary token', () => {
84
+ const docs = '| `accent-primary` | `#2563EB` | Primary CTA |';
85
+ expect(extractPrimaryColor(docs)).toBe('#2563EB');
86
+ });
87
+
88
+ it('skips dark background colors', () => {
89
+ const docs = '| bg | #0F172A |\n| accent-primary | #2563EB |';
90
+ expect(extractPrimaryColor(docs)).toBe('#2563EB');
91
+ });
92
+ });
93
+
94
+ describe('isSuspiciousProductName', () => {
95
+ it('flags directory-like names', () => {
96
+ expect(isSuspiciousProductName('my-cool-project')).toBe(true);
97
+ expect(isSuspiciousProductName('my-app')).toBe(true);
98
+ });
99
+
100
+ it('allows real product names', () => {
101
+ expect(isSuspiciousProductName('Gateco')).toBe(false);
102
+ expect(isSuspiciousProductName('SuperApp')).toBe(false);
103
+ });
104
+ });
105
+
106
+ describe('extractPricing', () => {
107
+ it('extracts pricing tiers from markdown table', () => {
108
+ const docs = `## Pricing\n| Plan | Price |\n|---|---|\n| Free | Free |\n| Pro | $99/month minimum |\n| Enterprise | Custom pricing |`;
109
+ const tiers = extractPricing(docs);
110
+ expect(tiers).toBeDefined();
111
+ expect(tiers!.length).toBe(3);
112
+ expect(tiers![0].name).toContain('Free');
113
+ expect(tiers![1].price).toBe('$99');
114
+ expect(tiers![2].price).toBe('Custom');
115
+ });
116
+
117
+ it('returns undefined when no pricing found', () => {
118
+ const docs = '# Product\nJust a product.';
119
+ expect(extractPricing(docs)).toBeUndefined();
120
+ });
121
+ });
@@ -0,0 +1,90 @@
1
+ /**
2
+ * Tests for frontend design language analyzer
3
+ * Verifies CSS variable extraction, component library detection, and tailwind parsing
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 { analyzeFrontendDesign } from '../../src/generators/frontend-design-analyzer.js';
11
+
12
+ describe('analyzeFrontendDesign', () => {
13
+ let tmpDir: string;
14
+
15
+ beforeEach(async () => {
16
+ tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'popeye-fe-design-'));
17
+ });
18
+
19
+ afterEach(async () => {
20
+ await fs.rm(tmpDir, { recursive: true, force: true });
21
+ });
22
+
23
+ it('returns null when no frontend app directory exists', async () => {
24
+ const result = await analyzeFrontendDesign(tmpDir);
25
+
26
+ expect(result).toBeNull();
27
+ });
28
+
29
+ it('extracts CSS custom properties from index.css', async () => {
30
+ const frontendDir = path.join(tmpDir, 'apps', 'frontend', 'src');
31
+ await fs.mkdir(frontendDir, { recursive: true });
32
+ await fs.writeFile(
33
+ path.join(frontendDir, 'index.css'),
34
+ `:root {
35
+ --primary: 222.2 47.4% 11.2%;
36
+ --radius: 0.5rem;
37
+ }
38
+
39
+ .dark {
40
+ --primary: 210 40% 98%;
41
+ }
42
+ `
43
+ );
44
+ // Need package.json for component lib detection
45
+ await fs.writeFile(
46
+ path.join(tmpDir, 'apps', 'frontend', 'package.json'),
47
+ JSON.stringify({ dependencies: { '@shadcn/ui': '^1.0.0' } })
48
+ );
49
+
50
+ const result = await analyzeFrontendDesign(tmpDir);
51
+
52
+ expect(result).not.toBeNull();
53
+ expect(result!.primaryColor).toBeDefined();
54
+ expect(result!.borderRadius).toBe('0.5rem');
55
+ expect(result!.darkMode).toBe(true);
56
+ expect(result!.source).toBe('css-variables');
57
+ });
58
+
59
+ it('detects shadcn component library from package.json', async () => {
60
+ const frontendDir = path.join(tmpDir, 'apps', 'frontend');
61
+ await fs.mkdir(frontendDir, { recursive: true });
62
+ await fs.writeFile(
63
+ path.join(frontendDir, 'package.json'),
64
+ JSON.stringify({
65
+ dependencies: { '@shadcn/ui': '^1.0.0', react: '^18.0.0' },
66
+ })
67
+ );
68
+
69
+ const result = await analyzeFrontendDesign(tmpDir);
70
+
71
+ expect(result).not.toBeNull();
72
+ expect(result!.componentLibrary).toBe('shadcn');
73
+ });
74
+
75
+ it('detects MUI component library', async () => {
76
+ const frontendDir = path.join(tmpDir, 'apps', 'frontend');
77
+ await fs.mkdir(frontendDir, { recursive: true });
78
+ await fs.writeFile(
79
+ path.join(frontendDir, 'package.json'),
80
+ JSON.stringify({
81
+ dependencies: { '@mui/material': '^5.0.0', react: '^18.0.0' },
82
+ })
83
+ );
84
+
85
+ const result = await analyzeFrontendDesign(tmpDir);
86
+
87
+ expect(result).not.toBeNull();
88
+ expect(result!.componentLibrary).toBe('mui');
89
+ });
90
+ });
@@ -0,0 +1,183 @@
1
+ /**
2
+ * Tests for website content quality gate
3
+ * Verifies that validateWebsiteContextOrThrow blocks generic websites
4
+ * and validateWebsiteContext returns structured soft validation results
5
+ */
6
+
7
+ import { describe, it, expect } from 'vitest';
8
+ import {
9
+ validateWebsiteContextOrThrow,
10
+ validateWebsiteContext,
11
+ } from '../../src/generators/website-context.js';
12
+ import type { WebsiteContentContext } from '../../src/generators/website-context.js';
13
+
14
+ function makeContext(overrides: Partial<WebsiteContentContext> = {}): WebsiteContentContext {
15
+ return {
16
+ productName: 'Gateco',
17
+ features: [
18
+ { title: 'Access Control', description: 'Fine-grained permissions' },
19
+ { title: 'Vector DB Agnostic', description: 'Works with any vector database' },
20
+ ],
21
+ rawDocs: '--- spec.md ---\n# Gateco\nLong enough content to pass validation threshold.\n' + 'x'.repeat(100),
22
+ strategy: {
23
+ icp: { primaryPersona: 'devs', painPoints: [], goals: [], objections: [] },
24
+ positioning: { category: 'Security', differentiators: [], valueProposition: 'Secure AI', proofPoints: [] },
25
+ messaging: { headline: 'h', subheadline: 's', elevatorPitch: 'e', longDescription: 'l' },
26
+ seoStrategy: { primaryKeywords: [], secondaryKeywords: [], longTailKeywords: [], titleTemplates: {}, metaDescriptions: {} },
27
+ siteArchitecture: { pages: [], navigation: [], footerSections: [] },
28
+ conversionStrategy: { primaryCta: { text: 'Go', href: '/' }, secondaryCta: { text: 'More', href: '/' }, trustSignals: [], socialProof: [], leadCapture: 'none' },
29
+ competitiveContext: { category: 'sec', competitors: [], differentiators: [] },
30
+ },
31
+ ...overrides,
32
+ };
33
+ }
34
+
35
+ describe('validateWebsiteContextOrThrow', () => {
36
+ it('passes validation with valid product context', () => {
37
+ const context = makeContext();
38
+
39
+ expect(() => validateWebsiteContextOrThrow(context, 'gateco')).not.toThrow();
40
+ });
41
+
42
+ it('throws on suspicious product name (directory-like)', () => {
43
+ const context = makeContext({ productName: 'read-all-files' });
44
+
45
+ expect(() => validateWebsiteContextOrThrow(context, 'read-all-files')).toThrow(
46
+ /looks like a directory name/
47
+ );
48
+ });
49
+
50
+ it('throws when no docs found', () => {
51
+ const context = makeContext({ rawDocs: '' });
52
+
53
+ expect(() => validateWebsiteContextOrThrow(context, 'test')).toThrow(
54
+ /No project documentation found/
55
+ );
56
+ });
57
+
58
+ it('throws when no features extracted', () => {
59
+ const context = makeContext({ features: [] });
60
+
61
+ expect(() => validateWebsiteContextOrThrow(context, 'test')).toThrow(
62
+ /No product features extracted/
63
+ );
64
+ });
65
+
66
+ it('includes POPEYE_DEBUG_WEBSITE hint in error message', () => {
67
+ const context = makeContext({ rawDocs: '' });
68
+
69
+ try {
70
+ validateWebsiteContextOrThrow(context, 'test');
71
+ expect.fail('Should have thrown');
72
+ } catch (error) {
73
+ expect((error as Error).message).toContain('POPEYE_DEBUG_WEBSITE=1');
74
+ }
75
+ });
76
+
77
+ it('still throws when passed is false from validateWebsiteContext', () => {
78
+ // Context with multiple blocking issues
79
+ const context = makeContext({
80
+ productName: 'my-app',
81
+ rawDocs: '',
82
+ features: [],
83
+ strategy: undefined,
84
+ });
85
+
86
+ expect(() => validateWebsiteContextOrThrow(context, 'my-app')).toThrow(
87
+ /Website generation blocked/
88
+ );
89
+ });
90
+ });
91
+
92
+ describe('validateWebsiteContext (soft mode)', () => {
93
+ it('returns passed=true with no issues for valid context', () => {
94
+ const context = makeContext({ tagline: 'Secure your AI' });
95
+ const result = validateWebsiteContext(context, 'gateco');
96
+
97
+ expect(result.passed).toBe(true);
98
+ expect(result.issues).toHaveLength(0);
99
+ expect(result.contentScore).toBeGreaterThanOrEqual(90);
100
+ });
101
+
102
+ it('returns warnings without marking passed=false', () => {
103
+ // Valid context but missing tagline triggers a warning, not an issue
104
+ const context = makeContext({ tagline: undefined });
105
+ const result = validateWebsiteContext(context, 'gateco');
106
+
107
+ expect(result.passed).toBe(true);
108
+ expect(result.warnings.length).toBeGreaterThan(0);
109
+ expect(result.warnings.some((w) => /tagline/i.test(w))).toBe(true);
110
+ });
111
+
112
+ it('detects default pricing fingerprint as a warning', () => {
113
+ const context = makeContext({
114
+ pricing: [
115
+ { name: 'Free', price: '$0', description: 'Basic', features: ['1 user'], cta: 'Start' },
116
+ { name: 'Pro', price: '$29', description: 'Team', features: ['10 users'], cta: 'Go', featured: true },
117
+ { name: 'Enterprise', price: 'Custom', description: 'Scale', features: ['Unlimited'], cta: 'Contact' },
118
+ ],
119
+ });
120
+ const result = validateWebsiteContext(context, 'gateco');
121
+
122
+ expect(result.passed).toBe(true);
123
+ expect(result.warnings.some((w) => /default values/i.test(w))).toBe(true);
124
+ });
125
+
126
+ it('does not flag custom pricing as default', () => {
127
+ const context = makeContext({
128
+ pricing: [
129
+ { name: 'Hobby', price: '$9', description: 'For side projects', features: ['5 APIs'], cta: 'Start' },
130
+ { name: 'Team', price: '$49', description: 'For teams', features: ['50 APIs'], cta: 'Go' },
131
+ ],
132
+ });
133
+ const result = validateWebsiteContext(context, 'gateco');
134
+
135
+ expect(result.warnings.some((w) => /default values/i.test(w))).toBe(false);
136
+ });
137
+
138
+ it('decreases contentScore with more defaults', () => {
139
+ const minimal = makeContext({
140
+ tagline: undefined,
141
+ description: undefined,
142
+ brand: undefined,
143
+ });
144
+ const rich = makeContext({
145
+ tagline: 'Secure your AI',
146
+ description: 'The best AI security platform',
147
+ brand: { primaryColor: '#3B82F6' },
148
+ });
149
+
150
+ const minResult = validateWebsiteContext(minimal, 'gateco');
151
+ const richResult = validateWebsiteContext(rich, 'gateco');
152
+
153
+ expect(richResult.contentScore).toBeGreaterThan(minResult.contentScore);
154
+ });
155
+
156
+ it('returns blocking issues for missing docs', () => {
157
+ const context = makeContext({ rawDocs: '' });
158
+ const result = validateWebsiteContext(context, 'test');
159
+
160
+ expect(result.passed).toBe(false);
161
+ expect(result.issues.some((i) => /documentation/i.test(i))).toBe(true);
162
+ });
163
+
164
+ it('warns about missing description', () => {
165
+ const context = makeContext({ description: undefined });
166
+ const result = validateWebsiteContext(context, 'gateco');
167
+
168
+ expect(result.warnings.some((w) => /description/i.test(w))).toBe(true);
169
+ });
170
+
171
+ it('clamps score to 0 when everything is wrong', () => {
172
+ const context: WebsiteContentContext = {
173
+ productName: 'my-app',
174
+ features: [],
175
+ rawDocs: '',
176
+ strategy: undefined,
177
+ };
178
+ const result = validateWebsiteContext(context, 'my-app');
179
+
180
+ expect(result.contentScore).toBe(0);
181
+ expect(result.passed).toBe(false);
182
+ });
183
+ });
@@ -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
+ });