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.
- package/CHANGELOG.md +54 -0
- package/README.md +50 -8
- 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 +90 -7
- 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 +36 -316
- package/dist/generators/all.js.map +1 -1
- package/dist/generators/doc-parser.d.ts +18 -3
- package/dist/generators/doc-parser.d.ts.map +1 -1
- package/dist/generators/doc-parser.js +81 -10
- package/dist/generators/doc-parser.js.map +1 -1
- 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 +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.map +1 -1
- package/dist/generators/templates/website-components.js +36 -11
- package/dist/generators/templates/website-components.js.map +1 -1
- package/dist/generators/templates/website-config.d.ts +15 -1
- package/dist/generators/templates/website-config.d.ts.map +1 -1
- package/dist/generators/templates/website-config.js +155 -13
- package/dist/generators/templates/website-config.js.map +1 -1
- 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.d.ts +10 -50
- package/dist/generators/templates/website.d.ts.map +1 -1
- package/dist/generators/templates/website.js +12 -788
- 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 +38 -2
- package/dist/generators/website-context.d.ts.map +1 -1
- package/dist/generators/website-context.js +179 -19
- package/dist/generators/website-context.js.map +1 -1
- 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 +2 -0
- package/dist/generators/website.d.ts.map +1 -1
- package/dist/generators/website.js +66 -4
- 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 +8 -0
- package/dist/state/index.d.ts.map +1 -1
- package/dist/state/index.js +10 -0
- package/dist/state/index.js.map +1 -1
- package/dist/types/workflow.d.ts +6 -0
- package/dist/types/workflow.d.ts.map +1 -1
- package/dist/types/workflow.js +2 -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/index.d.ts +1 -0
- package/dist/workflow/index.d.ts.map +1 -1
- package/dist/workflow/index.js +12 -0
- package/dist/workflow/index.js.map +1 -1
- package/dist/workflow/overview.d.ts.map +1 -1
- package/dist/workflow/overview.js +4 -0
- package/dist/workflow/overview.js.map +1 -1
- package/dist/workflow/plan-mode.d.ts +4 -3
- package/dist/workflow/plan-mode.d.ts.map +1 -1
- package/dist/workflow/plan-mode.js +69 -5
- package/dist/workflow/plan-mode.js.map +1 -1
- package/dist/workflow/website-strategy.d.ts +9 -0
- package/dist/workflow/website-strategy.d.ts.map +1 -1
- package/dist/workflow/website-strategy.js +73 -1
- package/dist/workflow/website-strategy.js.map +1 -1
- package/dist/workflow/website-updater.d.ts.map +1 -1
- package/dist/workflow/website-updater.js +15 -4
- package/dist/workflow/website-updater.js.map +1 -1
- package/package.json +1 -1
- package/src/cli/commands/create.ts +58 -4
- package/src/cli/interactive.ts +96 -7
- package/src/generators/all.ts +44 -332
- package/src/generators/doc-parser.ts +87 -10
- package/src/generators/frontend-design-analyzer.ts +261 -0
- package/src/generators/shared-packages.ts +500 -0
- package/src/generators/templates/index.ts +4 -0
- package/src/generators/templates/website-components.ts +36 -11
- package/src/generators/templates/website-config.ts +166 -13
- 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.ts +38 -851
- package/src/generators/website-content-scanner.ts +208 -0
- package/src/generators/website-context.ts +248 -20
- package/src/generators/website-debug.ts +130 -0
- package/src/generators/website.ts +71 -3
- package/src/generators/workspace-root.ts +113 -0
- package/src/state/index.ts +14 -0
- package/src/types/workflow.ts +6 -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/index.ts +12 -0
- package/src/workflow/overview.ts +6 -0
- package/src/workflow/plan-mode.ts +81 -7
- package/src/workflow/website-strategy.ts +75 -1
- package/src/workflow/website-updater.ts +17 -6
- 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 +1 -1
- 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 +109 -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/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/plan-mode.test.ts +111 -1
- 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
|
+
}
|
package/src/workflow/auto-fix.ts
CHANGED
|
@@ -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.
|
|
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
|
-
|
|
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
|
|
275
|
+
error: 'Build failed but no parseable errors found in output',
|
|
268
276
|
};
|
|
269
277
|
}
|
|
270
278
|
|
package/src/workflow/index.ts
CHANGED
|
@@ -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({
|
package/src/workflow/overview.ts
CHANGED
|
@@ -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
|
|
1032
|
+
`WARNING: Website strategy failed: ${errorMsg}`
|
|
1019
1033
|
);
|
|
1020
|
-
await logger.
|
|
1021
|
-
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
|
+
}
|