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,130 @@
1
+ /**
2
+ * Debug tracing for website generation pipeline
3
+ * Enabled via POPEYE_DEBUG_WEBSITE=1 environment variable
4
+ * Shows exactly which value came from where during website generation
5
+ */
6
+
7
+ /**
8
+ * Structured trace of website generation pipeline decisions
9
+ */
10
+ export interface WebsiteDebugTrace {
11
+ workspaceRoot: string;
12
+ docsFound: Array<{ path: string; size: number }>;
13
+ brandAssets: { logoPath?: string; logoOutputPath: string };
14
+ productName: { value: string; source: 'docs' | 'spec' | 'package.json' | 'directory' };
15
+ primaryColor: { value?: string; source: 'brand-docs' | 'frontend' | 'defaults' };
16
+ strategyStatus: 'success' | 'failed' | 'skipped';
17
+ strategyError?: string;
18
+ feDesignAnalysis?: { componentLib?: string; darkMode: boolean; primaryColor?: string };
19
+ templateValues: { headline?: string; features: number; pricingTiers: number };
20
+ /** Sections rendered with their data sources */
21
+ sectionsRendered: Array<{
22
+ name: string;
23
+ dataSource: 'strategy' | 'docs' | 'defaults' | 'skipped';
24
+ itemCount: number;
25
+ }>;
26
+ /** Validation result from quality gate */
27
+ validationPassed: boolean;
28
+ validationIssues: string[];
29
+ }
30
+
31
+ /**
32
+ * Check if debug tracing is enabled
33
+ *
34
+ * @returns True if POPEYE_DEBUG_WEBSITE=1 is set
35
+ */
36
+ export function isDebugEnabled(): boolean {
37
+ return process.env.POPEYE_DEBUG_WEBSITE === '1';
38
+ }
39
+
40
+ /**
41
+ * Format a debug trace for terminal output
42
+ *
43
+ * @param trace - The debug trace to format
44
+ * @returns Formatted string for terminal output
45
+ */
46
+ export function formatDebugTrace(trace: WebsiteDebugTrace): string {
47
+ const lines: string[] = [];
48
+
49
+ lines.push('');
50
+ lines.push('=== WEBSITE GENERATION DEBUG TRACE ===');
51
+ lines.push('');
52
+
53
+ lines.push(`Workspace Root: ${trace.workspaceRoot}`);
54
+ lines.push('');
55
+
56
+ lines.push(`Docs Found (${trace.docsFound.length}):`);
57
+ if (trace.docsFound.length === 0) {
58
+ lines.push(' (none)');
59
+ } else {
60
+ for (const doc of trace.docsFound) {
61
+ lines.push(` - ${doc.path} (${doc.size} chars)`);
62
+ }
63
+ }
64
+ lines.push('');
65
+
66
+ lines.push(`Brand Assets:`);
67
+ lines.push(` Logo Source: ${trace.brandAssets.logoPath || '(none)'}`);
68
+ lines.push(` Logo Output: ${trace.brandAssets.logoOutputPath}`);
69
+ lines.push('');
70
+
71
+ lines.push(`Product Name: "${trace.productName.value}" (from: ${trace.productName.source})`);
72
+ lines.push('');
73
+
74
+ lines.push(`Primary Color: ${trace.primaryColor.value || '(default)'} (from: ${trace.primaryColor.source})`);
75
+ lines.push('');
76
+
77
+ lines.push(`Strategy: ${trace.strategyStatus}`);
78
+ if (trace.strategyError) {
79
+ lines.push(` Error: ${trace.strategyError}`);
80
+ }
81
+ lines.push('');
82
+
83
+ if (trace.feDesignAnalysis) {
84
+ lines.push('Frontend Design Analysis:');
85
+ lines.push(` Component Library: ${trace.feDesignAnalysis.componentLib || '(unknown)'}`);
86
+ lines.push(` Dark Mode: ${trace.feDesignAnalysis.darkMode}`);
87
+ lines.push(` Primary Color: ${trace.feDesignAnalysis.primaryColor || '(none)'}`);
88
+ lines.push('');
89
+ }
90
+
91
+ lines.push('Template Values:');
92
+ lines.push(` Headline: ${trace.templateValues.headline || '(default)'}`);
93
+ lines.push(` Features: ${trace.templateValues.features}`);
94
+ lines.push(` Pricing Tiers: ${trace.templateValues.pricingTiers}`);
95
+ lines.push('');
96
+
97
+ lines.push(`Sections Rendered (${trace.sectionsRendered.length}):`);
98
+ if (trace.sectionsRendered.length === 0) {
99
+ lines.push(' (none)');
100
+ } else {
101
+ for (const section of trace.sectionsRendered) {
102
+ lines.push(` - ${section.name}: ${section.dataSource} (${section.itemCount} items)`);
103
+ }
104
+ }
105
+ lines.push('');
106
+
107
+ lines.push(`Validation: ${trace.validationPassed ? 'PASSED' : 'FAILED'}`);
108
+ if (trace.validationIssues.length > 0) {
109
+ for (const issue of trace.validationIssues) {
110
+ lines.push(` - ${issue}`);
111
+ }
112
+ }
113
+
114
+ lines.push('');
115
+ lines.push('=== END DEBUG TRACE ===');
116
+ lines.push('');
117
+
118
+ return lines.join('\n');
119
+ }
120
+
121
+ /**
122
+ * Print debug trace to console if debug mode is enabled
123
+ *
124
+ * @param trace - The debug trace to print
125
+ */
126
+ export function printDebugTrace(trace: WebsiteDebugTrace): void {
127
+ if (isDebugEnabled()) {
128
+ console.log(formatDebugTrace(trace));
129
+ }
130
+ }
@@ -7,27 +7,51 @@ 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';
51
+ import { validateWebsiteContextOrThrow } from './website-context.js';
52
+ import { scanGeneratedContent } from './website-content-scanner.js';
53
+ import { printDebugTrace, isDebugEnabled } from './website-debug.js';
54
+ import type { WebsiteDebugTrace } from './website-debug.js';
31
55
 
32
56
  /**
33
57
  * Project generation result
@@ -53,6 +77,10 @@ export interface WebsiteGeneratorOptions {
53
77
  skipDocker?: boolean;
54
78
  /** Skip README (fullstack has root README) */
55
79
  skipReadme?: boolean;
80
+ /** Content context from user docs for populating templates */
81
+ contentContext?: WebsiteContentContext;
82
+ /** Skip content validation (scaffold-only use) */
83
+ skipValidation?: boolean;
56
84
  }
57
85
 
58
86
  /**
@@ -88,6 +116,8 @@ export async function generateWebsiteProject(
88
116
  workspaceMode = false,
89
117
  skipDocker = false,
90
118
  skipReadme = false,
119
+ contentContext,
120
+ skipValidation = false,
91
121
  } = options;
92
122
 
93
123
  const projectName = spec.name || 'my-project';
@@ -103,11 +133,13 @@ export async function generateWebsiteProject(
103
133
  await ensureDir(path.join(projectDir, 'src', 'app', 'pricing'));
104
134
  await ensureDir(path.join(projectDir, 'src', 'app', 'docs'));
105
135
  await ensureDir(path.join(projectDir, 'src', 'app', 'blog'));
136
+ await ensureDir(path.join(projectDir, 'src', 'app', 'api', 'lead'));
106
137
  await ensureDir(path.join(projectDir, 'src', 'components'));
107
138
  await ensureDir(path.join(projectDir, 'src', 'lib'));
108
139
  await ensureDir(path.join(projectDir, 'content', 'blog'));
109
140
  await ensureDir(path.join(projectDir, 'content', 'docs'));
110
141
  await ensureDir(path.join(projectDir, 'public'));
142
+ await ensureDir(path.join(projectDir, 'public', 'brand'));
111
143
  await ensureDir(path.join(projectDir, 'tests'));
112
144
 
113
145
  // Only create .popeye dir in standalone mode
@@ -115,6 +147,51 @@ export async function generateWebsiteProject(
115
147
  await ensureDir(path.join(projectDir, '.popeye'));
116
148
  }
117
149
 
150
+ // Validate content context quality gate
151
+ if (!skipValidation) {
152
+ const validationContext = contentContext || {
153
+ productName: projectName,
154
+ features: [],
155
+ rawDocs: '',
156
+ };
157
+ validateWebsiteContextOrThrow(validationContext, projectName);
158
+ }
159
+
160
+ // Debug trace
161
+ if (isDebugEnabled() && contentContext) {
162
+ const trace: WebsiteDebugTrace = {
163
+ workspaceRoot: projectDir,
164
+ docsFound: contentContext.rawDocs
165
+ ? contentContext.rawDocs.split(/^--- .+ ---$/m).filter(Boolean).map((s, i) => ({
166
+ path: `doc-${i}`,
167
+ size: s.length,
168
+ }))
169
+ : [],
170
+ brandAssets: {
171
+ logoPath: contentContext.brand?.logoPath,
172
+ logoOutputPath: contentContext.brandAssets?.logoOutputPath || 'public/brand/logo.svg',
173
+ },
174
+ productName: {
175
+ value: contentContext.productName,
176
+ source: contentContext.rawDocs ? 'docs' : 'directory',
177
+ },
178
+ primaryColor: {
179
+ value: contentContext.brand?.primaryColor,
180
+ source: contentContext.brand?.primaryColor ? 'brand-docs' : 'defaults',
181
+ },
182
+ strategyStatus: contentContext.strategy ? 'success' : 'skipped',
183
+ templateValues: {
184
+ headline: contentContext.strategy?.messaging.headline,
185
+ features: contentContext.features.length,
186
+ pricingTiers: contentContext.pricing?.length || 0,
187
+ },
188
+ sectionsRendered: [],
189
+ validationPassed: true,
190
+ validationIssues: [],
191
+ };
192
+ printDebugTrace(trace);
193
+ }
194
+
118
195
  // Generate and write files
119
196
  const files: Array<{ path: string; content: string }> = [
120
197
  // Config files
@@ -132,7 +209,11 @@ export async function generateWebsiteProject(
132
209
  },
133
210
  {
134
211
  path: path.join(projectDir, 'tailwind.config.ts'),
135
- content: generateWebsiteTailwindConfig(),
212
+ content: generateWebsiteTailwindConfig({
213
+ primaryColor: contentContext?.brand?.primaryColor,
214
+ workspaceMode,
215
+ projectName: workspaceMode ? projectName : undefined,
216
+ }),
136
217
  },
137
218
  {
138
219
  path: path.join(projectDir, 'postcss.config.js'),
@@ -150,19 +231,19 @@ export async function generateWebsiteProject(
150
231
  // App Router files
151
232
  {
152
233
  path: path.join(projectDir, 'src', 'app', 'layout.tsx'),
153
- content: generateWebsiteLayout(projectName),
234
+ content: generateWebsiteLayout(projectName, contentContext),
154
235
  },
155
236
  {
156
237
  path: path.join(projectDir, 'src', 'app', 'globals.css'),
157
- content: generateWebsiteGlobalsCss(),
238
+ content: generateWebsiteGlobalsCss(contentContext),
158
239
  },
159
240
  {
160
241
  path: path.join(projectDir, 'src', 'app', 'page.tsx'),
161
- content: generateWebsiteLandingPage(projectName),
242
+ content: generateWebsiteLandingPage(projectName, contentContext),
162
243
  },
163
244
  {
164
245
  path: path.join(projectDir, 'src', 'app', 'pricing', 'page.tsx'),
165
- content: generateWebsitePricingPage(projectName),
246
+ content: generateWebsitePricingPage(projectName, contentContext),
166
247
  },
167
248
  {
168
249
  path: path.join(projectDir, 'src', 'app', 'docs', 'page.tsx'),
@@ -173,14 +254,64 @@ export async function generateWebsiteProject(
173
254
  content: generateWebsiteBlogPage(),
174
255
  },
175
256
 
257
+ // Shared components
258
+ {
259
+ path: path.join(projectDir, 'src', 'components', 'Header.tsx'),
260
+ content: generateWebsiteHeader(projectName, contentContext, contentContext?.strategy),
261
+ },
262
+ {
263
+ path: path.join(projectDir, 'src', 'components', 'Footer.tsx'),
264
+ content: generateWebsiteFooter(projectName, contentContext, contentContext?.strategy),
265
+ },
266
+ {
267
+ path: path.join(projectDir, 'src', 'components', 'JsonLd.tsx'),
268
+ content: generateJsonLdComponent(),
269
+ },
270
+ {
271
+ path: path.join(projectDir, 'src', 'components', 'ContactForm.tsx'),
272
+ content: generateContactForm(contentContext?.strategy),
273
+ },
274
+ {
275
+ path: path.join(projectDir, 'src', 'lib', 'navigation.ts'),
276
+ content: generateWebsiteNavigation(contentContext?.strategy),
277
+ },
278
+ {
279
+ path: path.join(projectDir, 'src', 'lib', 'metadata.ts'),
280
+ content: generateMetaHelper(projectName, contentContext?.strategy),
281
+ },
282
+
283
+ // Lead capture API route
284
+ {
285
+ path: path.join(projectDir, 'src', 'app', 'api', 'lead', 'route.ts'),
286
+ content: generateLeadCaptureRoute(
287
+ contentContext?.strategy?.conversionStrategy.leadCapture || 'webhook'
288
+ ),
289
+ },
290
+
176
291
  // SEO files
177
292
  {
178
293
  path: path.join(projectDir, 'src', 'app', 'sitemap.ts'),
179
- content: generateWebsiteSitemap(projectName),
294
+ content: generateEnhancedSitemap(projectName, contentContext?.strategy),
180
295
  },
181
296
  {
182
297
  path: path.join(projectDir, 'src', 'app', 'robots.ts'),
183
- content: generateWebsiteRobots(projectName),
298
+ content: generateEnhancedRobots(projectName),
299
+ },
300
+
301
+ // Error pages
302
+ {
303
+ path: path.join(projectDir, 'src', 'app', 'not-found.tsx'),
304
+ content: generate404Page(projectName, contentContext),
305
+ },
306
+ {
307
+ path: path.join(projectDir, 'src', 'app', 'error.tsx'),
308
+ content: generate500Page(projectName),
309
+ },
310
+
311
+ // PWA manifest
312
+ {
313
+ path: path.join(projectDir, 'public', 'manifest.webmanifest'),
314
+ content: generateWebManifest(projectName, contentContext),
184
315
  },
185
316
 
186
317
  // Test files
@@ -218,7 +349,10 @@ export async function generateWebsiteProject(
218
349
  // Environment
219
350
  {
220
351
  path: path.join(projectDir, '.env.example'),
221
- content: 'NEXT_PUBLIC_SITE_URL=http://localhost:3001\nNEXT_PUBLIC_APP_URL=http://localhost:3000\n',
352
+ content: 'NEXT_PUBLIC_SITE_URL=http://localhost:3001\nNEXT_PUBLIC_APP_URL=http://localhost:3000\n' +
353
+ generateLeadCaptureEnvExample(
354
+ contentContext?.strategy?.conversionStrategy.leadCapture || 'webhook'
355
+ ),
222
356
  },
223
357
  {
224
358
  path: path.join(projectDir, '.gitignore'),
@@ -231,10 +365,22 @@ export async function generateWebsiteProject(
231
365
  if (!workspaceMode) {
232
366
  files.push({
233
367
  path: path.join(projectDir, '.popeye', 'website-spec.json'),
234
- content: generateWebsiteSpec(projectName),
368
+ content: generateWebsiteSpec(projectName, contentContext),
235
369
  });
236
370
  }
237
371
 
372
+ // Copy logo to public/brand/ if brand context has one
373
+ if (contentContext?.brand?.logoPath) {
374
+ try {
375
+ const logoExt = path.extname(contentContext.brand.logoPath);
376
+ const destPath = path.join(projectDir, 'public', 'brand', `logo${logoExt}`);
377
+ await fs.copyFile(contentContext.brand.logoPath, destPath);
378
+ filesCreated.push(destPath);
379
+ } catch {
380
+ // Non-blocking: logo copy failure should not stop generation
381
+ }
382
+ }
383
+
238
384
  // Add README if not skipped
239
385
  if (!skipReadme) {
240
386
  files.push({
@@ -257,6 +403,18 @@ export async function generateWebsiteProject(
257
403
  filesCreated.push(file.path);
258
404
  }
259
405
 
406
+ // Post-generation content scan for placeholder fingerprints
407
+ try {
408
+ const scanResult = await scanGeneratedContent(projectDir);
409
+ if (scanResult.issues.length > 0) {
410
+ for (const issue of scanResult.issues) {
411
+ console.warn(`[content-scan] ${issue.severity}: ${issue.message} in ${issue.file}`);
412
+ }
413
+ }
414
+ } catch {
415
+ // Non-blocking: scan failures should not stop generation
416
+ }
417
+
260
418
  return {
261
419
  success: true,
262
420
  projectDir,
@@ -0,0 +1,113 @@
1
+ /**
2
+ * Workspace root detection
3
+ * Resolves the workspace root directory by walking up the directory tree,
4
+ * looking for Popeye config, monorepo indicators, or package.json with workspaces
5
+ */
6
+
7
+ import { promises as fs } from 'node:fs';
8
+ import path from 'node:path';
9
+
10
+ /**
11
+ * Resolve the workspace root directory from a given working directory
12
+ *
13
+ * Heuristic priority:
14
+ * 1. Walk ancestors: first dir containing `.popeye/` -> workspace root
15
+ * 2. First dir containing `package.json` with "workspaces" field
16
+ * 3. First dir containing `pnpm-workspace.yaml` or `turbo.json`
17
+ * 4. `cwd` (fallback)
18
+ *
19
+ * @param cwd - The current working directory
20
+ * @returns The resolved workspace root path
21
+ */
22
+ export async function resolveWorkspaceRoot(cwd: string): Promise<string> {
23
+ let current = path.resolve(cwd);
24
+ const root = path.parse(current).root;
25
+
26
+ while (current !== root) {
27
+ // Check for .popeye/ directory
28
+ if (await dirExists(path.join(current, '.popeye'))) {
29
+ return current;
30
+ }
31
+
32
+ // Check for package.json with "workspaces" field
33
+ const pkgJsonPath = path.join(current, 'package.json');
34
+ if (await fileExists(pkgJsonPath)) {
35
+ try {
36
+ const content = await fs.readFile(pkgJsonPath, 'utf-8');
37
+ const pkg = JSON.parse(content);
38
+ if (pkg.workspaces) {
39
+ return current;
40
+ }
41
+ } catch {
42
+ // Invalid JSON, skip
43
+ }
44
+ }
45
+
46
+ // Check for pnpm-workspace.yaml or turbo.json
47
+ if (
48
+ (await fileExists(path.join(current, 'pnpm-workspace.yaml'))) ||
49
+ (await fileExists(path.join(current, 'turbo.json')))
50
+ ) {
51
+ return current;
52
+ }
53
+
54
+ current = path.dirname(current);
55
+ }
56
+
57
+ return cwd;
58
+ }
59
+
60
+ /**
61
+ * Build a list of directories to scan for docs and brand assets
62
+ * Includes workspace root, its parent, and relevant subdirectories
63
+ *
64
+ * @param cwd - The current working directory
65
+ * @returns Array of directories to scan (deduplicated)
66
+ */
67
+ export async function getScanDirectories(cwd: string): Promise<string[]> {
68
+ const workspaceRoot = await resolveWorkspaceRoot(cwd);
69
+ const parentDir = path.dirname(workspaceRoot);
70
+
71
+ const candidates = [workspaceRoot, parentDir];
72
+ const subdirs = ['docs', 'brand', 'assets'];
73
+
74
+ for (const base of [workspaceRoot, parentDir]) {
75
+ for (const sub of subdirs) {
76
+ candidates.push(path.join(base, sub));
77
+ }
78
+ }
79
+
80
+ // Deduplicate by resolved path and filter to existing directories
81
+ const seen = new Set<string>();
82
+ const result: string[] = [];
83
+
84
+ for (const dir of candidates) {
85
+ const resolved = path.resolve(dir);
86
+ if (seen.has(resolved)) continue;
87
+ seen.add(resolved);
88
+
89
+ if (await dirExists(resolved)) {
90
+ result.push(resolved);
91
+ }
92
+ }
93
+
94
+ return result;
95
+ }
96
+
97
+ async function fileExists(filePath: string): Promise<boolean> {
98
+ try {
99
+ const stat = await fs.stat(filePath);
100
+ return stat.isFile();
101
+ } catch {
102
+ return false;
103
+ }
104
+ }
105
+
106
+ async function dirExists(dirPath: string): Promise<boolean> {
107
+ try {
108
+ const stat = await fs.stat(dirPath);
109
+ return stat.isDirectory();
110
+ } catch {
111
+ return false;
112
+ }
113
+ }
@@ -338,6 +338,62 @@ 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 discovered source document paths in project state
357
+ *
358
+ * @param projectDir - The project root directory
359
+ * @param sourceDocPaths - Array of absolute paths to doc files
360
+ * @returns The updated state
361
+ */
362
+ export async function storeSourceDocPaths(
363
+ projectDir: string,
364
+ sourceDocPaths: string[]
365
+ ): Promise<ProjectState> {
366
+ return updateState(projectDir, { sourceDocPaths });
367
+ }
368
+
369
+ /**
370
+ * Store brand context in project state
371
+ *
372
+ * @param projectDir - The project root directory
373
+ * @param brandContext - Brand context with logo path and primary color
374
+ * @returns The updated state
375
+ */
376
+ export async function storeBrandContext(
377
+ projectDir: string,
378
+ brandContext: { logoPath?: string; primaryColor?: string }
379
+ ): Promise<ProjectState> {
380
+ return updateState(projectDir, { brandContext });
381
+ }
382
+
383
+ /**
384
+ * Store website strategy path in project state
385
+ *
386
+ * @param projectDir - The project root directory
387
+ * @param strategyPath - Relative path to strategy JSON file
388
+ * @returns The updated state
389
+ */
390
+ export async function storeWebsiteStrategyPath(
391
+ projectDir: string,
392
+ strategyPath: string
393
+ ): Promise<ProjectState> {
394
+ return updateState(projectDir, { websiteStrategy: strategyPath });
395
+ }
396
+
341
397
  /**
342
398
  * Mark the project as complete
343
399
  *
@@ -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
  /**