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.
- package/CHANGELOG.md +54 -0
- package/README.md +264 -63
- package/dist/adapters/gemini.d.ts +1 -0
- package/dist/adapters/gemini.d.ts.map +1 -1
- package/dist/adapters/gemini.js +9 -4
- package/dist/adapters/gemini.js.map +1 -1
- package/dist/adapters/grok.d.ts +1 -0
- package/dist/adapters/grok.d.ts.map +1 -1
- package/dist/adapters/grok.js +9 -4
- package/dist/adapters/grok.js.map +1 -1
- package/dist/adapters/openai.d.ts +1 -1
- package/dist/adapters/openai.d.ts.map +1 -1
- package/dist/adapters/openai.js +35 -9
- package/dist/adapters/openai.js.map +1 -1
- package/dist/cli/commands/create.d.ts.map +1 -1
- package/dist/cli/commands/create.js +54 -4
- package/dist/cli/commands/create.js.map +1 -1
- package/dist/cli/interactive.d.ts +29 -0
- package/dist/cli/interactive.d.ts.map +1 -1
- package/dist/cli/interactive.js +132 -7
- package/dist/cli/interactive.js.map +1 -1
- package/dist/generators/all.d.ts +8 -2
- package/dist/generators/all.d.ts.map +1 -1
- package/dist/generators/all.js +37 -316
- package/dist/generators/all.js.map +1 -1
- package/dist/generators/doc-parser.d.ts +64 -0
- package/dist/generators/doc-parser.d.ts.map +1 -0
- package/dist/generators/doc-parser.js +407 -0
- package/dist/generators/doc-parser.js.map +1 -0
- package/dist/generators/frontend-design-analyzer.d.ts +30 -0
- package/dist/generators/frontend-design-analyzer.d.ts.map +1 -0
- package/dist/generators/frontend-design-analyzer.js +208 -0
- package/dist/generators/frontend-design-analyzer.js.map +1 -0
- package/dist/generators/shared-packages.d.ts +45 -0
- package/dist/generators/shared-packages.d.ts.map +1 -0
- package/dist/generators/shared-packages.js +456 -0
- package/dist/generators/shared-packages.js.map +1 -0
- package/dist/generators/templates/index.d.ts +8 -0
- package/dist/generators/templates/index.d.ts.map +1 -1
- package/dist/generators/templates/index.js +8 -0
- package/dist/generators/templates/index.js.map +1 -1
- package/dist/generators/templates/website-components.d.ts +33 -0
- package/dist/generators/templates/website-components.d.ts.map +1 -0
- package/dist/generators/templates/website-components.js +303 -0
- package/dist/generators/templates/website-components.js.map +1 -0
- package/dist/generators/templates/website-config.d.ts +55 -0
- package/dist/generators/templates/website-config.d.ts.map +1 -0
- package/dist/generators/templates/website-config.js +425 -0
- package/dist/generators/templates/website-config.js.map +1 -0
- package/dist/generators/templates/website-conversion.d.ts +27 -0
- package/dist/generators/templates/website-conversion.d.ts.map +1 -0
- package/dist/generators/templates/website-conversion.js +326 -0
- package/dist/generators/templates/website-conversion.js.map +1 -0
- package/dist/generators/templates/website-landing.d.ts +24 -0
- package/dist/generators/templates/website-landing.d.ts.map +1 -0
- package/dist/generators/templates/website-landing.js +276 -0
- package/dist/generators/templates/website-landing.js.map +1 -0
- package/dist/generators/templates/website-layout.d.ts +42 -0
- package/dist/generators/templates/website-layout.d.ts.map +1 -0
- package/dist/generators/templates/website-layout.js +408 -0
- package/dist/generators/templates/website-layout.js.map +1 -0
- package/dist/generators/templates/website-pricing.d.ts +11 -0
- package/dist/generators/templates/website-pricing.d.ts.map +1 -0
- package/dist/generators/templates/website-pricing.js +313 -0
- package/dist/generators/templates/website-pricing.js.map +1 -0
- package/dist/generators/templates/website-sections.d.ts +102 -0
- package/dist/generators/templates/website-sections.d.ts.map +1 -0
- package/dist/generators/templates/website-sections.js +444 -0
- package/dist/generators/templates/website-sections.js.map +1 -0
- package/dist/generators/templates/website-seo.d.ts +76 -0
- package/dist/generators/templates/website-seo.d.ts.map +1 -0
- package/dist/generators/templates/website-seo.js +326 -0
- package/dist/generators/templates/website-seo.js.map +1 -0
- package/dist/generators/templates/website.d.ts +10 -83
- package/dist/generators/templates/website.d.ts.map +1 -1
- package/dist/generators/templates/website.js +12 -875
- package/dist/generators/templates/website.js.map +1 -1
- package/dist/generators/website-content-scanner.d.ts +37 -0
- package/dist/generators/website-content-scanner.d.ts.map +1 -0
- package/dist/generators/website-content-scanner.js +165 -0
- package/dist/generators/website-content-scanner.js.map +1 -0
- package/dist/generators/website-context.d.ts +119 -0
- package/dist/generators/website-context.d.ts.map +1 -0
- package/dist/generators/website-context.js +350 -0
- package/dist/generators/website-context.js.map +1 -0
- package/dist/generators/website-debug.d.ts +68 -0
- package/dist/generators/website-debug.d.ts.map +1 -0
- package/dist/generators/website-debug.js +93 -0
- package/dist/generators/website-debug.js.map +1 -0
- package/dist/generators/website.d.ts +5 -0
- package/dist/generators/website.d.ts.map +1 -1
- package/dist/generators/website.js +136 -11
- package/dist/generators/website.js.map +1 -1
- package/dist/generators/workspace-root.d.ts +27 -0
- package/dist/generators/workspace-root.d.ts.map +1 -0
- package/dist/generators/workspace-root.js +100 -0
- package/dist/generators/workspace-root.js.map +1 -0
- package/dist/state/index.d.ts +35 -0
- package/dist/state/index.d.ts.map +1 -1
- package/dist/state/index.js +40 -0
- package/dist/state/index.js.map +1 -1
- package/dist/types/consensus.d.ts +3 -0
- package/dist/types/consensus.d.ts.map +1 -1
- package/dist/types/consensus.js +1 -0
- package/dist/types/consensus.js.map +1 -1
- package/dist/types/website-strategy.d.ts +263 -0
- package/dist/types/website-strategy.d.ts.map +1 -0
- package/dist/types/website-strategy.js +105 -0
- package/dist/types/website-strategy.js.map +1 -0
- package/dist/types/workflow.d.ts +21 -0
- package/dist/types/workflow.d.ts.map +1 -1
- package/dist/types/workflow.js +8 -0
- package/dist/types/workflow.js.map +1 -1
- package/dist/upgrade/handlers.d.ts +15 -0
- package/dist/upgrade/handlers.d.ts.map +1 -1
- package/dist/upgrade/handlers.js +52 -0
- package/dist/upgrade/handlers.js.map +1 -1
- package/dist/workflow/auto-fix-bundler.d.ts +37 -0
- package/dist/workflow/auto-fix-bundler.d.ts.map +1 -0
- package/dist/workflow/auto-fix-bundler.js +320 -0
- package/dist/workflow/auto-fix-bundler.js.map +1 -0
- package/dist/workflow/auto-fix.d.ts.map +1 -1
- package/dist/workflow/auto-fix.js +10 -3
- package/dist/workflow/auto-fix.js.map +1 -1
- package/dist/workflow/consensus.d.ts.map +1 -1
- package/dist/workflow/consensus.js +2 -0
- package/dist/workflow/consensus.js.map +1 -1
- package/dist/workflow/execution-mode.d.ts.map +1 -1
- package/dist/workflow/execution-mode.js +18 -0
- package/dist/workflow/execution-mode.js.map +1 -1
- package/dist/workflow/index.d.ts +4 -0
- package/dist/workflow/index.d.ts.map +1 -1
- package/dist/workflow/index.js +37 -0
- package/dist/workflow/index.js.map +1 -1
- package/dist/workflow/overview.d.ts +89 -0
- package/dist/workflow/overview.d.ts.map +1 -0
- package/dist/workflow/overview.js +358 -0
- package/dist/workflow/overview.js.map +1 -0
- package/dist/workflow/plan-mode.d.ts +6 -4
- package/dist/workflow/plan-mode.d.ts.map +1 -1
- package/dist/workflow/plan-mode.js +148 -6
- package/dist/workflow/plan-mode.js.map +1 -1
- package/dist/workflow/website-strategy.d.ts +79 -0
- package/dist/workflow/website-strategy.d.ts.map +1 -0
- package/dist/workflow/website-strategy.js +310 -0
- package/dist/workflow/website-strategy.js.map +1 -0
- package/dist/workflow/website-updater.d.ts +17 -0
- package/dist/workflow/website-updater.d.ts.map +1 -0
- package/dist/workflow/website-updater.js +116 -0
- package/dist/workflow/website-updater.js.map +1 -0
- package/dist/workflow/workflow-logger.d.ts +1 -1
- package/dist/workflow/workflow-logger.d.ts.map +1 -1
- package/dist/workflow/workflow-logger.js.map +1 -1
- package/package.json +1 -1
- package/src/adapters/gemini.ts +10 -4
- package/src/adapters/grok.ts +10 -4
- package/src/adapters/openai.ts +38 -6
- package/src/cli/commands/create.ts +58 -4
- package/src/cli/interactive.ts +143 -7
- package/src/generators/all.ts +49 -332
- package/src/generators/doc-parser.ts +449 -0
- package/src/generators/frontend-design-analyzer.ts +261 -0
- package/src/generators/shared-packages.ts +500 -0
- package/src/generators/templates/index.ts +8 -0
- package/src/generators/templates/website-components.ts +330 -0
- package/src/generators/templates/website-config.ts +444 -0
- package/src/generators/templates/website-conversion.ts +341 -0
- package/src/generators/templates/website-landing.ts +331 -0
- package/src/generators/templates/website-layout.ts +443 -0
- package/src/generators/templates/website-pricing.ts +330 -0
- package/src/generators/templates/website-sections.ts +541 -0
- package/src/generators/templates/website-seo.ts +370 -0
- package/src/generators/templates/website.ts +38 -905
- package/src/generators/website-content-scanner.ts +208 -0
- package/src/generators/website-context.ts +493 -0
- package/src/generators/website-debug.ts +130 -0
- package/src/generators/website.ts +178 -20
- package/src/generators/workspace-root.ts +113 -0
- package/src/state/index.ts +56 -0
- package/src/types/consensus.ts +3 -0
- package/src/types/website-strategy.ts +243 -0
- package/src/types/workflow.ts +21 -0
- package/src/upgrade/handlers.ts +65 -0
- package/src/workflow/auto-fix-bundler.ts +392 -0
- package/src/workflow/auto-fix.ts +11 -3
- package/src/workflow/consensus.ts +2 -0
- package/src/workflow/execution-mode.ts +21 -0
- package/src/workflow/index.ts +37 -0
- package/src/workflow/overview.ts +475 -0
- package/src/workflow/plan-mode.ts +193 -8
- package/src/workflow/website-strategy.ts +379 -0
- package/src/workflow/website-updater.ts +142 -0
- package/src/workflow/workflow-logger.ts +1 -0
- package/tests/adapters/persona-switching.test.ts +63 -0
- package/tests/cli/project-naming.test.ts +136 -0
- package/tests/generators/doc-parser.test.ts +121 -0
- package/tests/generators/frontend-design-analyzer.test.ts +90 -0
- package/tests/generators/quality-gate.test.ts +183 -0
- package/tests/generators/shared-packages.test.ts +83 -0
- package/tests/generators/website-components.test.ts +159 -0
- package/tests/generators/website-config.test.ts +84 -0
- package/tests/generators/website-content-scanner.test.ts +181 -0
- package/tests/generators/website-context.test.ts +331 -0
- package/tests/generators/website-debug.test.ts +77 -0
- package/tests/generators/website-landing.test.ts +188 -0
- package/tests/generators/website-pricing.test.ts +98 -0
- package/tests/generators/website-sections.test.ts +245 -0
- package/tests/generators/website-seo-quality.test.ts +246 -0
- package/tests/generators/workspace-root.test.ts +105 -0
- package/tests/upgrade/handlers.test.ts +162 -0
- package/tests/workflow/auto-fix-bundler.test.ts +242 -0
- package/tests/workflow/overview.test.ts +392 -0
- package/tests/workflow/plan-mode.test.ts +111 -1
- 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:
|
|
294
|
+
content: generateEnhancedSitemap(projectName, contentContext?.strategy),
|
|
180
295
|
},
|
|
181
296
|
{
|
|
182
297
|
path: path.join(projectDir, 'src', 'app', 'robots.ts'),
|
|
183
|
-
content:
|
|
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
|
+
}
|
package/src/state/index.ts
CHANGED
|
@@ -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
|
*
|
package/src/types/consensus.ts
CHANGED
|
@@ -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
|
/**
|