@auto-engineer/component-implementer 0.10.5

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.
@@ -0,0 +1,747 @@
1
+ import { generateTextWithAI } from '@auto-engineer/ai-gateway';
2
+ import * as fs from 'fs/promises';
3
+ import * as path from 'path';
4
+ import * as ts from 'typescript';
5
+ import createDebug from 'debug';
6
+ const debug = createDebug('frontend-impl:agent');
7
+ const debugPlan = createDebug('frontend-impl:agent:plan');
8
+ // const debugErrors = createDebug('frontend-impl:agent:errors');
9
+ // const debugScreenshots = createDebug('frontend-impl:agent:screenshots');
10
+ // const debugFixes = createDebug('frontend-impl:agent:fixes');
11
+ const debugContext = createDebug('frontend-impl:agent:context');
12
+ const debugAI = createDebug('frontend-impl:agent:ai');
13
+ const debugFiles = createDebug('frontend-impl:agent:files');
14
+ const debugComponents = createDebug('frontend-impl:agent:components');
15
+ // Utility to extract props from interface
16
+ function extractPropsFromInterface(node, sourceFile) {
17
+ debugComponents('Extracting props from interface: %s', node.name.text);
18
+ const props = node.members.filter(ts.isPropertySignature).map((member) => {
19
+ const name = member.name.getText(sourceFile);
20
+ const type = member.type ? member.type.getText(sourceFile) : 'any';
21
+ debugComponents(' Property: %s: %s', name, type);
22
+ return { name, type };
23
+ });
24
+ debugComponents('Extracted %d props from interface', props.length);
25
+ return props;
26
+ }
27
+ // Utility to extract props from type alias
28
+ function extractPropsFromTypeAlias(node, sourceFile) {
29
+ debugComponents('Extracting props from type alias: %s', node.name.text);
30
+ if (!ts.isTypeLiteralNode(node.type)) {
31
+ debugComponents(' Type alias is not a type literal, skipping');
32
+ return [];
33
+ }
34
+ const props = node.type.members.filter(ts.isPropertySignature).map((member) => {
35
+ const name = member.name.getText(sourceFile);
36
+ const type = member.type ? member.type.getText(sourceFile) : 'any';
37
+ debugComponents(' Property: %s: %s', name, type);
38
+ return { name, type };
39
+ });
40
+ debugComponents('Extracted %d props from type alias', props.length);
41
+ return props;
42
+ }
43
+ // Extract atoms and their props from src/components/atoms
44
+ async function getAtomsWithProps(projectDir) {
45
+ const atomsDir = path.join(projectDir, 'src/components/atoms');
46
+ debugComponents('Getting atoms from: %s', atomsDir);
47
+ let files = [];
48
+ try {
49
+ files = (await fs.readdir(atomsDir)).filter((f) => f.endsWith('.tsx'));
50
+ debugComponents('Found %d atom files', files.length);
51
+ }
52
+ catch (error) {
53
+ debugComponents('Error reading atoms directory: %O', error);
54
+ return [];
55
+ }
56
+ const atoms = [];
57
+ for (const file of files) {
58
+ const filePath = path.join(atomsDir, file);
59
+ debugComponents('Processing atom file: %s', file);
60
+ const content = await fs.readFile(filePath, 'utf-8');
61
+ const sourceFile = ts.createSourceFile(file, content, ts.ScriptTarget.Latest, true);
62
+ let componentName = file.replace(/\.tsx$/, '');
63
+ componentName = componentName.charAt(0).toUpperCase() + componentName.slice(1);
64
+ debugComponents('Component name: %s', componentName);
65
+ let props = [];
66
+ ts.forEachChild(sourceFile, (node) => {
67
+ if (ts.isInterfaceDeclaration(node) &&
68
+ node.name.text.toLowerCase().includes(componentName.toLowerCase()) &&
69
+ node.modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword) === true) {
70
+ props = extractPropsFromInterface(node, sourceFile);
71
+ }
72
+ if (ts.isTypeAliasDeclaration(node) &&
73
+ node.name.text.toLowerCase().includes(componentName.toLowerCase()) &&
74
+ node.modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword) === true) {
75
+ props = extractPropsFromTypeAlias(node, sourceFile);
76
+ }
77
+ });
78
+ atoms.push({ name: componentName, props });
79
+ debugComponents('Added atom %s with %d props', componentName, props.length);
80
+ }
81
+ debugComponents('Total atoms extracted: %d', atoms.length);
82
+ return atoms;
83
+ }
84
+ const provider = undefined;
85
+ function extractJsonArray(text) {
86
+ debugAI('Extracting JSON array from text of length: %d', text.length);
87
+ const codeBlockMatch = text.match(/```(?:json)?\s*([\s\S]*?)\s*```/);
88
+ if (codeBlockMatch && codeBlockMatch[1]) {
89
+ debugAI('Found JSON in code block');
90
+ return codeBlockMatch[1].trim();
91
+ }
92
+ const arrayMatch = text.match(/(\[[\s\S]*\])/);
93
+ if (arrayMatch) {
94
+ debugAI('Found JSON array in text');
95
+ return arrayMatch[0];
96
+ }
97
+ debugAI('No JSON array found, returning original text');
98
+ return text;
99
+ }
100
+ export async function callAI(prompt, options) {
101
+ const temperature = options?.temperature ?? 0.2;
102
+ const maxTokens = options?.maxTokens ?? 4000 * 3;
103
+ debugAI('Calling AI with prompt length: %d, temperature: %f, maxTokens: %d', prompt.length, temperature, maxTokens);
104
+ const result = await generateTextWithAI(prompt, { provider, temperature, maxTokens });
105
+ debugAI('AI response received, length: %d', result.length);
106
+ return result.trim();
107
+ }
108
+ // eslint-disable-next-line complexity
109
+ export async function loadScheme(iaSchemeDir) {
110
+ const schemePath = path.join(iaSchemeDir, 'auto-ia-scheme.json');
111
+ debugContext('Loading IA scheme from: %s', schemePath);
112
+ try {
113
+ const content = await fs.readFile(schemePath, 'utf-8');
114
+ const scheme = JSON.parse(content);
115
+ debugContext('IA scheme loaded successfully');
116
+ debugContext('Scheme has %d pages, %d organisms, %d molecules, %d atoms', Object.keys(scheme.pages?.items ?? {}).length, Object.keys(scheme.organisms?.items ?? {}).length, Object.keys(scheme.molecules?.items ?? {}).length, Object.keys(scheme.atoms?.items ?? {}).length);
117
+ return scheme;
118
+ }
119
+ catch (err) {
120
+ if (typeof err === 'object' && err !== null && 'code' in err && err.code !== 'ENOENT') {
121
+ debugContext('Error loading scheme: %O', err);
122
+ throw err;
123
+ }
124
+ debugContext('Scheme file not found');
125
+ return undefined;
126
+ }
127
+ }
128
+ async function getGraphqlOperations(projectDir, files) {
129
+ const graphqlFiles = files.filter((f) => f.startsWith('src/graphql/') && f.endsWith('.ts'));
130
+ debugContext('Found %d GraphQL files', graphqlFiles.length);
131
+ const operations = {};
132
+ for (const filePath of graphqlFiles) {
133
+ try {
134
+ debugContext('Reading GraphQL file: %s', filePath);
135
+ const content = await fs.readFile(path.join(projectDir, filePath), 'utf-8');
136
+ const operationName = path.basename(filePath, '.ts');
137
+ operations[operationName] = content;
138
+ debugContext('Loaded GraphQL operations from %s', operationName);
139
+ }
140
+ catch (error) {
141
+ debugContext('Error reading GraphQL operations file %s: %O', filePath, error);
142
+ console.error(`Error reading GraphQL operations file ${filePath}:`, error);
143
+ }
144
+ }
145
+ debugContext('Total GraphQL operations loaded: %d', Object.keys(operations).length);
146
+ return operations;
147
+ }
148
+ async function getKeyFileContents(projectDir, files) {
149
+ const keyFiles = files.filter((f) => ['src/index.css', 'src/globals.css'].includes(f));
150
+ debugContext('Getting key file contents for %d files', keyFiles.length);
151
+ const contents = {};
152
+ for (const file of keyFiles) {
153
+ try {
154
+ debugContext('Reading key file: %s', file);
155
+ contents[file] = await fs.readFile(path.join(projectDir, file), 'utf-8');
156
+ debugContext('Key file %s loaded, size: %d bytes', file, contents[file].length);
157
+ }
158
+ catch (error) {
159
+ debugContext('Could not read key file %s: %O', file, error);
160
+ }
161
+ }
162
+ debugContext('Loaded %d key files', Object.keys(contents).length);
163
+ return contents;
164
+ }
165
+ function getFileTreeSummary(files, atoms) {
166
+ return [
167
+ ...files.filter((f) => f.startsWith('src/pages/') ||
168
+ f.startsWith('src/hooks/') ||
169
+ f.startsWith('src/components/templates') ||
170
+ f.startsWith('src/lib/') ||
171
+ ['src/App.tsx', 'src/routes.tsx', 'src/main.tsx'].includes(f)),
172
+ `src/components/atoms/ (atoms: ${atoms.map((a) => a.name).join(', ')})`,
173
+ ];
174
+ }
175
+ async function getTheme(designSystem) {
176
+ debugContext('Extracting theme from design system, content length: %d', designSystem.length);
177
+ try {
178
+ const themeMatch = designSystem.match(/## Theme\s*\n([\s\S]*?)(?=\n## |\n# |\n*$)/);
179
+ if (themeMatch && themeMatch[1]) {
180
+ const theme = themeMatch[1].trim();
181
+ debugContext('Theme extracted, length: %d', theme.length);
182
+ return theme;
183
+ }
184
+ debugContext('No theme section found in design system');
185
+ return '';
186
+ }
187
+ catch (error) {
188
+ debugContext('Error extracting theme: %O', error);
189
+ console.error(`Error reading design-system.md:`, error);
190
+ return '';
191
+ }
192
+ }
193
+ export async function getProjectContext(projectDir, iaSchemeDir, userPreferences, designSystem, failures) {
194
+ debugContext('Building project context for: %s', projectDir);
195
+ debugContext('IA scheme directory: %s', iaSchemeDir);
196
+ debugContext('User preferences length: %d', userPreferences.length);
197
+ debugContext('Design system length: %d', designSystem.length);
198
+ debugContext('Failures: %d', failures.length);
199
+ const files = await listFiles(projectDir);
200
+ debugContext('Found %d files in project', files.length);
201
+ const [scheme, atoms, graphqlOperations, keyFileContents, theme] = await Promise.all([
202
+ loadScheme(iaSchemeDir),
203
+ getAtomsWithProps(projectDir),
204
+ getGraphqlOperations(projectDir, files),
205
+ getKeyFileContents(projectDir, files),
206
+ getTheme(designSystem),
207
+ ]);
208
+ const fileTreeSummary = getFileTreeSummary(files, atoms);
209
+ debugContext('File tree summary created with %d entries', fileTreeSummary.length);
210
+ debugContext('Project context built successfully');
211
+ return {
212
+ scheme,
213
+ files,
214
+ atoms,
215
+ keyFileContents,
216
+ fileTreeSummary,
217
+ graphqlOperations,
218
+ userPreferences,
219
+ theme,
220
+ failures,
221
+ };
222
+ }
223
+ async function listFiles(dir, base = dir) {
224
+ debugFiles('Listing files in: %s', dir);
225
+ const entries = await fs.readdir(dir, { withFileTypes: true });
226
+ debugFiles('Found %d entries in directory', entries.length);
227
+ const files = await Promise.all(entries.map(async (entry) => {
228
+ if (entry.name === 'node_modules') {
229
+ debugFiles('Skipping node_modules');
230
+ return [];
231
+ }
232
+ const res = path.resolve(dir, entry.name);
233
+ if (entry.isDirectory()) {
234
+ debugFiles('Entering directory: %s', entry.name);
235
+ return listFiles(res, base);
236
+ }
237
+ else {
238
+ return [path.relative(base, res)];
239
+ }
240
+ }));
241
+ const flatFiles = files.flat();
242
+ debugFiles('Total files found: %d', flatFiles.length);
243
+ return flatFiles;
244
+ }
245
+ function makeBasePrompt(ctx) {
246
+ const keyFileContents = Object.entries(ctx.keyFileContents)
247
+ .map(([f, c]) => `--- ${f} ---\n${c}\n`)
248
+ .join('\n');
249
+ const graphqlDescriptions = Object.entries(ctx.graphqlOperations)
250
+ .map(([f, c]) => `--- ${f} ---\n${c}\n`)
251
+ .join('\n');
252
+ return `
253
+ <ROLE>
254
+ You are Auto, a masterful Frontend & Design Engineer who builds interactive works of art—scalable, modern React applications that feel as beautiful as they are functional.
255
+ </ROLE>
256
+
257
+ <TASK>
258
+ Transform the IA schema into a complete, production-ready application. Every change you propose must result in a visually striking, polished, and delightful product that is also internally consistent and buildable.
259
+ </TASK>
260
+
261
+ <GOALS>
262
+ - Deliver world-class UX (Notion/Linear/Stripe caliber) with seamless flows, harmonious layouts, and joyful interactions.
263
+ - Guarantee implementation completeness and consistency: no placeholders, no stubs, no undefined references, no dead routes.
264
+ - Respect all constraints (GraphQL ops files, theme tokens, user preferences).
265
+ </GOALS>
266
+
267
+ <INSTRUCTIONS>
268
+ ## Visual Excellence Mandate
269
+ - Typography hierarchy that reads with confidence; body copy airy and legible; labels precise.
270
+ - Spacing rhythm on Tailwind’s 4px scale; balanced proportions and breathing room across breakpoints.
271
+ - Interactive states for all controls: hover, focus, active, disabled.
272
+ - Micro-interactions using **Motion**: smooth fades, gentle slides, fluid expansions.
273
+ - Consistent iconography via lucide-react; aligned sizes and spacing.
274
+ - Responsive mastery: mobile, tablet, desktop must each feel intentionally designed.
275
+
276
+ ## Layout Rules
277
+ - **Single Page Applications (SPAs)**: Avoid page-level scrollbars; scrolling must happen only within defined content regions. Preserve a fluid, app-like feel across breakpoints.
278
+ - **Websites**: For marketing or static sites, full-page scrolling and vertical storytelling are acceptable. Employ elegant sections and natural scroll progression.
279
+ - For single-purpose flows (checkout, booking, signup), craft minimal, elegant, stepwise journeys.
280
+ - Avoid generic headings; communicate structure via layout, tokens, and spacing.
281
+
282
+ ## Component & Code Standards
283
+ - Atomic design: atoms → molecules → organisms → templates → pages; reuse atoms before creating anything new.
284
+ - Keep components concise (~50 lines when feasible) and fully typed (<Name>Props).
285
+ - Accessibility is mandatory: ARIA roles, focus management, keyboard navigation.
286
+ - Named exports only; avoid prop drilling via context or colocated state.
287
+
288
+ ## GraphQL Integration Rules
289
+ - Use Apollo Client hooks and only the documents in:
290
+ - src/graphql/queries.ts
291
+ - src/graphql/mutations.ts
292
+ - Do not add/modify/remove GraphQL documents; adapt the UI to the available shape.
293
+
294
+ ## Integrity & Completeness Contract
295
+ - No Partial Files: every created/updated file must be fully implementable—no TODOs, placeholders, or stubs.
296
+ - No Undefined References: do not reference any component, hook, util, icon, or route unless the same plan also creates or updates the exact file that provides it.
297
+ - Route Reachability: every page/route must be reachable from at least one navigational entry (sidebar/topbar/menu/CTA). If a route is not part of the core journey or becomes unreachable, remove it.
298
+ - Navigation Continuity: define a coherent journey (Landing → Auth → Onboarding → Dashboard → Feature → Settings). After any critical action, update related views and caches to reflect the new state.
299
+ - Router Source of Truth: update routing so there is a default index route for the main experience, all declared routes are reachable, and unused ones are pruned.
300
+ - File Dependency Order: list changes so that dependencies are created before dependents (atoms/molecules/templates first, then pages, then routing and providers).
301
+ - Key File Rule: key files contain all needed imports/specs; do not alter their imports/specs—implement only within the allowed surface.
302
+
303
+ ## Color Usage Contract
304
+ - A single accent color must never dominate the interface. Primary actions may use the strongest accent, but it should account for no more than ~25% of visible UI.
305
+ - Distribute semantic colors across the interface for balance and clarity:
306
+ - Growth or success metrics → positive color
307
+ - Completion or engagement metrics → secondary accent
308
+ - Targets, goals, or warnings → attention color
309
+ - Errors or urgent states → critical color
310
+ - Cards and surfaces should primarily use neutral backgrounds (white or light gray). Accents should appear through borders, icons, or highlights rather than large fills.
311
+ - Each dashboard view must showcase at least 3 distinct semantic colors to avoid monotony and reinforce hierarchy.
312
+ - Accents must always support meaning (not decoration alone) and follow consistent usage across the app.
313
+
314
+ ## Output Format (STRICT)
315
+ Respond ONLY with a JSON array. No prose. No markdown. Each item:
316
+ - "action": "create" | "update"
317
+ - "file": relative path from project root
318
+ - "description": concise, specific rationale for the change
319
+
320
+ Project Snapshot:
321
+ ${JSON.stringify(ctx.fileTreeSummary, null, 2)}
322
+
323
+ Available Atoms:
324
+ ${JSON.stringify(ctx.atoms, null, 2)}
325
+
326
+ Key Files:
327
+ ${keyFileContents}
328
+
329
+ IA Schema:
330
+ ${JSON.stringify(ctx.scheme, null, 2)}
331
+
332
+ GraphQL Operations:
333
+ ${graphqlDescriptions}
334
+ `;
335
+ }
336
+ async function planProject(ctx) {
337
+ debugPlan('Starting project planning');
338
+ const prompt = makeBasePrompt(ctx);
339
+ debugPlan('Generated prompt with length: %d', prompt.length);
340
+ const planText = await callAI(prompt);
341
+ debugPlan('Received plan response, length: %d', planText.length);
342
+ try {
343
+ const changes = JSON.parse(extractJsonArray(planText));
344
+ debugPlan('Successfully parsed plan with %d changes', changes.length);
345
+ changes.forEach((change, idx) => {
346
+ debugPlan('Change %d: %s %s - %s', idx + 1, change.action, change.file, change.description);
347
+ });
348
+ return changes;
349
+ }
350
+ catch (error) {
351
+ debugPlan('Failed to parse plan: %O', error);
352
+ console.error('Could not parse plan from LLM:', planText);
353
+ return [];
354
+ }
355
+ }
356
+ async function applyPlan(plan, ctx, projectDir) {
357
+ debugPlan('Applying plan with %d changes', plan.length);
358
+ for (const [index, change] of plan.entries()) {
359
+ debugPlan('Applying change %d/%d: %s %s', index + 1, plan.length, change.action, change.file);
360
+ let fileContent = '';
361
+ if (change.action === 'update') {
362
+ try {
363
+ fileContent = await fs.readFile(path.join(projectDir, change.file), 'utf-8');
364
+ debugPlan('Read existing file %s, size: %d bytes', change.file, fileContent.length);
365
+ }
366
+ catch {
367
+ debugPlan('File %s does not exist, will create', change.file);
368
+ }
369
+ }
370
+ const codePrompt = `${makeBasePrompt(ctx)}\nHere is the planned change:\n${JSON.stringify(change, null, 2)}\n${change.action === 'update' ? `Here is the current content of ${change.file}:\n${fileContent}\n` : ''}Please output ONLY the full new code for the file (no markdown, no triple backticks, just code, ready to write to disk).`;
371
+ const code = await callAI(codePrompt);
372
+ debugPlan('Generated code for %s, size: %d bytes', change.file, code.length);
373
+ const outPath = path.join(projectDir, change.file);
374
+ await fs.mkdir(path.dirname(outPath), { recursive: true });
375
+ await fs.writeFile(outPath, code, 'utf-8');
376
+ debugPlan('Successfully wrote file: %s', outPath);
377
+ console.log(`${change.action === 'update' ? 'Updated' : 'Created'} ${change.file}`);
378
+ }
379
+ debugPlan('Plan application complete');
380
+ }
381
+ // interface Fix {
382
+ // action: 'update';
383
+ // file: string;
384
+ // description: string;
385
+ // content: string;
386
+ // }
387
+ // Removed fixTsErrors function - checks now handled by check:client command
388
+ /*
389
+ async function fixTsErrors(ctx: ProjectContext, projectDir: string): Promise<boolean> {
390
+ debugErrors('Checking for TypeScript errors in: %s', projectDir);
391
+ const tsErrors = await getTsErrors(projectDir);
392
+ debugErrors('Found %d TypeScript errors', tsErrors.length);
393
+ console.log('Found', tsErrors.length, 'TypeScript errors');
394
+
395
+ if (tsErrors.length === 0) {
396
+ debugErrors('No TypeScript errors to fix');
397
+ return false;
398
+ }
399
+
400
+ debugErrors('TypeScript errors found:');
401
+ tsErrors.forEach((error, idx) => {
402
+ debugErrors(' Error %d: %s', idx + 1, error);
403
+ });
404
+
405
+ const errorFeedback = tsErrors.join('\n');
406
+ const fixupPrompt = `${makeBasePrompt(ctx)}\n
407
+ After your previous changes, the application produced the following TypeScript errors:\n\n${errorFeedback}\n
408
+ You must now fix **every** error listed above. This is a critical pass: if any error remains after your fix, your output is rejected.
409
+
410
+ Before generating code, analyze and validate your solution against every error. Use existing type definitions, component props, GraphQL typings, and shared interfaces from the project. Do not invent new types or structures unless absolutely necessary.
411
+
412
+ Strict rules:
413
+ - Never use \`any\`, \`as any\`, or unsafe type assertions
414
+ - Do not silence errors — resolve them fully and correctly
415
+ - Fix all errors in each file in one go
416
+ - Reuse existing logic or types instead of re-creating similar ones
417
+ - Do not modify the GraphQL files
418
+ - Do not submit partial updates; provide the full updated content of the file
419
+
420
+ Output must be a **JSON array** only. Each item must include:
421
+ - \`action\`: "update"
422
+ - \`file\`: relative path to the updated file
423
+ - \`description\`: "Fix TypeScript errors"
424
+ - \`content\`: full new content of the file, as a string
425
+
426
+ Do not include explanations, markdown, or code blocks.
427
+ `;
428
+ const fixupPlanText = await callAI(fixupPrompt);
429
+ let fixupPlan: Fix[] = [];
430
+ try {
431
+ fixupPlan = JSON.parse(extractJsonArray(fixupPlanText)) as Fix[];
432
+ debugFixes('Parsed TypeScript fixup plan with %d fixes', fixupPlan.length);
433
+ } catch (e) {
434
+ debugFixes('Failed to parse TypeScript fixup plan: %O', e);
435
+ console.error('Could not parse TS fixup plan from LLM:', e instanceof Error ? e.message : String(e));
436
+ }
437
+
438
+ console.log('Fixup plan has', fixupPlan.length, 'items');
439
+
440
+ for (const [index, fix] of fixupPlan.entries()) {
441
+ debugFixes('Applying fix %d/%d: %s', index + 1, fixupPlan.length, fix.file);
442
+ if (fix.action === 'update' && fix.file && fix.content) {
443
+ const outPath = path.join(projectDir, fix.file);
444
+ await fs.mkdir(path.dirname(outPath), { recursive: true });
445
+ await fs.writeFile(outPath, fix.content, 'utf-8');
446
+ debugFixes('Successfully fixed TypeScript errors in %s', fix.file);
447
+ console.log(`Fixed TS errors in ${fix.file}`);
448
+ }
449
+ }
450
+
451
+ debugFixes('TypeScript error fixing complete');
452
+ return true;
453
+ }
454
+ */
455
+ // Removed fixBuildErrors function - checks now handled by check:client command
456
+ /*
457
+ async function fixBuildErrors(ctx: ProjectContext, projectDir: string): Promise<boolean> {
458
+ debugErrors('Checking for build errors in: %s', projectDir);
459
+ const buildErrors = await getBuildErrors(projectDir);
460
+ debugErrors('Found %d build errors', buildErrors.length);
461
+ console.log('Found', buildErrors.length, 'build errors');
462
+
463
+ if (buildErrors.length === 0) {
464
+ debugErrors('No build errors to fix');
465
+ return false;
466
+ }
467
+
468
+ debugErrors('Build errors found:');
469
+ buildErrors.forEach((error, idx) => {
470
+ debugErrors(' Error %d: %s', idx + 1, error);
471
+ });
472
+
473
+ const errorFeedback = buildErrors.join('\n');
474
+ const fixupPrompt = `${makeBasePrompt(ctx)}\n
475
+ After your previous changes, the application produced the following build errors:\n\n${errorFeedback}\n
476
+ You must now fix **every** error listed above. This is a critical pass: if any error remains after your fix, your output is rejected.
477
+
478
+ Before generating code, analyze and validate your solution against every error. Use existing component props, imports, and shared interfaces from the project. Do not invent new structures unless absolutely necessary.
479
+
480
+ Strict rules:
481
+ - Never use unsafe imports or invalid module references
482
+ - Do not silence errors — resolve them fully and correctly
483
+ - Fix all errors in each file in one go
484
+ - Reuse existing logic instead of re-creating similar ones
485
+ - Do not modify the GraphQL files
486
+ - Do not submit partial updates; provide the full updated content of the file
487
+
488
+ Output must be a **JSON array** only. Each item must include:
489
+ - \`action\`: "update"
490
+ - \`file\`: relative path to the updated file
491
+ - \`description\`: "Fix build errors"
492
+ - \`content\`: full new content of the file, as a string
493
+
494
+ Do not include explanations, markdown, or code blocks.
495
+ `;
496
+ const fixupPlanText = await callAI(fixupPrompt);
497
+ let fixupPlan: Fix[] = [];
498
+ try {
499
+ fixupPlan = JSON.parse(extractJsonArray(fixupPlanText)) as Fix[];
500
+ debugFixes('Parsed build fixup plan with %d fixes', fixupPlan.length);
501
+ } catch (e) {
502
+ debugFixes('Failed to parse build fixup plan: %O', e);
503
+ console.error('Could not parse build fixup plan from LLM:', e instanceof Error ? e.message : String(e));
504
+ }
505
+
506
+ console.log('Fixup plan has', fixupPlan.length, 'items');
507
+
508
+ for (const [index, fix] of fixupPlan.entries()) {
509
+ debugFixes('Applying fix %d/%d: %s', index + 1, fixupPlan.length, fix.file);
510
+ if (fix.action === 'update' && fix.file && fix.content) {
511
+ const outPath = path.join(projectDir, fix.file);
512
+ await fs.mkdir(path.dirname(outPath), { recursive: true });
513
+ await fs.writeFile(outPath, fix.content, 'utf-8');
514
+ debugFixes('Successfully fixed build errors in %s', fix.file);
515
+ console.log(`Fixed build errors in ${fix.file}`);
516
+ }
517
+ }
518
+
519
+ debugFixes('Build error fixing complete');
520
+ return true;
521
+ }
522
+ */
523
+ // Helper to extract all page routes from the IA scheme - kept for potential future use
524
+ // Currently not used as error checking has been removed
525
+ /*
526
+ function extractPageRoutesFromScheme(scheme: Scheme | undefined): string[] {
527
+ debugContext('Extracting page routes from scheme');
528
+ if (scheme?.pages?.items && typeof scheme.pages.items === 'object') {
529
+ const routes = Object.values(scheme.pages.items)
530
+ .map((page) =>
531
+ typeof page === 'object' && 'route' in page && typeof page.route === 'string' ? page.route : undefined,
532
+ )
533
+ .filter((route): route is string => typeof route === 'string');
534
+ debugContext('Extracted %d routes: %o', routes.length, routes);
535
+ return routes;
536
+ }
537
+ debugContext('No page routes found in scheme');
538
+ return [];
539
+ }
540
+ */
541
+ // Removed checkRouteErrors function - checks now handled by check:client command
542
+ /*
543
+ async function checkRouteErrors(baseUrl: string, routes: string[]): Promise<string[]> {
544
+ const allConsoleErrors: string[] = [];
545
+ // Function removed - checks handled by check:client command
546
+ return allConsoleErrors;
547
+ }
548
+ */
549
+ // Removed applyFixes function - checks now handled by check:client command
550
+ /*
551
+ async function applyFixes(fixupPlan: any[], projectDir: string): Promise<void> {
552
+ // Function removed - checks handled by check:client command
553
+ }
554
+ */
555
+ // Removed fixConsoleErrors function - checks now handled by check:client command
556
+ /*
557
+ async function fixConsoleErrors(ctx: ProjectContext, projectDir: string): Promise<boolean> {
558
+ // Function removed - checks handled by check:client command
559
+ return false;
560
+ }
561
+ */
562
+ // Visual error reporting removed - can be handled separately if needed
563
+ /*
564
+ async function checkVisualErrors(baseUrl: string, routes: string[], theme: string): Promise<string> {
565
+ debugScreenshots('Checking visual errors for %d routes', routes.length);
566
+ const screenshots = await getPageScreenshots(baseUrl, routes);
567
+ debugScreenshots('Got %d screenshots', screenshots.length);
568
+
569
+ let allVisualErrors: string = '';
570
+ for (const [index, screenshot] of screenshots.entries()) {
571
+ debugScreenshots('Analyzing screenshot %d/%d for route: %s', index + 1, screenshots.length, screenshot.route);
572
+ console.log(`Checking visual errors for ${screenshot.route}`);
573
+ const error = await generateTextWithImageAI(
574
+ `
575
+ This is the theme used: ${theme}.
576
+ When analyzing UI screenshots, only flag high-impact visual issues that significantly affect usability, accessibility, or user comprehension. Ignore minor spacing inconsistencies, slight misalignments, and non-critical aesthetic variations unless they create a clear functional or accessibility problem. Focus feedback on elements that:
577
+ - Do not flag color or style choices that match the theme.
578
+ - Do not critique placeholder contrast, alignment, or heading hierarchy unless the text is truly unreadable or confusing.
579
+ - Ignore small alignment shifts, whitespace distribution, and center-aligned titles.
580
+ - Only highlight contrast issues if they fail WCAG standards or make text functionally unreadable.
581
+ - Do not mention the lack of loading indicators unless it causes a clear usability failure (e.g., users stuck or misled).
582
+ - Focus only on issues that break flow, block interaction, or seriously reduce clarity.
583
+ - Allow intentionally unique design elements like center-aligned titles.
584
+ - Do not report white space as an issue when the layout is intentionally minimal.
585
+ - Skip pixel-perfect feedback unless there’s a clear visual or structural flaw.
586
+ - Focus on readability, navigability, accessibility, and broken UI flows.
587
+
588
+
589
+ IMPORTANT: return in a nicely formatted markdown, easy to read for the user, not as array of markdown, pure markdown content! Include the route: ${screenshot.route} name, because I have multiple errors showing, and add an empty line at the end.
590
+ IMPORTANT: don't overly nest the markdown sections, just one # Visual Report, below it the name of the route: ## Route: _route_name_, and ### _types_of_issues_ per route (can have multiple under same route) and bullet list after that
591
+ IMPORTANT: return something only if you found valid errors, I don't want to show only the route name from the above request.
592
+ `,
593
+ screenshot.screenshot,
594
+ undefined,
595
+ );
596
+ if (error) {
597
+ debugScreenshots('Visual errors found on route %s', screenshot.route);
598
+ allVisualErrors += error;
599
+ } else {
600
+ debugScreenshots('No visual errors on route %s', screenshot.route);
601
+ }
602
+ }
603
+
604
+ debugScreenshots('Visual error check complete, total errors length: %d', allVisualErrors.length);
605
+ return allVisualErrors;
606
+ }
607
+
608
+ async function getPageScreenshots(baseUrl: string, routes: string[]): Promise<{ route: string; screenshot: string }[]> {
609
+ debugScreenshots('Taking screenshots for %d routes', routes.length);
610
+ const pageScreenshots: { route: string; screenshot: string }[] = [];
611
+
612
+ for (const [index, route] of routes.entries()) {
613
+ const url = baseUrl + (route.startsWith('/') ? route : '/' + route);
614
+ debugScreenshots('Taking screenshot %d/%d for: %s', index + 1, routes.length, url);
615
+ console.log(`Taking screenshot for ${url}`);
616
+
617
+ const screenshot = await getPageScreenshot(url);
618
+ if (screenshot) {
619
+ debugScreenshots('Screenshot captured for %s, size: %d bytes', route, screenshot.length);
620
+ pageScreenshots.push({
621
+ route: route,
622
+ screenshot: screenshot,
623
+ });
624
+ } else {
625
+ debugScreenshots('Failed to capture screenshot for %s', route);
626
+ }
627
+ }
628
+
629
+ debugScreenshots('Closing browser after screenshots');
630
+ await closeBrowser();
631
+ debugScreenshots('Captured %d screenshots', pageScreenshots.length);
632
+ return pageScreenshots;
633
+ }
634
+ */
635
+ // Visual error reporting removed - can be handled separately if needed
636
+ /*
637
+ async function reportVisualErrors(ctx: ProjectContext): Promise<void> {
638
+ debugScreenshots('Starting visual error report');
639
+ const baseUrl = 'http://localhost:8080';
640
+
641
+ let routes = extractPageRoutesFromScheme(ctx.scheme);
642
+ if (routes.length === 0) {
643
+ debugScreenshots('No routes found, defaulting to root');
644
+ routes = ['/'];
645
+ }
646
+ debugScreenshots('Reporting visual errors for %d routes', routes.length);
647
+
648
+ const allVisualErrors = await checkVisualErrors(baseUrl, routes, ctx.theme);
649
+
650
+ if (allVisualErrors) {
651
+ debugScreenshots('Visual errors report generated, length: %d', allVisualErrors.length);
652
+ } else {
653
+ debugScreenshots('No visual errors to report');
654
+ }
655
+
656
+ console.log(allVisualErrors);
657
+ }
658
+ */
659
+ // async function fixVisualErrors(ctx: ProjectContext, projectDir: string): Promise<boolean> {
660
+ // const baseUrl = 'http://localhost:8080';
661
+ // let routes = extractPageRoutesFromScheme(ctx.scheme);
662
+ // if (routes.length === 0) {
663
+ // routes = ['/'];
664
+ // }
665
+ //
666
+ // const allVisualErrors = await checkVisualErrors(baseUrl, routes, ctx.theme);
667
+ // console.log('Found', allVisualErrors, 'visual errors');
668
+ // if (allVisualErrors.length === 0) {
669
+ // await closeBrowser();
670
+ // return false;
671
+ // }
672
+ //
673
+ // const fixupPrompt = `${makeBasePrompt(ctx)}\n
674
+ // After your previous changes, the application has the following visual errors:\n\n${allVisualErrors}\n
675
+ // You must now fix **every** error listed above. This is a critical pass: if any error remains after your fix, your output is rejected.
676
+ //
677
+ // Before generating code, analyze and validate your solution against every error. Use existing types, props, and logic from the project. Do not invent new structures unless absolutely necessary.
678
+ //
679
+ // Strict rules:
680
+ // - Ignore connection or network errors
681
+ // - Never use \`any\`, unsafe type assertions, or silence errors
682
+ // - Do not silence errors — resolve them fully and correctly
683
+ // - Fix all errors in each file in one go
684
+ // - Reuse existing logic or types instead of re-creating similar ones
685
+ // - Do not submit partial updates; provide the full updated content of the file
686
+ //
687
+ // Output must be a **JSON array** only. Each item must include:
688
+ // - \`action\`: "update"
689
+ // - \`file\`: relative path to the updated file
690
+ // - \`description\`: "Fix console errors"
691
+ // - \`content\`: full new content of the file, as a string
692
+ //
693
+ // Do not include explanations, markdown, or code blocks.
694
+ // `;
695
+ // let fixupPlan: Fix[] = [];
696
+ // try {
697
+ // fixupPlan = JSON.parse(extractJsonArray(await callAI(fixupPrompt))) as Fix[];
698
+ // } catch (e) {
699
+ // console.error('Could not parse visual fixup plan from LLM:', e instanceof Error ? e.message : String(e));
700
+ // }
701
+ //
702
+ // console.log('Fixup plan has', fixupPlan.length, 'items');
703
+ // await applyFixes(fixupPlan, projectDir);
704
+ // await closeBrowser();
705
+ // return true;
706
+ // }
707
+ // Removed fixErrorsLoop function - checks now handled by check:client command
708
+ /*
709
+ async function fixErrorsLoop(ctx: ProjectContext, projectDir: string) {
710
+ // Function removed - checks handled by check:client command
711
+ }
712
+ */
713
+ export async function runAIAgent(projectDir, iaSchemeDir, designSystemPath, failures) {
714
+ debug('='.repeat(80));
715
+ debug('Starting FE implementer agent');
716
+ debug('Project directory: %s', projectDir);
717
+ debug('IA scheme directory: %s', iaSchemeDir);
718
+ debug('Design system path: %s', designSystemPath);
719
+ debug('='.repeat(80));
720
+ const userPreferencesFile = path.resolve(projectDir, 'design-system-principles.md');
721
+ debug('Loading user preferences from: %s', userPreferencesFile);
722
+ const userPreferences = await fs.readFile(userPreferencesFile, 'utf-8');
723
+ debug('User preferences loaded, size: %d bytes', userPreferences.length);
724
+ debug('Loading design system from: %s', designSystemPath);
725
+ const designSystem = await fs.readFile(designSystemPath, 'utf-8');
726
+ debug('Design system loaded, size: %d bytes', designSystem.length);
727
+ debug('Building project context...');
728
+ const ctx = await getProjectContext(projectDir, iaSchemeDir, userPreferences, designSystem, failures);
729
+ debug('Project context created successfully');
730
+ debug('Planning project implementation...');
731
+ const plan = await planProject(ctx);
732
+ debugPlan('Generated plan with %d items', plan.length);
733
+ debug('Applying implementation plan...');
734
+ await applyPlan(plan, ctx, projectDir);
735
+ debug('Plan applied successfully');
736
+ // Error checking removed - now handled by separate check:client command
737
+ debug('Skipping error correction phase - use check:client command for validation');
738
+ // Visual error reporting removed - can be handled separately if needed
739
+ debug('Skipping visual error report - use separate visual testing tools if needed');
740
+ // Browser cleanup no longer needed as we don't run checks
741
+ debug('Implementation complete - no browser cleanup needed');
742
+ debug('='.repeat(80));
743
+ console.log('AI project implementation complete!');
744
+ debug('AI agent completed successfully');
745
+ debug('='.repeat(80));
746
+ }
747
+ //# sourceMappingURL=agent.js.map