amd-gaia 0.14.3__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.14.3.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.14.3.dist-info → amd_gaia-0.15.1.dist-info}/entry_points.txt +1 -0
  4. {amd_gaia-0.14.3.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 -5621
  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.14.3.dist-info/RECORD +0 -168
  178. gaia/agents/code/app.py +0 -266
  179. gaia/llm/llm_client.py +0 -729
  180. {amd_gaia-0.14.3.dist-info → amd_gaia-0.15.1.dist-info}/WHEEL +0 -0
  181. {amd_gaia-0.14.3.dist-info → amd_gaia-0.15.1.dist-info}/top_level.txt +0 -0
@@ -1,1758 +1,1758 @@
1
- # Copyright(C) 2025-2026 Advanced Micro Devices, Inc. All rights reserved.
2
- # SPDX-License-Identifier: MIT
3
- """Generic web development tools for Code Agent.
4
-
5
- This mixin provides flexible, framework-agnostic tools for web development:
6
- - API endpoint generation with actual Prisma queries
7
- - React component generation (server and client)
8
- - Database schema management
9
- - Configuration updates
10
-
11
- Tools use the manage_* prefix to indicate they handle both creation and modification.
12
- All file I/O operations are delegated to FileIOToolsMixin for clean separation of concerns.
13
- """
14
-
15
- import logging
16
- import re
17
- import subprocess
18
- from pathlib import Path
19
- from typing import Any, Dict, List, Optional
20
-
21
- from gaia.agents.base.tools import tool
22
- from gaia.agents.code.prompts.code_patterns import (
23
- API_ROUTE_DYNAMIC_DELETE,
24
- API_ROUTE_DYNAMIC_GET,
25
- API_ROUTE_DYNAMIC_PATCH,
26
- API_ROUTE_GET,
27
- API_ROUTE_GET_PAGINATED,
28
- API_ROUTE_POST,
29
- APP_GLOBALS_CSS,
30
- APP_LAYOUT,
31
- CLIENT_COMPONENT_FORM,
32
- CLIENT_COMPONENT_TIMER,
33
- COMPONENT_TEST_ACTIONS,
34
- COMPONENT_TEST_FORM,
35
- LANDING_PAGE_WITH_LINKS,
36
- SERVER_COMPONENT_LIST,
37
- generate_actions_component,
38
- generate_api_imports,
39
- generate_detail_page,
40
- generate_field_display,
41
- generate_form_field,
42
- generate_form_field_assertions,
43
- generate_form_fill_actions,
44
- generate_new_page,
45
- generate_test_data_fields,
46
- generate_zod_schema,
47
- pluralize,
48
- )
49
-
50
- logger = logging.getLogger(__name__)
51
-
52
-
53
- def read_prisma_model(project_dir: str, model_name: str) -> Dict[str, Any]:
54
- """Read model definition from Prisma schema.
55
-
56
- Parses the Prisma schema file to extract field definitions and metadata
57
- for a specific model. This allows tools to adapt to the actual schema
58
- instead of relying on hardcoded assumptions.
59
-
60
- Args:
61
- project_dir: Path to the project directory
62
- model_name: Name of the model to read (case-insensitive)
63
-
64
- Returns:
65
- Dictionary with:
66
- success: Whether the model was found
67
- model_name: The model name (as defined in schema)
68
- fields: Dict of field names to types
69
- has_timestamps: Whether model has createdAt/updatedAt
70
- error: Error message if failed
71
- """
72
- schema_path = Path(project_dir) / "prisma" / "schema.prisma"
73
- if not schema_path.exists():
74
- return {
75
- "success": False,
76
- "error": "Schema file not found at prisma/schema.prisma",
77
- }
78
-
79
- try:
80
- content = schema_path.read_text()
81
- model_pattern = rf"model\s+{model_name}\s*\{{([^}}]+)\}}"
82
- match = re.search(model_pattern, content, re.DOTALL | re.IGNORECASE)
83
-
84
- if not match:
85
- return {
86
- "success": False,
87
- "error": f"Model {model_name} not found in schema",
88
- }
89
-
90
- # Parse fields from model body
91
- fields = {}
92
- for line in match.group(1).strip().split("\n"):
93
- line = line.strip()
94
- if not line or line.startswith("//") or line.startswith("@@"):
95
- continue
96
- # Skip field decorators like @id, @default, etc.
97
- if line.startswith("@") and not line.startswith("@@"):
98
- continue
99
- parts = line.split()
100
- if len(parts) >= 2:
101
- field_name = parts[0]
102
- field_type = parts[1].rstrip("?[]") # Remove optional/array markers
103
- fields[field_name] = field_type
104
-
105
- return {
106
- "success": True,
107
- "model_name": model_name,
108
- "fields": fields,
109
- "has_timestamps": "createdAt" in fields and "updatedAt" in fields,
110
- }
111
- except Exception as e:
112
- return {"success": False, "error": f"Failed to parse schema: {str(e)}"}
113
-
114
-
115
- class WebToolsMixin:
116
- """Mixin providing generic web development tools for the Code Agent.
117
-
118
- Tools are designed to be framework-agnostic where possible, with
119
- framework-specific logic in prompts rather than hardcoded in tools.
120
-
121
- All tools delegate file operations to FileIOToolsMixin to maintain
122
- clean separation of concerns.
123
- """
124
-
125
- def register_web_tools(self) -> None:
126
- """Register generic web development tools with the agent."""
127
-
128
- @tool
129
- def setup_app_styling(
130
- project_dir: str,
131
- app_title: str = "My App",
132
- app_description: str = "A modern web application",
133
- ) -> Dict[str, Any]:
134
- """Set up app-wide styling with modern design system.
135
-
136
- Creates/updates the root layout and globals.css with a modern dark theme
137
- that all pages will inherit. This should be run early in the project
138
- setup, after create-next-app.
139
-
140
- The design system includes:
141
- - Dark gradient background at the layout level
142
- - Glass morphism card effects (.glass-card)
143
- - Modern button variants (.btn-primary, .btn-secondary, .btn-danger)
144
- - Input field styling (.input-field)
145
- - Custom checkbox styling (.checkbox-modern)
146
- - Gradient text for titles (.page-title)
147
- - Back link styling (.link-back)
148
- - Custom scrollbar styling
149
-
150
- Args:
151
- project_dir: Path to the Next.js project directory
152
- app_title: Application title for metadata
153
- app_description: Application description for metadata
154
-
155
- Returns:
156
- Dictionary with success status and created/updated files
157
- """
158
- try:
159
- project_path = Path(project_dir)
160
- app_dir = project_path / "src" / "app"
161
-
162
- if not app_dir.exists():
163
- return {
164
- "success": False,
165
- "error": f"App directory not found: {app_dir}",
166
- "hint": "Run create-next-app first to initialize the project",
167
- }
168
-
169
- files_created = []
170
-
171
- # Generate layout.tsx
172
- layout_path = app_dir / "layout.tsx"
173
- layout_content = APP_LAYOUT.format(
174
- app_title=app_title,
175
- app_description=app_description,
176
- )
177
-
178
- # Write layout file using FileIOToolsMixin
179
- if hasattr(self, "write_file"):
180
- result = self.write_file(str(layout_path), layout_content)
181
- if result.get("success"):
182
- files_created.append(str(layout_path))
183
- else:
184
- layout_path.write_text(layout_content)
185
- files_created.append(str(layout_path))
186
-
187
- # Generate globals.css
188
- globals_path = app_dir / "globals.css"
189
- globals_content = APP_GLOBALS_CSS
190
-
191
- # Write globals file
192
- if hasattr(self, "write_file"):
193
- result = self.write_file(str(globals_path), globals_content)
194
- if result.get("success"):
195
- files_created.append(str(globals_path))
196
- else:
197
- globals_path.write_text(globals_content)
198
- files_created.append(str(globals_path))
199
-
200
- logger.info(f"Set up app-wide styling: {files_created}")
201
- return {
202
- "success": True,
203
- "message": "App-wide styling configured successfully",
204
- "files": files_created,
205
- "design_system": [
206
- ".glass-card - Glass morphism card effect",
207
- ".btn-primary - Primary gradient button",
208
- ".btn-secondary - Secondary button",
209
- ".btn-danger - Danger/delete button",
210
- ".input-field - Styled form input",
211
- ".checkbox-modern - Modern checkbox styling",
212
- ".page-title - Gradient title text",
213
- ".link-back - Back navigation link",
214
- ],
215
- }
216
- except Exception as e:
217
- logger.exception("Failed to set up app styling")
218
- return {"success": False, "error": str(e)}
219
-
220
- @tool
221
- def manage_api_endpoint(
222
- project_dir: str,
223
- resource_name: str,
224
- operations: List[str] = None,
225
- fields: Optional[Dict[str, str]] = None,
226
- enable_pagination: bool = False,
227
- ) -> Dict[str, Any]:
228
- """Manage API endpoints with actual Prisma operations.
229
-
230
- Creates or updates API routes with functional CRUD operations,
231
- validation, and error handling. Works for ANY resource type.
232
-
233
- REQUIREMENTS (Tier 2 - Prerequisites):
234
- - Must be called AFTER manage_data_model (needs Prisma model to exist)
235
- - Ensure 'prisma generate' was run (manage_data_model does this automatically)
236
- - API routes always import: NextResponse, prisma, z (zod)
237
- - Use try/catch with appropriate status codes (200, 201, 400, 500)
238
-
239
- Args:
240
- project_dir: Path to the web project directory
241
- resource_name: Resource name (e.g., "todo", "user", "product")
242
- operations: HTTP methods to implement (default: ["GET", "POST"])
243
- fields: Resource fields with types (for validation schema)
244
- enable_pagination: Whether to add pagination to GET endpoint
245
-
246
- Returns:
247
- Dictionary with success status and created files
248
- """
249
- try:
250
- operations = operations or ["GET", "POST"]
251
-
252
- project_path = Path(project_dir)
253
-
254
- # Phase 1 Fix (Issue #885): Read from Prisma schema instead of
255
- # using dangerous defaults. This makes tools schema-aware.
256
- if not fields:
257
- model_info = read_prisma_model(
258
- project_dir, resource_name.capitalize()
259
- )
260
- if model_info["success"]:
261
- # Convert Prisma types to our field types and filter out auto-fields
262
- prisma_to_field_type = {
263
- "String": "string",
264
- "Int": "number",
265
- "Float": "float",
266
- "Boolean": "boolean",
267
- "DateTime": "datetime",
268
- }
269
- fields = {}
270
- for field_name, prisma_type in model_info["fields"].items():
271
- # Skip auto-generated fields
272
- if field_name.lower() in {"id", "createdat", "updatedat"}:
273
- continue
274
- fields[field_name] = prisma_to_field_type.get(
275
- prisma_type, "string"
276
- )
277
-
278
- if not fields:
279
- return {
280
- "success": False,
281
- "error": f"Model {resource_name} has no user-facing fields in Prisma schema",
282
- "hint": "Run manage_data_model first to create the model with fields",
283
- }
284
- logger.info(
285
- f"Auto-read fields from Prisma schema for {resource_name}: {fields}"
286
- )
287
- else:
288
- return {
289
- "success": False,
290
- "error": f"Cannot find model {resource_name} in Prisma schema. {model_info.get('error', '')}",
291
- "hint": "Run manage_data_model first to create the Prisma model",
292
- }
293
-
294
- if not project_path.exists():
295
- return {
296
- "success": False,
297
- "error": f"Project directory does not exist: {project_dir}",
298
- }
299
-
300
- # Sanitize resource_name: remove path components, brackets, slashes
301
- # This prevents malformed paths like "todos/[id]s/route.ts"
302
- clean_resource = resource_name.strip()
303
- # Remove common path patterns that shouldn't be in resource names
304
- clean_resource = clean_resource.replace("/[id]", "").replace("[id]", "")
305
- clean_resource = clean_resource.rstrip("/").lstrip("/")
306
- # Extract just the base resource name if path-like
307
- if "/" in clean_resource:
308
- clean_resource = clean_resource.split("/")[0]
309
- # Remove any remaining special characters
310
- clean_resource = re.sub(r"[^\w]", "", clean_resource)
311
-
312
- if not clean_resource:
313
- return {
314
- "success": False,
315
- "error": f"Invalid resource_name: '{resource_name}' - must be a simple name like 'todo' or 'product'",
316
- "hint": "Use singular form without paths, e.g., 'todo' not 'todos/[id]'",
317
- }
318
-
319
- # Safety net: Ensure Prisma singleton exists before creating routes
320
- singleton_path = project_path / "src" / "lib" / "prisma.ts"
321
- if not singleton_path.exists():
322
- from gaia.agents.code.tools.prisma_tools import (
323
- PRISMA_SINGLETON_TEMPLATE,
324
- )
325
-
326
- singleton_path.parent.mkdir(parents=True, exist_ok=True)
327
- singleton_path.write_text(
328
- PRISMA_SINGLETON_TEMPLATE, encoding="utf-8"
329
- )
330
- logger.info(f"Auto-created Prisma singleton at {singleton_path}")
331
-
332
- # Generate resource variants from cleaned name
333
- resource = clean_resource.lower()
334
- Resource = clean_resource.capitalize()
335
- resource_plural = pluralize(resource)
336
-
337
- # Build API route content
338
- # Phase 4 Fix (Issue #885): Check all operations that need validation
339
- needs_validation = any(
340
- op in operations for op in ["POST", "PATCH", "PUT"]
341
- )
342
- imports = generate_api_imports(
343
- operations, uses_validation=needs_validation
344
- )
345
-
346
- # Generate validation schema if needed
347
- validation_schema = ""
348
- if needs_validation:
349
- validation_schema = generate_zod_schema(Resource, fields)
350
-
351
- # Generate handlers based on operations
352
- handlers = []
353
- for op in operations:
354
- if op == "GET":
355
- pattern = (
356
- API_ROUTE_GET_PAGINATED
357
- if enable_pagination
358
- else API_ROUTE_GET
359
- )
360
- handlers.append(
361
- pattern.format(
362
- resource=resource,
363
- Resource=Resource,
364
- resource_plural=resource_plural,
365
- )
366
- )
367
- elif op == "POST":
368
- handlers.append(
369
- API_ROUTE_POST.format(
370
- resource=resource,
371
- Resource=Resource,
372
- resource_plural=resource_plural,
373
- )
374
- )
375
-
376
- # Combine into complete file - use \n\n to separate handlers
377
- full_content = f"{imports}\n\n{validation_schema}\n\n{(chr(10) + chr(10)).join(handlers)}"
378
-
379
- # Write API route file
380
- api_file_path = Path(
381
- f"{project_dir}/src/app/api/{resource_plural}/route.ts"
382
- )
383
- api_file_path.parent.mkdir(parents=True, exist_ok=True)
384
-
385
- # Only write collection route if POST is in operations OR route doesn't exist
386
- # This prevents dynamic route calls from overwriting collection route
387
- created_files = []
388
- if "POST" in operations or not api_file_path.exists():
389
- api_file_path.write_text(full_content, encoding="utf-8")
390
- created_files.append(str(api_file_path))
391
- result = {"success": True}
392
-
393
- # Create dynamic route if PATCH or DELETE requested
394
- if (
395
- "PATCH" in operations
396
- or "DELETE" in operations
397
- or "PUT" in operations
398
- ):
399
- dynamic_handlers = []
400
- if "GET" in operations:
401
- dynamic_handlers.append(
402
- API_ROUTE_DYNAMIC_GET.format(
403
- resource=resource, Resource=Resource
404
- )
405
- )
406
- if "PATCH" in operations or "PUT" in operations:
407
- dynamic_handlers.append(
408
- API_ROUTE_DYNAMIC_PATCH.format(
409
- resource=resource, Resource=Resource
410
- )
411
- )
412
- if "DELETE" in operations:
413
- dynamic_handlers.append(
414
- API_ROUTE_DYNAMIC_DELETE.format(resource=resource)
415
- )
416
-
417
- dynamic_content = f"{imports}\n\n{validation_schema}\n\n{(chr(10) + chr(10)).join(dynamic_handlers)}"
418
- dynamic_file_path = Path(
419
- f"{project_dir}/src/app/api/{resource_plural}/[id]/route.ts"
420
- )
421
- dynamic_file_path.parent.mkdir(parents=True, exist_ok=True)
422
- dynamic_file_path.write_text(dynamic_content, encoding="utf-8")
423
- created_files.append(str(dynamic_file_path))
424
-
425
- logger.info(f"Created API endpoint for {resource}")
426
-
427
- return {
428
- "success": result.get("success", True),
429
- "resource": resource,
430
- "operations": operations,
431
- "files": created_files,
432
- }
433
-
434
- except Exception as e:
435
- logger.error(f"Error managing API endpoint: {e}")
436
- return {
437
- "success": False,
438
- "error": str(e),
439
- "error_type": "api_endpoint_error",
440
- "hint": "Check project directory structure and permissions",
441
- }
442
-
443
- @tool
444
- def manage_react_component(
445
- project_dir: str,
446
- component_name: str,
447
- component_type: str = "server",
448
- resource_name: Optional[str] = None,
449
- fields: Optional[Dict[str, str]] = None,
450
- variant: str = "list",
451
- ) -> Dict[str, Any]:
452
- """Manage React components with functional implementations.
453
-
454
- Creates or updates React components with real data fetching,
455
- state management, and event handlers. Works for ANY resource.
456
-
457
- REQUIREMENTS (Tier 2 - Prerequisites):
458
- - Must be called AFTER manage_api_endpoint (components need API routes)
459
- - Must be called AFTER manage_data_model (components need Prisma types)
460
- - Use 'import type { X } from @prisma/client' for type imports
461
- - Server components: import { prisma } from '@/lib/prisma'
462
- - Client components: NEVER import prisma directly - use API routes
463
-
464
- Args:
465
- project_dir: Path to the web project directory
466
- component_name: Component name (e.g., "TodoList", "UserForm")
467
- component_type: "server" or "client" component
468
- resource_name: Associated resource (for data operations)
469
- fields: Resource fields (for forms and display)
470
- variant: Component variant:
471
- - "list": List page showing all items (server component)
472
- - "form": Reusable form component for create/edit (client component)
473
- - "new": Create new item page (client page using form)
474
- - "detail": View/edit single item page with delete (client page)
475
- - "actions": Delete/edit button component (client component)
476
-
477
- Returns:
478
- Dictionary with success status and component path
479
- """
480
- try:
481
- project_path = Path(project_dir)
482
- if not project_path.exists():
483
- return {
484
- "success": False,
485
- "error": f"Project directory does not exist: {project_dir}",
486
- }
487
-
488
- # Sanitize resource_name if provided (same logic as manage_api_endpoint)
489
- clean_resource_name = resource_name
490
- if resource_name:
491
- clean_resource = resource_name.strip()
492
- clean_resource = clean_resource.replace("/[id]", "").replace(
493
- "[id]", ""
494
- )
495
- clean_resource = clean_resource.rstrip("/").lstrip("/")
496
- if "/" in clean_resource:
497
- clean_resource = clean_resource.split("/")[0]
498
- clean_resource = re.sub(r"[^\w]", "", clean_resource)
499
- clean_resource_name = (
500
- clean_resource if clean_resource else resource_name
501
- )
502
-
503
- # Auto-set component_type for client-side variants
504
- # These variants always generate client components with "use client"
505
- # This prevents the stub fallback when variant="form" but component_type
506
- # defaults to "server"
507
- if variant in ["form", "new", "detail", "actions", "artifact-timer"]:
508
- component_type = "client"
509
-
510
- # Phase 1 Fix (Issue #885): Read from Prisma schema instead of
511
- # using dangerous defaults. This makes tools schema-aware.
512
- if not fields and clean_resource_name:
513
- model_info = read_prisma_model(
514
- project_dir, clean_resource_name.capitalize()
515
- )
516
- if model_info["success"]:
517
- # Convert Prisma types to our field types and filter out auto-fields
518
- prisma_to_field_type = {
519
- "String": "string",
520
- "Int": "number",
521
- "Float": "float",
522
- "Boolean": "boolean",
523
- "DateTime": "datetime",
524
- }
525
- fields = {}
526
- for field_name, prisma_type in model_info["fields"].items():
527
- # Skip auto-generated fields
528
- if field_name.lower() in {"id", "createdat", "updatedat"}:
529
- continue
530
- fields[field_name] = prisma_to_field_type.get(
531
- prisma_type, "string"
532
- )
533
- if fields:
534
- logger.info(
535
- f"Auto-read fields from Prisma schema for {clean_resource_name}: {fields}"
536
- )
537
- # Note: We don't fail here - some components don't need fields
538
-
539
- content = ""
540
-
541
- if (
542
- component_type == "server"
543
- and variant == "list"
544
- and clean_resource_name
545
- ):
546
- # Generate server component with data fetching
547
- resource = clean_resource_name.lower()
548
- Resource = clean_resource_name.capitalize()
549
- resource_plural = pluralize(resource)
550
-
551
- field_display = generate_field_display(fields or {})
552
-
553
- content = SERVER_COMPONENT_LIST.format(
554
- resource=resource,
555
- Resource=Resource,
556
- resource_plural=resource_plural,
557
- field_display=field_display,
558
- )
559
-
560
- elif (
561
- component_type == "client"
562
- and variant == "form"
563
- and clean_resource_name
564
- ):
565
- # Generate client component with form and state
566
- resource = clean_resource_name.lower()
567
- Resource = clean_resource_name.capitalize()
568
-
569
- # Phase 3 Fix (Issue #885): Fail clearly if no fields available
570
- # instead of using dangerous defaults
571
- if not fields:
572
- return {
573
- "success": False,
574
- "error": f"No fields available for {clean_resource_name} form component",
575
- "hint": "Run manage_data_model first to create the Prisma model with fields",
576
- }
577
-
578
- # Generate form state fields
579
- form_state = []
580
- date_field_names = []
581
- for field_name, field_type in fields.items():
582
- if field_name not in ["id", "createdAt", "updatedAt"]:
583
- normalized_type = (
584
- field_type.lower()
585
- if isinstance(field_type, str)
586
- else str(field_type).lower()
587
- )
588
- default = (
589
- "0"
590
- if normalized_type
591
- in {"number", "int", "integer", "float"}
592
- else "false" if normalized_type == "boolean" else '""'
593
- )
594
- form_state.append(f" {field_name}: {default}")
595
- if normalized_type in {"date", "datetime", "timestamp"}:
596
- date_field_names.append(f'"{field_name}"')
597
-
598
- # Generate form fields
599
- form_fields = []
600
- for field_name, field_type in fields.items():
601
- if field_name not in ["id", "createdAt", "updatedAt"]:
602
- form_fields.append(
603
- generate_form_field(field_name, field_type)
604
- )
605
-
606
- content = CLIENT_COMPONENT_FORM.format(
607
- resource=resource,
608
- Resource=Resource,
609
- form_state_fields=",\n".join(form_state),
610
- date_fields=(
611
- f"[{', '.join(date_field_names)}] as const"
612
- if date_field_names
613
- else "[] as const"
614
- ),
615
- form_fields="\n".join(form_fields),
616
- )
617
-
618
- elif variant == "new" and clean_resource_name:
619
- # Generate "new" page that uses the form component
620
- content = generate_new_page(clean_resource_name)
621
-
622
- elif variant == "detail" and clean_resource_name:
623
- # Generate detail/edit page with form and delete functionality
624
- # Phase 3 Fix (Issue #885): Fail clearly if no fields available
625
- if not fields:
626
- return {
627
- "success": False,
628
- "error": f"No fields available for {clean_resource_name} detail page",
629
- "hint": "Run manage_data_model first to create the Prisma model with fields",
630
- }
631
- content = generate_detail_page(clean_resource_name, fields)
632
-
633
- elif variant == "artifact-timer":
634
- timer_component = component_name or (
635
- f"{clean_resource_name.capitalize()}Timer"
636
- if clean_resource_name
637
- else "CountdownTimer"
638
- )
639
- timer_component = re.sub(r"[^0-9A-Za-z_]", "", timer_component)
640
- if not timer_component:
641
- timer_component = "CountdownTimer"
642
- component_name = timer_component
643
- content = CLIENT_COMPONENT_TIMER.format(
644
- ComponentName=timer_component,
645
- )
646
-
647
- elif variant == "actions" and clean_resource_name:
648
- # Generate actions component with delete functionality
649
- content = generate_actions_component(clean_resource_name)
650
-
651
- else:
652
- # Generic component template
653
- content = f"""interface {component_name}Props {{
654
- // Add props here
655
- }}
656
-
657
- export function {component_name}({{ }}: {component_name}Props) {{
658
- return (
659
- <div>
660
- <h2>{component_name}</h2>
661
- </div>
662
- );
663
- }}"""
664
-
665
- # Determine file path (use clean_resource_name to avoid malformed paths)
666
- if (
667
- component_type == "server"
668
- and variant == "list"
669
- and clean_resource_name
670
- ):
671
- file_path = Path(
672
- f"{project_dir}/src/app/{pluralize(clean_resource_name)}/page.tsx"
673
- )
674
- elif variant == "form":
675
- file_path = Path(
676
- f"{project_dir}/src/components/{component_name}.tsx"
677
- )
678
- elif variant == "new" and clean_resource_name:
679
- file_path = Path(
680
- f"{project_dir}/src/app/{pluralize(clean_resource_name)}/new/page.tsx"
681
- )
682
- elif variant == "detail" and clean_resource_name:
683
- file_path = Path(
684
- f"{project_dir}/src/app/{pluralize(clean_resource_name)}/[id]/page.tsx"
685
- )
686
- elif variant == "actions" and clean_resource_name:
687
- file_path = Path(
688
- f"{project_dir}/src/components/{clean_resource_name.capitalize()}Actions.tsx"
689
- )
690
- else:
691
- file_path = Path(
692
- f"{project_dir}/src/components/{component_name}.tsx"
693
- )
694
-
695
- # Write component file
696
- file_path.parent.mkdir(parents=True, exist_ok=True)
697
- file_path.write_text(content, encoding="utf-8")
698
- result = {"success": True}
699
- created_files = [str(file_path)]
700
-
701
- # Generate component tests for form and actions variants
702
- if variant in ["form", "actions"] and clean_resource_name and fields:
703
- try:
704
- resource = clean_resource_name.lower()
705
- Resource = clean_resource_name.capitalize()
706
- resource_plural = pluralize(resource)
707
- test_data_fields = generate_test_data_fields(fields, variant=1)
708
-
709
- if variant == "form":
710
- # Generate form component test
711
- form_field_assertions = generate_form_field_assertions(
712
- fields
713
- )
714
- form_fill_actions = generate_form_fill_actions(fields)
715
-
716
- form_test_content = COMPONENT_TEST_FORM.format(
717
- Resource=Resource,
718
- resource_plural=resource_plural,
719
- form_field_assertions=form_field_assertions,
720
- form_fill_actions=form_fill_actions,
721
- test_data_fields=test_data_fields,
722
- )
723
- form_test_path = Path(
724
- f"{project_dir}/src/components/__tests__/{Resource}Form.test.tsx"
725
- )
726
- form_test_path.parent.mkdir(parents=True, exist_ok=True)
727
- form_test_path.write_text(
728
- form_test_content, encoding="utf-8"
729
- )
730
- created_files.append(str(form_test_path))
731
- logger.info(f"Created form component test for {Resource}")
732
-
733
- elif variant == "actions":
734
- # Generate actions component test
735
- actions_test_content = COMPONENT_TEST_ACTIONS.format(
736
- Resource=Resource,
737
- resource=resource,
738
- resource_plural=resource_plural,
739
- )
740
- actions_test_path = Path(
741
- f"{project_dir}/src/components/__tests__/{Resource}Actions.test.tsx"
742
- )
743
- actions_test_path.parent.mkdir(parents=True, exist_ok=True)
744
- actions_test_path.write_text(
745
- actions_test_content, encoding="utf-8"
746
- )
747
- created_files.append(str(actions_test_path))
748
- logger.info(
749
- f"Created actions component test for {Resource}"
750
- )
751
-
752
- except Exception as test_error:
753
- logger.warning(
754
- f"Could not generate component test: {test_error}"
755
- )
756
-
757
- logger.info(f"Created React component: {component_name}")
758
-
759
- return {
760
- "success": result.get("success", True),
761
- "component": component_name,
762
- "type": component_type,
763
- "file_path": str(file_path),
764
- "files": created_files,
765
- }
766
-
767
- except Exception as e:
768
- logger.error(f"Error managing React component: {e}")
769
- return {
770
- "success": False,
771
- "error": str(e),
772
- "error_type": "component_error",
773
- "hint": "Check project structure and component syntax",
774
- }
775
-
776
- @tool
777
- def update_landing_page(
778
- project_dir: str,
779
- resource_name: str,
780
- description: Optional[str] = None,
781
- ) -> Dict[str, Any]:
782
- """Update the landing page to include a link to the new resource.
783
-
784
- Modifies src/app/page.tsx to add navigation to the newly created
785
- resource pages. This ensures users can easily access the new features
786
- from the main page.
787
-
788
- Args:
789
- project_dir: Path to the Next.js project directory
790
- resource_name: Name of the resource (e.g., "todo", "product")
791
- description: Optional description for the link
792
-
793
- Returns:
794
- Dictionary with success status and updated file path
795
- """
796
- try:
797
- project_path = Path(project_dir)
798
- page_path = project_path / "src" / "app" / "page.tsx"
799
-
800
- if not page_path.exists():
801
- return {
802
- "success": False,
803
- "error": f"Landing page not found: {page_path}",
804
- "hint": "Ensure this is a Next.js project with app router",
805
- }
806
-
807
- resource = resource_name.lower()
808
- Resource = resource_name.capitalize()
809
- resource_plural = pluralize(resource)
810
- link_description = description or f"Manage your {resource_plural}"
811
-
812
- # Read current content
813
- current_content = page_path.read_text(encoding="utf-8")
814
-
815
- # Check if link already exists
816
- if (
817
- f'href="/{resource_plural}"' in current_content
818
- or f"href='/{resource_plural}'" in current_content
819
- ):
820
- return {
821
- "success": True,
822
- "message": f"Link to /{resource_plural} already exists in landing page",
823
- "file_path": str(page_path),
824
- "already_exists": True,
825
- }
826
-
827
- # Generate new landing page with link to resource using dark theme
828
- new_content = LANDING_PAGE_WITH_LINKS.format(
829
- resource_plural=resource_plural,
830
- Resource=Resource,
831
- link_description=link_description,
832
- )
833
-
834
- # Write updated content
835
- page_path.write_text(new_content, encoding="utf-8")
836
-
837
- logger.info(f"Updated landing page with link to /{resource_plural}")
838
-
839
- return {
840
- "success": True,
841
- "message": f"Landing page updated with link to /{resource_plural}",
842
- "file_path": str(page_path),
843
- "resource": resource_name,
844
- "link_path": f"/{resource_plural}",
845
- }
846
-
847
- except Exception as e:
848
- logger.error(f"Error updating landing page: {e}")
849
- return {
850
- "success": False,
851
- "error": str(e),
852
- "hint": "Check that src/app/page.tsx exists and is writable",
853
- }
854
-
855
- @tool
856
- def setup_nextjs_testing(
857
- project_dir: str,
858
- resource_name: Optional[str] = None,
859
- ) -> Dict[str, Any]:
860
- """Set up Vitest testing infrastructure for a Next.js project.
861
-
862
- Installs testing dependencies and creates configuration files:
863
- - Vitest + React Testing Library
864
- - vitest.config.ts with proper aliases and jsdom environment
865
- - tests/setup.ts with common mocks (next/navigation, Prisma)
866
- - Updates package.json with test scripts
867
-
868
- Should be called after the project is initialized but before running tests.
869
-
870
- Args:
871
- project_dir: Path to the Next.js project directory
872
- resource_name: Optional resource name to customize Prisma mocks
873
-
874
- Returns:
875
- Dictionary with success status and created files
876
- """
877
- from gaia.agents.code.prompts.code_patterns import TEST_SETUP, VITEST_CONFIG
878
-
879
- try:
880
- project_path = Path(project_dir)
881
- if not project_path.exists():
882
- return {
883
- "success": False,
884
- "error": f"Project directory does not exist: {project_dir}",
885
- }
886
-
887
- created_files = []
888
- resource = resource_name.lower() if resource_name else "todo"
889
-
890
- # Install testing dependencies
891
- install_cmd = (
892
- "npm install -D vitest @vitejs/plugin-react jsdom "
893
- "@testing-library/react @testing-library/jest-dom @testing-library/user-event "
894
- "@types/node"
895
- )
896
- install_result = subprocess.run(
897
- install_cmd,
898
- shell=True,
899
- cwd=project_dir,
900
- capture_output=True,
901
- text=True,
902
- timeout=1200,
903
- check=False,
904
- )
905
-
906
- if install_result.returncode != 0:
907
- return {
908
- "success": False,
909
- "error": "Failed to install testing dependencies",
910
- "details": install_result.stderr,
911
- "hint": "Check npm configuration and network connectivity",
912
- }
913
-
914
- # Create vitest.config.ts
915
- vitest_config_path = project_path / "vitest.config.ts"
916
- vitest_config_path.write_text(VITEST_CONFIG, encoding="utf-8")
917
- created_files.append(str(vitest_config_path))
918
-
919
- # Create tests/setup.ts with resource-specific Prisma mocks
920
- tests_dir = project_path / "tests"
921
- tests_dir.mkdir(exist_ok=True)
922
-
923
- setup_content = TEST_SETUP.format(resource=resource)
924
- setup_path = tests_dir / "setup.ts"
925
- setup_path.write_text(setup_content, encoding="utf-8")
926
- created_files.append(str(setup_path))
927
-
928
- # Update package.json to add test scripts
929
- package_json_path = project_path / "package.json"
930
- if package_json_path.exists():
931
- import json
932
-
933
- package_data = json.loads(
934
- package_json_path.read_text(encoding="utf-8")
935
- )
936
-
937
- if "scripts" not in package_data:
938
- package_data["scripts"] = {}
939
-
940
- # Add test scripts if not present
941
- if "test" not in package_data["scripts"]:
942
- package_data["scripts"]["test"] = "vitest run"
943
- if "test:watch" not in package_data["scripts"]:
944
- package_data["scripts"]["test:watch"] = "vitest"
945
- if "test:coverage" not in package_data["scripts"]:
946
- package_data["scripts"][
947
- "test:coverage"
948
- ] = "vitest run --coverage"
949
-
950
- package_json_path.write_text(
951
- json.dumps(package_data, indent=2) + "\n", encoding="utf-8"
952
- )
953
- created_files.append(str(package_json_path))
954
-
955
- logger.info(f"Set up Vitest testing infrastructure in {project_dir}")
956
-
957
- return {
958
- "success": True,
959
- "message": "Testing infrastructure configured successfully",
960
- "files": created_files,
961
- "dependencies_installed": [
962
- "vitest",
963
- "@vitejs/plugin-react",
964
- "jsdom",
965
- "@testing-library/react",
966
- "@testing-library/jest-dom",
967
- "@testing-library/user-event",
968
- ],
969
- "scripts_added": {
970
- "test": "vitest run",
971
- "test:watch": "vitest",
972
- "test:coverage": "vitest run --coverage",
973
- },
974
- }
975
-
976
- except subprocess.TimeoutExpired:
977
- return {
978
- "success": False,
979
- "error": "npm install timed out",
980
- "hint": "Check network connectivity and try again",
981
- }
982
- except Exception as e:
983
- logger.error(f"Error setting up testing: {e}")
984
- return {
985
- "success": False,
986
- "error": str(e),
987
- "hint": "Check project structure and npm configuration",
988
- }
989
-
990
- @tool
991
- def validate_crud_completeness(
992
- project_dir: str, resource_name: str
993
- ) -> Dict[str, Any]:
994
- """Validate that all necessary CRUD files exist for a resource.
995
-
996
- Checks for the presence of all required files for a complete CRUD application:
997
- - API routes (collection and item endpoints)
998
- - Pages (list, new, detail/edit)
999
- - Components (form)
1000
- - Database model
1001
-
1002
- Args:
1003
- project_dir: Path to the project directory
1004
- resource_name: Resource name to validate (e.g., "todo", "user")
1005
-
1006
- Returns:
1007
- Dictionary with validation results and lists of existing/missing files
1008
- """
1009
- try:
1010
- project_path = Path(project_dir)
1011
- if not project_path.exists():
1012
- return {
1013
- "success": False,
1014
- "error": f"Project directory does not exist: {project_dir}",
1015
- }
1016
-
1017
- resource = resource_name.lower()
1018
- Resource = resource_name.capitalize()
1019
- resource_plural = pluralize(resource)
1020
-
1021
- # Define expected files
1022
- expected_files = {
1023
- "api_routes": {
1024
- f"src/app/api/{resource_plural}/route.ts": "Collection API route (GET list, POST create)",
1025
- f"src/app/api/{resource_plural}/[id]/route.ts": "Item API route (GET single, PATCH update, DELETE)",
1026
- },
1027
- "pages": {
1028
- f"src/app/{resource_plural}/page.tsx": "List page showing all items",
1029
- f"src/app/{resource_plural}/new/page.tsx": "Create new item page",
1030
- f"src/app/{resource_plural}/[id]/page.tsx": "View/edit single item page",
1031
- },
1032
- "components": {
1033
- f"src/components/{Resource}Form.tsx": "Reusable form component for create/edit"
1034
- },
1035
- }
1036
-
1037
- # Check which files exist
1038
- missing_files = {}
1039
- existing_files = {}
1040
-
1041
- for category, files in expected_files.items():
1042
- missing_files[category] = []
1043
- existing_files[category] = []
1044
-
1045
- for file_path, description in files.items():
1046
- full_path = project_path / file_path
1047
- if full_path.exists():
1048
- existing_files[category].append(
1049
- {"path": file_path, "description": description}
1050
- )
1051
- else:
1052
- missing_files[category].append(
1053
- {"path": file_path, "description": description}
1054
- )
1055
-
1056
- # Check if Prisma model exists
1057
- schema_file = project_path / "prisma" / "schema.prisma"
1058
- model_exists = False
1059
- if schema_file.exists():
1060
- schema_content = schema_file.read_text()
1061
- model_exists = f"model {Resource}" in schema_content
1062
-
1063
- # Calculate completeness
1064
- total_files = sum(len(files) for files in expected_files.values())
1065
- existing_count = sum(len(files) for files in existing_files.values())
1066
- missing_count = sum(len(files) for files in missing_files.values())
1067
-
1068
- all_complete = missing_count == 0 and model_exists
1069
-
1070
- logger.info(
1071
- f"CRUD completeness check for {resource}: {existing_count}/{total_files} files exist"
1072
- )
1073
-
1074
- return {
1075
- "success": True,
1076
- "complete": all_complete,
1077
- "resource": resource,
1078
- "model_exists": model_exists,
1079
- "existing_files": existing_files,
1080
- "missing_files": missing_files,
1081
- "stats": {
1082
- "total": total_files,
1083
- "existing": existing_count,
1084
- "missing": missing_count,
1085
- },
1086
- }
1087
-
1088
- except Exception as e:
1089
- logger.error(f"Error validating CRUD completeness: {e}")
1090
- return {
1091
- "success": False,
1092
- "error": str(e),
1093
- "error_type": "validation_error",
1094
- }
1095
-
1096
- @tool
1097
- def generate_crud_scaffold(
1098
- project_dir: str, resource_name: str, fields: Dict[str, str]
1099
- ) -> Dict[str, Any]:
1100
- """Generate a complete CRUD scaffold with all necessary files.
1101
-
1102
- This high-level tool orchestrates multiple operations to create
1103
- a fully functional CRUD application for a resource. It generates:
1104
- - API routes for all CRUD operations
1105
- - List page to view all items
1106
- - Form component for create/edit
1107
- - Create page (new item)
1108
- - Detail/edit page (single item with delete)
1109
-
1110
- Args:
1111
- project_dir: Path to the project directory
1112
- resource_name: Resource name (e.g., "todo", "product")
1113
- fields: Dictionary of field names to types
1114
-
1115
- Returns:
1116
- Dictionary with generation results and validation status
1117
- """
1118
- try:
1119
- results = {
1120
- "api_routes": [],
1121
- "pages": [],
1122
- "components": [],
1123
- "errors": [],
1124
- }
1125
-
1126
- logger.info(f"Generating complete CRUD scaffold for {resource_name}...")
1127
-
1128
- # 1. Generate API endpoints (all CRUD operations)
1129
- logger.info(" → Generating API routes...")
1130
- api_result = manage_api_endpoint(
1131
- project_dir=project_dir,
1132
- resource_name=resource_name,
1133
- operations=["GET", "POST", "PATCH", "DELETE"],
1134
- fields=fields,
1135
- enable_pagination=True,
1136
- )
1137
- if api_result.get("success"):
1138
- results["api_routes"].extend(api_result.get("files", []))
1139
- else:
1140
- results["errors"].append(
1141
- f"API generation failed: {api_result.get('error')}"
1142
- )
1143
-
1144
- # 2. Generate list page (server component)
1145
- logger.info(" → Generating list page...")
1146
- list_result = manage_react_component(
1147
- project_dir=project_dir,
1148
- component_name=f"{resource_name.capitalize()}List",
1149
- component_type="server",
1150
- resource_name=resource_name,
1151
- fields=fields,
1152
- variant="list",
1153
- )
1154
- if list_result.get("success"):
1155
- results["pages"].append(list_result.get("file_path"))
1156
- else:
1157
- results["errors"].append(
1158
- f"List page generation failed: {list_result.get('error')}"
1159
- )
1160
-
1161
- # 3. Generate form component (reusable for create/edit)
1162
- logger.info(" → Generating form component...")
1163
- form_result = manage_react_component(
1164
- project_dir=project_dir,
1165
- component_name=f"{resource_name.capitalize()}Form",
1166
- component_type="client",
1167
- resource_name=resource_name,
1168
- fields=fields,
1169
- variant="form",
1170
- )
1171
- if form_result.get("success"):
1172
- results["components"].append(form_result.get("file_path"))
1173
- else:
1174
- results["errors"].append(
1175
- f"Form component generation failed: {form_result.get('error')}"
1176
- )
1177
-
1178
- # 4. Generate new page (create page)
1179
- logger.info(" → Generating create (new) page...")
1180
- new_result = manage_react_component(
1181
- project_dir=project_dir,
1182
- component_name=f"New{resource_name.capitalize()}Page",
1183
- component_type="client",
1184
- resource_name=resource_name,
1185
- fields=fields,
1186
- variant="new",
1187
- )
1188
- if new_result.get("success"):
1189
- results["pages"].append(new_result.get("file_path"))
1190
- else:
1191
- results["errors"].append(
1192
- f"New page generation failed: {new_result.get('error')}"
1193
- )
1194
-
1195
- # 5. Generate detail page (view/edit page with delete)
1196
- logger.info(" → Generating detail/edit page...")
1197
- detail_result = manage_react_component(
1198
- project_dir=project_dir,
1199
- component_name=f"{resource_name.capitalize()}DetailPage",
1200
- component_type="client",
1201
- resource_name=resource_name,
1202
- fields=fields,
1203
- variant="detail",
1204
- )
1205
- if detail_result.get("success"):
1206
- results["pages"].append(detail_result.get("file_path"))
1207
- else:
1208
- results["errors"].append(
1209
- f"Detail page generation failed: {detail_result.get('error')}"
1210
- )
1211
-
1212
- # 6. Generate actions component (edit/delete buttons)
1213
- logger.info(" → Generating actions component...")
1214
- actions_result = manage_react_component(
1215
- project_dir=project_dir,
1216
- component_name=f"{resource_name.capitalize()}Actions",
1217
- component_type="client",
1218
- resource_name=resource_name,
1219
- fields=fields,
1220
- variant="actions",
1221
- )
1222
- if actions_result.get("success"):
1223
- results["components"].append(actions_result.get("file_path"))
1224
- else:
1225
- results["errors"].append(
1226
- f"Actions component generation failed: {actions_result.get('error')}"
1227
- )
1228
-
1229
- # 7. Validate completeness
1230
- logger.info(" → Validating completeness...")
1231
- validation = validate_crud_completeness(project_dir, resource_name)
1232
-
1233
- success = len(results["errors"]) == 0
1234
- logger.info(
1235
- f"CRUD scaffold generation {'succeeded' if success else 'completed with errors'}"
1236
- )
1237
-
1238
- return {
1239
- "success": success,
1240
- "resource": resource_name,
1241
- "generated": results,
1242
- "validation": validation,
1243
- }
1244
-
1245
- except Exception as e:
1246
- logger.error(f"Error generating CRUD scaffold: {e}")
1247
- return {
1248
- "success": False,
1249
- "error": str(e),
1250
- "error_type": "scaffold_generation_error",
1251
- }
1252
-
1253
- @tool
1254
- def manage_data_model(
1255
- project_dir: str,
1256
- model_name: str,
1257
- fields: Dict[str, str],
1258
- relationships: Optional[List[Dict[str, str]]] = None,
1259
- ) -> Dict[str, Any]:
1260
- """Manage database models with Prisma ORM.
1261
-
1262
- Creates or updates Prisma model definitions. Works for ANY model type.
1263
-
1264
- Args:
1265
- project_dir: Path to the project directory
1266
- model_name: Model name (singular, PascalCase, e.g., "User", "Product")
1267
- fields: Dictionary of field names to types
1268
- Supported: "string", "text", "number", "float", "boolean",
1269
- "date", "datetime", "timestamp", "email", "url"
1270
- relationships: Optional list of relationships
1271
- [{"type": "hasMany", "model": "Post"}]
1272
-
1273
- Returns:
1274
- Dictionary with success status and schema file path
1275
- """
1276
- try:
1277
- project_path = Path(project_dir)
1278
- if not project_path.exists():
1279
- return {
1280
- "success": False,
1281
- "error": f"Project directory does not exist: {project_dir}",
1282
- }
1283
-
1284
- schema_file = project_path / "prisma" / "schema.prisma"
1285
-
1286
- if not schema_file.exists():
1287
- return {
1288
- "success": False,
1289
- "error": "schema.prisma not found. Initialize Prisma first.",
1290
- }
1291
-
1292
- # Read existing schema
1293
- schema_content = schema_file.read_text()
1294
-
1295
- # Validate schema doesn't have forbidden output field in generator block
1296
- if "output" in schema_content:
1297
- # Check if it's in generator client block specifically
1298
- generator_match = re.search(
1299
- r"generator\s+client\s*\{[^}]*output[^}]*\}",
1300
- schema_content,
1301
- re.DOTALL,
1302
- )
1303
- if generator_match:
1304
- # Auto-fix: remove the output line
1305
- fixed_content = re.sub(
1306
- r'\n\s*output\s*=\s*"[^"]*"', "", schema_content
1307
- )
1308
- schema_file.write_text(fixed_content, encoding="utf-8")
1309
- schema_content = fixed_content
1310
- logger.warning(
1311
- "Removed invalid 'output' field from generator client block"
1312
- )
1313
-
1314
- # Generate field definitions
1315
- field_lines = []
1316
- field_lines.append(" id Int @id @default(autoincrement())")
1317
-
1318
- # Map types to Prisma types
1319
- type_mapping = {
1320
- "string": "String",
1321
- "text": "String",
1322
- "number": "Int",
1323
- "float": "Float",
1324
- "boolean": "Boolean",
1325
- "date": "DateTime",
1326
- "datetime": "DateTime",
1327
- "timestamp": "DateTime",
1328
- "email": "String",
1329
- "url": "String",
1330
- }
1331
-
1332
- # Define reserved fields that are auto-generated
1333
- reserved_fields = {"id", "createdat", "updatedat"}
1334
-
1335
- # Build field lines from user input (skip reserved fields)
1336
- for field_name, field_type in fields.items():
1337
- if field_name.lower() in reserved_fields:
1338
- logger.warning(
1339
- f"Skipping reserved field '{field_name}' - auto-generated by Prisma"
1340
- )
1341
- continue
1342
- prisma_type = type_mapping.get(field_type.lower(), "String")
1343
- field_lines.append(f" {field_name:<12} {prisma_type}")
1344
-
1345
- # Add relationships if provided
1346
- if relationships:
1347
- for rel in relationships:
1348
- rel_type = rel.get("type", "hasMany")
1349
- rel_model = rel.get("model")
1350
- if rel_type == "hasMany":
1351
- field_lines.append(
1352
- f" {rel_model.lower()}s {rel_model}[]"
1353
- )
1354
- elif rel_type == "hasOne":
1355
- field_lines.append(
1356
- f" {rel_model.lower()} {rel_model}?"
1357
- )
1358
-
1359
- # Always add timestamps - they're standard for Prisma and our templates expect them
1360
- # Note: Reserved fields (including createdAt/updatedAt) are already skipped
1361
- # from user input above, so there's no risk of duplication
1362
- field_lines.append(" createdAt DateTime @default(now())")
1363
- field_lines.append(" updatedAt DateTime @updatedAt")
1364
-
1365
- # Check if model already exists in schema
1366
- model_pattern = rf"model\s+{model_name}\s*\{{"
1367
- if re.search(model_pattern, schema_content):
1368
- return {
1369
- "success": False,
1370
- "error": f"Model '{model_name}' already exists in schema",
1371
- "error_type": "duplicate_model",
1372
- "hint": "Use a different model name or edit the existing model",
1373
- "suggested_fix": f"Read schema.prisma to see existing {model_name} definition",
1374
- }
1375
-
1376
- # Generate model definition
1377
- model_definition = f"""
1378
-
1379
- model {model_name} {{
1380
- {chr(10).join(field_lines)}
1381
- }}
1382
- """
1383
-
1384
- # Save original schema for rollback
1385
- original_schema = schema_content
1386
-
1387
- # Append to schema
1388
- schema_content += model_definition
1389
-
1390
- # Write new schema
1391
- schema_file.write_text(schema_content, encoding="utf-8")
1392
-
1393
- # Validate with prisma format
1394
- validate_result = subprocess.run(
1395
- f'npx prisma format --schema="{schema_file}"',
1396
- cwd=str(project_path),
1397
- shell=True,
1398
- capture_output=True,
1399
- text=True,
1400
- encoding="utf-8",
1401
- errors="replace",
1402
- timeout=600,
1403
- check=False,
1404
- )
1405
-
1406
- if validate_result.returncode != 0:
1407
- # Rollback to original schema
1408
- schema_file.write_text(original_schema, encoding="utf-8")
1409
- return {
1410
- "success": False,
1411
- "error": f"Schema validation failed: {validate_result.stderr}",
1412
- "error_type": "schema_validation_error",
1413
- "hint": "The schema changes caused validation errors",
1414
- "suggested_fix": "Check field types and model syntax",
1415
- }
1416
-
1417
- result = {"success": True}
1418
-
1419
- logger.info(f"Added Prisma model: {model_name}")
1420
-
1421
- # Auto-generate Prisma client types
1422
- prisma_generated = False
1423
- generation_note = ""
1424
-
1425
- try:
1426
- # Format schema first
1427
- subprocess.run(
1428
- f'npx prisma format --schema="{schema_file}"',
1429
- cwd=str(project_path),
1430
- shell=True,
1431
- capture_output=True,
1432
- text=True,
1433
- encoding="utf-8",
1434
- errors="replace",
1435
- timeout=600,
1436
- check=False,
1437
- )
1438
-
1439
- # Generate Prisma client types
1440
- generate_result = subprocess.run(
1441
- f'npx prisma generate --schema="{schema_file}"',
1442
- cwd=str(project_path),
1443
- shell=True,
1444
- capture_output=True,
1445
- text=True,
1446
- encoding="utf-8",
1447
- errors="replace",
1448
- timeout=1200,
1449
- check=False,
1450
- )
1451
-
1452
- if generate_result.returncode != 0:
1453
- stderr = generate_result.stderr
1454
- logger.error(f"prisma generate failed: {stderr}")
1455
- return {
1456
- "success": False,
1457
- "error": f"prisma generate failed: {stderr}",
1458
- "schema_file": str(schema_file),
1459
- "fix_hint": "Check schema.prisma for syntax errors",
1460
- }
1461
-
1462
- # Verify Prisma client was actually generated
1463
- client_index = (
1464
- project_path
1465
- / "node_modules"
1466
- / ".prisma"
1467
- / "client"
1468
- / "index.js"
1469
- )
1470
- if not client_index.exists():
1471
- logger.error("Prisma client not generated despite no errors")
1472
- return {
1473
- "success": False,
1474
- "error": "Prisma client not generated despite no errors",
1475
- "fix_hint": "Run 'npm install' then 'npx prisma generate'",
1476
- }
1477
-
1478
- prisma_generated = True
1479
- generation_note = (
1480
- "Schema updated and Prisma client generated successfully"
1481
- )
1482
- logger.info(generation_note)
1483
-
1484
- # Push schema changes to database
1485
- logger.info(f"Running prisma db push in {project_dir}")
1486
- db_push_result = subprocess.run(
1487
- "npx prisma db push",
1488
- cwd=str(project_path),
1489
- shell=True,
1490
- capture_output=True,
1491
- text=True,
1492
- encoding="utf-8",
1493
- errors="replace",
1494
- timeout=1200,
1495
- check=False,
1496
- )
1497
-
1498
- if db_push_result.returncode != 0:
1499
- logger.error(f"prisma db push failed: {db_push_result.stderr}")
1500
- return {
1501
- "success": False,
1502
- "error": f"prisma db push failed: {db_push_result.stderr}",
1503
- "fix_hint": "Check DATABASE_URL in .env file",
1504
- "generated": True, # Client was generated successfully
1505
- "pushed": False,
1506
- }
1507
-
1508
- generation_note = "Schema updated, Prisma client generated, and database pushed successfully"
1509
- logger.info(generation_note)
1510
-
1511
- except Exception as e:
1512
- # Prisma generation failed - block the operation
1513
- logger.error(f"Could not generate Prisma client: {e}")
1514
- return {
1515
- "success": False,
1516
- "error": f"Could not generate Prisma client: {e}",
1517
- "fix_hint": "Ensure prisma is installed (npm install)",
1518
- }
1519
-
1520
- return {
1521
- "success": result.get("success", True),
1522
- "model_name": model_name,
1523
- "schema_file": str(schema_file),
1524
- "schema_updated": True,
1525
- "prisma_generated": prisma_generated,
1526
- "note": generation_note,
1527
- }
1528
-
1529
- except Exception as e:
1530
- logger.error(f"Error managing data model: {e}")
1531
- return {
1532
- "success": False,
1533
- "error": str(e),
1534
- "error_type": "data_model_error",
1535
- }
1536
-
1537
- @tool
1538
- def manage_prisma_client(project_dir: str) -> Dict[str, Any]:
1539
- """Manage Prisma client generation and database sync.
1540
-
1541
- Generates the Prisma client and pushes schema changes to the database.
1542
-
1543
- Args:
1544
- project_dir: Path to the project directory
1545
-
1546
- Returns:
1547
- Dictionary with success status and commands to run
1548
- """
1549
- try:
1550
- project_path = Path(project_dir)
1551
- if not project_path.exists():
1552
- return {
1553
- "success": False,
1554
- "error": f"Project directory does not exist: {project_dir}",
1555
- }
1556
-
1557
- # Check if Prisma is configured
1558
- schema_file = project_path / "prisma" / "schema.prisma"
1559
- if not schema_file.exists():
1560
- return {
1561
- "success": False,
1562
- "error": "Prisma not initialized. schema.prisma not found.",
1563
- }
1564
-
1565
- # Provide guidance for Prisma operations
1566
- commands = [
1567
- "npm run db:generate # Generate Prisma Client",
1568
- "npm run db:push # Push schema to database",
1569
- "npm run db:studio # Open Prisma Studio (optional)",
1570
- ]
1571
-
1572
- logger.info("Prisma client management guidance provided")
1573
-
1574
- return {
1575
- "success": True,
1576
- "commands": commands,
1577
- "working_dir": str(project_path),
1578
- }
1579
-
1580
- except Exception as e:
1581
- logger.error(f"Error managing Prisma client: {e}")
1582
- return {
1583
- "success": False,
1584
- "error": str(e),
1585
- "error_type": "prisma_client_error",
1586
- }
1587
-
1588
- @tool
1589
- def manage_web_config(
1590
- project_dir: str, config_type: str, updates: Dict[str, Any]
1591
- ) -> Dict[str, Any]:
1592
- """Manage web application configuration files.
1593
-
1594
- Updates configuration files like .env, next.config.js, etc.
1595
- Delegates actual file operations to file_io.
1596
-
1597
- Args:
1598
- project_dir: Path to the project directory
1599
- config_type: Type of config ("env", "nextjs", "tailwind")
1600
- updates: Dictionary of configuration updates
1601
-
1602
- Returns:
1603
- Dictionary with success status
1604
- """
1605
- try:
1606
- project_path = Path(project_dir)
1607
- if not project_path.exists():
1608
- return {
1609
- "success": False,
1610
- "error": f"Project directory does not exist: {project_dir}",
1611
- }
1612
-
1613
- if config_type == "env":
1614
- env_file = project_path / ".env"
1615
- if not env_file.exists():
1616
- # Create new .env file
1617
- content = "\n".join(f"{k}={v}" for k, v in updates.items())
1618
- else:
1619
- # Update existing
1620
- content = env_file.read_text()
1621
- for key, value in updates.items():
1622
- if f"{key}=" in content:
1623
- lines = content.split("\n")
1624
- content = "\n".join(
1625
- (
1626
- f"{key}={value}"
1627
- if line.startswith(f"{key}=")
1628
- else line
1629
- )
1630
- for line in lines
1631
- )
1632
- else:
1633
- content += f"\n{key}={value}"
1634
-
1635
- env_file.write_text(content, encoding="utf-8")
1636
-
1637
- return {
1638
- "success": True,
1639
- "config_type": config_type,
1640
- "file": str(env_file),
1641
- "updates": updates,
1642
- }
1643
- else:
1644
- return {
1645
- "success": True,
1646
- "config_type": config_type,
1647
- "updates": updates,
1648
- }
1649
-
1650
- except Exception as e:
1651
- logger.error(f"Error managing config: {e}")
1652
- return {
1653
- "success": False,
1654
- "error": str(e),
1655
- "error_type": "config_error",
1656
- }
1657
-
1658
- @tool
1659
- def generate_style_tests(
1660
- project_dir: str, resource_name: str = "Item"
1661
- ) -> Dict[str, Any]:
1662
- """Generate CSS and styling tests for the project.
1663
-
1664
- Creates test files that validate:
1665
- 1. CSS file integrity (no TypeScript in CSS - Issue #1002)
1666
- 2. Tailwind directive presence
1667
- 3. Design system class definitions
1668
- 4. Layout imports globals.css
1669
- 5. App router structure
1670
-
1671
- Tests are placed in the project's /tests directory.
1672
-
1673
- Args:
1674
- project_dir: Path to the Next.js project directory
1675
- resource_name: Resource name for component styling tests
1676
-
1677
- Returns:
1678
- Dictionary with success status and generated files
1679
- """
1680
- from gaia.agents.code.prompts.code_patterns import (
1681
- generate_routes_test_content,
1682
- generate_style_test_content,
1683
- )
1684
-
1685
- try:
1686
- project_path = Path(project_dir)
1687
- tests_dir = project_path / "tests"
1688
- styling_dir = tests_dir / "styling"
1689
-
1690
- # Ensure directories exist
1691
- tests_dir.mkdir(parents=True, exist_ok=True)
1692
- styling_dir.mkdir(parents=True, exist_ok=True)
1693
-
1694
- files_created = []
1695
-
1696
- # 1. Generate styles.test.ts (main CSS integrity test)
1697
- styles_test_path = tests_dir / "styles.test.ts"
1698
- styles_content = generate_style_test_content(resource_name)
1699
-
1700
- if hasattr(self, "write_file"):
1701
- result = self.write_file(str(styles_test_path), styles_content)
1702
- if result.get("success"):
1703
- files_created.append(str(styles_test_path))
1704
- else:
1705
- styles_test_path.write_text(styles_content)
1706
- files_created.append(str(styles_test_path))
1707
-
1708
- # 2. Generate routes.test.ts (app router structure test)
1709
- routes_test_path = styling_dir / "routes.test.ts"
1710
- routes_content = generate_routes_test_content(resource_name)
1711
-
1712
- if hasattr(self, "write_file"):
1713
- result = self.write_file(str(routes_test_path), routes_content)
1714
- if result.get("success"):
1715
- files_created.append(str(routes_test_path))
1716
- else:
1717
- routes_test_path.write_text(routes_content)
1718
- files_created.append(str(routes_test_path))
1719
-
1720
- # 3. Install glob package if not present (needed for tests)
1721
- package_json = project_path / "package.json"
1722
- if package_json.exists():
1723
- pkg_content = package_json.read_text()
1724
- if '"glob"' not in pkg_content:
1725
- try:
1726
- subprocess.run(
1727
- ["npm", "install", "--save-dev", "glob", "@types/glob"],
1728
- cwd=str(project_path),
1729
- capture_output=True,
1730
- text=True,
1731
- timeout=600,
1732
- check=False,
1733
- )
1734
- logger.info("Installed glob package for style tests")
1735
- except Exception as e:
1736
- logger.warning(f"Could not install glob package: {e}")
1737
-
1738
- logger.info(
1739
- f"Generated {len(files_created)} style test files for {resource_name}"
1740
- )
1741
-
1742
- return {
1743
- "success": True,
1744
- "files": files_created,
1745
- "message": f"Generated style tests for {resource_name}",
1746
- "tests_description": [
1747
- "styles.test.ts - CSS integrity (TypeScript detection, Tailwind, braces)",
1748
- "styling/routes.test.ts - App router structure and styling consistency",
1749
- ],
1750
- }
1751
-
1752
- except Exception as e:
1753
- logger.error(f"Error generating style tests: {e}")
1754
- return {
1755
- "success": False,
1756
- "error": str(e),
1757
- "error_type": "test_generation_error",
1758
- }
1
+ # Copyright(C) 2025-2026 Advanced Micro Devices, Inc. All rights reserved.
2
+ # SPDX-License-Identifier: MIT
3
+ """Generic web development tools for Code Agent.
4
+
5
+ This mixin provides flexible, framework-agnostic tools for web development:
6
+ - API endpoint generation with actual Prisma queries
7
+ - React component generation (server and client)
8
+ - Database schema management
9
+ - Configuration updates
10
+
11
+ Tools use the manage_* prefix to indicate they handle both creation and modification.
12
+ All file I/O operations are delegated to FileIOToolsMixin for clean separation of concerns.
13
+ """
14
+
15
+ import logging
16
+ import re
17
+ import subprocess
18
+ from pathlib import Path
19
+ from typing import Any, Dict, List, Optional
20
+
21
+ from gaia.agents.base.tools import tool
22
+ from gaia.agents.code.prompts.code_patterns import (
23
+ API_ROUTE_DYNAMIC_DELETE,
24
+ API_ROUTE_DYNAMIC_GET,
25
+ API_ROUTE_DYNAMIC_PATCH,
26
+ API_ROUTE_GET,
27
+ API_ROUTE_GET_PAGINATED,
28
+ API_ROUTE_POST,
29
+ APP_GLOBALS_CSS,
30
+ APP_LAYOUT,
31
+ CLIENT_COMPONENT_FORM,
32
+ CLIENT_COMPONENT_TIMER,
33
+ COMPONENT_TEST_ACTIONS,
34
+ COMPONENT_TEST_FORM,
35
+ LANDING_PAGE_WITH_LINKS,
36
+ SERVER_COMPONENT_LIST,
37
+ generate_actions_component,
38
+ generate_api_imports,
39
+ generate_detail_page,
40
+ generate_field_display,
41
+ generate_form_field,
42
+ generate_form_field_assertions,
43
+ generate_form_fill_actions,
44
+ generate_new_page,
45
+ generate_test_data_fields,
46
+ generate_zod_schema,
47
+ pluralize,
48
+ )
49
+
50
+ logger = logging.getLogger(__name__)
51
+
52
+
53
+ def read_prisma_model(project_dir: str, model_name: str) -> Dict[str, Any]:
54
+ """Read model definition from Prisma schema.
55
+
56
+ Parses the Prisma schema file to extract field definitions and metadata
57
+ for a specific model. This allows tools to adapt to the actual schema
58
+ instead of relying on hardcoded assumptions.
59
+
60
+ Args:
61
+ project_dir: Path to the project directory
62
+ model_name: Name of the model to read (case-insensitive)
63
+
64
+ Returns:
65
+ Dictionary with:
66
+ success: Whether the model was found
67
+ model_name: The model name (as defined in schema)
68
+ fields: Dict of field names to types
69
+ has_timestamps: Whether model has createdAt/updatedAt
70
+ error: Error message if failed
71
+ """
72
+ schema_path = Path(project_dir) / "prisma" / "schema.prisma"
73
+ if not schema_path.exists():
74
+ return {
75
+ "success": False,
76
+ "error": "Schema file not found at prisma/schema.prisma",
77
+ }
78
+
79
+ try:
80
+ content = schema_path.read_text()
81
+ model_pattern = rf"model\s+{model_name}\s*\{{([^}}]+)\}}"
82
+ match = re.search(model_pattern, content, re.DOTALL | re.IGNORECASE)
83
+
84
+ if not match:
85
+ return {
86
+ "success": False,
87
+ "error": f"Model {model_name} not found in schema",
88
+ }
89
+
90
+ # Parse fields from model body
91
+ fields = {}
92
+ for line in match.group(1).strip().split("\n"):
93
+ line = line.strip()
94
+ if not line or line.startswith("//") or line.startswith("@@"):
95
+ continue
96
+ # Skip field decorators like @id, @default, etc.
97
+ if line.startswith("@") and not line.startswith("@@"):
98
+ continue
99
+ parts = line.split()
100
+ if len(parts) >= 2:
101
+ field_name = parts[0]
102
+ field_type = parts[1].rstrip("?[]") # Remove optional/array markers
103
+ fields[field_name] = field_type
104
+
105
+ return {
106
+ "success": True,
107
+ "model_name": model_name,
108
+ "fields": fields,
109
+ "has_timestamps": "createdAt" in fields and "updatedAt" in fields,
110
+ }
111
+ except Exception as e:
112
+ return {"success": False, "error": f"Failed to parse schema: {str(e)}"}
113
+
114
+
115
+ class WebToolsMixin:
116
+ """Mixin providing generic web development tools for the Code Agent.
117
+
118
+ Tools are designed to be framework-agnostic where possible, with
119
+ framework-specific logic in prompts rather than hardcoded in tools.
120
+
121
+ All tools delegate file operations to FileIOToolsMixin to maintain
122
+ clean separation of concerns.
123
+ """
124
+
125
+ def register_web_tools(self) -> None:
126
+ """Register generic web development tools with the agent."""
127
+
128
+ @tool
129
+ def setup_app_styling(
130
+ project_dir: str,
131
+ app_title: str = "My App",
132
+ app_description: str = "A modern web application",
133
+ ) -> Dict[str, Any]:
134
+ """Set up app-wide styling with modern design system.
135
+
136
+ Creates/updates the root layout and globals.css with a modern dark theme
137
+ that all pages will inherit. This should be run early in the project
138
+ setup, after create-next-app.
139
+
140
+ The design system includes:
141
+ - Dark gradient background at the layout level
142
+ - Glass morphism card effects (.glass-card)
143
+ - Modern button variants (.btn-primary, .btn-secondary, .btn-danger)
144
+ - Input field styling (.input-field)
145
+ - Custom checkbox styling (.checkbox-modern)
146
+ - Gradient text for titles (.page-title)
147
+ - Back link styling (.link-back)
148
+ - Custom scrollbar styling
149
+
150
+ Args:
151
+ project_dir: Path to the Next.js project directory
152
+ app_title: Application title for metadata
153
+ app_description: Application description for metadata
154
+
155
+ Returns:
156
+ Dictionary with success status and created/updated files
157
+ """
158
+ try:
159
+ project_path = Path(project_dir)
160
+ app_dir = project_path / "src" / "app"
161
+
162
+ if not app_dir.exists():
163
+ return {
164
+ "success": False,
165
+ "error": f"App directory not found: {app_dir}",
166
+ "hint": "Run create-next-app first to initialize the project",
167
+ }
168
+
169
+ files_created = []
170
+
171
+ # Generate layout.tsx
172
+ layout_path = app_dir / "layout.tsx"
173
+ layout_content = APP_LAYOUT.format(
174
+ app_title=app_title,
175
+ app_description=app_description,
176
+ )
177
+
178
+ # Write layout file using FileIOToolsMixin
179
+ if hasattr(self, "write_file"):
180
+ result = self.write_file(str(layout_path), layout_content)
181
+ if result.get("success"):
182
+ files_created.append(str(layout_path))
183
+ else:
184
+ layout_path.write_text(layout_content)
185
+ files_created.append(str(layout_path))
186
+
187
+ # Generate globals.css
188
+ globals_path = app_dir / "globals.css"
189
+ globals_content = APP_GLOBALS_CSS
190
+
191
+ # Write globals file
192
+ if hasattr(self, "write_file"):
193
+ result = self.write_file(str(globals_path), globals_content)
194
+ if result.get("success"):
195
+ files_created.append(str(globals_path))
196
+ else:
197
+ globals_path.write_text(globals_content)
198
+ files_created.append(str(globals_path))
199
+
200
+ logger.info(f"Set up app-wide styling: {files_created}")
201
+ return {
202
+ "success": True,
203
+ "message": "App-wide styling configured successfully",
204
+ "files": files_created,
205
+ "design_system": [
206
+ ".glass-card - Glass morphism card effect",
207
+ ".btn-primary - Primary gradient button",
208
+ ".btn-secondary - Secondary button",
209
+ ".btn-danger - Danger/delete button",
210
+ ".input-field - Styled form input",
211
+ ".checkbox-modern - Modern checkbox styling",
212
+ ".page-title - Gradient title text",
213
+ ".link-back - Back navigation link",
214
+ ],
215
+ }
216
+ except Exception as e:
217
+ logger.exception("Failed to set up app styling")
218
+ return {"success": False, "error": str(e)}
219
+
220
+ @tool
221
+ def manage_api_endpoint(
222
+ project_dir: str,
223
+ resource_name: str,
224
+ operations: List[str] = None,
225
+ fields: Optional[Dict[str, str]] = None,
226
+ enable_pagination: bool = False,
227
+ ) -> Dict[str, Any]:
228
+ """Manage API endpoints with actual Prisma operations.
229
+
230
+ Creates or updates API routes with functional CRUD operations,
231
+ validation, and error handling. Works for ANY resource type.
232
+
233
+ REQUIREMENTS (Tier 2 - Prerequisites):
234
+ - Must be called AFTER manage_data_model (needs Prisma model to exist)
235
+ - Ensure 'prisma generate' was run (manage_data_model does this automatically)
236
+ - API routes always import: NextResponse, prisma, z (zod)
237
+ - Use try/catch with appropriate status codes (200, 201, 400, 500)
238
+
239
+ Args:
240
+ project_dir: Path to the web project directory
241
+ resource_name: Resource name (e.g., "todo", "user", "product")
242
+ operations: HTTP methods to implement (default: ["GET", "POST"])
243
+ fields: Resource fields with types (for validation schema)
244
+ enable_pagination: Whether to add pagination to GET endpoint
245
+
246
+ Returns:
247
+ Dictionary with success status and created files
248
+ """
249
+ try:
250
+ operations = operations or ["GET", "POST"]
251
+
252
+ project_path = Path(project_dir)
253
+
254
+ # Phase 1 Fix (Issue #885): Read from Prisma schema instead of
255
+ # using dangerous defaults. This makes tools schema-aware.
256
+ if not fields:
257
+ model_info = read_prisma_model(
258
+ project_dir, resource_name.capitalize()
259
+ )
260
+ if model_info["success"]:
261
+ # Convert Prisma types to our field types and filter out auto-fields
262
+ prisma_to_field_type = {
263
+ "String": "string",
264
+ "Int": "number",
265
+ "Float": "float",
266
+ "Boolean": "boolean",
267
+ "DateTime": "datetime",
268
+ }
269
+ fields = {}
270
+ for field_name, prisma_type in model_info["fields"].items():
271
+ # Skip auto-generated fields
272
+ if field_name.lower() in {"id", "createdat", "updatedat"}:
273
+ continue
274
+ fields[field_name] = prisma_to_field_type.get(
275
+ prisma_type, "string"
276
+ )
277
+
278
+ if not fields:
279
+ return {
280
+ "success": False,
281
+ "error": f"Model {resource_name} has no user-facing fields in Prisma schema",
282
+ "hint": "Run manage_data_model first to create the model with fields",
283
+ }
284
+ logger.info(
285
+ f"Auto-read fields from Prisma schema for {resource_name}: {fields}"
286
+ )
287
+ else:
288
+ return {
289
+ "success": False,
290
+ "error": f"Cannot find model {resource_name} in Prisma schema. {model_info.get('error', '')}",
291
+ "hint": "Run manage_data_model first to create the Prisma model",
292
+ }
293
+
294
+ if not project_path.exists():
295
+ return {
296
+ "success": False,
297
+ "error": f"Project directory does not exist: {project_dir}",
298
+ }
299
+
300
+ # Sanitize resource_name: remove path components, brackets, slashes
301
+ # This prevents malformed paths like "todos/[id]s/route.ts"
302
+ clean_resource = resource_name.strip()
303
+ # Remove common path patterns that shouldn't be in resource names
304
+ clean_resource = clean_resource.replace("/[id]", "").replace("[id]", "")
305
+ clean_resource = clean_resource.rstrip("/").lstrip("/")
306
+ # Extract just the base resource name if path-like
307
+ if "/" in clean_resource:
308
+ clean_resource = clean_resource.split("/")[0]
309
+ # Remove any remaining special characters
310
+ clean_resource = re.sub(r"[^\w]", "", clean_resource)
311
+
312
+ if not clean_resource:
313
+ return {
314
+ "success": False,
315
+ "error": f"Invalid resource_name: '{resource_name}' - must be a simple name like 'todo' or 'product'",
316
+ "hint": "Use singular form without paths, e.g., 'todo' not 'todos/[id]'",
317
+ }
318
+
319
+ # Safety net: Ensure Prisma singleton exists before creating routes
320
+ singleton_path = project_path / "src" / "lib" / "prisma.ts"
321
+ if not singleton_path.exists():
322
+ from gaia.agents.code.tools.prisma_tools import (
323
+ PRISMA_SINGLETON_TEMPLATE,
324
+ )
325
+
326
+ singleton_path.parent.mkdir(parents=True, exist_ok=True)
327
+ singleton_path.write_text(
328
+ PRISMA_SINGLETON_TEMPLATE, encoding="utf-8"
329
+ )
330
+ logger.info(f"Auto-created Prisma singleton at {singleton_path}")
331
+
332
+ # Generate resource variants from cleaned name
333
+ resource = clean_resource.lower()
334
+ Resource = clean_resource.capitalize()
335
+ resource_plural = pluralize(resource)
336
+
337
+ # Build API route content
338
+ # Phase 4 Fix (Issue #885): Check all operations that need validation
339
+ needs_validation = any(
340
+ op in operations for op in ["POST", "PATCH", "PUT"]
341
+ )
342
+ imports = generate_api_imports(
343
+ operations, uses_validation=needs_validation
344
+ )
345
+
346
+ # Generate validation schema if needed
347
+ validation_schema = ""
348
+ if needs_validation:
349
+ validation_schema = generate_zod_schema(Resource, fields)
350
+
351
+ # Generate handlers based on operations
352
+ handlers = []
353
+ for op in operations:
354
+ if op == "GET":
355
+ pattern = (
356
+ API_ROUTE_GET_PAGINATED
357
+ if enable_pagination
358
+ else API_ROUTE_GET
359
+ )
360
+ handlers.append(
361
+ pattern.format(
362
+ resource=resource,
363
+ Resource=Resource,
364
+ resource_plural=resource_plural,
365
+ )
366
+ )
367
+ elif op == "POST":
368
+ handlers.append(
369
+ API_ROUTE_POST.format(
370
+ resource=resource,
371
+ Resource=Resource,
372
+ resource_plural=resource_plural,
373
+ )
374
+ )
375
+
376
+ # Combine into complete file - use \n\n to separate handlers
377
+ full_content = f"{imports}\n\n{validation_schema}\n\n{(chr(10) + chr(10)).join(handlers)}"
378
+
379
+ # Write API route file
380
+ api_file_path = Path(
381
+ f"{project_dir}/src/app/api/{resource_plural}/route.ts"
382
+ )
383
+ api_file_path.parent.mkdir(parents=True, exist_ok=True)
384
+
385
+ # Only write collection route if POST is in operations OR route doesn't exist
386
+ # This prevents dynamic route calls from overwriting collection route
387
+ created_files = []
388
+ if "POST" in operations or not api_file_path.exists():
389
+ api_file_path.write_text(full_content, encoding="utf-8")
390
+ created_files.append(str(api_file_path))
391
+ result = {"success": True}
392
+
393
+ # Create dynamic route if PATCH or DELETE requested
394
+ if (
395
+ "PATCH" in operations
396
+ or "DELETE" in operations
397
+ or "PUT" in operations
398
+ ):
399
+ dynamic_handlers = []
400
+ if "GET" in operations:
401
+ dynamic_handlers.append(
402
+ API_ROUTE_DYNAMIC_GET.format(
403
+ resource=resource, Resource=Resource
404
+ )
405
+ )
406
+ if "PATCH" in operations or "PUT" in operations:
407
+ dynamic_handlers.append(
408
+ API_ROUTE_DYNAMIC_PATCH.format(
409
+ resource=resource, Resource=Resource
410
+ )
411
+ )
412
+ if "DELETE" in operations:
413
+ dynamic_handlers.append(
414
+ API_ROUTE_DYNAMIC_DELETE.format(resource=resource)
415
+ )
416
+
417
+ dynamic_content = f"{imports}\n\n{validation_schema}\n\n{(chr(10) + chr(10)).join(dynamic_handlers)}"
418
+ dynamic_file_path = Path(
419
+ f"{project_dir}/src/app/api/{resource_plural}/[id]/route.ts"
420
+ )
421
+ dynamic_file_path.parent.mkdir(parents=True, exist_ok=True)
422
+ dynamic_file_path.write_text(dynamic_content, encoding="utf-8")
423
+ created_files.append(str(dynamic_file_path))
424
+
425
+ logger.info(f"Created API endpoint for {resource}")
426
+
427
+ return {
428
+ "success": result.get("success", True),
429
+ "resource": resource,
430
+ "operations": operations,
431
+ "files": created_files,
432
+ }
433
+
434
+ except Exception as e:
435
+ logger.error(f"Error managing API endpoint: {e}")
436
+ return {
437
+ "success": False,
438
+ "error": str(e),
439
+ "error_type": "api_endpoint_error",
440
+ "hint": "Check project directory structure and permissions",
441
+ }
442
+
443
+ @tool
444
+ def manage_react_component(
445
+ project_dir: str,
446
+ component_name: str,
447
+ component_type: str = "server",
448
+ resource_name: Optional[str] = None,
449
+ fields: Optional[Dict[str, str]] = None,
450
+ variant: str = "list",
451
+ ) -> Dict[str, Any]:
452
+ """Manage React components with functional implementations.
453
+
454
+ Creates or updates React components with real data fetching,
455
+ state management, and event handlers. Works for ANY resource.
456
+
457
+ REQUIREMENTS (Tier 2 - Prerequisites):
458
+ - Must be called AFTER manage_api_endpoint (components need API routes)
459
+ - Must be called AFTER manage_data_model (components need Prisma types)
460
+ - Use 'import type { X } from @prisma/client' for type imports
461
+ - Server components: import { prisma } from '@/lib/prisma'
462
+ - Client components: NEVER import prisma directly - use API routes
463
+
464
+ Args:
465
+ project_dir: Path to the web project directory
466
+ component_name: Component name (e.g., "TodoList", "UserForm")
467
+ component_type: "server" or "client" component
468
+ resource_name: Associated resource (for data operations)
469
+ fields: Resource fields (for forms and display)
470
+ variant: Component variant:
471
+ - "list": List page showing all items (server component)
472
+ - "form": Reusable form component for create/edit (client component)
473
+ - "new": Create new item page (client page using form)
474
+ - "detail": View/edit single item page with delete (client page)
475
+ - "actions": Delete/edit button component (client component)
476
+
477
+ Returns:
478
+ Dictionary with success status and component path
479
+ """
480
+ try:
481
+ project_path = Path(project_dir)
482
+ if not project_path.exists():
483
+ return {
484
+ "success": False,
485
+ "error": f"Project directory does not exist: {project_dir}",
486
+ }
487
+
488
+ # Sanitize resource_name if provided (same logic as manage_api_endpoint)
489
+ clean_resource_name = resource_name
490
+ if resource_name:
491
+ clean_resource = resource_name.strip()
492
+ clean_resource = clean_resource.replace("/[id]", "").replace(
493
+ "[id]", ""
494
+ )
495
+ clean_resource = clean_resource.rstrip("/").lstrip("/")
496
+ if "/" in clean_resource:
497
+ clean_resource = clean_resource.split("/")[0]
498
+ clean_resource = re.sub(r"[^\w]", "", clean_resource)
499
+ clean_resource_name = (
500
+ clean_resource if clean_resource else resource_name
501
+ )
502
+
503
+ # Auto-set component_type for client-side variants
504
+ # These variants always generate client components with "use client"
505
+ # This prevents the stub fallback when variant="form" but component_type
506
+ # defaults to "server"
507
+ if variant in ["form", "new", "detail", "actions", "artifact-timer"]:
508
+ component_type = "client"
509
+
510
+ # Phase 1 Fix (Issue #885): Read from Prisma schema instead of
511
+ # using dangerous defaults. This makes tools schema-aware.
512
+ if not fields and clean_resource_name:
513
+ model_info = read_prisma_model(
514
+ project_dir, clean_resource_name.capitalize()
515
+ )
516
+ if model_info["success"]:
517
+ # Convert Prisma types to our field types and filter out auto-fields
518
+ prisma_to_field_type = {
519
+ "String": "string",
520
+ "Int": "number",
521
+ "Float": "float",
522
+ "Boolean": "boolean",
523
+ "DateTime": "datetime",
524
+ }
525
+ fields = {}
526
+ for field_name, prisma_type in model_info["fields"].items():
527
+ # Skip auto-generated fields
528
+ if field_name.lower() in {"id", "createdat", "updatedat"}:
529
+ continue
530
+ fields[field_name] = prisma_to_field_type.get(
531
+ prisma_type, "string"
532
+ )
533
+ if fields:
534
+ logger.info(
535
+ f"Auto-read fields from Prisma schema for {clean_resource_name}: {fields}"
536
+ )
537
+ # Note: We don't fail here - some components don't need fields
538
+
539
+ content = ""
540
+
541
+ if (
542
+ component_type == "server"
543
+ and variant == "list"
544
+ and clean_resource_name
545
+ ):
546
+ # Generate server component with data fetching
547
+ resource = clean_resource_name.lower()
548
+ Resource = clean_resource_name.capitalize()
549
+ resource_plural = pluralize(resource)
550
+
551
+ field_display = generate_field_display(fields or {})
552
+
553
+ content = SERVER_COMPONENT_LIST.format(
554
+ resource=resource,
555
+ Resource=Resource,
556
+ resource_plural=resource_plural,
557
+ field_display=field_display,
558
+ )
559
+
560
+ elif (
561
+ component_type == "client"
562
+ and variant == "form"
563
+ and clean_resource_name
564
+ ):
565
+ # Generate client component with form and state
566
+ resource = clean_resource_name.lower()
567
+ Resource = clean_resource_name.capitalize()
568
+
569
+ # Phase 3 Fix (Issue #885): Fail clearly if no fields available
570
+ # instead of using dangerous defaults
571
+ if not fields:
572
+ return {
573
+ "success": False,
574
+ "error": f"No fields available for {clean_resource_name} form component",
575
+ "hint": "Run manage_data_model first to create the Prisma model with fields",
576
+ }
577
+
578
+ # Generate form state fields
579
+ form_state = []
580
+ date_field_names = []
581
+ for field_name, field_type in fields.items():
582
+ if field_name not in ["id", "createdAt", "updatedAt"]:
583
+ normalized_type = (
584
+ field_type.lower()
585
+ if isinstance(field_type, str)
586
+ else str(field_type).lower()
587
+ )
588
+ default = (
589
+ "0"
590
+ if normalized_type
591
+ in {"number", "int", "integer", "float"}
592
+ else "false" if normalized_type == "boolean" else '""'
593
+ )
594
+ form_state.append(f" {field_name}: {default}")
595
+ if normalized_type in {"date", "datetime", "timestamp"}:
596
+ date_field_names.append(f'"{field_name}"')
597
+
598
+ # Generate form fields
599
+ form_fields = []
600
+ for field_name, field_type in fields.items():
601
+ if field_name not in ["id", "createdAt", "updatedAt"]:
602
+ form_fields.append(
603
+ generate_form_field(field_name, field_type)
604
+ )
605
+
606
+ content = CLIENT_COMPONENT_FORM.format(
607
+ resource=resource,
608
+ Resource=Resource,
609
+ form_state_fields=",\n".join(form_state),
610
+ date_fields=(
611
+ f"[{', '.join(date_field_names)}] as const"
612
+ if date_field_names
613
+ else "[] as const"
614
+ ),
615
+ form_fields="\n".join(form_fields),
616
+ )
617
+
618
+ elif variant == "new" and clean_resource_name:
619
+ # Generate "new" page that uses the form component
620
+ content = generate_new_page(clean_resource_name)
621
+
622
+ elif variant == "detail" and clean_resource_name:
623
+ # Generate detail/edit page with form and delete functionality
624
+ # Phase 3 Fix (Issue #885): Fail clearly if no fields available
625
+ if not fields:
626
+ return {
627
+ "success": False,
628
+ "error": f"No fields available for {clean_resource_name} detail page",
629
+ "hint": "Run manage_data_model first to create the Prisma model with fields",
630
+ }
631
+ content = generate_detail_page(clean_resource_name, fields)
632
+
633
+ elif variant == "artifact-timer":
634
+ timer_component = component_name or (
635
+ f"{clean_resource_name.capitalize()}Timer"
636
+ if clean_resource_name
637
+ else "CountdownTimer"
638
+ )
639
+ timer_component = re.sub(r"[^0-9A-Za-z_]", "", timer_component)
640
+ if not timer_component:
641
+ timer_component = "CountdownTimer"
642
+ component_name = timer_component
643
+ content = CLIENT_COMPONENT_TIMER.format(
644
+ ComponentName=timer_component,
645
+ )
646
+
647
+ elif variant == "actions" and clean_resource_name:
648
+ # Generate actions component with delete functionality
649
+ content = generate_actions_component(clean_resource_name)
650
+
651
+ else:
652
+ # Generic component template
653
+ content = f"""interface {component_name}Props {{
654
+ // Add props here
655
+ }}
656
+
657
+ export function {component_name}({{ }}: {component_name}Props) {{
658
+ return (
659
+ <div>
660
+ <h2>{component_name}</h2>
661
+ </div>
662
+ );
663
+ }}"""
664
+
665
+ # Determine file path (use clean_resource_name to avoid malformed paths)
666
+ if (
667
+ component_type == "server"
668
+ and variant == "list"
669
+ and clean_resource_name
670
+ ):
671
+ file_path = Path(
672
+ f"{project_dir}/src/app/{pluralize(clean_resource_name)}/page.tsx"
673
+ )
674
+ elif variant == "form":
675
+ file_path = Path(
676
+ f"{project_dir}/src/components/{component_name}.tsx"
677
+ )
678
+ elif variant == "new" and clean_resource_name:
679
+ file_path = Path(
680
+ f"{project_dir}/src/app/{pluralize(clean_resource_name)}/new/page.tsx"
681
+ )
682
+ elif variant == "detail" and clean_resource_name:
683
+ file_path = Path(
684
+ f"{project_dir}/src/app/{pluralize(clean_resource_name)}/[id]/page.tsx"
685
+ )
686
+ elif variant == "actions" and clean_resource_name:
687
+ file_path = Path(
688
+ f"{project_dir}/src/components/{clean_resource_name.capitalize()}Actions.tsx"
689
+ )
690
+ else:
691
+ file_path = Path(
692
+ f"{project_dir}/src/components/{component_name}.tsx"
693
+ )
694
+
695
+ # Write component file
696
+ file_path.parent.mkdir(parents=True, exist_ok=True)
697
+ file_path.write_text(content, encoding="utf-8")
698
+ result = {"success": True}
699
+ created_files = [str(file_path)]
700
+
701
+ # Generate component tests for form and actions variants
702
+ if variant in ["form", "actions"] and clean_resource_name and fields:
703
+ try:
704
+ resource = clean_resource_name.lower()
705
+ Resource = clean_resource_name.capitalize()
706
+ resource_plural = pluralize(resource)
707
+ test_data_fields = generate_test_data_fields(fields, variant=1)
708
+
709
+ if variant == "form":
710
+ # Generate form component test
711
+ form_field_assertions = generate_form_field_assertions(
712
+ fields
713
+ )
714
+ form_fill_actions = generate_form_fill_actions(fields)
715
+
716
+ form_test_content = COMPONENT_TEST_FORM.format(
717
+ Resource=Resource,
718
+ resource_plural=resource_plural,
719
+ form_field_assertions=form_field_assertions,
720
+ form_fill_actions=form_fill_actions,
721
+ test_data_fields=test_data_fields,
722
+ )
723
+ form_test_path = Path(
724
+ f"{project_dir}/src/components/__tests__/{Resource}Form.test.tsx"
725
+ )
726
+ form_test_path.parent.mkdir(parents=True, exist_ok=True)
727
+ form_test_path.write_text(
728
+ form_test_content, encoding="utf-8"
729
+ )
730
+ created_files.append(str(form_test_path))
731
+ logger.info(f"Created form component test for {Resource}")
732
+
733
+ elif variant == "actions":
734
+ # Generate actions component test
735
+ actions_test_content = COMPONENT_TEST_ACTIONS.format(
736
+ Resource=Resource,
737
+ resource=resource,
738
+ resource_plural=resource_plural,
739
+ )
740
+ actions_test_path = Path(
741
+ f"{project_dir}/src/components/__tests__/{Resource}Actions.test.tsx"
742
+ )
743
+ actions_test_path.parent.mkdir(parents=True, exist_ok=True)
744
+ actions_test_path.write_text(
745
+ actions_test_content, encoding="utf-8"
746
+ )
747
+ created_files.append(str(actions_test_path))
748
+ logger.info(
749
+ f"Created actions component test for {Resource}"
750
+ )
751
+
752
+ except Exception as test_error:
753
+ logger.warning(
754
+ f"Could not generate component test: {test_error}"
755
+ )
756
+
757
+ logger.info(f"Created React component: {component_name}")
758
+
759
+ return {
760
+ "success": result.get("success", True),
761
+ "component": component_name,
762
+ "type": component_type,
763
+ "file_path": str(file_path),
764
+ "files": created_files,
765
+ }
766
+
767
+ except Exception as e:
768
+ logger.error(f"Error managing React component: {e}")
769
+ return {
770
+ "success": False,
771
+ "error": str(e),
772
+ "error_type": "component_error",
773
+ "hint": "Check project structure and component syntax",
774
+ }
775
+
776
+ @tool
777
+ def update_landing_page(
778
+ project_dir: str,
779
+ resource_name: str,
780
+ description: Optional[str] = None,
781
+ ) -> Dict[str, Any]:
782
+ """Update the landing page to include a link to the new resource.
783
+
784
+ Modifies src/app/page.tsx to add navigation to the newly created
785
+ resource pages. This ensures users can easily access the new features
786
+ from the main page.
787
+
788
+ Args:
789
+ project_dir: Path to the Next.js project directory
790
+ resource_name: Name of the resource (e.g., "todo", "product")
791
+ description: Optional description for the link
792
+
793
+ Returns:
794
+ Dictionary with success status and updated file path
795
+ """
796
+ try:
797
+ project_path = Path(project_dir)
798
+ page_path = project_path / "src" / "app" / "page.tsx"
799
+
800
+ if not page_path.exists():
801
+ return {
802
+ "success": False,
803
+ "error": f"Landing page not found: {page_path}",
804
+ "hint": "Ensure this is a Next.js project with app router",
805
+ }
806
+
807
+ resource = resource_name.lower()
808
+ Resource = resource_name.capitalize()
809
+ resource_plural = pluralize(resource)
810
+ link_description = description or f"Manage your {resource_plural}"
811
+
812
+ # Read current content
813
+ current_content = page_path.read_text(encoding="utf-8")
814
+
815
+ # Check if link already exists
816
+ if (
817
+ f'href="/{resource_plural}"' in current_content
818
+ or f"href='/{resource_plural}'" in current_content
819
+ ):
820
+ return {
821
+ "success": True,
822
+ "message": f"Link to /{resource_plural} already exists in landing page",
823
+ "file_path": str(page_path),
824
+ "already_exists": True,
825
+ }
826
+
827
+ # Generate new landing page with link to resource using dark theme
828
+ new_content = LANDING_PAGE_WITH_LINKS.format(
829
+ resource_plural=resource_plural,
830
+ Resource=Resource,
831
+ link_description=link_description,
832
+ )
833
+
834
+ # Write updated content
835
+ page_path.write_text(new_content, encoding="utf-8")
836
+
837
+ logger.info(f"Updated landing page with link to /{resource_plural}")
838
+
839
+ return {
840
+ "success": True,
841
+ "message": f"Landing page updated with link to /{resource_plural}",
842
+ "file_path": str(page_path),
843
+ "resource": resource_name,
844
+ "link_path": f"/{resource_plural}",
845
+ }
846
+
847
+ except Exception as e:
848
+ logger.error(f"Error updating landing page: {e}")
849
+ return {
850
+ "success": False,
851
+ "error": str(e),
852
+ "hint": "Check that src/app/page.tsx exists and is writable",
853
+ }
854
+
855
+ @tool
856
+ def setup_nextjs_testing(
857
+ project_dir: str,
858
+ resource_name: Optional[str] = None,
859
+ ) -> Dict[str, Any]:
860
+ """Set up Vitest testing infrastructure for a Next.js project.
861
+
862
+ Installs testing dependencies and creates configuration files:
863
+ - Vitest + React Testing Library
864
+ - vitest.config.ts with proper aliases and jsdom environment
865
+ - tests/setup.ts with common mocks (next/navigation, Prisma)
866
+ - Updates package.json with test scripts
867
+
868
+ Should be called after the project is initialized but before running tests.
869
+
870
+ Args:
871
+ project_dir: Path to the Next.js project directory
872
+ resource_name: Optional resource name to customize Prisma mocks
873
+
874
+ Returns:
875
+ Dictionary with success status and created files
876
+ """
877
+ from gaia.agents.code.prompts.code_patterns import TEST_SETUP, VITEST_CONFIG
878
+
879
+ try:
880
+ project_path = Path(project_dir)
881
+ if not project_path.exists():
882
+ return {
883
+ "success": False,
884
+ "error": f"Project directory does not exist: {project_dir}",
885
+ }
886
+
887
+ created_files = []
888
+ resource = resource_name.lower() if resource_name else "todo"
889
+
890
+ # Install testing dependencies
891
+ install_cmd = (
892
+ "npm install -D vitest @vitejs/plugin-react jsdom "
893
+ "@testing-library/react @testing-library/jest-dom @testing-library/user-event "
894
+ "@types/node"
895
+ )
896
+ install_result = subprocess.run(
897
+ install_cmd,
898
+ shell=True,
899
+ cwd=project_dir,
900
+ capture_output=True,
901
+ text=True,
902
+ timeout=1200,
903
+ check=False,
904
+ )
905
+
906
+ if install_result.returncode != 0:
907
+ return {
908
+ "success": False,
909
+ "error": "Failed to install testing dependencies",
910
+ "details": install_result.stderr,
911
+ "hint": "Check npm configuration and network connectivity",
912
+ }
913
+
914
+ # Create vitest.config.ts
915
+ vitest_config_path = project_path / "vitest.config.ts"
916
+ vitest_config_path.write_text(VITEST_CONFIG, encoding="utf-8")
917
+ created_files.append(str(vitest_config_path))
918
+
919
+ # Create tests/setup.ts with resource-specific Prisma mocks
920
+ tests_dir = project_path / "tests"
921
+ tests_dir.mkdir(exist_ok=True)
922
+
923
+ setup_content = TEST_SETUP.format(resource=resource)
924
+ setup_path = tests_dir / "setup.ts"
925
+ setup_path.write_text(setup_content, encoding="utf-8")
926
+ created_files.append(str(setup_path))
927
+
928
+ # Update package.json to add test scripts
929
+ package_json_path = project_path / "package.json"
930
+ if package_json_path.exists():
931
+ import json
932
+
933
+ package_data = json.loads(
934
+ package_json_path.read_text(encoding="utf-8")
935
+ )
936
+
937
+ if "scripts" not in package_data:
938
+ package_data["scripts"] = {}
939
+
940
+ # Add test scripts if not present
941
+ if "test" not in package_data["scripts"]:
942
+ package_data["scripts"]["test"] = "vitest run"
943
+ if "test:watch" not in package_data["scripts"]:
944
+ package_data["scripts"]["test:watch"] = "vitest"
945
+ if "test:coverage" not in package_data["scripts"]:
946
+ package_data["scripts"][
947
+ "test:coverage"
948
+ ] = "vitest run --coverage"
949
+
950
+ package_json_path.write_text(
951
+ json.dumps(package_data, indent=2) + "\n", encoding="utf-8"
952
+ )
953
+ created_files.append(str(package_json_path))
954
+
955
+ logger.info(f"Set up Vitest testing infrastructure in {project_dir}")
956
+
957
+ return {
958
+ "success": True,
959
+ "message": "Testing infrastructure configured successfully",
960
+ "files": created_files,
961
+ "dependencies_installed": [
962
+ "vitest",
963
+ "@vitejs/plugin-react",
964
+ "jsdom",
965
+ "@testing-library/react",
966
+ "@testing-library/jest-dom",
967
+ "@testing-library/user-event",
968
+ ],
969
+ "scripts_added": {
970
+ "test": "vitest run",
971
+ "test:watch": "vitest",
972
+ "test:coverage": "vitest run --coverage",
973
+ },
974
+ }
975
+
976
+ except subprocess.TimeoutExpired:
977
+ return {
978
+ "success": False,
979
+ "error": "npm install timed out",
980
+ "hint": "Check network connectivity and try again",
981
+ }
982
+ except Exception as e:
983
+ logger.error(f"Error setting up testing: {e}")
984
+ return {
985
+ "success": False,
986
+ "error": str(e),
987
+ "hint": "Check project structure and npm configuration",
988
+ }
989
+
990
+ @tool
991
+ def validate_crud_completeness(
992
+ project_dir: str, resource_name: str
993
+ ) -> Dict[str, Any]:
994
+ """Validate that all necessary CRUD files exist for a resource.
995
+
996
+ Checks for the presence of all required files for a complete CRUD application:
997
+ - API routes (collection and item endpoints)
998
+ - Pages (list, new, detail/edit)
999
+ - Components (form)
1000
+ - Database model
1001
+
1002
+ Args:
1003
+ project_dir: Path to the project directory
1004
+ resource_name: Resource name to validate (e.g., "todo", "user")
1005
+
1006
+ Returns:
1007
+ Dictionary with validation results and lists of existing/missing files
1008
+ """
1009
+ try:
1010
+ project_path = Path(project_dir)
1011
+ if not project_path.exists():
1012
+ return {
1013
+ "success": False,
1014
+ "error": f"Project directory does not exist: {project_dir}",
1015
+ }
1016
+
1017
+ resource = resource_name.lower()
1018
+ Resource = resource_name.capitalize()
1019
+ resource_plural = pluralize(resource)
1020
+
1021
+ # Define expected files
1022
+ expected_files = {
1023
+ "api_routes": {
1024
+ f"src/app/api/{resource_plural}/route.ts": "Collection API route (GET list, POST create)",
1025
+ f"src/app/api/{resource_plural}/[id]/route.ts": "Item API route (GET single, PATCH update, DELETE)",
1026
+ },
1027
+ "pages": {
1028
+ f"src/app/{resource_plural}/page.tsx": "List page showing all items",
1029
+ f"src/app/{resource_plural}/new/page.tsx": "Create new item page",
1030
+ f"src/app/{resource_plural}/[id]/page.tsx": "View/edit single item page",
1031
+ },
1032
+ "components": {
1033
+ f"src/components/{Resource}Form.tsx": "Reusable form component for create/edit"
1034
+ },
1035
+ }
1036
+
1037
+ # Check which files exist
1038
+ missing_files = {}
1039
+ existing_files = {}
1040
+
1041
+ for category, files in expected_files.items():
1042
+ missing_files[category] = []
1043
+ existing_files[category] = []
1044
+
1045
+ for file_path, description in files.items():
1046
+ full_path = project_path / file_path
1047
+ if full_path.exists():
1048
+ existing_files[category].append(
1049
+ {"path": file_path, "description": description}
1050
+ )
1051
+ else:
1052
+ missing_files[category].append(
1053
+ {"path": file_path, "description": description}
1054
+ )
1055
+
1056
+ # Check if Prisma model exists
1057
+ schema_file = project_path / "prisma" / "schema.prisma"
1058
+ model_exists = False
1059
+ if schema_file.exists():
1060
+ schema_content = schema_file.read_text()
1061
+ model_exists = f"model {Resource}" in schema_content
1062
+
1063
+ # Calculate completeness
1064
+ total_files = sum(len(files) for files in expected_files.values())
1065
+ existing_count = sum(len(files) for files in existing_files.values())
1066
+ missing_count = sum(len(files) for files in missing_files.values())
1067
+
1068
+ all_complete = missing_count == 0 and model_exists
1069
+
1070
+ logger.info(
1071
+ f"CRUD completeness check for {resource}: {existing_count}/{total_files} files exist"
1072
+ )
1073
+
1074
+ return {
1075
+ "success": True,
1076
+ "complete": all_complete,
1077
+ "resource": resource,
1078
+ "model_exists": model_exists,
1079
+ "existing_files": existing_files,
1080
+ "missing_files": missing_files,
1081
+ "stats": {
1082
+ "total": total_files,
1083
+ "existing": existing_count,
1084
+ "missing": missing_count,
1085
+ },
1086
+ }
1087
+
1088
+ except Exception as e:
1089
+ logger.error(f"Error validating CRUD completeness: {e}")
1090
+ return {
1091
+ "success": False,
1092
+ "error": str(e),
1093
+ "error_type": "validation_error",
1094
+ }
1095
+
1096
+ @tool
1097
+ def generate_crud_scaffold(
1098
+ project_dir: str, resource_name: str, fields: Dict[str, str]
1099
+ ) -> Dict[str, Any]:
1100
+ """Generate a complete CRUD scaffold with all necessary files.
1101
+
1102
+ This high-level tool orchestrates multiple operations to create
1103
+ a fully functional CRUD application for a resource. It generates:
1104
+ - API routes for all CRUD operations
1105
+ - List page to view all items
1106
+ - Form component for create/edit
1107
+ - Create page (new item)
1108
+ - Detail/edit page (single item with delete)
1109
+
1110
+ Args:
1111
+ project_dir: Path to the project directory
1112
+ resource_name: Resource name (e.g., "todo", "product")
1113
+ fields: Dictionary of field names to types
1114
+
1115
+ Returns:
1116
+ Dictionary with generation results and validation status
1117
+ """
1118
+ try:
1119
+ results = {
1120
+ "api_routes": [],
1121
+ "pages": [],
1122
+ "components": [],
1123
+ "errors": [],
1124
+ }
1125
+
1126
+ logger.info(f"Generating complete CRUD scaffold for {resource_name}...")
1127
+
1128
+ # 1. Generate API endpoints (all CRUD operations)
1129
+ logger.info(" → Generating API routes...")
1130
+ api_result = manage_api_endpoint(
1131
+ project_dir=project_dir,
1132
+ resource_name=resource_name,
1133
+ operations=["GET", "POST", "PATCH", "DELETE"],
1134
+ fields=fields,
1135
+ enable_pagination=True,
1136
+ )
1137
+ if api_result.get("success"):
1138
+ results["api_routes"].extend(api_result.get("files", []))
1139
+ else:
1140
+ results["errors"].append(
1141
+ f"API generation failed: {api_result.get('error')}"
1142
+ )
1143
+
1144
+ # 2. Generate list page (server component)
1145
+ logger.info(" → Generating list page...")
1146
+ list_result = manage_react_component(
1147
+ project_dir=project_dir,
1148
+ component_name=f"{resource_name.capitalize()}List",
1149
+ component_type="server",
1150
+ resource_name=resource_name,
1151
+ fields=fields,
1152
+ variant="list",
1153
+ )
1154
+ if list_result.get("success"):
1155
+ results["pages"].append(list_result.get("file_path"))
1156
+ else:
1157
+ results["errors"].append(
1158
+ f"List page generation failed: {list_result.get('error')}"
1159
+ )
1160
+
1161
+ # 3. Generate form component (reusable for create/edit)
1162
+ logger.info(" → Generating form component...")
1163
+ form_result = manage_react_component(
1164
+ project_dir=project_dir,
1165
+ component_name=f"{resource_name.capitalize()}Form",
1166
+ component_type="client",
1167
+ resource_name=resource_name,
1168
+ fields=fields,
1169
+ variant="form",
1170
+ )
1171
+ if form_result.get("success"):
1172
+ results["components"].append(form_result.get("file_path"))
1173
+ else:
1174
+ results["errors"].append(
1175
+ f"Form component generation failed: {form_result.get('error')}"
1176
+ )
1177
+
1178
+ # 4. Generate new page (create page)
1179
+ logger.info(" → Generating create (new) page...")
1180
+ new_result = manage_react_component(
1181
+ project_dir=project_dir,
1182
+ component_name=f"New{resource_name.capitalize()}Page",
1183
+ component_type="client",
1184
+ resource_name=resource_name,
1185
+ fields=fields,
1186
+ variant="new",
1187
+ )
1188
+ if new_result.get("success"):
1189
+ results["pages"].append(new_result.get("file_path"))
1190
+ else:
1191
+ results["errors"].append(
1192
+ f"New page generation failed: {new_result.get('error')}"
1193
+ )
1194
+
1195
+ # 5. Generate detail page (view/edit page with delete)
1196
+ logger.info(" → Generating detail/edit page...")
1197
+ detail_result = manage_react_component(
1198
+ project_dir=project_dir,
1199
+ component_name=f"{resource_name.capitalize()}DetailPage",
1200
+ component_type="client",
1201
+ resource_name=resource_name,
1202
+ fields=fields,
1203
+ variant="detail",
1204
+ )
1205
+ if detail_result.get("success"):
1206
+ results["pages"].append(detail_result.get("file_path"))
1207
+ else:
1208
+ results["errors"].append(
1209
+ f"Detail page generation failed: {detail_result.get('error')}"
1210
+ )
1211
+
1212
+ # 6. Generate actions component (edit/delete buttons)
1213
+ logger.info(" → Generating actions component...")
1214
+ actions_result = manage_react_component(
1215
+ project_dir=project_dir,
1216
+ component_name=f"{resource_name.capitalize()}Actions",
1217
+ component_type="client",
1218
+ resource_name=resource_name,
1219
+ fields=fields,
1220
+ variant="actions",
1221
+ )
1222
+ if actions_result.get("success"):
1223
+ results["components"].append(actions_result.get("file_path"))
1224
+ else:
1225
+ results["errors"].append(
1226
+ f"Actions component generation failed: {actions_result.get('error')}"
1227
+ )
1228
+
1229
+ # 7. Validate completeness
1230
+ logger.info(" → Validating completeness...")
1231
+ validation = validate_crud_completeness(project_dir, resource_name)
1232
+
1233
+ success = len(results["errors"]) == 0
1234
+ logger.info(
1235
+ f"CRUD scaffold generation {'succeeded' if success else 'completed with errors'}"
1236
+ )
1237
+
1238
+ return {
1239
+ "success": success,
1240
+ "resource": resource_name,
1241
+ "generated": results,
1242
+ "validation": validation,
1243
+ }
1244
+
1245
+ except Exception as e:
1246
+ logger.error(f"Error generating CRUD scaffold: {e}")
1247
+ return {
1248
+ "success": False,
1249
+ "error": str(e),
1250
+ "error_type": "scaffold_generation_error",
1251
+ }
1252
+
1253
+ @tool
1254
+ def manage_data_model(
1255
+ project_dir: str,
1256
+ model_name: str,
1257
+ fields: Dict[str, str],
1258
+ relationships: Optional[List[Dict[str, str]]] = None,
1259
+ ) -> Dict[str, Any]:
1260
+ """Manage database models with Prisma ORM.
1261
+
1262
+ Creates or updates Prisma model definitions. Works for ANY model type.
1263
+
1264
+ Args:
1265
+ project_dir: Path to the project directory
1266
+ model_name: Model name (singular, PascalCase, e.g., "User", "Product")
1267
+ fields: Dictionary of field names to types
1268
+ Supported: "string", "text", "number", "float", "boolean",
1269
+ "date", "datetime", "timestamp", "email", "url"
1270
+ relationships: Optional list of relationships
1271
+ [{"type": "hasMany", "model": "Post"}]
1272
+
1273
+ Returns:
1274
+ Dictionary with success status and schema file path
1275
+ """
1276
+ try:
1277
+ project_path = Path(project_dir)
1278
+ if not project_path.exists():
1279
+ return {
1280
+ "success": False,
1281
+ "error": f"Project directory does not exist: {project_dir}",
1282
+ }
1283
+
1284
+ schema_file = project_path / "prisma" / "schema.prisma"
1285
+
1286
+ if not schema_file.exists():
1287
+ return {
1288
+ "success": False,
1289
+ "error": "schema.prisma not found. Initialize Prisma first.",
1290
+ }
1291
+
1292
+ # Read existing schema
1293
+ schema_content = schema_file.read_text()
1294
+
1295
+ # Validate schema doesn't have forbidden output field in generator block
1296
+ if "output" in schema_content:
1297
+ # Check if it's in generator client block specifically
1298
+ generator_match = re.search(
1299
+ r"generator\s+client\s*\{[^}]*output[^}]*\}",
1300
+ schema_content,
1301
+ re.DOTALL,
1302
+ )
1303
+ if generator_match:
1304
+ # Auto-fix: remove the output line
1305
+ fixed_content = re.sub(
1306
+ r'\n\s*output\s*=\s*"[^"]*"', "", schema_content
1307
+ )
1308
+ schema_file.write_text(fixed_content, encoding="utf-8")
1309
+ schema_content = fixed_content
1310
+ logger.warning(
1311
+ "Removed invalid 'output' field from generator client block"
1312
+ )
1313
+
1314
+ # Generate field definitions
1315
+ field_lines = []
1316
+ field_lines.append(" id Int @id @default(autoincrement())")
1317
+
1318
+ # Map types to Prisma types
1319
+ type_mapping = {
1320
+ "string": "String",
1321
+ "text": "String",
1322
+ "number": "Int",
1323
+ "float": "Float",
1324
+ "boolean": "Boolean",
1325
+ "date": "DateTime",
1326
+ "datetime": "DateTime",
1327
+ "timestamp": "DateTime",
1328
+ "email": "String",
1329
+ "url": "String",
1330
+ }
1331
+
1332
+ # Define reserved fields that are auto-generated
1333
+ reserved_fields = {"id", "createdat", "updatedat"}
1334
+
1335
+ # Build field lines from user input (skip reserved fields)
1336
+ for field_name, field_type in fields.items():
1337
+ if field_name.lower() in reserved_fields:
1338
+ logger.warning(
1339
+ f"Skipping reserved field '{field_name}' - auto-generated by Prisma"
1340
+ )
1341
+ continue
1342
+ prisma_type = type_mapping.get(field_type.lower(), "String")
1343
+ field_lines.append(f" {field_name:<12} {prisma_type}")
1344
+
1345
+ # Add relationships if provided
1346
+ if relationships:
1347
+ for rel in relationships:
1348
+ rel_type = rel.get("type", "hasMany")
1349
+ rel_model = rel.get("model")
1350
+ if rel_type == "hasMany":
1351
+ field_lines.append(
1352
+ f" {rel_model.lower()}s {rel_model}[]"
1353
+ )
1354
+ elif rel_type == "hasOne":
1355
+ field_lines.append(
1356
+ f" {rel_model.lower()} {rel_model}?"
1357
+ )
1358
+
1359
+ # Always add timestamps - they're standard for Prisma and our templates expect them
1360
+ # Note: Reserved fields (including createdAt/updatedAt) are already skipped
1361
+ # from user input above, so there's no risk of duplication
1362
+ field_lines.append(" createdAt DateTime @default(now())")
1363
+ field_lines.append(" updatedAt DateTime @updatedAt")
1364
+
1365
+ # Check if model already exists in schema
1366
+ model_pattern = rf"model\s+{model_name}\s*\{{"
1367
+ if re.search(model_pattern, schema_content):
1368
+ return {
1369
+ "success": False,
1370
+ "error": f"Model '{model_name}' already exists in schema",
1371
+ "error_type": "duplicate_model",
1372
+ "hint": "Use a different model name or edit the existing model",
1373
+ "suggested_fix": f"Read schema.prisma to see existing {model_name} definition",
1374
+ }
1375
+
1376
+ # Generate model definition
1377
+ model_definition = f"""
1378
+
1379
+ model {model_name} {{
1380
+ {chr(10).join(field_lines)}
1381
+ }}
1382
+ """
1383
+
1384
+ # Save original schema for rollback
1385
+ original_schema = schema_content
1386
+
1387
+ # Append to schema
1388
+ schema_content += model_definition
1389
+
1390
+ # Write new schema
1391
+ schema_file.write_text(schema_content, encoding="utf-8")
1392
+
1393
+ # Validate with prisma format
1394
+ validate_result = subprocess.run(
1395
+ f'npx prisma format --schema="{schema_file}"',
1396
+ cwd=str(project_path),
1397
+ shell=True,
1398
+ capture_output=True,
1399
+ text=True,
1400
+ encoding="utf-8",
1401
+ errors="replace",
1402
+ timeout=600,
1403
+ check=False,
1404
+ )
1405
+
1406
+ if validate_result.returncode != 0:
1407
+ # Rollback to original schema
1408
+ schema_file.write_text(original_schema, encoding="utf-8")
1409
+ return {
1410
+ "success": False,
1411
+ "error": f"Schema validation failed: {validate_result.stderr}",
1412
+ "error_type": "schema_validation_error",
1413
+ "hint": "The schema changes caused validation errors",
1414
+ "suggested_fix": "Check field types and model syntax",
1415
+ }
1416
+
1417
+ result = {"success": True}
1418
+
1419
+ logger.info(f"Added Prisma model: {model_name}")
1420
+
1421
+ # Auto-generate Prisma client types
1422
+ prisma_generated = False
1423
+ generation_note = ""
1424
+
1425
+ try:
1426
+ # Format schema first
1427
+ subprocess.run(
1428
+ f'npx prisma format --schema="{schema_file}"',
1429
+ cwd=str(project_path),
1430
+ shell=True,
1431
+ capture_output=True,
1432
+ text=True,
1433
+ encoding="utf-8",
1434
+ errors="replace",
1435
+ timeout=600,
1436
+ check=False,
1437
+ )
1438
+
1439
+ # Generate Prisma client types
1440
+ generate_result = subprocess.run(
1441
+ f'npx prisma generate --schema="{schema_file}"',
1442
+ cwd=str(project_path),
1443
+ shell=True,
1444
+ capture_output=True,
1445
+ text=True,
1446
+ encoding="utf-8",
1447
+ errors="replace",
1448
+ timeout=1200,
1449
+ check=False,
1450
+ )
1451
+
1452
+ if generate_result.returncode != 0:
1453
+ stderr = generate_result.stderr
1454
+ logger.error(f"prisma generate failed: {stderr}")
1455
+ return {
1456
+ "success": False,
1457
+ "error": f"prisma generate failed: {stderr}",
1458
+ "schema_file": str(schema_file),
1459
+ "fix_hint": "Check schema.prisma for syntax errors",
1460
+ }
1461
+
1462
+ # Verify Prisma client was actually generated
1463
+ client_index = (
1464
+ project_path
1465
+ / "node_modules"
1466
+ / ".prisma"
1467
+ / "client"
1468
+ / "index.js"
1469
+ )
1470
+ if not client_index.exists():
1471
+ logger.error("Prisma client not generated despite no errors")
1472
+ return {
1473
+ "success": False,
1474
+ "error": "Prisma client not generated despite no errors",
1475
+ "fix_hint": "Run 'npm install' then 'npx prisma generate'",
1476
+ }
1477
+
1478
+ prisma_generated = True
1479
+ generation_note = (
1480
+ "Schema updated and Prisma client generated successfully"
1481
+ )
1482
+ logger.info(generation_note)
1483
+
1484
+ # Push schema changes to database
1485
+ logger.info(f"Running prisma db push in {project_dir}")
1486
+ db_push_result = subprocess.run(
1487
+ "npx prisma db push",
1488
+ cwd=str(project_path),
1489
+ shell=True,
1490
+ capture_output=True,
1491
+ text=True,
1492
+ encoding="utf-8",
1493
+ errors="replace",
1494
+ timeout=1200,
1495
+ check=False,
1496
+ )
1497
+
1498
+ if db_push_result.returncode != 0:
1499
+ logger.error(f"prisma db push failed: {db_push_result.stderr}")
1500
+ return {
1501
+ "success": False,
1502
+ "error": f"prisma db push failed: {db_push_result.stderr}",
1503
+ "fix_hint": "Check DATABASE_URL in .env file",
1504
+ "generated": True, # Client was generated successfully
1505
+ "pushed": False,
1506
+ }
1507
+
1508
+ generation_note = "Schema updated, Prisma client generated, and database pushed successfully"
1509
+ logger.info(generation_note)
1510
+
1511
+ except Exception as e:
1512
+ # Prisma generation failed - block the operation
1513
+ logger.error(f"Could not generate Prisma client: {e}")
1514
+ return {
1515
+ "success": False,
1516
+ "error": f"Could not generate Prisma client: {e}",
1517
+ "fix_hint": "Ensure prisma is installed (npm install)",
1518
+ }
1519
+
1520
+ return {
1521
+ "success": result.get("success", True),
1522
+ "model_name": model_name,
1523
+ "schema_file": str(schema_file),
1524
+ "schema_updated": True,
1525
+ "prisma_generated": prisma_generated,
1526
+ "note": generation_note,
1527
+ }
1528
+
1529
+ except Exception as e:
1530
+ logger.error(f"Error managing data model: {e}")
1531
+ return {
1532
+ "success": False,
1533
+ "error": str(e),
1534
+ "error_type": "data_model_error",
1535
+ }
1536
+
1537
+ @tool
1538
+ def manage_prisma_client(project_dir: str) -> Dict[str, Any]:
1539
+ """Manage Prisma client generation and database sync.
1540
+
1541
+ Generates the Prisma client and pushes schema changes to the database.
1542
+
1543
+ Args:
1544
+ project_dir: Path to the project directory
1545
+
1546
+ Returns:
1547
+ Dictionary with success status and commands to run
1548
+ """
1549
+ try:
1550
+ project_path = Path(project_dir)
1551
+ if not project_path.exists():
1552
+ return {
1553
+ "success": False,
1554
+ "error": f"Project directory does not exist: {project_dir}",
1555
+ }
1556
+
1557
+ # Check if Prisma is configured
1558
+ schema_file = project_path / "prisma" / "schema.prisma"
1559
+ if not schema_file.exists():
1560
+ return {
1561
+ "success": False,
1562
+ "error": "Prisma not initialized. schema.prisma not found.",
1563
+ }
1564
+
1565
+ # Provide guidance for Prisma operations
1566
+ commands = [
1567
+ "npm run db:generate # Generate Prisma Client",
1568
+ "npm run db:push # Push schema to database",
1569
+ "npm run db:studio # Open Prisma Studio (optional)",
1570
+ ]
1571
+
1572
+ logger.info("Prisma client management guidance provided")
1573
+
1574
+ return {
1575
+ "success": True,
1576
+ "commands": commands,
1577
+ "working_dir": str(project_path),
1578
+ }
1579
+
1580
+ except Exception as e:
1581
+ logger.error(f"Error managing Prisma client: {e}")
1582
+ return {
1583
+ "success": False,
1584
+ "error": str(e),
1585
+ "error_type": "prisma_client_error",
1586
+ }
1587
+
1588
+ @tool
1589
+ def manage_web_config(
1590
+ project_dir: str, config_type: str, updates: Dict[str, Any]
1591
+ ) -> Dict[str, Any]:
1592
+ """Manage web application configuration files.
1593
+
1594
+ Updates configuration files like .env, next.config.js, etc.
1595
+ Delegates actual file operations to file_io.
1596
+
1597
+ Args:
1598
+ project_dir: Path to the project directory
1599
+ config_type: Type of config ("env", "nextjs", "tailwind")
1600
+ updates: Dictionary of configuration updates
1601
+
1602
+ Returns:
1603
+ Dictionary with success status
1604
+ """
1605
+ try:
1606
+ project_path = Path(project_dir)
1607
+ if not project_path.exists():
1608
+ return {
1609
+ "success": False,
1610
+ "error": f"Project directory does not exist: {project_dir}",
1611
+ }
1612
+
1613
+ if config_type == "env":
1614
+ env_file = project_path / ".env"
1615
+ if not env_file.exists():
1616
+ # Create new .env file
1617
+ content = "\n".join(f"{k}={v}" for k, v in updates.items())
1618
+ else:
1619
+ # Update existing
1620
+ content = env_file.read_text()
1621
+ for key, value in updates.items():
1622
+ if f"{key}=" in content:
1623
+ lines = content.split("\n")
1624
+ content = "\n".join(
1625
+ (
1626
+ f"{key}={value}"
1627
+ if line.startswith(f"{key}=")
1628
+ else line
1629
+ )
1630
+ for line in lines
1631
+ )
1632
+ else:
1633
+ content += f"\n{key}={value}"
1634
+
1635
+ env_file.write_text(content, encoding="utf-8")
1636
+
1637
+ return {
1638
+ "success": True,
1639
+ "config_type": config_type,
1640
+ "file": str(env_file),
1641
+ "updates": updates,
1642
+ }
1643
+ else:
1644
+ return {
1645
+ "success": True,
1646
+ "config_type": config_type,
1647
+ "updates": updates,
1648
+ }
1649
+
1650
+ except Exception as e:
1651
+ logger.error(f"Error managing config: {e}")
1652
+ return {
1653
+ "success": False,
1654
+ "error": str(e),
1655
+ "error_type": "config_error",
1656
+ }
1657
+
1658
+ @tool
1659
+ def generate_style_tests(
1660
+ project_dir: str, resource_name: str = "Item"
1661
+ ) -> Dict[str, Any]:
1662
+ """Generate CSS and styling tests for the project.
1663
+
1664
+ Creates test files that validate:
1665
+ 1. CSS file integrity (no TypeScript in CSS - Issue #1002)
1666
+ 2. Tailwind directive presence
1667
+ 3. Design system class definitions
1668
+ 4. Layout imports globals.css
1669
+ 5. App router structure
1670
+
1671
+ Tests are placed in the project's /tests directory.
1672
+
1673
+ Args:
1674
+ project_dir: Path to the Next.js project directory
1675
+ resource_name: Resource name for component styling tests
1676
+
1677
+ Returns:
1678
+ Dictionary with success status and generated files
1679
+ """
1680
+ from gaia.agents.code.prompts.code_patterns import (
1681
+ generate_routes_test_content,
1682
+ generate_style_test_content,
1683
+ )
1684
+
1685
+ try:
1686
+ project_path = Path(project_dir)
1687
+ tests_dir = project_path / "tests"
1688
+ styling_dir = tests_dir / "styling"
1689
+
1690
+ # Ensure directories exist
1691
+ tests_dir.mkdir(parents=True, exist_ok=True)
1692
+ styling_dir.mkdir(parents=True, exist_ok=True)
1693
+
1694
+ files_created = []
1695
+
1696
+ # 1. Generate styles.test.ts (main CSS integrity test)
1697
+ styles_test_path = tests_dir / "styles.test.ts"
1698
+ styles_content = generate_style_test_content(resource_name)
1699
+
1700
+ if hasattr(self, "write_file"):
1701
+ result = self.write_file(str(styles_test_path), styles_content)
1702
+ if result.get("success"):
1703
+ files_created.append(str(styles_test_path))
1704
+ else:
1705
+ styles_test_path.write_text(styles_content)
1706
+ files_created.append(str(styles_test_path))
1707
+
1708
+ # 2. Generate routes.test.ts (app router structure test)
1709
+ routes_test_path = styling_dir / "routes.test.ts"
1710
+ routes_content = generate_routes_test_content(resource_name)
1711
+
1712
+ if hasattr(self, "write_file"):
1713
+ result = self.write_file(str(routes_test_path), routes_content)
1714
+ if result.get("success"):
1715
+ files_created.append(str(routes_test_path))
1716
+ else:
1717
+ routes_test_path.write_text(routes_content)
1718
+ files_created.append(str(routes_test_path))
1719
+
1720
+ # 3. Install glob package if not present (needed for tests)
1721
+ package_json = project_path / "package.json"
1722
+ if package_json.exists():
1723
+ pkg_content = package_json.read_text()
1724
+ if '"glob"' not in pkg_content:
1725
+ try:
1726
+ subprocess.run(
1727
+ ["npm", "install", "--save-dev", "glob", "@types/glob"],
1728
+ cwd=str(project_path),
1729
+ capture_output=True,
1730
+ text=True,
1731
+ timeout=600,
1732
+ check=False,
1733
+ )
1734
+ logger.info("Installed glob package for style tests")
1735
+ except Exception as e:
1736
+ logger.warning(f"Could not install glob package: {e}")
1737
+
1738
+ logger.info(
1739
+ f"Generated {len(files_created)} style test files for {resource_name}"
1740
+ )
1741
+
1742
+ return {
1743
+ "success": True,
1744
+ "files": files_created,
1745
+ "message": f"Generated style tests for {resource_name}",
1746
+ "tests_description": [
1747
+ "styles.test.ts - CSS integrity (TypeScript detection, Tailwind, braces)",
1748
+ "styling/routes.test.ts - App router structure and styling consistency",
1749
+ ],
1750
+ }
1751
+
1752
+ except Exception as e:
1753
+ logger.error(f"Error generating style tests: {e}")
1754
+ return {
1755
+ "success": False,
1756
+ "error": str(e),
1757
+ "error_type": "test_generation_error",
1758
+ }