popeye-cli 1.4.7 → 1.5.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 (134) hide show
  1. package/README.md +222 -63
  2. package/dist/adapters/gemini.d.ts +1 -0
  3. package/dist/adapters/gemini.d.ts.map +1 -1
  4. package/dist/adapters/gemini.js +9 -4
  5. package/dist/adapters/gemini.js.map +1 -1
  6. package/dist/adapters/grok.d.ts +1 -0
  7. package/dist/adapters/grok.d.ts.map +1 -1
  8. package/dist/adapters/grok.js +9 -4
  9. package/dist/adapters/grok.js.map +1 -1
  10. package/dist/adapters/openai.d.ts +1 -1
  11. package/dist/adapters/openai.d.ts.map +1 -1
  12. package/dist/adapters/openai.js +35 -9
  13. package/dist/adapters/openai.js.map +1 -1
  14. package/dist/cli/interactive.d.ts.map +1 -1
  15. package/dist/cli/interactive.js +42 -0
  16. package/dist/cli/interactive.js.map +1 -1
  17. package/dist/generators/all.d.ts +4 -1
  18. package/dist/generators/all.d.ts.map +1 -1
  19. package/dist/generators/all.js +2 -1
  20. package/dist/generators/all.js.map +1 -1
  21. package/dist/generators/doc-parser.d.ts +49 -0
  22. package/dist/generators/doc-parser.d.ts.map +1 -0
  23. package/dist/generators/doc-parser.js +336 -0
  24. package/dist/generators/doc-parser.js.map +1 -0
  25. package/dist/generators/templates/index.d.ts +4 -0
  26. package/dist/generators/templates/index.d.ts.map +1 -1
  27. package/dist/generators/templates/index.js +4 -0
  28. package/dist/generators/templates/index.js.map +1 -1
  29. package/dist/generators/templates/website-components.d.ts +33 -0
  30. package/dist/generators/templates/website-components.d.ts.map +1 -0
  31. package/dist/generators/templates/website-components.js +278 -0
  32. package/dist/generators/templates/website-components.js.map +1 -0
  33. package/dist/generators/templates/website-config.d.ts +41 -0
  34. package/dist/generators/templates/website-config.d.ts.map +1 -0
  35. package/dist/generators/templates/website-config.js +283 -0
  36. package/dist/generators/templates/website-config.js.map +1 -0
  37. package/dist/generators/templates/website-conversion.d.ts +27 -0
  38. package/dist/generators/templates/website-conversion.d.ts.map +1 -0
  39. package/dist/generators/templates/website-conversion.js +326 -0
  40. package/dist/generators/templates/website-conversion.js.map +1 -0
  41. package/dist/generators/templates/website-seo.d.ts +76 -0
  42. package/dist/generators/templates/website-seo.d.ts.map +1 -0
  43. package/dist/generators/templates/website-seo.js +326 -0
  44. package/dist/generators/templates/website-seo.js.map +1 -0
  45. package/dist/generators/templates/website.d.ts +14 -47
  46. package/dist/generators/templates/website.d.ts.map +1 -1
  47. package/dist/generators/templates/website.js +412 -499
  48. package/dist/generators/templates/website.js.map +1 -1
  49. package/dist/generators/website-context.d.ts +83 -0
  50. package/dist/generators/website-context.d.ts.map +1 -0
  51. package/dist/generators/website-context.js +190 -0
  52. package/dist/generators/website-context.js.map +1 -0
  53. package/dist/generators/website.d.ts +3 -0
  54. package/dist/generators/website.d.ts.map +1 -1
  55. package/dist/generators/website.js +73 -10
  56. package/dist/generators/website.js.map +1 -1
  57. package/dist/state/index.d.ts +27 -0
  58. package/dist/state/index.d.ts.map +1 -1
  59. package/dist/state/index.js +30 -0
  60. package/dist/state/index.js.map +1 -1
  61. package/dist/types/consensus.d.ts +3 -0
  62. package/dist/types/consensus.d.ts.map +1 -1
  63. package/dist/types/consensus.js +1 -0
  64. package/dist/types/consensus.js.map +1 -1
  65. package/dist/types/website-strategy.d.ts +263 -0
  66. package/dist/types/website-strategy.d.ts.map +1 -0
  67. package/dist/types/website-strategy.js +105 -0
  68. package/dist/types/website-strategy.js.map +1 -0
  69. package/dist/types/workflow.d.ts +15 -0
  70. package/dist/types/workflow.d.ts.map +1 -1
  71. package/dist/types/workflow.js +6 -0
  72. package/dist/types/workflow.js.map +1 -1
  73. package/dist/workflow/consensus.d.ts.map +1 -1
  74. package/dist/workflow/consensus.js +2 -0
  75. package/dist/workflow/consensus.js.map +1 -1
  76. package/dist/workflow/execution-mode.d.ts.map +1 -1
  77. package/dist/workflow/execution-mode.js +18 -0
  78. package/dist/workflow/execution-mode.js.map +1 -1
  79. package/dist/workflow/index.d.ts +3 -0
  80. package/dist/workflow/index.d.ts.map +1 -1
  81. package/dist/workflow/index.js +25 -0
  82. package/dist/workflow/index.js.map +1 -1
  83. package/dist/workflow/overview.d.ts +89 -0
  84. package/dist/workflow/overview.d.ts.map +1 -0
  85. package/dist/workflow/overview.js +354 -0
  86. package/dist/workflow/overview.js.map +1 -0
  87. package/dist/workflow/plan-mode.d.ts +2 -1
  88. package/dist/workflow/plan-mode.d.ts.map +1 -1
  89. package/dist/workflow/plan-mode.js +83 -5
  90. package/dist/workflow/plan-mode.js.map +1 -1
  91. package/dist/workflow/website-strategy.d.ts +70 -0
  92. package/dist/workflow/website-strategy.d.ts.map +1 -0
  93. package/dist/workflow/website-strategy.js +238 -0
  94. package/dist/workflow/website-strategy.js.map +1 -0
  95. package/dist/workflow/website-updater.d.ts +17 -0
  96. package/dist/workflow/website-updater.d.ts.map +1 -0
  97. package/dist/workflow/website-updater.js +105 -0
  98. package/dist/workflow/website-updater.js.map +1 -0
  99. package/dist/workflow/workflow-logger.d.ts +1 -1
  100. package/dist/workflow/workflow-logger.d.ts.map +1 -1
  101. package/dist/workflow/workflow-logger.js.map +1 -1
  102. package/package.json +1 -1
  103. package/src/adapters/gemini.ts +10 -4
  104. package/src/adapters/grok.ts +10 -4
  105. package/src/adapters/openai.ts +38 -6
  106. package/src/cli/interactive.ts +47 -0
  107. package/src/generators/all.ts +6 -1
  108. package/src/generators/doc-parser.ts +372 -0
  109. package/src/generators/templates/index.ts +4 -0
  110. package/src/generators/templates/website-components.ts +305 -0
  111. package/src/generators/templates/website-config.ts +291 -0
  112. package/src/generators/templates/website-conversion.ts +341 -0
  113. package/src/generators/templates/website-seo.ts +370 -0
  114. package/src/generators/templates/website.ts +451 -505
  115. package/src/generators/website-context.ts +265 -0
  116. package/src/generators/website.ts +109 -19
  117. package/src/state/index.ts +42 -0
  118. package/src/types/consensus.ts +3 -0
  119. package/src/types/website-strategy.ts +243 -0
  120. package/src/types/workflow.ts +15 -0
  121. package/src/workflow/consensus.ts +2 -0
  122. package/src/workflow/execution-mode.ts +21 -0
  123. package/src/workflow/index.ts +25 -0
  124. package/src/workflow/overview.ts +469 -0
  125. package/src/workflow/plan-mode.ts +115 -4
  126. package/src/workflow/website-strategy.ts +305 -0
  127. package/src/workflow/website-updater.ts +131 -0
  128. package/src/workflow/workflow-logger.ts +1 -0
  129. package/tests/adapters/persona-switching.test.ts +63 -0
  130. package/tests/generators/website-components.test.ts +159 -0
  131. package/tests/generators/website-context.test.ts +222 -0
  132. package/tests/generators/website-seo-quality.test.ts +246 -0
  133. package/tests/workflow/overview.test.ts +392 -0
  134. package/tests/workflow/website-strategy.test.ts +191 -0
@@ -0,0 +1,265 @@
1
+ /**
2
+ * Website content context builder
3
+ * Discovers user documentation and brand assets to populate website templates
4
+ * with project-specific content instead of generic placeholders
5
+ */
6
+
7
+ import { promises as fs } from 'node:fs';
8
+ import path from 'node:path';
9
+ import {
10
+ stripCodeFences,
11
+ extractProductName,
12
+ extractTagline,
13
+ extractDescription,
14
+ extractFeatures,
15
+ extractPricing,
16
+ extractPrimaryColor,
17
+ } from './doc-parser.js';
18
+ import type { WebsiteStrategyDocument, BrandAssetsContract } from '../types/website-strategy.js';
19
+
20
+ /**
21
+ * Structured content context for website generation
22
+ */
23
+ export interface WebsiteContentContext {
24
+ productName: string;
25
+ tagline?: string;
26
+ description?: string;
27
+ features: Array<{ title: string; description: string }>;
28
+ pricing?: Array<{
29
+ name: string;
30
+ price: string;
31
+ period?: string;
32
+ description: string;
33
+ features: string[];
34
+ cta: string;
35
+ featured?: boolean;
36
+ }>;
37
+ brand?: {
38
+ primaryColor?: string;
39
+ colorScheme?: Record<string, string>;
40
+ logoPath?: string;
41
+ };
42
+ rawDocs: string;
43
+ /** Website marketing strategy (generated by AI from product context) */
44
+ strategy?: WebsiteStrategyDocument;
45
+ /** Resolved brand assets contract for deterministic logo/favicon placement */
46
+ brandAssets?: BrandAssetsContract;
47
+ }
48
+
49
+ /**
50
+ * Resolve brand assets into a deterministic contract
51
+ * Searches for logo/favicon in project directory, sets canonical output paths
52
+ *
53
+ * @param cwd - Directory to scan for brand assets
54
+ * @param brandContext - Optional existing brand context from state
55
+ * @returns Resolved brand assets contract
56
+ */
57
+ export async function resolveBrandAssets(
58
+ cwd: string,
59
+ brandContext?: { logoPath?: string; primaryColor?: string }
60
+ ): Promise<BrandAssetsContract> {
61
+ const assets = await findBrandAssets(cwd);
62
+ const logoPath = brandContext?.logoPath || assets.logoPath;
63
+ const ext = logoPath ? path.extname(logoPath) : '.svg';
64
+
65
+ return {
66
+ logoPath,
67
+ logoOutputPath: `public/brand/logo${ext}`,
68
+ primaryColor: brandContext?.primaryColor,
69
+ };
70
+ }
71
+
72
+ /** Patterns to match documentation files */
73
+ const DOC_PATTERNS = [
74
+ /spec/i,
75
+ /pricing/i,
76
+ /color/i,
77
+ /brand/i,
78
+ /ui[\s_-]?spec/i,
79
+ /^readme\.md$/i,
80
+ /overview/i,
81
+ /features/i,
82
+ /product/i,
83
+ ];
84
+
85
+ /** Directories to exclude from scanning */
86
+ const EXCLUDED_DIRS = [
87
+ 'node_modules',
88
+ '.popeye',
89
+ '.git',
90
+ 'dist',
91
+ 'build',
92
+ '.next',
93
+ '__pycache__',
94
+ 'venv',
95
+ '.venv',
96
+ 'coverage',
97
+ ];
98
+
99
+ /**
100
+ * Discover project documentation files in a directory
101
+ *
102
+ * @param cwd - Directory to scan for documentation
103
+ * @returns Array of absolute paths to discovered doc files
104
+ */
105
+ export async function discoverProjectDocs(cwd: string): Promise<string[]> {
106
+ const docs: string[] = [];
107
+
108
+ try {
109
+ const entries = await fs.readdir(cwd, { withFileTypes: true });
110
+
111
+ for (const entry of entries) {
112
+ if (entry.isDirectory() && EXCLUDED_DIRS.includes(entry.name)) {
113
+ continue;
114
+ }
115
+
116
+ if (entry.isFile() && entry.name.endsWith('.md')) {
117
+ const matches = DOC_PATTERNS.some((pattern) => pattern.test(entry.name));
118
+ if (matches) {
119
+ docs.push(path.join(cwd, entry.name));
120
+ }
121
+ }
122
+
123
+ // Check one level of subdirectories (e.g., docs/)
124
+ if (entry.isDirectory() && entry.name === 'docs') {
125
+ try {
126
+ const subEntries = await fs.readdir(path.join(cwd, entry.name), {
127
+ withFileTypes: true,
128
+ });
129
+ for (const subEntry of subEntries) {
130
+ if (subEntry.isFile() && subEntry.name.endsWith('.md')) {
131
+ docs.push(path.join(cwd, entry.name, subEntry.name));
132
+ }
133
+ }
134
+ } catch {
135
+ // Skip unreadable directories
136
+ }
137
+ }
138
+ }
139
+ } catch {
140
+ // Directory not accessible
141
+ }
142
+
143
+ // Sort: color/brand docs first (small but critical), then others
144
+ // This ensures brand context isn't truncated by the maxLength cap
145
+ docs.sort((a, b) => {
146
+ const nameA = path.basename(a).toLowerCase();
147
+ const nameB = path.basename(b).toLowerCase();
148
+ const priorityA = /color|brand/.test(nameA) ? 0 : 1;
149
+ const priorityB = /color|brand/.test(nameB) ? 0 : 1;
150
+ return priorityA - priorityB;
151
+ });
152
+
153
+ return docs;
154
+ }
155
+
156
+ /**
157
+ * Find brand assets (logo files) in a directory
158
+ *
159
+ * @param cwd - Directory to scan for brand assets
160
+ * @returns Object with optional logoPath
161
+ */
162
+ export async function findBrandAssets(
163
+ cwd: string
164
+ ): Promise<{ logoPath?: string }> {
165
+ const logoExtensions = ['.png', '.svg', '.jpg', '.jpeg', '.webp'];
166
+
167
+ try {
168
+ const entries = await fs.readdir(cwd, { withFileTypes: true });
169
+
170
+ for (const entry of entries) {
171
+ if (!entry.isFile()) continue;
172
+
173
+ const lowerName = entry.name.toLowerCase();
174
+ const hasLogoInName = lowerName.includes('logo');
175
+ const hasValidExt = logoExtensions.some((ext) => lowerName.endsWith(ext));
176
+
177
+ if (hasLogoInName && hasValidExt) {
178
+ return { logoPath: path.join(cwd, entry.name) };
179
+ }
180
+ }
181
+ } catch {
182
+ // Directory not accessible
183
+ }
184
+
185
+ return {};
186
+ }
187
+
188
+ /**
189
+ * Read and concatenate project documentation files
190
+ *
191
+ * @param docPaths - Array of absolute paths to doc files
192
+ * @param maxLength - Maximum combined length (default 15000 chars)
193
+ * @returns Combined documentation content with file headers
194
+ */
195
+ export async function readProjectDocs(
196
+ docPaths: string[],
197
+ maxLength: number = 15000
198
+ ): Promise<string> {
199
+ const sections: string[] = [];
200
+ let totalLength = 0;
201
+
202
+ for (const docPath of docPaths) {
203
+ if (totalLength >= maxLength) break;
204
+
205
+ try {
206
+ const content = await fs.readFile(docPath, 'utf-8');
207
+ const fileName = path.basename(docPath);
208
+ const header = `--- ${fileName} ---`;
209
+ const remaining = maxLength - totalLength;
210
+ const trimmedContent =
211
+ content.length > remaining ? content.slice(0, remaining) + '...' : content;
212
+
213
+ sections.push(`${header}\n${trimmedContent}`);
214
+ totalLength += header.length + 1 + trimmedContent.length;
215
+ } catch {
216
+ // Skip unreadable files
217
+ }
218
+ }
219
+
220
+ return sections.join('\n\n');
221
+ }
222
+
223
+ /**
224
+ * Build a structured website content context from discovered docs
225
+ *
226
+ * @param cwd - Working directory to scan for docs
227
+ * @param projectName - The project name (folder name fallback)
228
+ * @param specification - Optional expanded specification text
229
+ * @returns Structured content context for website templates
230
+ */
231
+ export async function buildWebsiteContext(
232
+ cwd: string,
233
+ projectName: string,
234
+ specification?: string
235
+ ): Promise<WebsiteContentContext> {
236
+ const docPaths = await discoverProjectDocs(cwd);
237
+ const rawDocs = docPaths.length > 0 ? await readProjectDocs(docPaths) : '';
238
+ const brandAssets = await findBrandAssets(cwd);
239
+
240
+ // Strip markdown code fences that wrap entire doc files
241
+ const cleanDocs = stripCodeFences(rawDocs);
242
+
243
+ const context: WebsiteContentContext = {
244
+ productName: extractProductName(cleanDocs, specification) || projectName,
245
+ features: extractFeatures(cleanDocs, specification),
246
+ rawDocs,
247
+ };
248
+
249
+ context.tagline = extractTagline(cleanDocs, context.productName);
250
+ context.description = extractDescription(cleanDocs, specification);
251
+ context.pricing = extractPricing(cleanDocs);
252
+
253
+ // Extract brand info
254
+ if (brandAssets.logoPath) {
255
+ context.brand = { ...context.brand, logoPath: brandAssets.logoPath };
256
+ }
257
+
258
+ const primaryColor = extractPrimaryColor(cleanDocs);
259
+ if (primaryColor) {
260
+ context.brand = { ...context.brand, primaryColor };
261
+ }
262
+
263
+ return context;
264
+ }
265
+
@@ -7,27 +7,47 @@ import { promises as fs } from 'node:fs';
7
7
  import path from 'node:path';
8
8
  import type { ProjectSpec } from '../types/project.js';
9
9
  import {
10
- generateWebsitePackageJson,
11
- generateNextConfig,
12
- generateWebsiteTsconfig,
13
- generateWebsiteTailwindConfig,
14
- generateWebsitePostcssConfig,
15
10
  generateWebsiteLayout,
16
11
  generateWebsiteGlobalsCss,
17
12
  generateWebsiteLandingPage,
18
13
  generateWebsitePricingPage,
19
- generateWebsiteSitemap,
20
- generateWebsiteRobots,
21
- generateWebsiteDockerfile,
22
14
  generateWebsiteReadme,
23
15
  generateWebsiteSpec,
24
- generateWebsiteVitestConfig,
25
- generateWebsiteVitestSetup,
26
16
  generateWebsiteTest,
27
17
  generateWebsiteDocsPage,
28
18
  generateWebsiteBlogPage,
29
- generateWebsiteNextEnv,
30
19
  } from './templates/website.js';
20
+ import {
21
+ generateWebsitePackageJson,
22
+ generateNextConfig,
23
+ generateWebsiteTsconfig,
24
+ generateWebsiteTailwindConfig,
25
+ generateWebsitePostcssConfig,
26
+ generateWebsiteDockerfile,
27
+ generateWebsiteVitestConfig,
28
+ generateWebsiteVitestSetup,
29
+ generateWebsiteNextEnv,
30
+ } from './templates/website-config.js';
31
+ import {
32
+ generateWebsiteHeader,
33
+ generateWebsiteFooter,
34
+ generateWebsiteNavigation,
35
+ } from './templates/website-components.js';
36
+ import {
37
+ generateJsonLdComponent,
38
+ generateEnhancedSitemap,
39
+ generateEnhancedRobots,
40
+ generate404Page,
41
+ generate500Page,
42
+ generateWebManifest,
43
+ generateMetaHelper,
44
+ } from './templates/website-seo.js';
45
+ import {
46
+ generateLeadCaptureRoute,
47
+ generateContactForm,
48
+ generateLeadCaptureEnvExample,
49
+ } from './templates/website-conversion.js';
50
+ import type { WebsiteContentContext } from './website-context.js';
31
51
 
32
52
  /**
33
53
  * Project generation result
@@ -53,6 +73,8 @@ export interface WebsiteGeneratorOptions {
53
73
  skipDocker?: boolean;
54
74
  /** Skip README (fullstack has root README) */
55
75
  skipReadme?: boolean;
76
+ /** Content context from user docs for populating templates */
77
+ contentContext?: WebsiteContentContext;
56
78
  }
57
79
 
58
80
  /**
@@ -88,6 +110,7 @@ export async function generateWebsiteProject(
88
110
  workspaceMode = false,
89
111
  skipDocker = false,
90
112
  skipReadme = false,
113
+ contentContext,
91
114
  } = options;
92
115
 
93
116
  const projectName = spec.name || 'my-project';
@@ -103,11 +126,13 @@ export async function generateWebsiteProject(
103
126
  await ensureDir(path.join(projectDir, 'src', 'app', 'pricing'));
104
127
  await ensureDir(path.join(projectDir, 'src', 'app', 'docs'));
105
128
  await ensureDir(path.join(projectDir, 'src', 'app', 'blog'));
129
+ await ensureDir(path.join(projectDir, 'src', 'app', 'api', 'lead'));
106
130
  await ensureDir(path.join(projectDir, 'src', 'components'));
107
131
  await ensureDir(path.join(projectDir, 'src', 'lib'));
108
132
  await ensureDir(path.join(projectDir, 'content', 'blog'));
109
133
  await ensureDir(path.join(projectDir, 'content', 'docs'));
110
134
  await ensureDir(path.join(projectDir, 'public'));
135
+ await ensureDir(path.join(projectDir, 'public', 'brand'));
111
136
  await ensureDir(path.join(projectDir, 'tests'));
112
137
 
113
138
  // Only create .popeye dir in standalone mode
@@ -150,19 +175,19 @@ export async function generateWebsiteProject(
150
175
  // App Router files
151
176
  {
152
177
  path: path.join(projectDir, 'src', 'app', 'layout.tsx'),
153
- content: generateWebsiteLayout(projectName),
178
+ content: generateWebsiteLayout(projectName, contentContext),
154
179
  },
155
180
  {
156
181
  path: path.join(projectDir, 'src', 'app', 'globals.css'),
157
- content: generateWebsiteGlobalsCss(),
182
+ content: generateWebsiteGlobalsCss(contentContext),
158
183
  },
159
184
  {
160
185
  path: path.join(projectDir, 'src', 'app', 'page.tsx'),
161
- content: generateWebsiteLandingPage(projectName),
186
+ content: generateWebsiteLandingPage(projectName, contentContext),
162
187
  },
163
188
  {
164
189
  path: path.join(projectDir, 'src', 'app', 'pricing', 'page.tsx'),
165
- content: generateWebsitePricingPage(projectName),
190
+ content: generateWebsitePricingPage(projectName, contentContext),
166
191
  },
167
192
  {
168
193
  path: path.join(projectDir, 'src', 'app', 'docs', 'page.tsx'),
@@ -173,14 +198,64 @@ export async function generateWebsiteProject(
173
198
  content: generateWebsiteBlogPage(),
174
199
  },
175
200
 
201
+ // Shared components
202
+ {
203
+ path: path.join(projectDir, 'src', 'components', 'Header.tsx'),
204
+ content: generateWebsiteHeader(projectName, contentContext, contentContext?.strategy),
205
+ },
206
+ {
207
+ path: path.join(projectDir, 'src', 'components', 'Footer.tsx'),
208
+ content: generateWebsiteFooter(projectName, contentContext, contentContext?.strategy),
209
+ },
210
+ {
211
+ path: path.join(projectDir, 'src', 'components', 'JsonLd.tsx'),
212
+ content: generateJsonLdComponent(),
213
+ },
214
+ {
215
+ path: path.join(projectDir, 'src', 'components', 'ContactForm.tsx'),
216
+ content: generateContactForm(contentContext?.strategy),
217
+ },
218
+ {
219
+ path: path.join(projectDir, 'src', 'lib', 'navigation.ts'),
220
+ content: generateWebsiteNavigation(contentContext?.strategy),
221
+ },
222
+ {
223
+ path: path.join(projectDir, 'src', 'lib', 'metadata.ts'),
224
+ content: generateMetaHelper(projectName, contentContext?.strategy),
225
+ },
226
+
227
+ // Lead capture API route
228
+ {
229
+ path: path.join(projectDir, 'src', 'app', 'api', 'lead', 'route.ts'),
230
+ content: generateLeadCaptureRoute(
231
+ contentContext?.strategy?.conversionStrategy.leadCapture || 'webhook'
232
+ ),
233
+ },
234
+
176
235
  // SEO files
177
236
  {
178
237
  path: path.join(projectDir, 'src', 'app', 'sitemap.ts'),
179
- content: generateWebsiteSitemap(projectName),
238
+ content: generateEnhancedSitemap(projectName, contentContext?.strategy),
180
239
  },
181
240
  {
182
241
  path: path.join(projectDir, 'src', 'app', 'robots.ts'),
183
- content: generateWebsiteRobots(projectName),
242
+ content: generateEnhancedRobots(projectName),
243
+ },
244
+
245
+ // Error pages
246
+ {
247
+ path: path.join(projectDir, 'src', 'app', 'not-found.tsx'),
248
+ content: generate404Page(projectName, contentContext),
249
+ },
250
+ {
251
+ path: path.join(projectDir, 'src', 'app', 'error.tsx'),
252
+ content: generate500Page(projectName),
253
+ },
254
+
255
+ // PWA manifest
256
+ {
257
+ path: path.join(projectDir, 'public', 'manifest.webmanifest'),
258
+ content: generateWebManifest(projectName, contentContext),
184
259
  },
185
260
 
186
261
  // Test files
@@ -218,7 +293,10 @@ export async function generateWebsiteProject(
218
293
  // Environment
219
294
  {
220
295
  path: path.join(projectDir, '.env.example'),
221
- content: 'NEXT_PUBLIC_SITE_URL=http://localhost:3001\nNEXT_PUBLIC_APP_URL=http://localhost:3000\n',
296
+ content: 'NEXT_PUBLIC_SITE_URL=http://localhost:3001\nNEXT_PUBLIC_APP_URL=http://localhost:3000\n' +
297
+ generateLeadCaptureEnvExample(
298
+ contentContext?.strategy?.conversionStrategy.leadCapture || 'webhook'
299
+ ),
222
300
  },
223
301
  {
224
302
  path: path.join(projectDir, '.gitignore'),
@@ -231,10 +309,22 @@ export async function generateWebsiteProject(
231
309
  if (!workspaceMode) {
232
310
  files.push({
233
311
  path: path.join(projectDir, '.popeye', 'website-spec.json'),
234
- content: generateWebsiteSpec(projectName),
312
+ content: generateWebsiteSpec(projectName, contentContext),
235
313
  });
236
314
  }
237
315
 
316
+ // Copy logo to public/ if brand context has one
317
+ if (contentContext?.brand?.logoPath) {
318
+ try {
319
+ const logoExt = path.extname(contentContext.brand.logoPath);
320
+ const destPath = path.join(projectDir, 'public', `logo${logoExt}`);
321
+ await fs.copyFile(contentContext.brand.logoPath, destPath);
322
+ filesCreated.push(destPath);
323
+ } catch {
324
+ // Non-blocking: logo copy failure should not stop generation
325
+ }
326
+ }
327
+
238
328
  // Add README if not skipped
239
329
  if (!skipReadme) {
240
330
  files.push({
@@ -338,6 +338,48 @@ export async function storeSpecification(
338
338
  return updateState(projectDir, { specification });
339
339
  }
340
340
 
341
+ /**
342
+ * Store discovered user documentation in project state
343
+ *
344
+ * @param projectDir - The project root directory
345
+ * @param userDocs - Combined user documentation content
346
+ * @returns The updated state
347
+ */
348
+ export async function storeUserDocs(
349
+ projectDir: string,
350
+ userDocs: string
351
+ ): Promise<ProjectState> {
352
+ return updateState(projectDir, { userDocs });
353
+ }
354
+
355
+ /**
356
+ * Store brand context in project state
357
+ *
358
+ * @param projectDir - The project root directory
359
+ * @param brandContext - Brand context with logo path and primary color
360
+ * @returns The updated state
361
+ */
362
+ export async function storeBrandContext(
363
+ projectDir: string,
364
+ brandContext: { logoPath?: string; primaryColor?: string }
365
+ ): Promise<ProjectState> {
366
+ return updateState(projectDir, { brandContext });
367
+ }
368
+
369
+ /**
370
+ * Store website strategy path in project state
371
+ *
372
+ * @param projectDir - The project root directory
373
+ * @param strategyPath - Relative path to strategy JSON file
374
+ * @returns The updated state
375
+ */
376
+ export async function storeWebsiteStrategyPath(
377
+ projectDir: string,
378
+ strategyPath: string
379
+ ): Promise<ProjectState> {
380
+ return updateState(projectDir, { websiteStrategy: strategyPath });
381
+ }
382
+
341
383
  /**
342
384
  * Mark the project as complete
343
385
  *
@@ -89,6 +89,8 @@ export interface ConsensusConfig {
89
89
  useOptimizedConsensus?: boolean;
90
90
  /** Additional reviewers beyond primary (for parallel reviews) */
91
91
  additionalReviewers?: AIProvider[];
92
+ /** Custom reviewer persona for domain-specific reviews (e.g., marketing strategist for website projects) */
93
+ reviewerPersona?: string;
92
94
  }
93
95
 
94
96
  /**
@@ -150,6 +152,7 @@ export const ConsensusConfigSchema = z.object({
150
152
  escalationAction: z.enum(['pause', 'continue', 'abort']).default('pause'),
151
153
  temperature: z.number().min(0).max(2).default(0.3),
152
154
  maxTokens: z.number().min(100).max(32000).default(4096),
155
+ reviewerPersona: z.string().optional(),
153
156
  });
154
157
 
155
158
  /**