@auto-engineer/component-implementor-react 1.102.0 → 1.103.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 (83) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/.turbo/turbo-test.log +5 -5
  3. package/.turbo/turbo-type-check.log +1 -1
  4. package/CHANGELOG.md +54 -0
  5. package/dist/src/commands/implement-component.d.ts.map +1 -1
  6. package/dist/src/commands/implement-component.js +4 -0
  7. package/dist/src/commands/implement-component.js.map +1 -1
  8. package/dist/src/commands/implement-component.test.js +52 -0
  9. package/dist/src/commands/implement-component.test.js.map +1 -1
  10. package/dist/src/pipeline/run-pipeline.d.ts +8 -0
  11. package/dist/src/pipeline/run-pipeline.d.ts.map +1 -1
  12. package/dist/src/pipeline/run-pipeline.js +8 -0
  13. package/dist/src/pipeline/run-pipeline.js.map +1 -1
  14. package/dist/src/pipeline/run-pipeline.test.js +39 -3
  15. package/dist/src/pipeline/run-pipeline.test.js.map +1 -1
  16. package/dist/src/pipeline/steps/generate-component.test.js +4 -0
  17. package/dist/src/pipeline/steps/generate-component.test.js.map +1 -1
  18. package/dist/src/pipeline/steps/generate-story.test.js +4 -0
  19. package/dist/src/pipeline/steps/generate-story.test.js.map +1 -1
  20. package/dist/src/pipeline/steps/generate-test.test.js +4 -0
  21. package/dist/src/pipeline/steps/generate-test.test.js.map +1 -1
  22. package/dist/src/pipeline/steps/lint-fix-loop.d.ts.map +1 -1
  23. package/dist/src/pipeline/steps/lint-fix-loop.js +4 -3
  24. package/dist/src/pipeline/steps/lint-fix-loop.js.map +1 -1
  25. package/dist/src/pipeline/steps/lint-fix-loop.test.js +17 -0
  26. package/dist/src/pipeline/steps/lint-fix-loop.test.js.map +1 -1
  27. package/dist/src/pipeline/steps/story-fix-loop.d.ts.map +1 -1
  28. package/dist/src/pipeline/steps/story-fix-loop.js +1 -0
  29. package/dist/src/pipeline/steps/story-fix-loop.js.map +1 -1
  30. package/dist/src/pipeline/steps/story-fix-loop.test.js +4 -0
  31. package/dist/src/pipeline/steps/story-fix-loop.test.js.map +1 -1
  32. package/dist/src/pipeline/steps/storybook-test.test.js +4 -0
  33. package/dist/src/pipeline/steps/storybook-test.test.js.map +1 -1
  34. package/dist/src/pipeline/steps/test-fix-loop.d.ts.map +1 -1
  35. package/dist/src/pipeline/steps/test-fix-loop.js +1 -0
  36. package/dist/src/pipeline/steps/test-fix-loop.js.map +1 -1
  37. package/dist/src/pipeline/steps/test-fix-loop.test.js +4 -0
  38. package/dist/src/pipeline/steps/test-fix-loop.test.js.map +1 -1
  39. package/dist/src/pipeline/steps/type-fix-loop.d.ts.map +1 -1
  40. package/dist/src/pipeline/steps/type-fix-loop.js +1 -0
  41. package/dist/src/pipeline/steps/type-fix-loop.js.map +1 -1
  42. package/dist/src/pipeline/steps/type-fix-loop.test.js +4 -0
  43. package/dist/src/pipeline/steps/type-fix-loop.test.js.map +1 -1
  44. package/dist/src/prompt.d.ts +3 -3
  45. package/dist/src/prompt.d.ts.map +1 -1
  46. package/dist/src/prompt.js +29 -9
  47. package/dist/src/prompt.js.map +1 -1
  48. package/dist/src/prompt.test.js +143 -0
  49. package/dist/src/prompt.test.js.map +1 -1
  50. package/dist/src/tools/lint-runner.d.ts.map +1 -1
  51. package/dist/src/tools/lint-runner.js +9 -4
  52. package/dist/src/tools/lint-runner.js.map +1 -1
  53. package/dist/src/tools/lint-runner.test.js +3 -7
  54. package/dist/src/tools/lint-runner.test.js.map +1 -1
  55. package/dist/src/tools/test-runner.js +8 -4
  56. package/dist/src/tools/test-runner.js.map +1 -1
  57. package/dist/src/tools/test-runner.test.js +49 -3
  58. package/dist/src/tools/test-runner.test.js.map +1 -1
  59. package/dist/tsconfig.tsbuildinfo +1 -1
  60. package/package.json +3 -3
  61. package/scripts/generate-all.ts +673 -0
  62. package/src/commands/implement-component.test.ts +52 -0
  63. package/src/commands/implement-component.ts +4 -0
  64. package/src/pipeline/run-pipeline.test.ts +39 -3
  65. package/src/pipeline/run-pipeline.ts +16 -0
  66. package/src/pipeline/steps/generate-component.test.ts +4 -0
  67. package/src/pipeline/steps/generate-story.test.ts +4 -0
  68. package/src/pipeline/steps/generate-test.test.ts +4 -0
  69. package/src/pipeline/steps/lint-fix-loop.test.ts +21 -0
  70. package/src/pipeline/steps/lint-fix-loop.ts +5 -3
  71. package/src/pipeline/steps/story-fix-loop.test.ts +4 -0
  72. package/src/pipeline/steps/story-fix-loop.ts +1 -0
  73. package/src/pipeline/steps/storybook-test.test.ts +4 -0
  74. package/src/pipeline/steps/test-fix-loop.test.ts +4 -0
  75. package/src/pipeline/steps/test-fix-loop.ts +1 -0
  76. package/src/pipeline/steps/type-fix-loop.test.ts +4 -0
  77. package/src/pipeline/steps/type-fix-loop.ts +1 -0
  78. package/src/prompt.test.ts +176 -0
  79. package/src/prompt.ts +29 -9
  80. package/src/tools/lint-runner.test.ts +6 -7
  81. package/src/tools/lint-runner.ts +10 -4
  82. package/src/tools/test-runner.test.ts +55 -3
  83. package/src/tools/test-runner.ts +8 -4
package/package.json CHANGED
@@ -6,13 +6,13 @@
6
6
  "dependencies": {
7
7
  "ai": "^6.0.0",
8
8
  "debug": "^4.4.1",
9
- "@auto-engineer/message-bus": "1.102.0",
10
- "@auto-engineer/model-factory": "1.102.0"
9
+ "@auto-engineer/message-bus": "1.103.0",
10
+ "@auto-engineer/model-factory": "1.103.0"
11
11
  },
12
12
  "devDependencies": {
13
13
  "vitest": "^3.2.1"
14
14
  },
15
- "version": "1.102.0",
15
+ "version": "1.103.0",
16
16
  "publishConfig": {
17
17
  "access": "public"
18
18
  },
@@ -0,0 +1,673 @@
1
+ import 'dotenv/config';
2
+ import { execSync } from 'node:child_process';
3
+ import {
4
+ existsSync,
5
+ readFileSync as fsReadFileSync,
6
+ mkdirSync,
7
+ readdirSync,
8
+ rmSync,
9
+ statSync,
10
+ writeFileSync,
11
+ } from 'node:fs';
12
+ import { mkdir } from 'node:fs/promises';
13
+ import path, { dirname, resolve } from 'node:path';
14
+ import { fileURLToPath } from 'node:url';
15
+ import { createModelFromEnv } from '@auto-engineer/model-factory';
16
+ import {
17
+ buildPipelineSteps,
18
+ type PipelineConfig,
19
+ type PipelineContext,
20
+ type PipelineModels,
21
+ runPipeline,
22
+ } from '../src/pipeline/run-pipeline';
23
+ import { buildFullProjectSection } from '../src/project-context';
24
+
25
+ const __filename = fileURLToPath(import.meta.url);
26
+ const __dirname = dirname(__filename);
27
+ const PKG_ROOT = resolve(__dirname, '..');
28
+ const INPUTS_DIR = resolve(PKG_ROOT, 'inputs');
29
+ const OUTPUTS_DIR = resolve(PKG_ROOT, 'outputs');
30
+ const BIOME_SOURCE = resolve(PKG_ROOT, '..', 'generate-react-client', 'starter', 'biome.json');
31
+
32
+ type ArchitectComponent = {
33
+ componentId: string;
34
+ componentName: string;
35
+ isNew: boolean;
36
+ atomicType: string;
37
+ composes?: string[];
38
+ specDeltas: {
39
+ structure: string[];
40
+ rendering: string[];
41
+ interaction: string[];
42
+ styling: string[];
43
+ };
44
+ props?: Array<{
45
+ name: string;
46
+ type: string;
47
+ required: boolean;
48
+ default?: string;
49
+ description: string;
50
+ category: 'data' | 'callback' | 'slot' | 'visual' | 'state' | 'config';
51
+ }>;
52
+ storyVariants?: Array<{
53
+ name: string;
54
+ description: string;
55
+ args: Record<string, unknown>;
56
+ needsPlayFunction?: boolean;
57
+ playDescription?: string;
58
+ }>;
59
+ dataContract?: Record<string, unknown>;
60
+ };
61
+
62
+ function log(msg: string) {
63
+ console.log(` ${msg}`);
64
+ }
65
+
66
+ function heading(msg: string) {
67
+ console.log(`\n${'─'.repeat(60)}`);
68
+ console.log(` ${msg}`);
69
+ console.log(`${'─'.repeat(60)}\n`);
70
+ }
71
+
72
+ function pascalCase(id: string): string {
73
+ return id
74
+ .split('-')
75
+ .map((s) => s.charAt(0).toUpperCase() + s.slice(1))
76
+ .join('');
77
+ }
78
+
79
+ function isUiComponent(id: string): boolean {
80
+ return id.startsWith('ui-components-');
81
+ }
82
+
83
+ function uiComponentName(id: string): string {
84
+ return pascalCase(id.replace('ui-components-', ''));
85
+ }
86
+
87
+ function componentPath(id: string): string {
88
+ if (isUiComponent(id)) {
89
+ return `src/components/ui/${uiComponentName(id)}.tsx`;
90
+ }
91
+ return `src/components/${pascalCase(id)}.tsx`;
92
+ }
93
+
94
+ function filePathToImportAlias(filePath: string): string {
95
+ let rel = filePath;
96
+ if (rel.startsWith('./')) {
97
+ rel = rel.slice(2);
98
+ }
99
+ if (rel.startsWith('src/')) {
100
+ rel = rel.slice(4);
101
+ }
102
+ rel = rel.replace(/\.tsx?$/, '');
103
+ return `@/${rel}`;
104
+ }
105
+
106
+ function discoverModels(): string[] {
107
+ if (!existsSync(INPUTS_DIR)) return [];
108
+
109
+ return readdirSync(INPUTS_DIR)
110
+ .filter((entry) => {
111
+ const entryPath = resolve(INPUTS_DIR, entry);
112
+ return (
113
+ entry.startsWith('model-') &&
114
+ statSync(entryPath).isDirectory() &&
115
+ existsSync(resolve(entryPath, 'spec-deltas.json'))
116
+ );
117
+ })
118
+ .sort();
119
+ }
120
+
121
+ function loadSpecDeltas(model: string): ArchitectComponent[] {
122
+ const filePath = resolve(INPUTS_DIR, model, 'spec-deltas.json');
123
+ const raw = JSON.parse(fsReadFileSync(filePath, 'utf-8'));
124
+ return raw as ArchitectComponent[];
125
+ }
126
+
127
+ function collectUiComponentIds(components: ArchitectComponent[]): Set<string> {
128
+ const ids = new Set<string>();
129
+ for (const comp of components) {
130
+ for (const dep of comp.composes ?? []) {
131
+ if (isUiComponent(dep)) {
132
+ ids.add(dep);
133
+ }
134
+ }
135
+ if (isUiComponent(comp.componentId)) {
136
+ ids.add(comp.componentId);
137
+ }
138
+ }
139
+ return ids;
140
+ }
141
+
142
+ function topologicalSort(components: ArchitectComponent[]): ArchitectComponent[] {
143
+ const idToComponent = new Map<string, ArchitectComponent>();
144
+ for (const comp of components) {
145
+ idToComponent.set(comp.componentId, comp);
146
+ }
147
+
148
+ const visited = new Set<string>();
149
+ const sorted: ArchitectComponent[] = [];
150
+
151
+ function visit(id: string) {
152
+ if (visited.has(id)) return;
153
+ visited.add(id);
154
+
155
+ const comp = idToComponent.get(id);
156
+ if (!comp) return;
157
+
158
+ for (const dep of comp.composes ?? []) {
159
+ if (!isUiComponent(dep) && idToComponent.has(dep)) {
160
+ visit(dep);
161
+ }
162
+ }
163
+
164
+ sorted.push(comp);
165
+ }
166
+
167
+ for (const comp of components) {
168
+ visit(comp.componentId);
169
+ }
170
+
171
+ return sorted;
172
+ }
173
+
174
+ function getSemanticElement(name: string): { tag: string; selfClosing: boolean; extraProps: string } {
175
+ const lower = name.toLowerCase();
176
+ if (lower.includes('button')) return { tag: 'button', selfClosing: false, extraProps: ' type="button"' };
177
+ if (lower.includes('input') || lower === 'textarea')
178
+ return { tag: lower === 'textarea' ? 'textarea' : 'input', selfClosing: lower !== 'textarea', extraProps: '' };
179
+ if (lower.includes('label')) return { tag: 'label', selfClosing: false, extraProps: '' };
180
+ if (lower.includes('separator') || lower.includes('divider'))
181
+ return { tag: 'hr', selfClosing: true, extraProps: ' role="separator"' };
182
+ if (lower.includes('dialog') || lower.includes('modal'))
183
+ return { tag: 'div', selfClosing: false, extraProps: ' role="dialog"' };
184
+ if (lower.includes('checkbox')) return { tag: 'input', selfClosing: true, extraProps: ' type="checkbox"' };
185
+ if (lower.includes('select')) return { tag: 'select', selfClosing: false, extraProps: '' };
186
+ if (lower.includes('link') || lower.includes('anchor'))
187
+ return { tag: 'a', selfClosing: false, extraProps: ' href="#"' };
188
+ if (lower.includes('image') || lower.includes('avatar') || lower.includes('img'))
189
+ return { tag: 'img', selfClosing: true, extraProps: ' alt="" src=""' };
190
+ if (lower.includes('heading')) return { tag: 'h2', selfClosing: false, extraProps: '' };
191
+ if (lower.includes('badge') || lower.includes('chip')) return { tag: 'span', selfClosing: false, extraProps: '' };
192
+ if (lower.includes('list')) return { tag: 'ul', selfClosing: false, extraProps: '' };
193
+ if (lower.includes('table')) return { tag: 'table', selfClosing: false, extraProps: '' };
194
+ if (lower.includes('form')) return { tag: 'form', selfClosing: false, extraProps: '' };
195
+ if (lower.includes('nav')) return { tag: 'nav', selfClosing: false, extraProps: '' };
196
+ if (lower.includes('section')) return { tag: 'section', selfClosing: false, extraProps: '' };
197
+ if (lower.includes('header')) return { tag: 'header', selfClosing: false, extraProps: '' };
198
+ if (lower.includes('footer')) return { tag: 'footer', selfClosing: false, extraProps: '' };
199
+ return { tag: 'div', selfClosing: false, extraProps: '' };
200
+ }
201
+
202
+ function generateUiStub(name: string): string {
203
+ const { tag, selfClosing, extraProps } = getSemanticElement(name);
204
+
205
+ if (selfClosing) {
206
+ return [
207
+ `import type React from 'react';`,
208
+ ``,
209
+ `export type ${name}Props = {`,
210
+ ` children?: React.ReactNode;`,
211
+ ` className?: string;`,
212
+ ` [key: string]: unknown;`,
213
+ `};`,
214
+ ``,
215
+ `export function ${name}({ className, ...props }: ${name}Props) {`,
216
+ ` return <${tag} className={className}${extraProps} {...props} />;`,
217
+ `}`,
218
+ ``,
219
+ ].join('\n');
220
+ }
221
+
222
+ return [
223
+ `import type React from 'react';`,
224
+ ``,
225
+ `export type ${name}Props = {`,
226
+ ` children?: React.ReactNode;`,
227
+ ` className?: string;`,
228
+ ` [key: string]: unknown;`,
229
+ `};`,
230
+ ``,
231
+ `export function ${name}({ children, className, ...props }: ${name}Props) {`,
232
+ ` return <${tag} className={className}${extraProps} {...props}>{children}</${tag}>;`,
233
+ `}`,
234
+ ``,
235
+ ].join('\n');
236
+ }
237
+
238
+ function scaffoldClientProject(targetDir: string, uiComponentIds: Set<string>) {
239
+ mkdirSync(resolve(targetDir, 'src/components/ui'), { recursive: true });
240
+ mkdirSync(resolve(targetDir, 'src/components'), { recursive: true });
241
+ mkdirSync(resolve(targetDir, 'src/lib'), { recursive: true });
242
+
243
+ writeFileSync(
244
+ resolve(targetDir, 'package.json'),
245
+ JSON.stringify(
246
+ {
247
+ name: 'generated-client',
248
+ private: true,
249
+ type: 'module',
250
+ scripts: {
251
+ 'type-check': 'tsc --noEmit',
252
+ test: 'vitest run',
253
+ lint: 'biome check src/',
254
+ 'lint:fix': 'biome check --write src/',
255
+ },
256
+ dependencies: {
257
+ react: '^19.0.0',
258
+ 'react-dom': '^19.0.0',
259
+ 'class-variance-authority': '^0.7.1',
260
+ clsx: '^2.1.1',
261
+ 'tailwind-merge': '^3.0.0',
262
+ },
263
+ devDependencies: {
264
+ '@biomejs/biome': '2.3.10',
265
+ '@testing-library/jest-dom': '^6.6.0',
266
+ '@testing-library/react': '^16.0.0',
267
+ '@testing-library/user-event': '^14.5.0',
268
+ '@types/react': '^19.0.0',
269
+ '@types/react-dom': '^19.0.0',
270
+ '@storybook/react-vite': '^8.0.0',
271
+ jsdom: '^25.0.0',
272
+ typescript: '^5.7.0',
273
+ vitest: '^3.2.0',
274
+ tailwindcss: '^4.0.0',
275
+ },
276
+ },
277
+ null,
278
+ 2,
279
+ ),
280
+ );
281
+
282
+ writeFileSync(
283
+ resolve(targetDir, 'tsconfig.json'),
284
+ JSON.stringify(
285
+ {
286
+ compilerOptions: {
287
+ target: 'ES2022',
288
+ module: 'ESNext',
289
+ moduleResolution: 'bundler',
290
+ jsx: 'react-jsx',
291
+ strict: true,
292
+ esModuleInterop: true,
293
+ skipLibCheck: true,
294
+ outDir: 'dist',
295
+ rootDir: '.',
296
+ baseUrl: '.',
297
+ paths: { '@/*': ['./src/*'] },
298
+ types: ['vitest/globals'],
299
+ },
300
+ include: ['src'],
301
+ },
302
+ null,
303
+ 2,
304
+ ),
305
+ );
306
+
307
+ writeFileSync(
308
+ resolve(targetDir, 'vitest.config.ts'),
309
+ [
310
+ `import { defineConfig } from 'vitest/config';`,
311
+ `import path from 'path';`,
312
+ ``,
313
+ `export default defineConfig({`,
314
+ ` test: {`,
315
+ ` environment: 'jsdom',`,
316
+ ` globals: true,`,
317
+ ` setupFiles: ['./src/test-setup.ts'],`,
318
+ ` },`,
319
+ ` resolve: {`,
320
+ ` alias: {`,
321
+ ` '@': path.resolve(__dirname, './src'),`,
322
+ ` },`,
323
+ ` },`,
324
+ `});`,
325
+ ``,
326
+ ].join('\n'),
327
+ );
328
+
329
+ writeFileSync(resolve(targetDir, 'src/test-setup.ts'), `import '@testing-library/jest-dom/vitest';\n`);
330
+
331
+ writeFileSync(
332
+ resolve(targetDir, 'src/index.css'),
333
+ [
334
+ `@import 'tailwindcss';`,
335
+ ``,
336
+ `@theme {`,
337
+ ` --color-background: #ffffff;`,
338
+ ` --color-foreground: #0a0a0a;`,
339
+ ` --color-primary: #171717;`,
340
+ ` --color-primary-foreground: #fafafa;`,
341
+ ` --color-secondary: #f5f5f5;`,
342
+ ` --color-secondary-foreground: #171717;`,
343
+ ` --color-muted: #f5f5f5;`,
344
+ ` --color-muted-foreground: #737373;`,
345
+ ` --color-accent: #f5f5f5;`,
346
+ ` --color-accent-foreground: #171717;`,
347
+ ` --color-destructive: #ef4444;`,
348
+ ` --color-destructive-foreground: #fafafa;`,
349
+ ` --color-border: #e5e5e5;`,
350
+ ` --color-input: #e5e5e5;`,
351
+ ` --color-ring: #171717;`,
352
+ ` --color-popover: #ffffff;`,
353
+ ` --color-popover-foreground: #0a0a0a;`,
354
+ ` --color-card: #ffffff;`,
355
+ ` --color-card-foreground: #0a0a0a;`,
356
+ ` --radius-sm: 0.25rem;`,
357
+ ` --radius-md: 0.375rem;`,
358
+ ` --radius-lg: 0.5rem;`,
359
+ `}`,
360
+ ``,
361
+ ].join('\n'),
362
+ );
363
+
364
+ writeFileSync(
365
+ resolve(targetDir, 'src/lib/utils.ts'),
366
+ [
367
+ `import { type ClassValue, clsx } from 'clsx';`,
368
+ `import { twMerge } from 'tailwind-merge';`,
369
+ ``,
370
+ `export function cn(...inputs: ClassValue[]) {`,
371
+ ` return twMerge(clsx(inputs));`,
372
+ `}`,
373
+ ``,
374
+ ].join('\n'),
375
+ );
376
+
377
+ if (existsSync(BIOME_SOURCE)) {
378
+ const biomeConfig = JSON.parse(fsReadFileSync(BIOME_SOURCE, 'utf-8'));
379
+ biomeConfig.vcs = { enabled: false };
380
+ writeFileSync(resolve(targetDir, 'biome.json'), JSON.stringify(biomeConfig, null, '\t'));
381
+ }
382
+
383
+ for (const uiId of uiComponentIds) {
384
+ const name = uiComponentName(uiId);
385
+ const stubPath = resolve(targetDir, 'src/components/ui', `${name}.tsx`);
386
+ writeFileSync(stubPath, generateUiStub(name));
387
+ }
388
+
389
+ const barrelLines: string[] = [];
390
+ for (const uiId of [...uiComponentIds].sort()) {
391
+ const name = uiComponentName(uiId);
392
+ barrelLines.push(`export { ${name} } from './${name}';`);
393
+ barrelLines.push(`export type { ${name}Props } from './${name}';`);
394
+ }
395
+ writeFileSync(resolve(targetDir, 'src/components/ui/index.ts'), `${barrelLines.join('\n')}\n`);
396
+ }
397
+
398
+ function buildPipelineConfig(): PipelineConfig {
399
+ return {
400
+ models: {
401
+ generateTest:
402
+ process.env.STEP_GENERATE_TEST_MODEL ??
403
+ process.env.IMPL_MODEL ??
404
+ process.env.CUSTOM_PROVIDER_DEFAULT_MODEL ??
405
+ '',
406
+ generateComponent:
407
+ process.env.STEP_GENERATE_COMPONENT_MODEL ??
408
+ process.env.IMPL_MODEL ??
409
+ process.env.CUSTOM_PROVIDER_DEFAULT_MODEL ??
410
+ '',
411
+ typeFixer:
412
+ process.env.STEP_TYPE_FIXER_MODEL ??
413
+ process.env.IMPL_FIXER_MODEL ??
414
+ process.env.IMPL_MODEL ??
415
+ process.env.CUSTOM_PROVIDER_DEFAULT_MODEL ??
416
+ '',
417
+ testFixer:
418
+ process.env.STEP_TEST_FIXER_MODEL ??
419
+ process.env.IMPL_FIXER_MODEL ??
420
+ process.env.IMPL_MODEL ??
421
+ process.env.CUSTOM_PROVIDER_DEFAULT_MODEL ??
422
+ '',
423
+ lintFixer:
424
+ process.env.STEP_LINT_FIXER_MODEL ??
425
+ process.env.IMPL_FIXER_MODEL ??
426
+ process.env.IMPL_MODEL ??
427
+ process.env.CUSTOM_PROVIDER_DEFAULT_MODEL ??
428
+ '',
429
+ storyFixer:
430
+ process.env.STEP_STORY_FIXER_MODEL ??
431
+ process.env.IMPL_FIXER_MODEL ??
432
+ process.env.IMPL_MODEL ??
433
+ process.env.CUSTOM_PROVIDER_DEFAULT_MODEL ??
434
+ '',
435
+ },
436
+ maxTypeFixIterations: 3,
437
+ maxTestFixIterations: 5,
438
+ maxLintFixIterations: 3,
439
+ maxStoryFixIterations: 2,
440
+ enableStorybookTest: process.env.ENABLE_STORYBOOK_TEST === 'true',
441
+ enableVisualTest: process.env.ENABLE_VISUAL_TEST === 'true',
442
+ };
443
+ }
444
+
445
+ type ComponentResult = {
446
+ componentId: string;
447
+ success: boolean;
448
+ error?: string;
449
+ llmCalls: number;
450
+ fixIterations: number;
451
+ typeFixIterations: number;
452
+ testFixIterations: number;
453
+ lintFixIterations: number;
454
+ storyFixIterations: number;
455
+ };
456
+
457
+ async function generateModel(model: string): Promise<ComponentResult[]> {
458
+ heading(`Model: ${model}`);
459
+
460
+ const allComponents = loadSpecDeltas(model);
461
+ log(`Loaded ${allComponents.length} total specs from spec-deltas.json`);
462
+
463
+ const components = allComponents.filter((c) => !isUiComponent(c.componentId));
464
+ log(
465
+ `${components.length} components to generate (${allComponents.length - components.length} ui-component stubs excluded)`,
466
+ );
467
+
468
+ const uiComponentIds = collectUiComponentIds(allComponents);
469
+ log(`${uiComponentIds.size} ui-component stubs to scaffold`);
470
+
471
+ const targetDir = resolve(OUTPUTS_DIR, model, 'client');
472
+
473
+ if (existsSync(targetDir)) {
474
+ rmSync(targetDir, { recursive: true, force: true });
475
+ }
476
+
477
+ scaffoldClientProject(targetDir, uiComponentIds);
478
+ log(`Scaffolded client project at ${targetDir}`);
479
+
480
+ log('Installing dependencies...');
481
+ execSync('pnpm install --ignore-workspace --no-frozen-lockfile', { cwd: targetDir, stdio: 'pipe' });
482
+ log('Dependencies installed');
483
+
484
+ const sorted = topologicalSort(components);
485
+ log(`Generation order: ${sorted.map((c) => c.componentId).join(' → ')}`);
486
+
487
+ const llmModel = createModelFromEnv();
488
+ const pipelineModels: PipelineModels = {
489
+ generateTest: llmModel,
490
+ generateComponent: llmModel,
491
+ typeFixer: llmModel,
492
+ testFixer: llmModel,
493
+ lintFixer: llmModel,
494
+ storyFixer: llmModel,
495
+ };
496
+ const pipelineConfig = buildPipelineConfig();
497
+
498
+ const results: ComponentResult[] = [];
499
+
500
+ for (const comp of sorted) {
501
+ const startTime = Date.now();
502
+ log(`\n ── ${comp.componentId} ──`);
503
+
504
+ const componentName = pascalCase(comp.componentId);
505
+ const rawPath = `src/components/${componentName}.tsx`;
506
+ const compPath = componentPath(comp.componentId);
507
+ const storyPath = compPath.replace('.tsx', '.stories.tsx');
508
+ const compFullPath = path.resolve(targetDir, compPath);
509
+ const testFullPath = path.join(path.dirname(compFullPath), `${componentName}.test.tsx`);
510
+ const storyFullPath = path.resolve(targetDir, storyPath);
511
+ const componentImportPath = filePathToImportAlias(rawPath);
512
+
513
+ const composesResolved = (comp.composes ?? []).map((depId) => ({
514
+ id: depId,
515
+ path: componentPath(depId),
516
+ }));
517
+
518
+ const projectSection = await buildFullProjectSection(targetDir, composesResolved);
519
+ await mkdir(path.dirname(testFullPath), { recursive: true });
520
+
521
+ const ctx: PipelineContext = {
522
+ componentName,
523
+ componentPath: compFullPath,
524
+ testPath: testFullPath,
525
+ storyPath: storyFullPath,
526
+ componentImportPath,
527
+ targetDir,
528
+ specDeltas: {
529
+ structure: comp.specDeltas.structure,
530
+ rendering: comp.specDeltas.rendering,
531
+ interaction: comp.specDeltas.interaction,
532
+ styling: comp.specDeltas.styling,
533
+ },
534
+ projectSection,
535
+ props: comp.props,
536
+ composes: composesResolved,
537
+ isModify: false,
538
+ storyVariants: comp.storyVariants,
539
+ llmCalls: 0,
540
+ fixIterations: 0,
541
+ typeFixIterations: 0,
542
+ testFixIterations: 0,
543
+ lintFixIterations: 0,
544
+ storyFixIterations: 0,
545
+ };
546
+
547
+ const steps = buildPipelineSteps(pipelineModels, pipelineConfig, ctx);
548
+
549
+ try {
550
+ const result = await runPipeline(steps, ctx);
551
+ const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
552
+
553
+ if (result.success) {
554
+ log(` ✓ ${componentName} (${elapsed}s) [llm=${result.llmCalls} fix=${result.fixIterations}]`);
555
+ } else {
556
+ log(` ✗ ${componentName}: ${result.error} (${elapsed}s)`);
557
+ }
558
+
559
+ results.push({
560
+ componentId: comp.componentId,
561
+ success: result.success,
562
+ error: result.error,
563
+ llmCalls: result.llmCalls,
564
+ fixIterations: result.fixIterations,
565
+ typeFixIterations: result.typeFixIterations,
566
+ testFixIterations: result.testFixIterations,
567
+ lintFixIterations: result.lintFixIterations,
568
+ storyFixIterations: result.storyFixIterations,
569
+ });
570
+ } catch (error) {
571
+ const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
572
+ const errorMessage = error instanceof Error ? error.message : String(error);
573
+ log(` ✗ ${componentName}: ${errorMessage} (${elapsed}s)`);
574
+ results.push({
575
+ componentId: comp.componentId,
576
+ success: false,
577
+ error: errorMessage,
578
+ llmCalls: ctx.llmCalls,
579
+ fixIterations: ctx.fixIterations,
580
+ typeFixIterations: ctx.typeFixIterations,
581
+ testFixIterations: ctx.testFixIterations,
582
+ lintFixIterations: ctx.lintFixIterations,
583
+ storyFixIterations: ctx.storyFixIterations,
584
+ });
585
+ }
586
+ }
587
+
588
+ return results;
589
+ }
590
+
591
+ function printSummary(allResults: Map<string, ComponentResult[]>) {
592
+ console.log('\n');
593
+ console.log('╔══════════════════════════════════════════════════╗');
594
+ console.log('║ Generation Summary ║');
595
+ console.log('╚══════════════════════════════════════════════════╝');
596
+
597
+ let totalComponents = 0;
598
+ let totalSuccess = 0;
599
+
600
+ for (const [model, results] of allResults) {
601
+ const succeeded = results.filter((r) => r.success).length;
602
+ const failed = results.filter((r) => !r.success).length;
603
+ totalComponents += results.length;
604
+ totalSuccess += succeeded;
605
+
606
+ console.log(`\n ${model}: ${succeeded}/${results.length} succeeded, ${failed} failed`);
607
+
608
+ for (const r of results) {
609
+ const status = r.success ? '✓' : '✗';
610
+ const metrics = `llm=${r.llmCalls} fix=${r.fixIterations} (type=${r.typeFixIterations} test=${r.testFixIterations} lint=${r.lintFixIterations} story=${r.storyFixIterations})`;
611
+ const error = r.error ? ` — ${r.error.slice(0, 80)}` : '';
612
+ console.log(` ${status} ${r.componentId} [${metrics}]${error}`);
613
+ }
614
+ }
615
+
616
+ console.log(`\n Total: ${totalSuccess}/${totalComponents} components succeeded`);
617
+ console.log('');
618
+ }
619
+
620
+ async function main() {
621
+ const agentModel = process.env.CUSTOM_PROVIDER_DEFAULT_MODEL ?? 'unknown';
622
+ const providerName = process.env.CUSTOM_PROVIDER_NAME ?? 'unknown';
623
+
624
+ const args = process.argv.slice(2);
625
+ const modelArgIndex = args.indexOf('--model');
626
+ const singleModel = modelArgIndex !== -1 ? args[modelArgIndex + 1] : undefined;
627
+
628
+ console.log('');
629
+ console.log('╔══════════════════════════════════════════════════╗');
630
+ console.log('║ Component Implementor — Generate All Models ║');
631
+ console.log('╠══════════════════════════════════════════════════╣');
632
+ console.log(`║ Model: ${agentModel.padEnd(37)}║`);
633
+ console.log(`║ Provider: ${providerName.padEnd(37)}║`);
634
+ if (singleModel) {
635
+ console.log(`║ Filter: ${singleModel.padEnd(37)}║`);
636
+ }
637
+ console.log('╚══════════════════════════════════════════════════╝');
638
+
639
+ const allModels = discoverModels();
640
+
641
+ if (allModels.length === 0) {
642
+ console.error('\n ERROR: No models found in inputs/');
643
+ console.error(' Expected: inputs/model-a/spec-deltas.json');
644
+ process.exit(1);
645
+ }
646
+
647
+ const models = singleModel ? allModels.filter((m) => m === singleModel) : allModels;
648
+
649
+ if (models.length === 0) {
650
+ console.error(`\n ERROR: Model "${singleModel}" not found in inputs/`);
651
+ console.error(` Available: ${allModels.join(', ')}`);
652
+ process.exit(1);
653
+ }
654
+
655
+ log(`Running ${models.length} model(s): ${models.join(', ')}`);
656
+
657
+ const allResults = new Map<string, ComponentResult[]>();
658
+
659
+ for (const model of models) {
660
+ const results = await generateModel(model);
661
+ allResults.set(model, results);
662
+
663
+ const summaryPath = resolve(OUTPUTS_DIR, model, 'results.json');
664
+ writeFileSync(summaryPath, JSON.stringify(results, null, 2));
665
+ }
666
+
667
+ printSummary(allResults);
668
+ }
669
+
670
+ main().catch((err) => {
671
+ console.error('Fatal error:', err);
672
+ process.exit(1);
673
+ });