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.
- package/README.md +222 -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/interactive.d.ts.map +1 -1
- package/dist/cli/interactive.js +42 -0
- package/dist/cli/interactive.js.map +1 -1
- package/dist/generators/all.d.ts +4 -1
- package/dist/generators/all.d.ts.map +1 -1
- package/dist/generators/all.js +2 -1
- package/dist/generators/all.js.map +1 -1
- package/dist/generators/doc-parser.d.ts +49 -0
- package/dist/generators/doc-parser.d.ts.map +1 -0
- package/dist/generators/doc-parser.js +336 -0
- package/dist/generators/doc-parser.js.map +1 -0
- package/dist/generators/templates/index.d.ts +4 -0
- package/dist/generators/templates/index.d.ts.map +1 -1
- package/dist/generators/templates/index.js +4 -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 +278 -0
- package/dist/generators/templates/website-components.js.map +1 -0
- package/dist/generators/templates/website-config.d.ts +41 -0
- package/dist/generators/templates/website-config.d.ts.map +1 -0
- package/dist/generators/templates/website-config.js +283 -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-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 +14 -47
- package/dist/generators/templates/website.d.ts.map +1 -1
- package/dist/generators/templates/website.js +412 -499
- package/dist/generators/templates/website.js.map +1 -1
- package/dist/generators/website-context.d.ts +83 -0
- package/dist/generators/website-context.d.ts.map +1 -0
- package/dist/generators/website-context.js +190 -0
- package/dist/generators/website-context.js.map +1 -0
- package/dist/generators/website.d.ts +3 -0
- package/dist/generators/website.d.ts.map +1 -1
- package/dist/generators/website.js +73 -10
- package/dist/generators/website.js.map +1 -1
- package/dist/state/index.d.ts +27 -0
- package/dist/state/index.d.ts.map +1 -1
- package/dist/state/index.js +30 -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 +15 -0
- package/dist/types/workflow.d.ts.map +1 -1
- package/dist/types/workflow.js +6 -0
- package/dist/types/workflow.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 +3 -0
- package/dist/workflow/index.d.ts.map +1 -1
- package/dist/workflow/index.js +25 -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 +354 -0
- package/dist/workflow/overview.js.map +1 -0
- package/dist/workflow/plan-mode.d.ts +2 -1
- package/dist/workflow/plan-mode.d.ts.map +1 -1
- package/dist/workflow/plan-mode.js +83 -5
- package/dist/workflow/plan-mode.js.map +1 -1
- package/dist/workflow/website-strategy.d.ts +70 -0
- package/dist/workflow/website-strategy.d.ts.map +1 -0
- package/dist/workflow/website-strategy.js +238 -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 +105 -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/interactive.ts +47 -0
- package/src/generators/all.ts +6 -1
- package/src/generators/doc-parser.ts +372 -0
- package/src/generators/templates/index.ts +4 -0
- package/src/generators/templates/website-components.ts +305 -0
- package/src/generators/templates/website-config.ts +291 -0
- package/src/generators/templates/website-conversion.ts +341 -0
- package/src/generators/templates/website-seo.ts +370 -0
- package/src/generators/templates/website.ts +451 -505
- package/src/generators/website-context.ts +265 -0
- package/src/generators/website.ts +109 -19
- package/src/state/index.ts +42 -0
- package/src/types/consensus.ts +3 -0
- package/src/types/website-strategy.ts +243 -0
- package/src/types/workflow.ts +15 -0
- package/src/workflow/consensus.ts +2 -0
- package/src/workflow/execution-mode.ts +21 -0
- package/src/workflow/index.ts +25 -0
- package/src/workflow/overview.ts +469 -0
- package/src/workflow/plan-mode.ts +115 -4
- package/src/workflow/website-strategy.ts +305 -0
- package/src/workflow/website-updater.ts +131 -0
- package/src/workflow/workflow-logger.ts +1 -0
- package/tests/adapters/persona-switching.test.ts +63 -0
- package/tests/generators/website-components.test.ts +159 -0
- package/tests/generators/website-context.test.ts +222 -0
- package/tests/generators/website-seo-quality.test.ts +246 -0
- package/tests/workflow/overview.test.ts +392 -0
- 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:
|
|
238
|
+
content: generateEnhancedSitemap(projectName, contentContext?.strategy),
|
|
180
239
|
},
|
|
181
240
|
{
|
|
182
241
|
path: path.join(projectDir, 'src', 'app', 'robots.ts'),
|
|
183
|
-
content:
|
|
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({
|
package/src/state/index.ts
CHANGED
|
@@ -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
|
*
|
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
|
/**
|