amd-gaia 0.15.0__py3-none-any.whl → 0.15.1__py3-none-any.whl

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 (181) hide show
  1. {amd_gaia-0.15.0.dist-info → amd_gaia-0.15.1.dist-info}/METADATA +223 -223
  2. amd_gaia-0.15.1.dist-info/RECORD +178 -0
  3. {amd_gaia-0.15.0.dist-info → amd_gaia-0.15.1.dist-info}/entry_points.txt +1 -0
  4. {amd_gaia-0.15.0.dist-info → amd_gaia-0.15.1.dist-info}/licenses/LICENSE.md +20 -20
  5. gaia/__init__.py +29 -29
  6. gaia/agents/__init__.py +19 -19
  7. gaia/agents/base/__init__.py +9 -9
  8. gaia/agents/base/agent.py +2177 -2177
  9. gaia/agents/base/api_agent.py +120 -120
  10. gaia/agents/base/console.py +1841 -1841
  11. gaia/agents/base/errors.py +237 -237
  12. gaia/agents/base/mcp_agent.py +86 -86
  13. gaia/agents/base/tools.py +83 -83
  14. gaia/agents/blender/agent.py +556 -556
  15. gaia/agents/blender/agent_simple.py +133 -135
  16. gaia/agents/blender/app.py +211 -211
  17. gaia/agents/blender/app_simple.py +41 -41
  18. gaia/agents/blender/core/__init__.py +16 -16
  19. gaia/agents/blender/core/materials.py +506 -506
  20. gaia/agents/blender/core/objects.py +316 -316
  21. gaia/agents/blender/core/rendering.py +225 -225
  22. gaia/agents/blender/core/scene.py +220 -220
  23. gaia/agents/blender/core/view.py +146 -146
  24. gaia/agents/chat/__init__.py +9 -9
  25. gaia/agents/chat/agent.py +835 -835
  26. gaia/agents/chat/app.py +1058 -1058
  27. gaia/agents/chat/session.py +508 -508
  28. gaia/agents/chat/tools/__init__.py +15 -15
  29. gaia/agents/chat/tools/file_tools.py +96 -96
  30. gaia/agents/chat/tools/rag_tools.py +1729 -1729
  31. gaia/agents/chat/tools/shell_tools.py +436 -436
  32. gaia/agents/code/__init__.py +7 -7
  33. gaia/agents/code/agent.py +549 -549
  34. gaia/agents/code/cli.py +377 -0
  35. gaia/agents/code/models.py +135 -135
  36. gaia/agents/code/orchestration/__init__.py +24 -24
  37. gaia/agents/code/orchestration/checklist_executor.py +1763 -1763
  38. gaia/agents/code/orchestration/checklist_generator.py +713 -713
  39. gaia/agents/code/orchestration/factories/__init__.py +9 -9
  40. gaia/agents/code/orchestration/factories/base.py +63 -63
  41. gaia/agents/code/orchestration/factories/nextjs_factory.py +118 -118
  42. gaia/agents/code/orchestration/factories/python_factory.py +106 -106
  43. gaia/agents/code/orchestration/orchestrator.py +841 -841
  44. gaia/agents/code/orchestration/project_analyzer.py +391 -391
  45. gaia/agents/code/orchestration/steps/__init__.py +67 -67
  46. gaia/agents/code/orchestration/steps/base.py +188 -188
  47. gaia/agents/code/orchestration/steps/error_handler.py +314 -314
  48. gaia/agents/code/orchestration/steps/nextjs.py +828 -828
  49. gaia/agents/code/orchestration/steps/python.py +307 -307
  50. gaia/agents/code/orchestration/template_catalog.py +469 -469
  51. gaia/agents/code/orchestration/workflows/__init__.py +14 -14
  52. gaia/agents/code/orchestration/workflows/base.py +80 -80
  53. gaia/agents/code/orchestration/workflows/nextjs.py +186 -186
  54. gaia/agents/code/orchestration/workflows/python.py +94 -94
  55. gaia/agents/code/prompts/__init__.py +11 -11
  56. gaia/agents/code/prompts/base_prompt.py +77 -77
  57. gaia/agents/code/prompts/code_patterns.py +2036 -2036
  58. gaia/agents/code/prompts/nextjs_prompt.py +40 -40
  59. gaia/agents/code/prompts/python_prompt.py +109 -109
  60. gaia/agents/code/schema_inference.py +365 -365
  61. gaia/agents/code/system_prompt.py +41 -41
  62. gaia/agents/code/tools/__init__.py +42 -42
  63. gaia/agents/code/tools/cli_tools.py +1138 -1138
  64. gaia/agents/code/tools/code_formatting.py +319 -319
  65. gaia/agents/code/tools/code_tools.py +769 -769
  66. gaia/agents/code/tools/error_fixing.py +1347 -1347
  67. gaia/agents/code/tools/external_tools.py +180 -180
  68. gaia/agents/code/tools/file_io.py +845 -845
  69. gaia/agents/code/tools/prisma_tools.py +190 -190
  70. gaia/agents/code/tools/project_management.py +1016 -1016
  71. gaia/agents/code/tools/testing.py +321 -321
  72. gaia/agents/code/tools/typescript_tools.py +122 -122
  73. gaia/agents/code/tools/validation_parsing.py +461 -461
  74. gaia/agents/code/tools/validation_tools.py +806 -806
  75. gaia/agents/code/tools/web_dev_tools.py +1758 -1758
  76. gaia/agents/code/validators/__init__.py +16 -16
  77. gaia/agents/code/validators/antipattern_checker.py +241 -241
  78. gaia/agents/code/validators/ast_analyzer.py +197 -197
  79. gaia/agents/code/validators/requirements_validator.py +145 -145
  80. gaia/agents/code/validators/syntax_validator.py +171 -171
  81. gaia/agents/docker/__init__.py +7 -7
  82. gaia/agents/docker/agent.py +642 -642
  83. gaia/agents/emr/__init__.py +8 -8
  84. gaia/agents/emr/agent.py +1506 -1506
  85. gaia/agents/emr/cli.py +1322 -1322
  86. gaia/agents/emr/constants.py +475 -475
  87. gaia/agents/emr/dashboard/__init__.py +4 -4
  88. gaia/agents/emr/dashboard/server.py +1974 -1974
  89. gaia/agents/jira/__init__.py +11 -11
  90. gaia/agents/jira/agent.py +894 -894
  91. gaia/agents/jira/jql_templates.py +299 -299
  92. gaia/agents/routing/__init__.py +7 -7
  93. gaia/agents/routing/agent.py +567 -570
  94. gaia/agents/routing/system_prompt.py +75 -75
  95. gaia/agents/summarize/__init__.py +11 -0
  96. gaia/agents/summarize/agent.py +885 -0
  97. gaia/agents/summarize/prompts.py +129 -0
  98. gaia/api/__init__.py +23 -23
  99. gaia/api/agent_registry.py +238 -238
  100. gaia/api/app.py +305 -305
  101. gaia/api/openai_server.py +575 -575
  102. gaia/api/schemas.py +186 -186
  103. gaia/api/sse_handler.py +373 -373
  104. gaia/apps/__init__.py +4 -4
  105. gaia/apps/llm/__init__.py +6 -6
  106. gaia/apps/llm/app.py +173 -169
  107. gaia/apps/summarize/app.py +116 -633
  108. gaia/apps/summarize/html_viewer.py +133 -133
  109. gaia/apps/summarize/pdf_formatter.py +284 -284
  110. gaia/audio/__init__.py +2 -2
  111. gaia/audio/audio_client.py +439 -439
  112. gaia/audio/audio_recorder.py +269 -269
  113. gaia/audio/kokoro_tts.py +599 -599
  114. gaia/audio/whisper_asr.py +432 -432
  115. gaia/chat/__init__.py +16 -16
  116. gaia/chat/app.py +430 -430
  117. gaia/chat/prompts.py +522 -522
  118. gaia/chat/sdk.py +1228 -1225
  119. gaia/cli.py +5481 -5632
  120. gaia/database/__init__.py +10 -10
  121. gaia/database/agent.py +176 -176
  122. gaia/database/mixin.py +290 -290
  123. gaia/database/testing.py +64 -64
  124. gaia/eval/batch_experiment.py +2332 -2332
  125. gaia/eval/claude.py +542 -542
  126. gaia/eval/config.py +37 -37
  127. gaia/eval/email_generator.py +512 -512
  128. gaia/eval/eval.py +3179 -3179
  129. gaia/eval/groundtruth.py +1130 -1130
  130. gaia/eval/transcript_generator.py +582 -582
  131. gaia/eval/webapp/README.md +167 -167
  132. gaia/eval/webapp/package-lock.json +875 -875
  133. gaia/eval/webapp/package.json +20 -20
  134. gaia/eval/webapp/public/app.js +3402 -3402
  135. gaia/eval/webapp/public/index.html +87 -87
  136. gaia/eval/webapp/public/styles.css +3661 -3661
  137. gaia/eval/webapp/server.js +415 -415
  138. gaia/eval/webapp/test-setup.js +72 -72
  139. gaia/llm/__init__.py +9 -2
  140. gaia/llm/base_client.py +60 -0
  141. gaia/llm/exceptions.py +12 -0
  142. gaia/llm/factory.py +70 -0
  143. gaia/llm/lemonade_client.py +3236 -3221
  144. gaia/llm/lemonade_manager.py +294 -294
  145. gaia/llm/providers/__init__.py +9 -0
  146. gaia/llm/providers/claude.py +108 -0
  147. gaia/llm/providers/lemonade.py +120 -0
  148. gaia/llm/providers/openai_provider.py +79 -0
  149. gaia/llm/vlm_client.py +382 -382
  150. gaia/logger.py +189 -189
  151. gaia/mcp/agent_mcp_server.py +245 -245
  152. gaia/mcp/blender_mcp_client.py +138 -138
  153. gaia/mcp/blender_mcp_server.py +648 -648
  154. gaia/mcp/context7_cache.py +332 -332
  155. gaia/mcp/external_services.py +518 -518
  156. gaia/mcp/mcp_bridge.py +811 -550
  157. gaia/mcp/servers/__init__.py +6 -6
  158. gaia/mcp/servers/docker_mcp.py +83 -83
  159. gaia/perf_analysis.py +361 -0
  160. gaia/rag/__init__.py +10 -10
  161. gaia/rag/app.py +293 -293
  162. gaia/rag/demo.py +304 -304
  163. gaia/rag/pdf_utils.py +235 -235
  164. gaia/rag/sdk.py +2194 -2194
  165. gaia/security.py +163 -163
  166. gaia/talk/app.py +289 -289
  167. gaia/talk/sdk.py +538 -538
  168. gaia/testing/__init__.py +87 -87
  169. gaia/testing/assertions.py +330 -330
  170. gaia/testing/fixtures.py +333 -333
  171. gaia/testing/mocks.py +493 -493
  172. gaia/util.py +46 -46
  173. gaia/utils/__init__.py +33 -33
  174. gaia/utils/file_watcher.py +675 -675
  175. gaia/utils/parsing.py +223 -223
  176. gaia/version.py +100 -100
  177. amd_gaia-0.15.0.dist-info/RECORD +0 -168
  178. gaia/agents/code/app.py +0 -266
  179. gaia/llm/llm_client.py +0 -723
  180. {amd_gaia-0.15.0.dist-info → amd_gaia-0.15.1.dist-info}/WHEEL +0 -0
  181. {amd_gaia-0.15.0.dist-info → amd_gaia-0.15.1.dist-info}/top_level.txt +0 -0
@@ -1,828 +1,828 @@
1
- # Copyright(C) 2025-2026 Advanced Micro Devices, Inc. All rights reserved.
2
- # SPDX-License-Identifier: MIT
3
- """
4
- Next.js step implementations.
5
-
6
- Steps wrap the existing Code Agent tools with standardized interfaces.
7
- """
8
-
9
- from dataclasses import dataclass, field
10
- from pathlib import Path
11
- from typing import Any, Dict, Optional, Tuple
12
-
13
- from .base import BaseStep, ErrorCategory, StepResult, UserContext
14
-
15
- # Package versions (matching nextjs_prompt.py)
16
- NEXTJS_VERSION = "14.2.33"
17
- PRISMA_VERSION = "5.22.0"
18
- ZOD_VERSION = "3.23.8"
19
-
20
-
21
- @dataclass
22
- class CreateNextAppStep(BaseStep):
23
- """Step to create a new Next.js application."""
24
-
25
- name: str = "create_next_app"
26
- description: str = "Initialize Next.js project"
27
-
28
- def should_skip(self, context: UserContext) -> Optional[str]:
29
- """Skip if package.json already exists."""
30
- package_json = Path(context.project_dir) / "package.json"
31
- if package_json.exists():
32
- return "package.json already exists, project already initialized"
33
- return None
34
-
35
- def get_tool_invocation(
36
- self, context: UserContext
37
- ) -> Optional[Tuple[str, Dict[str, Any]]]:
38
- """Return run_cli_command invocation."""
39
- return (
40
- "run_cli_command",
41
- {
42
- "command": f"npx -y create-next-app@{NEXTJS_VERSION} . --typescript --tailwind --eslint --app --src-dir --yes",
43
- "working_dir": context.project_dir,
44
- "timeout": 1200,
45
- },
46
- )
47
-
48
- def handle_result(self, result: Any, context: UserContext) -> StepResult:
49
- """Convert tool result to StepResult."""
50
- if isinstance(result, dict):
51
- if result.get("success") or result.get("return_code") == 0:
52
- return StepResult.ok(
53
- "Next.js project created successfully",
54
- files=["package.json", "tsconfig.json", "src/app/page.tsx"],
55
- )
56
- return StepResult.make_error(
57
- "Failed to create Next.js project",
58
- result.get("error") or result.get("stderr", "Unknown error"),
59
- ErrorCategory.COMPILATION,
60
- )
61
- return StepResult.make_error(
62
- "Unexpected result format", str(result), ErrorCategory.UNKNOWN
63
- )
64
-
65
-
66
- @dataclass
67
- class SetupAppStylingStep(BaseStep):
68
- """Step to set up app-wide modern styling.
69
-
70
- Creates the root layout and globals.css with a modern dark theme
71
- design system that all pages inherit.
72
- """
73
-
74
- name: str = "setup_styling"
75
- description: str = "Set up modern app styling"
76
- app_title: str = "My App"
77
- app_description: str = "A modern web application"
78
-
79
- def get_tool_invocation(
80
- self, context: UserContext
81
- ) -> Optional[Tuple[str, Dict[str, Any]]]:
82
- """Return setup_app_styling invocation."""
83
- # Derive app title from entity name or use default
84
- title = f"{context.entity_name or 'My'} App"
85
- description = f"A modern {(context.entity_name or 'web').lower()} application"
86
-
87
- return (
88
- "setup_app_styling",
89
- {
90
- "project_dir": context.project_dir,
91
- "app_title": title,
92
- "app_description": description,
93
- },
94
- )
95
-
96
- def handle_result(self, result: Any, context: UserContext) -> StepResult:
97
- """Convert tool result to StepResult."""
98
- if isinstance(result, dict):
99
- if result.get("success"):
100
- return StepResult.ok(
101
- "App styling configured with modern design system",
102
- files=result.get("files", []),
103
- )
104
- return StepResult.make_error(
105
- "Failed to set up app styling",
106
- result.get("error", "Unknown error"),
107
- ErrorCategory.CONFIGURATION,
108
- )
109
- return StepResult.make_error(
110
- "Unexpected result format", str(result), ErrorCategory.UNKNOWN
111
- )
112
-
113
-
114
- @dataclass
115
- class InstallDependenciesStep(BaseStep):
116
- """Step to install additional dependencies."""
117
-
118
- name: str = "install_deps"
119
- description: str = "Install Prisma and Zod"
120
-
121
- def should_skip(self, context: UserContext) -> Optional[str]:
122
- """Skip if prisma is already in package.json."""
123
- package_json = Path(context.project_dir) / "package.json"
124
- if package_json.exists():
125
- content = package_json.read_text()
126
- if "prisma" in content and "@prisma/client" in content:
127
- return "Dependencies already installed"
128
- return None
129
-
130
- def get_tool_invocation(
131
- self, context: UserContext
132
- ) -> Optional[Tuple[str, Dict[str, Any]]]:
133
- """Return run_cli_command invocation."""
134
- return (
135
- "run_cli_command",
136
- {
137
- "command": f"npm install prisma@^{PRISMA_VERSION} @prisma/client@^{PRISMA_VERSION} zod@^{ZOD_VERSION}",
138
- "working_dir": context.project_dir,
139
- "timeout": 1200,
140
- },
141
- )
142
-
143
- def handle_result(self, result: Any, context: UserContext) -> StepResult:
144
- """Convert tool result to StepResult."""
145
- if isinstance(result, dict):
146
- if result.get("success") or result.get("return_code") == 0:
147
- return StepResult.ok("Dependencies installed successfully")
148
- return StepResult.make_error(
149
- "Failed to install dependencies",
150
- result.get("error") or result.get("stderr", "Unknown error"),
151
- ErrorCategory.DEPENDENCY,
152
- )
153
- return StepResult.make_error(
154
- "Unexpected result format", str(result), ErrorCategory.UNKNOWN
155
- )
156
-
157
-
158
- @dataclass
159
- class PrismaInitStep(BaseStep):
160
- """Step to initialize Prisma with SQLite."""
161
-
162
- name: str = "prisma_init"
163
- description: str = "Initialize Prisma"
164
-
165
- def should_skip(self, context: UserContext) -> Optional[str]:
166
- """Skip if prisma directory already exists."""
167
- prisma_dir = Path(context.project_dir) / "prisma"
168
- if prisma_dir.exists() and (prisma_dir / "schema.prisma").exists():
169
- return "Prisma already initialized"
170
- return None
171
-
172
- def get_tool_invocation(
173
- self, context: UserContext
174
- ) -> Optional[Tuple[str, Dict[str, Any]]]:
175
- """Return run_cli_command invocation."""
176
- return (
177
- "run_cli_command",
178
- {
179
- "command": "npx -y prisma init --datasource-provider sqlite",
180
- "working_dir": context.project_dir,
181
- "timeout": 600,
182
- },
183
- )
184
-
185
- def handle_result(self, result: Any, context: UserContext) -> StepResult:
186
- """Convert tool result to StepResult."""
187
- if isinstance(result, dict):
188
- if result.get("success") or result.get("return_code") == 0:
189
- return StepResult.ok(
190
- "Prisma initialized with SQLite",
191
- files=["prisma/schema.prisma"],
192
- )
193
- return StepResult.make_error(
194
- "Failed to initialize Prisma",
195
- result.get("error") or result.get("stderr", "Unknown error"),
196
- ErrorCategory.CONFIGURATION,
197
- )
198
- return StepResult.make_error(
199
- "Unexpected result format", str(result), ErrorCategory.UNKNOWN
200
- )
201
-
202
-
203
- @dataclass
204
- class ManageDataModelStep(BaseStep):
205
- """Step to create Prisma data model."""
206
-
207
- name: str = "data_model"
208
- description: str = "Create Prisma model"
209
- entity_name: str = "Item"
210
- fields: Dict[str, str] = field(default_factory=lambda: {"title": "string"})
211
-
212
- def validate_preconditions(self, context: UserContext) -> Optional[str]:
213
- """Check that Prisma is initialized before managing data model."""
214
- schema_path = Path(context.project_dir) / "prisma" / "schema.prisma"
215
- if not schema_path.exists():
216
- return (
217
- "Prisma not initialized. The prisma/schema.prisma file must exist. "
218
- "Run 'npx prisma init --datasource-provider sqlite' first."
219
- )
220
- return None
221
-
222
- def get_tool_invocation(
223
- self, context: UserContext
224
- ) -> Optional[Tuple[str, Dict[str, Any]]]:
225
- """Return manage_data_model invocation."""
226
- return (
227
- "manage_data_model",
228
- {
229
- "project_dir": context.project_dir,
230
- "model_name": self.entity_name,
231
- "fields": self.fields,
232
- },
233
- )
234
-
235
- def handle_result(self, result: Any, context: UserContext) -> StepResult:
236
- """Convert tool result to StepResult."""
237
- if isinstance(result, dict):
238
- if result.get("success"):
239
- # Store generated files in context
240
- files = result.get("files", [])
241
- return StepResult.ok(
242
- f"Prisma model {self.entity_name} created",
243
- files=files,
244
- model_name=self.entity_name,
245
- )
246
- return StepResult.make_error(
247
- f"Failed to create {self.entity_name} model",
248
- result.get("error", "Unknown error"),
249
- ErrorCategory.COMPILATION,
250
- )
251
- return StepResult.make_error(
252
- "Unexpected result format", str(result), ErrorCategory.UNKNOWN
253
- )
254
-
255
-
256
- @dataclass
257
- class SetupPrismaStep(BaseStep):
258
- """Step to set up Prisma client after schema changes.
259
-
260
- This creates the Prisma singleton, generates client types, and pushes to DB.
261
- Must run AFTER ManageDataModelStep and BEFORE API endpoint steps.
262
- """
263
-
264
- name: str = "setup_prisma"
265
- description: str = "Set up Prisma client and database"
266
-
267
- def validate_preconditions(self, context: UserContext) -> Optional[str]:
268
- """Check that Prisma schema exists before setup."""
269
- schema_path = Path(context.project_dir) / "prisma" / "schema.prisma"
270
- if not schema_path.exists():
271
- return (
272
- "Prisma schema not found. The prisma/schema.prisma file must exist. "
273
- "Run 'npx prisma init' first."
274
- )
275
- return None
276
-
277
- def get_tool_invocation(
278
- self, context: UserContext
279
- ) -> Optional[Tuple[str, Dict[str, Any]]]:
280
- """Return setup_prisma invocation."""
281
- return (
282
- "setup_prisma",
283
- {
284
- "project_dir": context.project_dir,
285
- "regenerate": True,
286
- "push_db": True,
287
- },
288
- )
289
-
290
- def handle_result(self, result: Any, context: UserContext) -> StepResult:
291
- """Convert tool result to StepResult."""
292
- if isinstance(result, dict):
293
- if result.get("success"):
294
- files = []
295
- if result.get("singleton_path"):
296
- files.append(result["singleton_path"])
297
- return StepResult.ok(
298
- "Prisma client set up successfully",
299
- files=files,
300
- generated=result.get("generated", False),
301
- pushed=result.get("pushed", False),
302
- )
303
- return StepResult.make_error(
304
- "Failed to set up Prisma",
305
- result.get("error", "Unknown error"),
306
- ErrorCategory.COMPILATION,
307
- )
308
- return StepResult.make_error(
309
- "Unexpected result format", str(result), ErrorCategory.UNKNOWN
310
- )
311
-
312
-
313
- @dataclass
314
- class ManageApiEndpointStep(BaseStep):
315
- """Step to create collection API endpoint (GET, POST)."""
316
-
317
- name: str = "api_collection"
318
- description: str = "Create collection API"
319
- entity_name: str = "Item"
320
- fields: Dict[str, str] = field(default_factory=lambda: {"title": "string"})
321
-
322
- def validate_preconditions(self, context: UserContext) -> Optional[str]:
323
- """Check that Prisma singleton exists before creating API endpoints."""
324
- prisma_lib = Path(context.project_dir) / "src" / "lib" / "prisma.ts"
325
- if not prisma_lib.exists():
326
- return (
327
- "Prisma client not set up. The src/lib/prisma.ts file must exist. "
328
- "Run 'setup_prisma' tool first to create the Prisma singleton."
329
- )
330
- return None
331
-
332
- def get_tool_invocation(
333
- self, context: UserContext
334
- ) -> Optional[Tuple[str, Dict[str, Any]]]:
335
- """Return manage_api_endpoint invocation."""
336
- return (
337
- "manage_api_endpoint",
338
- {
339
- "project_dir": context.project_dir,
340
- "resource_name": self.entity_name.lower(),
341
- "operations": ["GET", "POST"],
342
- "fields": self.fields,
343
- },
344
- )
345
-
346
- def handle_result(self, result: Any, context: UserContext) -> StepResult:
347
- """Convert tool result to StepResult."""
348
- if isinstance(result, dict):
349
- if result.get("success"):
350
- files = result.get("files", [])
351
- return StepResult.ok(
352
- f"Collection API for {self.entity_name} created",
353
- files=files,
354
- )
355
- return StepResult.make_error(
356
- f"Failed to create collection API for {self.entity_name}",
357
- result.get("error", "Unknown error"),
358
- ErrorCategory.COMPILATION,
359
- )
360
- return StepResult.make_error(
361
- "Unexpected result format", str(result), ErrorCategory.UNKNOWN
362
- )
363
-
364
-
365
- @dataclass
366
- class ManageApiEndpointDynamicStep(BaseStep):
367
- """Step to create dynamic API endpoint (GET, PATCH, DELETE for [id])."""
368
-
369
- name: str = "api_item"
370
- description: str = "Create item API"
371
- entity_name: str = "Item"
372
- fields: Dict[str, str] = field(default_factory=lambda: {"title": "string"})
373
-
374
- def get_tool_invocation(
375
- self, context: UserContext
376
- ) -> Optional[Tuple[str, Dict[str, Any]]]:
377
- """Return manage_api_endpoint invocation for dynamic route.
378
-
379
- Note: The tool automatically creates the [id]/route.ts file when
380
- PATCH/DELETE operations are requested.
381
- """
382
- return (
383
- "manage_api_endpoint",
384
- {
385
- "project_dir": context.project_dir,
386
- "resource_name": self.entity_name.lower(),
387
- "operations": ["GET", "PATCH", "DELETE"],
388
- "fields": self.fields,
389
- },
390
- )
391
-
392
- def handle_result(self, result: Any, context: UserContext) -> StepResult:
393
- """Convert tool result to StepResult."""
394
- if isinstance(result, dict):
395
- if result.get("success"):
396
- files = result.get("files", [])
397
- return StepResult.ok(
398
- f"Item API for {self.entity_name} created",
399
- files=files,
400
- )
401
- return StepResult.make_error(
402
- f"Failed to create item API for {self.entity_name}",
403
- result.get("error", "Unknown error"),
404
- ErrorCategory.COMPILATION,
405
- )
406
- return StepResult.make_error(
407
- "Unexpected result format", str(result), ErrorCategory.UNKNOWN
408
- )
409
-
410
-
411
- @dataclass
412
- class ManageReactComponentStep(BaseStep):
413
- """Step to create React component."""
414
-
415
- name: str = "component"
416
- description: str = "Create React component"
417
- entity_name: str = "Item"
418
- variant: str = "list" # list, form, new, detail, actions
419
- fields: Dict[str, str] = field(default_factory=lambda: {"title": "string"})
420
-
421
- def get_tool_invocation(
422
- self, context: UserContext
423
- ) -> Optional[Tuple[str, Dict[str, Any]]]:
424
- """Return manage_react_component invocation.
425
-
426
- Note: resource_name is REQUIRED for the tool to generate correct paths.
427
- Without it, all variants fall back to src/components/{component_name}.tsx.
428
- """
429
- # For "form" variant, validation expects TodoForm.tsx (not Todo.tsx)
430
- if self.variant == "form":
431
- component_name = f"{self.entity_name}Form"
432
- else:
433
- component_name = self.entity_name
434
-
435
- return (
436
- "manage_react_component",
437
- {
438
- "project_dir": context.project_dir,
439
- "component_name": component_name,
440
- "resource_name": self.entity_name.lower(), # Required for path generation
441
- "variant": self.variant,
442
- "fields": self.fields,
443
- },
444
- )
445
-
446
- def handle_result(self, result: Any, context: UserContext) -> StepResult:
447
- """Convert tool result to StepResult."""
448
- if isinstance(result, dict):
449
- if result.get("success"):
450
- files = result.get("files", [])
451
- return StepResult.ok(
452
- f"{self.entity_name} {self.variant} component created",
453
- files=files,
454
- variant=self.variant,
455
- )
456
- return StepResult.make_error(
457
- f"Failed to create {self.entity_name} {self.variant}",
458
- result.get("error", "Unknown error"),
459
- ErrorCategory.COMPILATION,
460
- )
461
- return StepResult.make_error(
462
- "Unexpected result format", str(result), ErrorCategory.UNKNOWN
463
- )
464
-
465
-
466
- @dataclass
467
- class ValidateCrudStructureStep(BaseStep):
468
- """Step to validate CRUD structure."""
469
-
470
- name: str = "validate_structure"
471
- description: str = "Validate CRUD structure"
472
- entity_name: str = "Item"
473
-
474
- def get_tool_invocation(
475
- self, context: UserContext
476
- ) -> Optional[Tuple[str, Dict[str, Any]]]:
477
- """Return validate_crud_structure invocation."""
478
- return (
479
- "validate_crud_structure",
480
- {
481
- "project_dir": context.project_dir,
482
- "resource_name": self.entity_name.lower(),
483
- },
484
- )
485
-
486
- def handle_result(self, result: Any, context: UserContext) -> StepResult:
487
- """Convert tool result to StepResult."""
488
- if isinstance(result, dict):
489
- if result.get("success") or result.get("valid"):
490
- return StepResult.ok("CRUD structure validated successfully")
491
- missing = result.get("missing_files", [])
492
- return StepResult.make_error(
493
- "CRUD structure validation failed",
494
- f"Missing files: {missing}",
495
- ErrorCategory.VALIDATION,
496
- )
497
- return StepResult.make_error(
498
- "Unexpected result format", str(result), ErrorCategory.UNKNOWN
499
- )
500
-
501
-
502
- @dataclass
503
- class ValidateTypescriptStep(BaseStep):
504
- """Step to validate TypeScript."""
505
-
506
- name: str = "validate_typescript"
507
- description: str = "Run TypeScript validation"
508
-
509
- def get_tool_invocation(
510
- self, context: UserContext
511
- ) -> Optional[Tuple[str, Dict[str, Any]]]:
512
- """Return validate_typescript invocation."""
513
- return (
514
- "validate_typescript",
515
- {
516
- "project_dir": context.project_dir,
517
- },
518
- )
519
-
520
- def handle_result(self, result: Any, context: UserContext) -> StepResult:
521
- """Convert tool result to StepResult."""
522
- if isinstance(result, dict):
523
- if result.get("success") or result.get("valid"):
524
- return StepResult.ok("TypeScript validation passed")
525
- errors = result.get("errors", [])
526
- return StepResult.make_error(
527
- "TypeScript validation failed",
528
- "\n".join(errors) if errors else result.get("error", "Unknown error"),
529
- ErrorCategory.COMPILATION,
530
- )
531
- return StepResult.make_error(
532
- "Unexpected result format", str(result), ErrorCategory.UNKNOWN
533
- )
534
-
535
-
536
- @dataclass
537
- class TestCrudApiStep(BaseStep):
538
- """Step to test CRUD API."""
539
-
540
- name: str = "test_api"
541
- description: str = "Test CRUD operations"
542
- entity_name: str = "Item"
543
-
544
- def get_tool_invocation(
545
- self, context: UserContext
546
- ) -> Optional[Tuple[str, Dict[str, Any]]]:
547
- """Return test_crud_api invocation."""
548
- return (
549
- "test_crud_api",
550
- {
551
- "project_dir": context.project_dir,
552
- "model_name": self.entity_name,
553
- },
554
- )
555
-
556
- def handle_result(self, result: Any, context: UserContext) -> StepResult:
557
- """Convert tool result to StepResult."""
558
- if isinstance(result, dict):
559
- if result.get("success"):
560
- return StepResult.ok(
561
- "CRUD API tests passed",
562
- tests_passed=result.get("tests_passed", 0),
563
- )
564
- # Test failures are warnings, not hard errors
565
- # The code was generated - tests may fail due to database/server issues
566
- test_result = result.get("result", {})
567
- passed = test_result.get("tests_passed", 0)
568
- failed = test_result.get("tests_failed", 0)
569
- details = test_result.get("results", {})
570
- # Build summary of which tests failed
571
- failed_tests = [k for k, v in details.items() if not v.get("pass")]
572
- return StepResult.warning(
573
- f"API tests: {passed} passed, {failed} failed ({', '.join(failed_tests)})",
574
- tests_passed=passed,
575
- tests_failed=failed,
576
- failed_tests=failed_tests,
577
- )
578
- return StepResult.make_error(
579
- "Unexpected result format", str(result), ErrorCategory.UNKNOWN
580
- )
581
-
582
-
583
- @dataclass
584
- class UpdateLandingPageStep(BaseStep):
585
- """Step to update landing page with navigation."""
586
-
587
- name: str = "update_landing"
588
- description: str = "Update landing page"
589
- entity_name: str = "Item"
590
-
591
- def get_tool_invocation(
592
- self, context: UserContext
593
- ) -> Optional[Tuple[str, Dict[str, Any]]]:
594
- """Return update_landing_page invocation."""
595
- return (
596
- "update_landing_page",
597
- {
598
- "project_dir": context.project_dir,
599
- "resource_name": self.entity_name.lower(),
600
- },
601
- )
602
-
603
- def handle_result(self, result: Any, context: UserContext) -> StepResult:
604
- """Convert tool result to StepResult."""
605
- if isinstance(result, dict):
606
- if result.get("success"):
607
- return StepResult.ok("Landing page updated with navigation link")
608
- return StepResult.make_error(
609
- "Failed to update landing page",
610
- result.get("error", "Unknown error"),
611
- ErrorCategory.COMPILATION,
612
- )
613
- return StepResult.make_error(
614
- "Unexpected result format", str(result), ErrorCategory.UNKNOWN
615
- )
616
-
617
-
618
- @dataclass
619
- class SetupTestingStep(BaseStep):
620
- """Step to set up testing infrastructure."""
621
-
622
- name: str = "setup_testing"
623
- description: str = "Set up Vitest and testing libraries"
624
-
625
- def should_skip(self, context: UserContext) -> Optional[str]:
626
- """Skip if vitest is already configured."""
627
- vitest_config = Path(context.project_dir) / "vitest.config.ts"
628
- if vitest_config.exists():
629
- return "Vitest already configured"
630
- return None
631
-
632
- def get_tool_invocation(
633
- self, context: UserContext
634
- ) -> Optional[Tuple[str, Dict[str, Any]]]:
635
- """Return setup_nextjs_testing invocation."""
636
- return (
637
- "setup_nextjs_testing",
638
- {
639
- "project_dir": context.project_dir,
640
- },
641
- )
642
-
643
- def handle_result(self, result: Any, context: UserContext) -> StepResult:
644
- """Convert tool result to StepResult."""
645
- if isinstance(result, dict):
646
- if result.get("success"):
647
- return StepResult.ok(
648
- "Testing infrastructure set up",
649
- files=result.get("files", []),
650
- )
651
- return StepResult.make_error(
652
- "Failed to set up testing",
653
- result.get("error", "Unknown error"),
654
- ErrorCategory.CONFIGURATION,
655
- )
656
- return StepResult.make_error(
657
- "Unexpected result format", str(result), ErrorCategory.UNKNOWN
658
- )
659
-
660
-
661
- @dataclass
662
- class RunTestsStep(BaseStep):
663
- """Step to run all tests."""
664
-
665
- name: str = "run_tests"
666
- description: str = "Run npm test"
667
-
668
- def get_tool_invocation(
669
- self, context: UserContext
670
- ) -> Optional[Tuple[str, Dict[str, Any]]]:
671
- """Return run_cli_command invocation for npm test."""
672
- return (
673
- "run_cli_command",
674
- {
675
- "command": "npm test",
676
- "working_dir": context.project_dir,
677
- "timeout": 1200,
678
- },
679
- )
680
-
681
- def handle_result(self, result: Any, context: UserContext) -> StepResult:
682
- """Convert tool result to StepResult."""
683
- if isinstance(result, dict):
684
- if result.get("success") or result.get("return_code") == 0:
685
- return StepResult.ok("All tests passed")
686
- # Tests failing is a warning, not a hard error
687
- return StepResult.warning(
688
- "Some tests failed",
689
- stderr=result.get("stderr", ""),
690
- stdout=result.get("stdout", ""),
691
- )
692
- return StepResult.make_error(
693
- "Unexpected result format", str(result), ErrorCategory.UNKNOWN
694
- )
695
-
696
-
697
- @dataclass
698
- class ValidateStylesStep(BaseStep):
699
- """Step to validate CSS files and design system consistency.
700
-
701
- This step validates:
702
- 1. CSS files contain valid CSS (not TypeScript/JavaScript) - CRITICAL
703
- 2. globals.css has Tailwind directives
704
- 3. layout.tsx imports globals.css
705
- 4. Custom classes used in components are defined in globals.css
706
-
707
- Addresses Issue #1002: CSS file contains TypeScript code instead of CSS.
708
- """
709
-
710
- name: str = "validate_styles"
711
- description: str = "Validate CSS files and design system"
712
- resource_name: Optional[str] = None
713
-
714
- def validate_preconditions(self, context: UserContext) -> Optional[str]:
715
- """Check that styling files exist before validation."""
716
- globals_css = Path(context.project_dir) / "src" / "app" / "globals.css"
717
- if not globals_css.exists():
718
- return (
719
- "globals.css not found. The src/app/globals.css file must exist. "
720
- "Run 'setup_app_styling' first."
721
- )
722
- return None
723
-
724
- def get_tool_invocation(
725
- self, context: UserContext
726
- ) -> Optional[Tuple[str, Dict[str, Any]]]:
727
- """Return validate_styles invocation."""
728
- params = {
729
- "project_dir": context.project_dir,
730
- }
731
- if self.resource_name:
732
- params["_resource_name"] = self.resource_name
733
- return ("validate_styles", params)
734
-
735
- def handle_result(self, result: Any, context: UserContext) -> StepResult:
736
- """Convert tool result to StepResult."""
737
- if isinstance(result, dict):
738
- if result.get("success") or result.get("is_valid"):
739
- warnings = result.get("warnings", [])
740
- if warnings:
741
- return StepResult.warning(
742
- "Styling validated with warnings",
743
- warnings=warnings,
744
- )
745
- return StepResult.ok("Styling validated successfully")
746
-
747
- errors = result.get("errors", [])
748
- # Check if any errors are CRITICAL (blocking)
749
- critical_errors = [e for e in errors if "CRITICAL" in e]
750
- if critical_errors:
751
- return StepResult.make_error(
752
- "CRITICAL styling validation failed",
753
- "\n".join(critical_errors),
754
- ErrorCategory.VALIDATION,
755
- retryable=True, # Allow LLM to retry with correct CSS
756
- )
757
- return StepResult.make_error(
758
- "Styling validation failed",
759
- "\n".join(errors) if errors else result.get("error", "Unknown error"),
760
- ErrorCategory.VALIDATION,
761
- )
762
- return StepResult.make_error(
763
- "Unexpected result format", str(result), ErrorCategory.UNKNOWN
764
- )
765
-
766
-
767
- @dataclass
768
- class GenerateStyleTestsStep(BaseStep):
769
- """Step to generate CSS and styling tests for the project.
770
-
771
- Creates test files that validate:
772
- 1. CSS file integrity (no TypeScript in CSS)
773
- 2. Tailwind directive presence
774
- 3. Design system class definitions
775
- 4. Layout imports globals.css
776
-
777
- Tests are placed in the project's /tests directory.
778
- """
779
-
780
- name: str = "generate_style_tests"
781
- description: str = "Generate CSS and styling tests"
782
- resource_name: str = "Item"
783
-
784
- def should_skip(self, context: UserContext) -> Optional[str]:
785
- """Skip if style tests already exist."""
786
- styles_test = Path(context.project_dir) / "tests" / "styles.test.ts"
787
- if styles_test.exists():
788
- return "Style tests already exist"
789
- return None
790
-
791
- def validate_preconditions(self, context: UserContext) -> Optional[str]:
792
- """Check that testing is set up before generating style tests."""
793
- vitest_config = Path(context.project_dir) / "vitest.config.ts"
794
- if not vitest_config.exists():
795
- return (
796
- "Vitest not configured. Run 'setup_testing' first to set up "
797
- "the testing infrastructure."
798
- )
799
- return None
800
-
801
- def get_tool_invocation(
802
- self, context: UserContext
803
- ) -> Optional[Tuple[str, Dict[str, Any]]]:
804
- """Return generate_style_tests invocation."""
805
- return (
806
- "generate_style_tests",
807
- {
808
- "project_dir": context.project_dir,
809
- "resource_name": self.resource_name,
810
- },
811
- )
812
-
813
- def handle_result(self, result: Any, context: UserContext) -> StepResult:
814
- """Convert tool result to StepResult."""
815
- if isinstance(result, dict):
816
- if result.get("success"):
817
- return StepResult.ok(
818
- "Style tests generated successfully",
819
- files=result.get("files", []),
820
- )
821
- return StepResult.make_error(
822
- "Failed to generate style tests",
823
- result.get("error", "Unknown error"),
824
- ErrorCategory.COMPILATION,
825
- )
826
- return StepResult.make_error(
827
- "Unexpected result format", str(result), ErrorCategory.UNKNOWN
828
- )
1
+ # Copyright(C) 2025-2026 Advanced Micro Devices, Inc. All rights reserved.
2
+ # SPDX-License-Identifier: MIT
3
+ """
4
+ Next.js step implementations.
5
+
6
+ Steps wrap the existing Code Agent tools with standardized interfaces.
7
+ """
8
+
9
+ from dataclasses import dataclass, field
10
+ from pathlib import Path
11
+ from typing import Any, Dict, Optional, Tuple
12
+
13
+ from .base import BaseStep, ErrorCategory, StepResult, UserContext
14
+
15
+ # Package versions (matching nextjs_prompt.py)
16
+ NEXTJS_VERSION = "14.2.33"
17
+ PRISMA_VERSION = "5.22.0"
18
+ ZOD_VERSION = "3.23.8"
19
+
20
+
21
+ @dataclass
22
+ class CreateNextAppStep(BaseStep):
23
+ """Step to create a new Next.js application."""
24
+
25
+ name: str = "create_next_app"
26
+ description: str = "Initialize Next.js project"
27
+
28
+ def should_skip(self, context: UserContext) -> Optional[str]:
29
+ """Skip if package.json already exists."""
30
+ package_json = Path(context.project_dir) / "package.json"
31
+ if package_json.exists():
32
+ return "package.json already exists, project already initialized"
33
+ return None
34
+
35
+ def get_tool_invocation(
36
+ self, context: UserContext
37
+ ) -> Optional[Tuple[str, Dict[str, Any]]]:
38
+ """Return run_cli_command invocation."""
39
+ return (
40
+ "run_cli_command",
41
+ {
42
+ "command": f"npx -y create-next-app@{NEXTJS_VERSION} . --typescript --tailwind --eslint --app --src-dir --yes",
43
+ "working_dir": context.project_dir,
44
+ "timeout": 1200,
45
+ },
46
+ )
47
+
48
+ def handle_result(self, result: Any, context: UserContext) -> StepResult:
49
+ """Convert tool result to StepResult."""
50
+ if isinstance(result, dict):
51
+ if result.get("success") or result.get("return_code") == 0:
52
+ return StepResult.ok(
53
+ "Next.js project created successfully",
54
+ files=["package.json", "tsconfig.json", "src/app/page.tsx"],
55
+ )
56
+ return StepResult.make_error(
57
+ "Failed to create Next.js project",
58
+ result.get("error") or result.get("stderr", "Unknown error"),
59
+ ErrorCategory.COMPILATION,
60
+ )
61
+ return StepResult.make_error(
62
+ "Unexpected result format", str(result), ErrorCategory.UNKNOWN
63
+ )
64
+
65
+
66
+ @dataclass
67
+ class SetupAppStylingStep(BaseStep):
68
+ """Step to set up app-wide modern styling.
69
+
70
+ Creates the root layout and globals.css with a modern dark theme
71
+ design system that all pages inherit.
72
+ """
73
+
74
+ name: str = "setup_styling"
75
+ description: str = "Set up modern app styling"
76
+ app_title: str = "My App"
77
+ app_description: str = "A modern web application"
78
+
79
+ def get_tool_invocation(
80
+ self, context: UserContext
81
+ ) -> Optional[Tuple[str, Dict[str, Any]]]:
82
+ """Return setup_app_styling invocation."""
83
+ # Derive app title from entity name or use default
84
+ title = f"{context.entity_name or 'My'} App"
85
+ description = f"A modern {(context.entity_name or 'web').lower()} application"
86
+
87
+ return (
88
+ "setup_app_styling",
89
+ {
90
+ "project_dir": context.project_dir,
91
+ "app_title": title,
92
+ "app_description": description,
93
+ },
94
+ )
95
+
96
+ def handle_result(self, result: Any, context: UserContext) -> StepResult:
97
+ """Convert tool result to StepResult."""
98
+ if isinstance(result, dict):
99
+ if result.get("success"):
100
+ return StepResult.ok(
101
+ "App styling configured with modern design system",
102
+ files=result.get("files", []),
103
+ )
104
+ return StepResult.make_error(
105
+ "Failed to set up app styling",
106
+ result.get("error", "Unknown error"),
107
+ ErrorCategory.CONFIGURATION,
108
+ )
109
+ return StepResult.make_error(
110
+ "Unexpected result format", str(result), ErrorCategory.UNKNOWN
111
+ )
112
+
113
+
114
+ @dataclass
115
+ class InstallDependenciesStep(BaseStep):
116
+ """Step to install additional dependencies."""
117
+
118
+ name: str = "install_deps"
119
+ description: str = "Install Prisma and Zod"
120
+
121
+ def should_skip(self, context: UserContext) -> Optional[str]:
122
+ """Skip if prisma is already in package.json."""
123
+ package_json = Path(context.project_dir) / "package.json"
124
+ if package_json.exists():
125
+ content = package_json.read_text()
126
+ if "prisma" in content and "@prisma/client" in content:
127
+ return "Dependencies already installed"
128
+ return None
129
+
130
+ def get_tool_invocation(
131
+ self, context: UserContext
132
+ ) -> Optional[Tuple[str, Dict[str, Any]]]:
133
+ """Return run_cli_command invocation."""
134
+ return (
135
+ "run_cli_command",
136
+ {
137
+ "command": f"npm install prisma@^{PRISMA_VERSION} @prisma/client@^{PRISMA_VERSION} zod@^{ZOD_VERSION}",
138
+ "working_dir": context.project_dir,
139
+ "timeout": 1200,
140
+ },
141
+ )
142
+
143
+ def handle_result(self, result: Any, context: UserContext) -> StepResult:
144
+ """Convert tool result to StepResult."""
145
+ if isinstance(result, dict):
146
+ if result.get("success") or result.get("return_code") == 0:
147
+ return StepResult.ok("Dependencies installed successfully")
148
+ return StepResult.make_error(
149
+ "Failed to install dependencies",
150
+ result.get("error") or result.get("stderr", "Unknown error"),
151
+ ErrorCategory.DEPENDENCY,
152
+ )
153
+ return StepResult.make_error(
154
+ "Unexpected result format", str(result), ErrorCategory.UNKNOWN
155
+ )
156
+
157
+
158
+ @dataclass
159
+ class PrismaInitStep(BaseStep):
160
+ """Step to initialize Prisma with SQLite."""
161
+
162
+ name: str = "prisma_init"
163
+ description: str = "Initialize Prisma"
164
+
165
+ def should_skip(self, context: UserContext) -> Optional[str]:
166
+ """Skip if prisma directory already exists."""
167
+ prisma_dir = Path(context.project_dir) / "prisma"
168
+ if prisma_dir.exists() and (prisma_dir / "schema.prisma").exists():
169
+ return "Prisma already initialized"
170
+ return None
171
+
172
+ def get_tool_invocation(
173
+ self, context: UserContext
174
+ ) -> Optional[Tuple[str, Dict[str, Any]]]:
175
+ """Return run_cli_command invocation."""
176
+ return (
177
+ "run_cli_command",
178
+ {
179
+ "command": "npx -y prisma init --datasource-provider sqlite",
180
+ "working_dir": context.project_dir,
181
+ "timeout": 600,
182
+ },
183
+ )
184
+
185
+ def handle_result(self, result: Any, context: UserContext) -> StepResult:
186
+ """Convert tool result to StepResult."""
187
+ if isinstance(result, dict):
188
+ if result.get("success") or result.get("return_code") == 0:
189
+ return StepResult.ok(
190
+ "Prisma initialized with SQLite",
191
+ files=["prisma/schema.prisma"],
192
+ )
193
+ return StepResult.make_error(
194
+ "Failed to initialize Prisma",
195
+ result.get("error") or result.get("stderr", "Unknown error"),
196
+ ErrorCategory.CONFIGURATION,
197
+ )
198
+ return StepResult.make_error(
199
+ "Unexpected result format", str(result), ErrorCategory.UNKNOWN
200
+ )
201
+
202
+
203
+ @dataclass
204
+ class ManageDataModelStep(BaseStep):
205
+ """Step to create Prisma data model."""
206
+
207
+ name: str = "data_model"
208
+ description: str = "Create Prisma model"
209
+ entity_name: str = "Item"
210
+ fields: Dict[str, str] = field(default_factory=lambda: {"title": "string"})
211
+
212
+ def validate_preconditions(self, context: UserContext) -> Optional[str]:
213
+ """Check that Prisma is initialized before managing data model."""
214
+ schema_path = Path(context.project_dir) / "prisma" / "schema.prisma"
215
+ if not schema_path.exists():
216
+ return (
217
+ "Prisma not initialized. The prisma/schema.prisma file must exist. "
218
+ "Run 'npx prisma init --datasource-provider sqlite' first."
219
+ )
220
+ return None
221
+
222
+ def get_tool_invocation(
223
+ self, context: UserContext
224
+ ) -> Optional[Tuple[str, Dict[str, Any]]]:
225
+ """Return manage_data_model invocation."""
226
+ return (
227
+ "manage_data_model",
228
+ {
229
+ "project_dir": context.project_dir,
230
+ "model_name": self.entity_name,
231
+ "fields": self.fields,
232
+ },
233
+ )
234
+
235
+ def handle_result(self, result: Any, context: UserContext) -> StepResult:
236
+ """Convert tool result to StepResult."""
237
+ if isinstance(result, dict):
238
+ if result.get("success"):
239
+ # Store generated files in context
240
+ files = result.get("files", [])
241
+ return StepResult.ok(
242
+ f"Prisma model {self.entity_name} created",
243
+ files=files,
244
+ model_name=self.entity_name,
245
+ )
246
+ return StepResult.make_error(
247
+ f"Failed to create {self.entity_name} model",
248
+ result.get("error", "Unknown error"),
249
+ ErrorCategory.COMPILATION,
250
+ )
251
+ return StepResult.make_error(
252
+ "Unexpected result format", str(result), ErrorCategory.UNKNOWN
253
+ )
254
+
255
+
256
+ @dataclass
257
+ class SetupPrismaStep(BaseStep):
258
+ """Step to set up Prisma client after schema changes.
259
+
260
+ This creates the Prisma singleton, generates client types, and pushes to DB.
261
+ Must run AFTER ManageDataModelStep and BEFORE API endpoint steps.
262
+ """
263
+
264
+ name: str = "setup_prisma"
265
+ description: str = "Set up Prisma client and database"
266
+
267
+ def validate_preconditions(self, context: UserContext) -> Optional[str]:
268
+ """Check that Prisma schema exists before setup."""
269
+ schema_path = Path(context.project_dir) / "prisma" / "schema.prisma"
270
+ if not schema_path.exists():
271
+ return (
272
+ "Prisma schema not found. The prisma/schema.prisma file must exist. "
273
+ "Run 'npx prisma init' first."
274
+ )
275
+ return None
276
+
277
+ def get_tool_invocation(
278
+ self, context: UserContext
279
+ ) -> Optional[Tuple[str, Dict[str, Any]]]:
280
+ """Return setup_prisma invocation."""
281
+ return (
282
+ "setup_prisma",
283
+ {
284
+ "project_dir": context.project_dir,
285
+ "regenerate": True,
286
+ "push_db": True,
287
+ },
288
+ )
289
+
290
+ def handle_result(self, result: Any, context: UserContext) -> StepResult:
291
+ """Convert tool result to StepResult."""
292
+ if isinstance(result, dict):
293
+ if result.get("success"):
294
+ files = []
295
+ if result.get("singleton_path"):
296
+ files.append(result["singleton_path"])
297
+ return StepResult.ok(
298
+ "Prisma client set up successfully",
299
+ files=files,
300
+ generated=result.get("generated", False),
301
+ pushed=result.get("pushed", False),
302
+ )
303
+ return StepResult.make_error(
304
+ "Failed to set up Prisma",
305
+ result.get("error", "Unknown error"),
306
+ ErrorCategory.COMPILATION,
307
+ )
308
+ return StepResult.make_error(
309
+ "Unexpected result format", str(result), ErrorCategory.UNKNOWN
310
+ )
311
+
312
+
313
+ @dataclass
314
+ class ManageApiEndpointStep(BaseStep):
315
+ """Step to create collection API endpoint (GET, POST)."""
316
+
317
+ name: str = "api_collection"
318
+ description: str = "Create collection API"
319
+ entity_name: str = "Item"
320
+ fields: Dict[str, str] = field(default_factory=lambda: {"title": "string"})
321
+
322
+ def validate_preconditions(self, context: UserContext) -> Optional[str]:
323
+ """Check that Prisma singleton exists before creating API endpoints."""
324
+ prisma_lib = Path(context.project_dir) / "src" / "lib" / "prisma.ts"
325
+ if not prisma_lib.exists():
326
+ return (
327
+ "Prisma client not set up. The src/lib/prisma.ts file must exist. "
328
+ "Run 'setup_prisma' tool first to create the Prisma singleton."
329
+ )
330
+ return None
331
+
332
+ def get_tool_invocation(
333
+ self, context: UserContext
334
+ ) -> Optional[Tuple[str, Dict[str, Any]]]:
335
+ """Return manage_api_endpoint invocation."""
336
+ return (
337
+ "manage_api_endpoint",
338
+ {
339
+ "project_dir": context.project_dir,
340
+ "resource_name": self.entity_name.lower(),
341
+ "operations": ["GET", "POST"],
342
+ "fields": self.fields,
343
+ },
344
+ )
345
+
346
+ def handle_result(self, result: Any, context: UserContext) -> StepResult:
347
+ """Convert tool result to StepResult."""
348
+ if isinstance(result, dict):
349
+ if result.get("success"):
350
+ files = result.get("files", [])
351
+ return StepResult.ok(
352
+ f"Collection API for {self.entity_name} created",
353
+ files=files,
354
+ )
355
+ return StepResult.make_error(
356
+ f"Failed to create collection API for {self.entity_name}",
357
+ result.get("error", "Unknown error"),
358
+ ErrorCategory.COMPILATION,
359
+ )
360
+ return StepResult.make_error(
361
+ "Unexpected result format", str(result), ErrorCategory.UNKNOWN
362
+ )
363
+
364
+
365
+ @dataclass
366
+ class ManageApiEndpointDynamicStep(BaseStep):
367
+ """Step to create dynamic API endpoint (GET, PATCH, DELETE for [id])."""
368
+
369
+ name: str = "api_item"
370
+ description: str = "Create item API"
371
+ entity_name: str = "Item"
372
+ fields: Dict[str, str] = field(default_factory=lambda: {"title": "string"})
373
+
374
+ def get_tool_invocation(
375
+ self, context: UserContext
376
+ ) -> Optional[Tuple[str, Dict[str, Any]]]:
377
+ """Return manage_api_endpoint invocation for dynamic route.
378
+
379
+ Note: The tool automatically creates the [id]/route.ts file when
380
+ PATCH/DELETE operations are requested.
381
+ """
382
+ return (
383
+ "manage_api_endpoint",
384
+ {
385
+ "project_dir": context.project_dir,
386
+ "resource_name": self.entity_name.lower(),
387
+ "operations": ["GET", "PATCH", "DELETE"],
388
+ "fields": self.fields,
389
+ },
390
+ )
391
+
392
+ def handle_result(self, result: Any, context: UserContext) -> StepResult:
393
+ """Convert tool result to StepResult."""
394
+ if isinstance(result, dict):
395
+ if result.get("success"):
396
+ files = result.get("files", [])
397
+ return StepResult.ok(
398
+ f"Item API for {self.entity_name} created",
399
+ files=files,
400
+ )
401
+ return StepResult.make_error(
402
+ f"Failed to create item API for {self.entity_name}",
403
+ result.get("error", "Unknown error"),
404
+ ErrorCategory.COMPILATION,
405
+ )
406
+ return StepResult.make_error(
407
+ "Unexpected result format", str(result), ErrorCategory.UNKNOWN
408
+ )
409
+
410
+
411
+ @dataclass
412
+ class ManageReactComponentStep(BaseStep):
413
+ """Step to create React component."""
414
+
415
+ name: str = "component"
416
+ description: str = "Create React component"
417
+ entity_name: str = "Item"
418
+ variant: str = "list" # list, form, new, detail, actions
419
+ fields: Dict[str, str] = field(default_factory=lambda: {"title": "string"})
420
+
421
+ def get_tool_invocation(
422
+ self, context: UserContext
423
+ ) -> Optional[Tuple[str, Dict[str, Any]]]:
424
+ """Return manage_react_component invocation.
425
+
426
+ Note: resource_name is REQUIRED for the tool to generate correct paths.
427
+ Without it, all variants fall back to src/components/{component_name}.tsx.
428
+ """
429
+ # For "form" variant, validation expects TodoForm.tsx (not Todo.tsx)
430
+ if self.variant == "form":
431
+ component_name = f"{self.entity_name}Form"
432
+ else:
433
+ component_name = self.entity_name
434
+
435
+ return (
436
+ "manage_react_component",
437
+ {
438
+ "project_dir": context.project_dir,
439
+ "component_name": component_name,
440
+ "resource_name": self.entity_name.lower(), # Required for path generation
441
+ "variant": self.variant,
442
+ "fields": self.fields,
443
+ },
444
+ )
445
+
446
+ def handle_result(self, result: Any, context: UserContext) -> StepResult:
447
+ """Convert tool result to StepResult."""
448
+ if isinstance(result, dict):
449
+ if result.get("success"):
450
+ files = result.get("files", [])
451
+ return StepResult.ok(
452
+ f"{self.entity_name} {self.variant} component created",
453
+ files=files,
454
+ variant=self.variant,
455
+ )
456
+ return StepResult.make_error(
457
+ f"Failed to create {self.entity_name} {self.variant}",
458
+ result.get("error", "Unknown error"),
459
+ ErrorCategory.COMPILATION,
460
+ )
461
+ return StepResult.make_error(
462
+ "Unexpected result format", str(result), ErrorCategory.UNKNOWN
463
+ )
464
+
465
+
466
+ @dataclass
467
+ class ValidateCrudStructureStep(BaseStep):
468
+ """Step to validate CRUD structure."""
469
+
470
+ name: str = "validate_structure"
471
+ description: str = "Validate CRUD structure"
472
+ entity_name: str = "Item"
473
+
474
+ def get_tool_invocation(
475
+ self, context: UserContext
476
+ ) -> Optional[Tuple[str, Dict[str, Any]]]:
477
+ """Return validate_crud_structure invocation."""
478
+ return (
479
+ "validate_crud_structure",
480
+ {
481
+ "project_dir": context.project_dir,
482
+ "resource_name": self.entity_name.lower(),
483
+ },
484
+ )
485
+
486
+ def handle_result(self, result: Any, context: UserContext) -> StepResult:
487
+ """Convert tool result to StepResult."""
488
+ if isinstance(result, dict):
489
+ if result.get("success") or result.get("valid"):
490
+ return StepResult.ok("CRUD structure validated successfully")
491
+ missing = result.get("missing_files", [])
492
+ return StepResult.make_error(
493
+ "CRUD structure validation failed",
494
+ f"Missing files: {missing}",
495
+ ErrorCategory.VALIDATION,
496
+ )
497
+ return StepResult.make_error(
498
+ "Unexpected result format", str(result), ErrorCategory.UNKNOWN
499
+ )
500
+
501
+
502
+ @dataclass
503
+ class ValidateTypescriptStep(BaseStep):
504
+ """Step to validate TypeScript."""
505
+
506
+ name: str = "validate_typescript"
507
+ description: str = "Run TypeScript validation"
508
+
509
+ def get_tool_invocation(
510
+ self, context: UserContext
511
+ ) -> Optional[Tuple[str, Dict[str, Any]]]:
512
+ """Return validate_typescript invocation."""
513
+ return (
514
+ "validate_typescript",
515
+ {
516
+ "project_dir": context.project_dir,
517
+ },
518
+ )
519
+
520
+ def handle_result(self, result: Any, context: UserContext) -> StepResult:
521
+ """Convert tool result to StepResult."""
522
+ if isinstance(result, dict):
523
+ if result.get("success") or result.get("valid"):
524
+ return StepResult.ok("TypeScript validation passed")
525
+ errors = result.get("errors", [])
526
+ return StepResult.make_error(
527
+ "TypeScript validation failed",
528
+ "\n".join(errors) if errors else result.get("error", "Unknown error"),
529
+ ErrorCategory.COMPILATION,
530
+ )
531
+ return StepResult.make_error(
532
+ "Unexpected result format", str(result), ErrorCategory.UNKNOWN
533
+ )
534
+
535
+
536
+ @dataclass
537
+ class TestCrudApiStep(BaseStep):
538
+ """Step to test CRUD API."""
539
+
540
+ name: str = "test_api"
541
+ description: str = "Test CRUD operations"
542
+ entity_name: str = "Item"
543
+
544
+ def get_tool_invocation(
545
+ self, context: UserContext
546
+ ) -> Optional[Tuple[str, Dict[str, Any]]]:
547
+ """Return test_crud_api invocation."""
548
+ return (
549
+ "test_crud_api",
550
+ {
551
+ "project_dir": context.project_dir,
552
+ "model_name": self.entity_name,
553
+ },
554
+ )
555
+
556
+ def handle_result(self, result: Any, context: UserContext) -> StepResult:
557
+ """Convert tool result to StepResult."""
558
+ if isinstance(result, dict):
559
+ if result.get("success"):
560
+ return StepResult.ok(
561
+ "CRUD API tests passed",
562
+ tests_passed=result.get("tests_passed", 0),
563
+ )
564
+ # Test failures are warnings, not hard errors
565
+ # The code was generated - tests may fail due to database/server issues
566
+ test_result = result.get("result", {})
567
+ passed = test_result.get("tests_passed", 0)
568
+ failed = test_result.get("tests_failed", 0)
569
+ details = test_result.get("results", {})
570
+ # Build summary of which tests failed
571
+ failed_tests = [k for k, v in details.items() if not v.get("pass")]
572
+ return StepResult.warning(
573
+ f"API tests: {passed} passed, {failed} failed ({', '.join(failed_tests)})",
574
+ tests_passed=passed,
575
+ tests_failed=failed,
576
+ failed_tests=failed_tests,
577
+ )
578
+ return StepResult.make_error(
579
+ "Unexpected result format", str(result), ErrorCategory.UNKNOWN
580
+ )
581
+
582
+
583
+ @dataclass
584
+ class UpdateLandingPageStep(BaseStep):
585
+ """Step to update landing page with navigation."""
586
+
587
+ name: str = "update_landing"
588
+ description: str = "Update landing page"
589
+ entity_name: str = "Item"
590
+
591
+ def get_tool_invocation(
592
+ self, context: UserContext
593
+ ) -> Optional[Tuple[str, Dict[str, Any]]]:
594
+ """Return update_landing_page invocation."""
595
+ return (
596
+ "update_landing_page",
597
+ {
598
+ "project_dir": context.project_dir,
599
+ "resource_name": self.entity_name.lower(),
600
+ },
601
+ )
602
+
603
+ def handle_result(self, result: Any, context: UserContext) -> StepResult:
604
+ """Convert tool result to StepResult."""
605
+ if isinstance(result, dict):
606
+ if result.get("success"):
607
+ return StepResult.ok("Landing page updated with navigation link")
608
+ return StepResult.make_error(
609
+ "Failed to update landing page",
610
+ result.get("error", "Unknown error"),
611
+ ErrorCategory.COMPILATION,
612
+ )
613
+ return StepResult.make_error(
614
+ "Unexpected result format", str(result), ErrorCategory.UNKNOWN
615
+ )
616
+
617
+
618
+ @dataclass
619
+ class SetupTestingStep(BaseStep):
620
+ """Step to set up testing infrastructure."""
621
+
622
+ name: str = "setup_testing"
623
+ description: str = "Set up Vitest and testing libraries"
624
+
625
+ def should_skip(self, context: UserContext) -> Optional[str]:
626
+ """Skip if vitest is already configured."""
627
+ vitest_config = Path(context.project_dir) / "vitest.config.ts"
628
+ if vitest_config.exists():
629
+ return "Vitest already configured"
630
+ return None
631
+
632
+ def get_tool_invocation(
633
+ self, context: UserContext
634
+ ) -> Optional[Tuple[str, Dict[str, Any]]]:
635
+ """Return setup_nextjs_testing invocation."""
636
+ return (
637
+ "setup_nextjs_testing",
638
+ {
639
+ "project_dir": context.project_dir,
640
+ },
641
+ )
642
+
643
+ def handle_result(self, result: Any, context: UserContext) -> StepResult:
644
+ """Convert tool result to StepResult."""
645
+ if isinstance(result, dict):
646
+ if result.get("success"):
647
+ return StepResult.ok(
648
+ "Testing infrastructure set up",
649
+ files=result.get("files", []),
650
+ )
651
+ return StepResult.make_error(
652
+ "Failed to set up testing",
653
+ result.get("error", "Unknown error"),
654
+ ErrorCategory.CONFIGURATION,
655
+ )
656
+ return StepResult.make_error(
657
+ "Unexpected result format", str(result), ErrorCategory.UNKNOWN
658
+ )
659
+
660
+
661
+ @dataclass
662
+ class RunTestsStep(BaseStep):
663
+ """Step to run all tests."""
664
+
665
+ name: str = "run_tests"
666
+ description: str = "Run npm test"
667
+
668
+ def get_tool_invocation(
669
+ self, context: UserContext
670
+ ) -> Optional[Tuple[str, Dict[str, Any]]]:
671
+ """Return run_cli_command invocation for npm test."""
672
+ return (
673
+ "run_cli_command",
674
+ {
675
+ "command": "npm test",
676
+ "working_dir": context.project_dir,
677
+ "timeout": 1200,
678
+ },
679
+ )
680
+
681
+ def handle_result(self, result: Any, context: UserContext) -> StepResult:
682
+ """Convert tool result to StepResult."""
683
+ if isinstance(result, dict):
684
+ if result.get("success") or result.get("return_code") == 0:
685
+ return StepResult.ok("All tests passed")
686
+ # Tests failing is a warning, not a hard error
687
+ return StepResult.warning(
688
+ "Some tests failed",
689
+ stderr=result.get("stderr", ""),
690
+ stdout=result.get("stdout", ""),
691
+ )
692
+ return StepResult.make_error(
693
+ "Unexpected result format", str(result), ErrorCategory.UNKNOWN
694
+ )
695
+
696
+
697
+ @dataclass
698
+ class ValidateStylesStep(BaseStep):
699
+ """Step to validate CSS files and design system consistency.
700
+
701
+ This step validates:
702
+ 1. CSS files contain valid CSS (not TypeScript/JavaScript) - CRITICAL
703
+ 2. globals.css has Tailwind directives
704
+ 3. layout.tsx imports globals.css
705
+ 4. Custom classes used in components are defined in globals.css
706
+
707
+ Addresses Issue #1002: CSS file contains TypeScript code instead of CSS.
708
+ """
709
+
710
+ name: str = "validate_styles"
711
+ description: str = "Validate CSS files and design system"
712
+ resource_name: Optional[str] = None
713
+
714
+ def validate_preconditions(self, context: UserContext) -> Optional[str]:
715
+ """Check that styling files exist before validation."""
716
+ globals_css = Path(context.project_dir) / "src" / "app" / "globals.css"
717
+ if not globals_css.exists():
718
+ return (
719
+ "globals.css not found. The src/app/globals.css file must exist. "
720
+ "Run 'setup_app_styling' first."
721
+ )
722
+ return None
723
+
724
+ def get_tool_invocation(
725
+ self, context: UserContext
726
+ ) -> Optional[Tuple[str, Dict[str, Any]]]:
727
+ """Return validate_styles invocation."""
728
+ params = {
729
+ "project_dir": context.project_dir,
730
+ }
731
+ if self.resource_name:
732
+ params["_resource_name"] = self.resource_name
733
+ return ("validate_styles", params)
734
+
735
+ def handle_result(self, result: Any, context: UserContext) -> StepResult:
736
+ """Convert tool result to StepResult."""
737
+ if isinstance(result, dict):
738
+ if result.get("success") or result.get("is_valid"):
739
+ warnings = result.get("warnings", [])
740
+ if warnings:
741
+ return StepResult.warning(
742
+ "Styling validated with warnings",
743
+ warnings=warnings,
744
+ )
745
+ return StepResult.ok("Styling validated successfully")
746
+
747
+ errors = result.get("errors", [])
748
+ # Check if any errors are CRITICAL (blocking)
749
+ critical_errors = [e for e in errors if "CRITICAL" in e]
750
+ if critical_errors:
751
+ return StepResult.make_error(
752
+ "CRITICAL styling validation failed",
753
+ "\n".join(critical_errors),
754
+ ErrorCategory.VALIDATION,
755
+ retryable=True, # Allow LLM to retry with correct CSS
756
+ )
757
+ return StepResult.make_error(
758
+ "Styling validation failed",
759
+ "\n".join(errors) if errors else result.get("error", "Unknown error"),
760
+ ErrorCategory.VALIDATION,
761
+ )
762
+ return StepResult.make_error(
763
+ "Unexpected result format", str(result), ErrorCategory.UNKNOWN
764
+ )
765
+
766
+
767
+ @dataclass
768
+ class GenerateStyleTestsStep(BaseStep):
769
+ """Step to generate CSS and styling tests for the project.
770
+
771
+ Creates test files that validate:
772
+ 1. CSS file integrity (no TypeScript in CSS)
773
+ 2. Tailwind directive presence
774
+ 3. Design system class definitions
775
+ 4. Layout imports globals.css
776
+
777
+ Tests are placed in the project's /tests directory.
778
+ """
779
+
780
+ name: str = "generate_style_tests"
781
+ description: str = "Generate CSS and styling tests"
782
+ resource_name: str = "Item"
783
+
784
+ def should_skip(self, context: UserContext) -> Optional[str]:
785
+ """Skip if style tests already exist."""
786
+ styles_test = Path(context.project_dir) / "tests" / "styles.test.ts"
787
+ if styles_test.exists():
788
+ return "Style tests already exist"
789
+ return None
790
+
791
+ def validate_preconditions(self, context: UserContext) -> Optional[str]:
792
+ """Check that testing is set up before generating style tests."""
793
+ vitest_config = Path(context.project_dir) / "vitest.config.ts"
794
+ if not vitest_config.exists():
795
+ return (
796
+ "Vitest not configured. Run 'setup_testing' first to set up "
797
+ "the testing infrastructure."
798
+ )
799
+ return None
800
+
801
+ def get_tool_invocation(
802
+ self, context: UserContext
803
+ ) -> Optional[Tuple[str, Dict[str, Any]]]:
804
+ """Return generate_style_tests invocation."""
805
+ return (
806
+ "generate_style_tests",
807
+ {
808
+ "project_dir": context.project_dir,
809
+ "resource_name": self.resource_name,
810
+ },
811
+ )
812
+
813
+ def handle_result(self, result: Any, context: UserContext) -> StepResult:
814
+ """Convert tool result to StepResult."""
815
+ if isinstance(result, dict):
816
+ if result.get("success"):
817
+ return StepResult.ok(
818
+ "Style tests generated successfully",
819
+ files=result.get("files", []),
820
+ )
821
+ return StepResult.make_error(
822
+ "Failed to generate style tests",
823
+ result.get("error", "Unknown error"),
824
+ ErrorCategory.COMPILATION,
825
+ )
826
+ return StepResult.make_error(
827
+ "Unexpected result format", str(result), ErrorCategory.UNKNOWN
828
+ )