popeye-cli 1.5.0 → 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 (161) hide show
  1. package/CHANGELOG.md +54 -0
  2. package/README.md +50 -8
  3. package/dist/cli/commands/create.d.ts.map +1 -1
  4. package/dist/cli/commands/create.js +54 -4
  5. package/dist/cli/commands/create.js.map +1 -1
  6. package/dist/cli/interactive.d.ts +29 -0
  7. package/dist/cli/interactive.d.ts.map +1 -1
  8. package/dist/cli/interactive.js +90 -7
  9. package/dist/cli/interactive.js.map +1 -1
  10. package/dist/generators/all.d.ts +4 -1
  11. package/dist/generators/all.d.ts.map +1 -1
  12. package/dist/generators/all.js +36 -316
  13. package/dist/generators/all.js.map +1 -1
  14. package/dist/generators/doc-parser.d.ts +18 -3
  15. package/dist/generators/doc-parser.d.ts.map +1 -1
  16. package/dist/generators/doc-parser.js +81 -10
  17. package/dist/generators/doc-parser.js.map +1 -1
  18. package/dist/generators/frontend-design-analyzer.d.ts +30 -0
  19. package/dist/generators/frontend-design-analyzer.d.ts.map +1 -0
  20. package/dist/generators/frontend-design-analyzer.js +208 -0
  21. package/dist/generators/frontend-design-analyzer.js.map +1 -0
  22. package/dist/generators/shared-packages.d.ts +45 -0
  23. package/dist/generators/shared-packages.d.ts.map +1 -0
  24. package/dist/generators/shared-packages.js +456 -0
  25. package/dist/generators/shared-packages.js.map +1 -0
  26. package/dist/generators/templates/index.d.ts +4 -0
  27. package/dist/generators/templates/index.d.ts.map +1 -1
  28. package/dist/generators/templates/index.js +4 -0
  29. package/dist/generators/templates/index.js.map +1 -1
  30. package/dist/generators/templates/website-components.d.ts.map +1 -1
  31. package/dist/generators/templates/website-components.js +36 -11
  32. package/dist/generators/templates/website-components.js.map +1 -1
  33. package/dist/generators/templates/website-config.d.ts +15 -1
  34. package/dist/generators/templates/website-config.d.ts.map +1 -1
  35. package/dist/generators/templates/website-config.js +155 -13
  36. package/dist/generators/templates/website-config.js.map +1 -1
  37. package/dist/generators/templates/website-landing.d.ts +24 -0
  38. package/dist/generators/templates/website-landing.d.ts.map +1 -0
  39. package/dist/generators/templates/website-landing.js +276 -0
  40. package/dist/generators/templates/website-landing.js.map +1 -0
  41. package/dist/generators/templates/website-layout.d.ts +42 -0
  42. package/dist/generators/templates/website-layout.d.ts.map +1 -0
  43. package/dist/generators/templates/website-layout.js +408 -0
  44. package/dist/generators/templates/website-layout.js.map +1 -0
  45. package/dist/generators/templates/website-pricing.d.ts +11 -0
  46. package/dist/generators/templates/website-pricing.d.ts.map +1 -0
  47. package/dist/generators/templates/website-pricing.js +313 -0
  48. package/dist/generators/templates/website-pricing.js.map +1 -0
  49. package/dist/generators/templates/website-sections.d.ts +102 -0
  50. package/dist/generators/templates/website-sections.d.ts.map +1 -0
  51. package/dist/generators/templates/website-sections.js +444 -0
  52. package/dist/generators/templates/website-sections.js.map +1 -0
  53. package/dist/generators/templates/website.d.ts +10 -50
  54. package/dist/generators/templates/website.d.ts.map +1 -1
  55. package/dist/generators/templates/website.js +12 -788
  56. package/dist/generators/templates/website.js.map +1 -1
  57. package/dist/generators/website-content-scanner.d.ts +37 -0
  58. package/dist/generators/website-content-scanner.d.ts.map +1 -0
  59. package/dist/generators/website-content-scanner.js +165 -0
  60. package/dist/generators/website-content-scanner.js.map +1 -0
  61. package/dist/generators/website-context.d.ts +38 -2
  62. package/dist/generators/website-context.d.ts.map +1 -1
  63. package/dist/generators/website-context.js +179 -19
  64. package/dist/generators/website-context.js.map +1 -1
  65. package/dist/generators/website-debug.d.ts +68 -0
  66. package/dist/generators/website-debug.d.ts.map +1 -0
  67. package/dist/generators/website-debug.js +93 -0
  68. package/dist/generators/website-debug.js.map +1 -0
  69. package/dist/generators/website.d.ts +2 -0
  70. package/dist/generators/website.d.ts.map +1 -1
  71. package/dist/generators/website.js +66 -4
  72. package/dist/generators/website.js.map +1 -1
  73. package/dist/generators/workspace-root.d.ts +27 -0
  74. package/dist/generators/workspace-root.d.ts.map +1 -0
  75. package/dist/generators/workspace-root.js +100 -0
  76. package/dist/generators/workspace-root.js.map +1 -0
  77. package/dist/state/index.d.ts +8 -0
  78. package/dist/state/index.d.ts.map +1 -1
  79. package/dist/state/index.js +10 -0
  80. package/dist/state/index.js.map +1 -1
  81. package/dist/types/workflow.d.ts +6 -0
  82. package/dist/types/workflow.d.ts.map +1 -1
  83. package/dist/types/workflow.js +2 -0
  84. package/dist/types/workflow.js.map +1 -1
  85. package/dist/upgrade/handlers.d.ts +15 -0
  86. package/dist/upgrade/handlers.d.ts.map +1 -1
  87. package/dist/upgrade/handlers.js +52 -0
  88. package/dist/upgrade/handlers.js.map +1 -1
  89. package/dist/workflow/auto-fix-bundler.d.ts +37 -0
  90. package/dist/workflow/auto-fix-bundler.d.ts.map +1 -0
  91. package/dist/workflow/auto-fix-bundler.js +320 -0
  92. package/dist/workflow/auto-fix-bundler.js.map +1 -0
  93. package/dist/workflow/auto-fix.d.ts.map +1 -1
  94. package/dist/workflow/auto-fix.js +10 -3
  95. package/dist/workflow/auto-fix.js.map +1 -1
  96. package/dist/workflow/index.d.ts +1 -0
  97. package/dist/workflow/index.d.ts.map +1 -1
  98. package/dist/workflow/index.js +12 -0
  99. package/dist/workflow/index.js.map +1 -1
  100. package/dist/workflow/overview.d.ts.map +1 -1
  101. package/dist/workflow/overview.js +4 -0
  102. package/dist/workflow/overview.js.map +1 -1
  103. package/dist/workflow/plan-mode.d.ts +4 -3
  104. package/dist/workflow/plan-mode.d.ts.map +1 -1
  105. package/dist/workflow/plan-mode.js +69 -5
  106. package/dist/workflow/plan-mode.js.map +1 -1
  107. package/dist/workflow/website-strategy.d.ts +9 -0
  108. package/dist/workflow/website-strategy.d.ts.map +1 -1
  109. package/dist/workflow/website-strategy.js +73 -1
  110. package/dist/workflow/website-strategy.js.map +1 -1
  111. package/dist/workflow/website-updater.d.ts.map +1 -1
  112. package/dist/workflow/website-updater.js +15 -4
  113. package/dist/workflow/website-updater.js.map +1 -1
  114. package/package.json +1 -1
  115. package/src/cli/commands/create.ts +58 -4
  116. package/src/cli/interactive.ts +96 -7
  117. package/src/generators/all.ts +44 -332
  118. package/src/generators/doc-parser.ts +87 -10
  119. package/src/generators/frontend-design-analyzer.ts +261 -0
  120. package/src/generators/shared-packages.ts +500 -0
  121. package/src/generators/templates/index.ts +4 -0
  122. package/src/generators/templates/website-components.ts +36 -11
  123. package/src/generators/templates/website-config.ts +166 -13
  124. package/src/generators/templates/website-landing.ts +331 -0
  125. package/src/generators/templates/website-layout.ts +443 -0
  126. package/src/generators/templates/website-pricing.ts +330 -0
  127. package/src/generators/templates/website-sections.ts +541 -0
  128. package/src/generators/templates/website.ts +38 -851
  129. package/src/generators/website-content-scanner.ts +208 -0
  130. package/src/generators/website-context.ts +248 -20
  131. package/src/generators/website-debug.ts +130 -0
  132. package/src/generators/website.ts +71 -3
  133. package/src/generators/workspace-root.ts +113 -0
  134. package/src/state/index.ts +14 -0
  135. package/src/types/workflow.ts +6 -0
  136. package/src/upgrade/handlers.ts +65 -0
  137. package/src/workflow/auto-fix-bundler.ts +392 -0
  138. package/src/workflow/auto-fix.ts +11 -3
  139. package/src/workflow/index.ts +12 -0
  140. package/src/workflow/overview.ts +6 -0
  141. package/src/workflow/plan-mode.ts +81 -7
  142. package/src/workflow/website-strategy.ts +75 -1
  143. package/src/workflow/website-updater.ts +17 -6
  144. package/tests/cli/project-naming.test.ts +136 -0
  145. package/tests/generators/doc-parser.test.ts +121 -0
  146. package/tests/generators/frontend-design-analyzer.test.ts +90 -0
  147. package/tests/generators/quality-gate.test.ts +183 -0
  148. package/tests/generators/shared-packages.test.ts +83 -0
  149. package/tests/generators/website-components.test.ts +1 -1
  150. package/tests/generators/website-config.test.ts +84 -0
  151. package/tests/generators/website-content-scanner.test.ts +181 -0
  152. package/tests/generators/website-context.test.ts +109 -0
  153. package/tests/generators/website-debug.test.ts +77 -0
  154. package/tests/generators/website-landing.test.ts +188 -0
  155. package/tests/generators/website-pricing.test.ts +98 -0
  156. package/tests/generators/website-sections.test.ts +245 -0
  157. package/tests/generators/workspace-root.test.ts +105 -0
  158. package/tests/upgrade/handlers.test.ts +162 -0
  159. package/tests/workflow/auto-fix-bundler.test.ts +242 -0
  160. package/tests/workflow/plan-mode.test.ts +111 -1
  161. package/tests/workflow/website-strategy.test.ts +55 -0
@@ -0,0 +1,392 @@
1
+ /**
2
+ * Bundler error parser and fixer for non-TypeScript build errors.
3
+ * Handles CSS/PostCSS/Tailwind, webpack module resolution, and generic bundler errors.
4
+ * Used as a fallback when parseTypeScriptErrors() returns empty but the build failed.
5
+ */
6
+
7
+ import { promises as fs } from 'node:fs';
8
+ import path from 'node:path';
9
+ import { executePrompt } from '../adapters/claude.js';
10
+ import type { AutoFixResult, BuildError } from './auto-fix.js';
11
+
12
+ /** Bundler-specific error with type classification */
13
+ export interface BundlerError extends BuildError {
14
+ type: 'css' | 'module-not-found' | 'syntax' | 'generic';
15
+ }
16
+
17
+ /** Config files that often need modification to fix CSS/bundler errors */
18
+ const RELATED_CONFIG_FILES = [
19
+ 'tailwind.config.ts',
20
+ 'tailwind.config.js',
21
+ 'postcss.config.js',
22
+ 'postcss.config.mjs',
23
+ 'next.config.js',
24
+ 'next.config.mjs',
25
+ 'next.config.ts',
26
+ 'vite.config.ts',
27
+ 'vite.config.js',
28
+ 'tsconfig.json',
29
+ 'package.json',
30
+ ];
31
+
32
+ /**
33
+ * Parse non-TypeScript build errors from bundler output.
34
+ * Catches CSS/PostCSS/Tailwind errors, module-not-found, and generic syntax errors.
35
+ */
36
+ export function parseBundlerErrors(output: string): BundlerError[] {
37
+ const errors: BundlerError[] = [];
38
+ const clean = output.replace(/\x1b\[[0-9;]*m/g, '');
39
+ const seen = new Set<string>();
40
+
41
+ // Pattern 1: CSS/PostCSS/Tailwind class not found
42
+ // "Syntax error: /abs/path/file.css The `className` class does not exist..."
43
+ const cssClassPattern = /Syntax error:\s*(\S+\.css)\s+(The [`'][\w-]+['`] class does not exist[^]*?)(?=\n\n|\n>|\nat\s|$)/gm;
44
+ let match;
45
+ while ((match = cssClassPattern.exec(clean)) !== null) {
46
+ const key = `css:${match[1]}`;
47
+ if (!seen.has(key)) {
48
+ seen.add(key);
49
+ errors.push({
50
+ file: match[1],
51
+ message: match[2].trim().split('\n')[0],
52
+ type: 'css',
53
+ });
54
+ }
55
+ }
56
+
57
+ // Pattern 2: File reference with line:col (bundler-style, non-TS)
58
+ // "./src/app/globals.css:1:1" followed by "Syntax error:" on next lines
59
+ const fileLinePattern = /^\.\/([\w/.@-]+\.(?:css|scss|less|json|mjs|cjs)):(\d+):(\d+)\s*$/gm;
60
+ while ((match = fileLinePattern.exec(clean)) !== null) {
61
+ const filePath = `./${match[1]}`;
62
+ const line = parseInt(match[2], 10);
63
+ const col = parseInt(match[3], 10);
64
+ // Grab up to 500 chars after match for context
65
+ const afterMatch = clean.slice(match.index + match[0].length, match.index + match[0].length + 500);
66
+ const errorLine = afterMatch.split('\n').find(l => l.trim().length > 10);
67
+ const key = `ref:${filePath}:${line}:${col}`;
68
+ if (!seen.has(key) && errorLine) {
69
+ seen.add(key);
70
+ errors.push({
71
+ file: filePath,
72
+ line,
73
+ column: col,
74
+ message: errorLine.trim(),
75
+ type: 'syntax',
76
+ });
77
+ }
78
+ }
79
+
80
+ // Pattern 3: Module not found
81
+ // "Module not found: Can't resolve 'package' in '/path'"
82
+ const modulePattern = /Module not found:\s*(?:Error:\s*)?Can't resolve '([^']+)'\s+in\s+'([^']+)'/gm;
83
+ while ((match = modulePattern.exec(clean)) !== null) {
84
+ const key = `module:${match[1]}:${match[2]}`;
85
+ if (!seen.has(key)) {
86
+ seen.add(key);
87
+ errors.push({
88
+ file: match[2],
89
+ message: `Cannot resolve module '${match[1]}'`,
90
+ type: 'module-not-found',
91
+ });
92
+ }
93
+ }
94
+
95
+ // Pattern 4: Generic "Build failed because of webpack errors" — extract file references
96
+ if (errors.length === 0 && /Build failed because of webpack errors/i.test(clean)) {
97
+ // Try to find Import trace lines: "./src/path/file.ext"
98
+ const importTracePattern = /^\.\/([\w/.@-]+\.\w+)\s*$/gm;
99
+ while ((match = importTracePattern.exec(clean)) !== null) {
100
+ const filePath = `./${match[1]}`;
101
+ const key = `trace:${filePath}`;
102
+ if (!seen.has(key)) {
103
+ seen.add(key);
104
+ errors.push({
105
+ file: filePath,
106
+ message: 'Referenced in build error import trace',
107
+ type: 'generic',
108
+ });
109
+ }
110
+ }
111
+ }
112
+
113
+ return errors;
114
+ }
115
+
116
+ /**
117
+ * Resolve the app directory from an error file path.
118
+ * E.g., "apps/website/src/foo.css" -> "<projectDir>/apps/website"
119
+ */
120
+ function resolveAppDir(projectDir: string, errorFile: string): string {
121
+ const appsMatch = errorFile.match(/apps\/([^/]+)\//);
122
+ if (appsMatch) {
123
+ return path.join(projectDir, 'apps', appsMatch[1]);
124
+ }
125
+ return projectDir;
126
+ }
127
+
128
+ /**
129
+ * Find config files related to the error (tailwind.config, postcss.config, etc.)
130
+ * Searches both the app directory and project root.
131
+ */
132
+ export async function findRelatedConfigs(
133
+ projectDir: string,
134
+ errorFile: string,
135
+ ): Promise<Array<{ path: string; content: string }>> {
136
+ const configs: Array<{ path: string; content: string }> = [];
137
+ const appDir = resolveAppDir(projectDir, errorFile);
138
+
139
+ // Search in both app dir and project root (deduped)
140
+ const searchDirs = [...new Set([appDir, projectDir])];
141
+
142
+ for (const dir of searchDirs) {
143
+ for (const configFile of RELATED_CONFIG_FILES) {
144
+ const configPath = path.join(dir, configFile);
145
+ try {
146
+ const content = await fs.readFile(configPath, 'utf-8');
147
+ // Cap config file size to avoid token bloat
148
+ configs.push({ path: configPath, content: content.slice(0, 4000) });
149
+ } catch { /* not found */ }
150
+ }
151
+ }
152
+
153
+ return configs;
154
+ }
155
+
156
+ /**
157
+ * Resolve the absolute path of a bundler error file.
158
+ * Tries multiple locations: absolute, relative to app dir, relative to project root.
159
+ */
160
+ async function resolveErrorFile(
161
+ projectDir: string,
162
+ errorFile: string,
163
+ ): Promise<{ resolvedPath: string; content: string } | null> {
164
+ const appDir = resolveAppDir(projectDir, errorFile);
165
+ const candidates = [
166
+ path.isAbsolute(errorFile) ? errorFile : null,
167
+ path.join(appDir, errorFile.replace(/^\.\//, '')),
168
+ path.join(projectDir, errorFile.replace(/^\.\//, '')),
169
+ ].filter(Boolean) as string[];
170
+
171
+ for (const p of candidates) {
172
+ try {
173
+ const content = await fs.readFile(p, 'utf-8');
174
+ return { resolvedPath: p, content };
175
+ } catch { /* continue */ }
176
+ }
177
+
178
+ return null;
179
+ }
180
+
181
+ /**
182
+ * Generate a fix prompt for bundler/CSS errors.
183
+ * Includes the error file, related config files, and instructions for multi-file fixes.
184
+ */
185
+ function generateBundlerFixPrompt(
186
+ errorFile: string,
187
+ fileContent: string,
188
+ errors: BundlerError[],
189
+ configs: Array<{ path: string; content: string }>,
190
+ rawOutput: string,
191
+ ): string {
192
+ const errorList = errors.map(e => `- ${e.type}: ${e.message}`).join('\n');
193
+
194
+ const configSection = configs.length > 0
195
+ ? '\n\n## Related Configuration Files:\n' +
196
+ configs.map(c => `### ${c.path}\n\`\`\`\n${c.content}\n\`\`\``).join('\n\n')
197
+ : '';
198
+
199
+ // Include raw output snippet for full context (last 1500 chars)
200
+ const outputSnippet = rawOutput.slice(-1500);
201
+
202
+ return `Fix the following build error. This is a CSS/bundler/config error, NOT a TypeScript error.
203
+
204
+ ## Error File: ${errorFile}
205
+
206
+ ## Errors:
207
+ ${errorList}
208
+
209
+ ## Raw Build Output (last 1500 chars):
210
+ \`\`\`
211
+ ${outputSnippet}
212
+ \`\`\`
213
+
214
+ ## Error file content:
215
+ \`\`\`
216
+ ${fileContent}
217
+ \`\`\`${configSection}
218
+
219
+ ## Instructions:
220
+ 1. Analyze the error carefully — it may be a CSS, Tailwind, PostCSS, or webpack bundler error
221
+ 2. The fix might be in the error file OR in a configuration file (e.g., tailwind.config.ts)
222
+ 3. For Tailwind "class does not exist" errors: the fix is usually adding the class definition to tailwind.config.ts theme.extend.colors (e.g., background: 'hsl(var(--background))')
223
+ 4. For module-not-found: the fix is usually a missing dependency or wrong import path
224
+ 5. Return your response using this exact format for EACH file that needs changes:
225
+
226
+ FILE: <absolute path to file>
227
+ \`\`\`
228
+ <complete fixed file content>
229
+ \`\`\`
230
+
231
+ If multiple files need changes, repeat the FILE: pattern for each.
232
+ Return ONLY the files that need changes.`.trim();
233
+ }
234
+
235
+ /**
236
+ * Parse Claude's multi-file response format.
237
+ * Extracts "FILE: <path>\n```\n<content>\n```" blocks.
238
+ */
239
+ export function parseMultiFileResponse(
240
+ response: string,
241
+ ): Array<{ targetPath: string; content: string }> {
242
+ const results: Array<{ targetPath: string; content: string }> = [];
243
+ const filePattern = /FILE:\s*(.+)\n```(?:\w*)\n([\s\S]*?)\n```/g;
244
+ let match;
245
+ while ((match = filePattern.exec(response)) !== null) {
246
+ const content = match[2].trim();
247
+ if (content.length > 20) {
248
+ results.push({ targetPath: match[1].trim(), content });
249
+ }
250
+ }
251
+ return results;
252
+ }
253
+
254
+ /**
255
+ * Resolve a target path from Claude's response to an actual writable file.
256
+ */
257
+ async function resolveTargetPath(
258
+ targetPath: string,
259
+ projectDir: string,
260
+ appDir: string,
261
+ configs: Array<{ path: string; content: string }>,
262
+ ): Promise<string | null> {
263
+ const candidates = [
264
+ path.isAbsolute(targetPath) ? targetPath : null,
265
+ path.join(appDir, targetPath.replace(/^\.\//, '')),
266
+ path.join(projectDir, targetPath.replace(/^\.\//, '')),
267
+ // Match against known config paths by basename
268
+ ...configs.map(c => c.path).filter(p => p.endsWith(path.basename(targetPath))),
269
+ ].filter(Boolean) as string[];
270
+
271
+ for (const p of candidates) {
272
+ try {
273
+ await fs.access(p);
274
+ return p;
275
+ } catch { /* continue */ }
276
+ }
277
+ return null;
278
+ }
279
+
280
+ /**
281
+ * Fix bundler errors by sending them to Claude with full context.
282
+ * Handles CSS/Tailwind/PostCSS/webpack errors that parseTypeScriptErrors() can't parse.
283
+ */
284
+ export async function fixBundlerErrors(
285
+ projectDir: string,
286
+ buildOutput: string,
287
+ errors: BundlerError[],
288
+ _language: string,
289
+ onProgress?: (message: string) => void,
290
+ ): Promise<AutoFixResult> {
291
+ const fixes: Array<{ file: string; description: string }> = [];
292
+
293
+ // Group errors by file
294
+ const errorsByFile = new Map<string, BundlerError[]>();
295
+ for (const error of errors) {
296
+ const existing = errorsByFile.get(error.file) || [];
297
+ existing.push(error);
298
+ errorsByFile.set(error.file, existing);
299
+ }
300
+
301
+ for (const [errorFile, fileErrors] of errorsByFile) {
302
+ onProgress?.(`Fixing ${fileErrors.length} bundler error(s) in ${path.basename(errorFile)}...`);
303
+
304
+ // Read the error file
305
+ const resolved = await resolveErrorFile(projectDir, errorFile);
306
+ if (!resolved) {
307
+ onProgress?.(`Cannot find ${errorFile}, skipping`);
308
+ continue;
309
+ }
310
+
311
+ // Find related config files
312
+ const configs = await findRelatedConfigs(projectDir, errorFile);
313
+
314
+ // Generate fix prompt
315
+ const prompt = generateBundlerFixPrompt(
316
+ errorFile, resolved.content, fileErrors, configs, buildOutput,
317
+ );
318
+
319
+ // Ask Claude to fix
320
+ const result = await executePrompt(prompt, {
321
+ allowedTools: [],
322
+ permissionMode: 'default',
323
+ });
324
+
325
+ if (!result.success || !result.response) {
326
+ onProgress?.(`Failed to get fix for ${path.basename(errorFile)}: ${result.error}`);
327
+ continue;
328
+ }
329
+
330
+ // Parse multi-file response
331
+ const fileChanges = parseMultiFileResponse(result.response);
332
+ const appDir = resolveAppDir(projectDir, errorFile);
333
+
334
+ for (const change of fileChanges) {
335
+ const resolvedTarget = await resolveTargetPath(
336
+ change.targetPath, projectDir, appDir, configs,
337
+ );
338
+
339
+ if (!resolvedTarget) {
340
+ onProgress?.(`Cannot resolve target path: ${change.targetPath}`);
341
+ continue;
342
+ }
343
+
344
+ await fs.writeFile(resolvedTarget, change.content, 'utf-8');
345
+ fixes.push({
346
+ file: resolvedTarget,
347
+ description: `Fixed bundler error: ${fileErrors[0].message.slice(0, 80)}`,
348
+ });
349
+ onProgress?.(`Fixed ${path.basename(resolvedTarget)}`);
350
+ }
351
+
352
+ // Fallback: if no FILE: pattern found, try single code-block extraction
353
+ if (fileChanges.length === 0) {
354
+ const singleBlock = result.response.match(/```(?:\w*)\n([\s\S]*?)\n```/);
355
+ if (singleBlock && singleBlock[1].length > 20) {
356
+ const content = singleBlock[1];
357
+ // Determine if this is a config fix or a direct file fix
358
+ let targetFile = resolved.resolvedPath;
359
+ if (
360
+ (content.includes('export default') || content.includes('module.exports')) &&
361
+ !resolved.resolvedPath.endsWith('.css')
362
+ ) {
363
+ const configMatch = configs.find(c =>
364
+ (content.includes('tailwind') && c.path.includes('tailwind')) ||
365
+ (content.includes('postcss') && c.path.includes('postcss')) ||
366
+ (content.includes('next') && c.path.includes('next.config')),
367
+ );
368
+ if (configMatch) targetFile = configMatch.path;
369
+ }
370
+
371
+ await fs.writeFile(targetFile, content, 'utf-8');
372
+ fixes.push({
373
+ file: targetFile,
374
+ description: `Fixed bundler error: ${fileErrors[0].message.slice(0, 80)}`,
375
+ });
376
+ onProgress?.(`Fixed ${path.basename(targetFile)}`);
377
+ }
378
+ }
379
+ }
380
+
381
+ return {
382
+ success: fixes.length > 0,
383
+ fixedErrors: fixes.length,
384
+ remainingErrors: 0,
385
+ attempts: 1,
386
+ fixes,
387
+ missingFileCount: 0,
388
+ totalErrorFiles: errorsByFile.size,
389
+ isStructuralIssue: false,
390
+ missingFiles: [],
391
+ };
392
+ }
@@ -7,6 +7,7 @@ import { promises as fs } from 'node:fs';
7
7
  import path from 'node:path';
8
8
  import { executePrompt } from '../adapters/claude.js';
9
9
  import { isWorkspace, type OutputLanguage } from '../types/project.js';
10
+ import { parseBundlerErrors, fixBundlerErrors } from './auto-fix-bundler.js';
10
11
 
11
12
  /** Standard workspace subdirectories to search when a file isn't at the root */
12
13
  const WORKSPACE_SUBDIRS = ['apps/frontend', 'apps/backend', 'apps/website', 'packages/frontend', 'packages/backend'];
@@ -250,10 +251,17 @@ export async function autoFixTypeScriptErrors(
250
251
 
251
252
  if (errors.length === 0) {
252
253
  // First attempt with zero parsed errors and no prior fixes: the build failed
253
- // but we can't parse the error format. This is NOT success.
254
+ // but we can't parse the error format. Try bundler error parser as fallback.
254
255
  const noParsedOnFirstAttempt = attempts === 1 && fixes.length === 0;
255
256
  if (noParsedOnFirstAttempt) {
256
- onProgress?.('No parseable TypeScript errors in build output (may be bundler/non-TS errors)');
257
+ // Fallback: try parsing CSS/PostCSS/Tailwind/webpack bundler errors
258
+ const bundlerErrors = parseBundlerErrors(currentOutput);
259
+ if (bundlerErrors.length > 0) {
260
+ onProgress?.(`Found ${bundlerErrors.length} bundler/CSS error(s), attempting fix...`);
261
+ return fixBundlerErrors(projectDir, currentOutput, bundlerErrors, language, onProgress);
262
+ }
263
+
264
+ onProgress?.('No parseable errors in build output (not TS or bundler format)');
257
265
  return {
258
266
  success: false,
259
267
  fixedErrors: 0,
@@ -264,7 +272,7 @@ export async function autoFixTypeScriptErrors(
264
272
  totalErrorFiles: 0,
265
273
  isStructuralIssue: false,
266
274
  missingFiles: [],
267
- error: 'Build failed but no parseable TypeScript errors found in output',
275
+ error: 'Build failed but no parseable errors found in output',
268
276
  };
269
277
  }
270
278
 
@@ -41,6 +41,7 @@ export * from './ui-designer.js';
41
41
  export * from './ui-verification.js';
42
42
  export * from './project-verification.js';
43
43
  export * from './auto-fix.js';
44
+ export * from './auto-fix-bundler.js';
44
45
  export * from './project-structure.js';
45
46
  // Note: plan-parser.js exports are accessible but have naming conflicts with plan-mode.js
46
47
  // Import directly from './plan-parser.js' if you need the extended TaskAppTag type (includes 'WEB')
@@ -269,6 +270,17 @@ export async function resumeWorkflow(
269
270
  }
270
271
 
271
272
  case 'execution': {
273
+ // Update website content before resuming execution
274
+ if (state.language === 'website' || state.language === 'all' || state.language === 'fullstack') {
275
+ try {
276
+ onProgress?.('website-update', 'Updating website with project context before execution resume...');
277
+ const { updateWebsiteContent } = await import('./website-updater.js');
278
+ await updateWebsiteContent(projectDir, state, state.language, (msg) => onProgress?.('website-update', msg));
279
+ } catch {
280
+ // Non-blocking: website content update failure should not stop workflow
281
+ }
282
+ }
283
+
272
284
  onProgress?.('workflow', 'Resuming Execution Mode...');
273
285
 
274
286
  const executionResult = await resumeExecutionMode({
@@ -279,6 +279,12 @@ function analyzeProject(
279
279
  'Project includes a website but was completed without any user documentation. Website likely has placeholder content.',
280
280
  'Run /overview fix to discover docs and update website template files');
281
281
  }
282
+ // Show strategy error if present
283
+ if (state.strategyError) {
284
+ push('error', 'strategy',
285
+ `Website strategy generation failed: ${state.strategyError}`,
286
+ 'Ensure OpenAI API key is set and product docs are available, then re-run planning');
287
+ }
282
288
  return issues;
283
289
  }
284
290
 
@@ -19,6 +19,7 @@ import {
19
19
  storeSpecification,
20
20
  storeUserDocs,
21
21
  storeBrandContext,
22
+ storeSourceDocPaths,
22
23
  storeWebsiteStrategyPath,
23
24
  addMilestones,
24
25
  } from '../state/index.js';
@@ -265,7 +266,7 @@ function isActionableTask(name: string): boolean {
265
266
  /**
266
267
  * Task app tag for fullstack projects
267
268
  */
268
- export type TaskAppTag = 'FE' | 'BE' | 'INT';
269
+ export type TaskAppTag = 'FE' | 'BE' | 'INT' | 'WEB';
269
270
 
270
271
  /**
271
272
  * Task with app targeting information for fullstack projects
@@ -274,7 +275,7 @@ export interface ParsedFullstackTask {
274
275
  name: string;
275
276
  description: string;
276
277
  appTag?: TaskAppTag;
277
- appTarget?: 'frontend' | 'backend' | 'unified';
278
+ appTarget?: 'frontend' | 'backend' | 'unified' | 'website';
278
279
  files?: string[];
279
280
  dependencies?: string[];
280
281
  acceptanceCriteria?: string[];
@@ -289,7 +290,7 @@ export interface ParsedFullstackTask {
289
290
  * @returns The parsed app tag or undefined
290
291
  */
291
292
  export function parseTaskTag(taskName: string): TaskAppTag | undefined {
292
- const tagMatch = taskName.match(/\[(FE|BE|INT)\]/i);
293
+ const tagMatch = taskName.match(/\[(FE|BE|INT|WEB)\]/i);
293
294
  if (tagMatch) {
294
295
  return tagMatch[1].toUpperCase() as TaskAppTag;
295
296
  }
@@ -302,10 +303,11 @@ export function parseTaskTag(taskName: string): TaskAppTag | undefined {
302
303
  * @param tag - The task tag
303
304
  * @returns The app target
304
305
  */
305
- export function tagToAppTarget(tag: TaskAppTag): 'frontend' | 'backend' | 'unified' {
306
+ export function tagToAppTarget(tag: TaskAppTag): 'frontend' | 'backend' | 'unified' | 'website' {
306
307
  switch (tag) {
307
308
  case 'FE': return 'frontend';
308
309
  case 'BE': return 'backend';
310
+ case 'WEB': return 'website';
309
311
  case 'INT': return 'unified';
310
312
  }
311
313
  }
@@ -377,6 +379,7 @@ export function validateFullstackPlan(plan: string): {
377
379
  feTasks: number;
378
380
  beTasks: number;
379
381
  intTasks: number;
382
+ webTasks: number;
380
383
  untaggedTasks: number;
381
384
  };
382
385
  } {
@@ -385,6 +388,7 @@ export function validateFullstackPlan(plan: string): {
385
388
  let feTasks = 0;
386
389
  let beTasks = 0;
387
390
  let intTasks = 0;
391
+ let webTasks = 0;
388
392
  let untaggedTasks = 0;
389
393
 
390
394
  // Find all task headers
@@ -401,6 +405,7 @@ export function validateFullstackPlan(plan: string): {
401
405
  case 'FE': feTasks++; break;
402
406
  case 'BE': beTasks++; break;
403
407
  case 'INT': intTasks++; break;
408
+ case 'WEB': webTasks++; break;
404
409
  }
405
410
  } else {
406
411
  untaggedTasks++;
@@ -435,6 +440,7 @@ export function validateFullstackPlan(plan: string): {
435
440
  feTasks,
436
441
  beTasks,
437
442
  intTasks,
443
+ webTasks,
438
444
  untaggedTasks,
439
445
  },
440
446
  };
@@ -599,6 +605,7 @@ export function parsePlanMilestones(plan: string): Omit<Milestone, 'id'>[] {
599
605
  while ((taskMatch = explicitTaskPattern.exec(plan)) !== null) {
600
606
  const name = taskMatch[1].trim()
601
607
  .replace(/^\*\*(.+)\*\*$/, '$1') // Remove bold
608
+ .replace(/^\[(?:FE|BE|INT|WEB|API|DB)\]\s*:?\s*/i, '') // Strip app tags before verb check
602
609
  .replace(/^:/, '') // Remove leading colon
603
610
  .trim();
604
611
 
@@ -917,10 +924,16 @@ export async function runPlanMode(
917
924
  userDocs = await readProjectDocs(docPaths);
918
925
  onProgress?.('doc-discovery', `Found ${docPaths.length} project doc(s)`);
919
926
  state = await storeUserDocs(projectDir, userDocs);
927
+ // Store source doc paths for later re-reading
928
+ state = await storeSourceDocPaths(projectDir, docPaths);
920
929
  await logger.info('init', 'docs_found', `Found ${docPaths.length} project docs`, {
921
930
  docCount: docPaths.length,
922
931
  docPaths: docPaths.map((p) => path.basename(p)),
923
932
  });
933
+
934
+ // Generate PROJECT_BRIEF.md in .popeye/ for persistent reference
935
+ await generateProjectBrief(projectDir, spec.name || state.name, userDocs, docPaths);
936
+ onProgress?.('doc-discovery', 'Generated PROJECT_BRIEF.md');
924
937
  }
925
938
  const brandAssets = await findBrandAssets(parentDir);
926
939
  if (brandAssets.logoPath) {
@@ -1013,12 +1026,13 @@ export async function runPlanMode(
1013
1026
  });
1014
1027
  } catch (strategyError) {
1015
1028
  // Non-blocking: strategy generation failure should not stop the workflow
1029
+ const errorMsg = strategyError instanceof Error ? strategyError.message : 'Unknown error';
1016
1030
  onProgress?.(
1017
1031
  'get-context',
1018
- `Website strategy skipped: ${strategyError instanceof Error ? strategyError.message : 'Unknown error'}`
1032
+ `WARNING: Website strategy failed: ${errorMsg}`
1019
1033
  );
1020
- await logger.warn('website-strategy', 'strategy_skipped', 'Website strategy was skipped', {
1021
- error: strategyError instanceof Error ? strategyError.message : 'Unknown error',
1034
+ await logger.error('website-strategy', 'strategy_failed', 'Website strategy generation failed', {
1035
+ error: errorMsg,
1022
1036
  });
1023
1037
  }
1024
1038
  }
@@ -1373,3 +1387,63 @@ export async function resumePlanMode(
1373
1387
  }
1374
1388
  );
1375
1389
  }
1390
+
1391
+ /**
1392
+ * Generate a PROJECT_BRIEF.md file in .popeye/ that summarizes
1393
+ * key project details extracted from user documentation.
1394
+ *
1395
+ * This brief is loaded at every workflow stage for consistent context.
1396
+ *
1397
+ * @param projectDir - The project root directory
1398
+ * @param projectName - The project/product name
1399
+ * @param userDocs - Combined user documentation content
1400
+ * @param docPaths - Absolute paths to source doc files
1401
+ */
1402
+ async function generateProjectBrief(
1403
+ projectDir: string,
1404
+ projectName: string,
1405
+ userDocs: string,
1406
+ docPaths: string[]
1407
+ ): Promise<void> {
1408
+ const briefDir = path.join(projectDir, '.popeye');
1409
+ const briefPath = path.join(briefDir, 'PROJECT_BRIEF.md');
1410
+
1411
+ // Extract key details from docs
1412
+ const productName = extractBriefField(userDocs, /^#\s+(.+)/m) || projectName;
1413
+ const tagline = extractBriefField(userDocs, /(?:tagline|subtitle|description)[:\s]+["']?(.+?)["']?\s*$/im) || '';
1414
+ const primaryColor = extractBriefField(userDocs, /#([0-9a-fA-F]{6})\b/) || '';
1415
+
1416
+ const briefContent = [
1417
+ `# Project Brief: ${productName}`,
1418
+ '',
1419
+ `**Product Name:** ${productName}`,
1420
+ tagline ? `**Tagline:** ${tagline}` : '',
1421
+ primaryColor ? `**Primary Color:** #${primaryColor}` : '',
1422
+ '',
1423
+ '## Source Documents',
1424
+ ...docPaths.map(p => `- ${path.basename(p)}`),
1425
+ '',
1426
+ '---',
1427
+ '*Auto-generated by Popeye. Re-read source docs for full context.*',
1428
+ '',
1429
+ ].filter(line => line !== undefined).join('\n');
1430
+
1431
+ try {
1432
+ await fs.mkdir(briefDir, { recursive: true });
1433
+ await fs.writeFile(briefPath, briefContent, 'utf-8');
1434
+ } catch {
1435
+ // Non-critical: brief generation failure shouldn't stop workflow
1436
+ }
1437
+ }
1438
+
1439
+ /**
1440
+ * Extract a field value from documentation using a regex pattern.
1441
+ *
1442
+ * @param content - Documentation content to search
1443
+ * @param pattern - Regex with a capture group for the value
1444
+ * @returns The captured value, or null if not found
1445
+ */
1446
+ function extractBriefField(content: string, pattern: RegExp): string | null {
1447
+ const match = content.match(pattern);
1448
+ return match?.[1]?.trim() || null;
1449
+ }