import fs from 'node:fs'; import path from 'node:path'; import { z } from 'zod'; import { config } from '../../config.js'; import { detectProject } from '../../lib/frameworks/detect.js'; import { kebabCase, pascalCase, camelCase } from '../../lib/strings.js'; import { resolveWorkspacePath } from '../../lib/sandbox.js'; import { errorResult, jsonResult, toErrorMessage } from '../shared.js'; function pluralize(word) { const lower = word.toLowerCase(); if (lower.endsWith('ies')) return word; if (lower.endsWith('y') && !/[aeiou]y$/i.test(word)) return `${word.slice(0, -1)}ies`; if (/(s|x|z|ch|sh)$/i.test(word)) return `${word}es`; if (lower.endsWith('s')) return word; return `${word}s`; } function requireDjango() { const detected = detectProject(config.workspaceRoot); if (detected.backend?.kind !== 'django') { throw new Error('No Django project detected. Expected manage.py or a Django dependency file.'); } return detected.backend; } function assertWritable(file, overwrite) { if (!overwrite && fs.existsSync(file)) { throw new Error(`"${file}" already exists. Pass overwrite: true to replace it.`); } } export function registerGenerateDjangoResourceTool(server) { server.registerTool('generate_django_resource', { title: 'Generate Django Resource', description: 'Scaffold a Django app resource: models.py model class, views (APIView-style stubs), urls.py, and admin registration. ' + 'Writes files under the given app directory (created if missing). Does not run migrate.', inputSchema: { name: z.string().min(1).describe('Resource/model name, e.g. "Post" or "blog_post".'), app: z .string() .optional() .describe('Django app directory relative to the project root. Defaults to a kebab-case plural of the name.'), overwrite: z.boolean().optional().default(false), }, }, async ({ name, app, overwrite }) => { try { const django = requireDjango(); const model = pascalCase(name); const appName = app?.trim() || kebabCase(pluralize(model)).replace(/-/g, '_'); const appDir = resolveWorkspacePath(django.root, appName); fs.mkdirSync(appDir, { recursive: true }); const files = []; const modelFile = path.join(appDir, 'models.py'); const viewsFile = path.join(appDir, 'views.py'); const urlsFile = path.join(appDir, 'urls.py'); const adminFile = path.join(appDir, 'admin.py'); const appsFile = path.join(appDir, 'apps.py'); const initFile = path.join(appDir, '__init__.py'); for (const file of [modelFile, viewsFile, urlsFile, adminFile, appsFile]) { assertWritable(file, overwrite ?? false); } if (!fs.existsSync(initFile)) { fs.writeFileSync(initFile, ''); files.push(initFile); } fs.writeFileSync(modelFile, `from django.db import models class ${model}(models.Model): title = models.CharField(max_length=255) created_at = models.DateTimeField(auto_now_add=True) def __str__(self) -> str: return self.title `); files.push(modelFile); fs.writeFileSync(viewsFile, `from django.http import JsonResponse from django.views import View from .models import ${model} class ${model}ListView(View): def get(self, request): items = list(${model}.objects.values("id", "title", "created_at")) return JsonResponse({"results": items}) `); files.push(viewsFile); const routeName = kebabCase(pluralize(model)); fs.writeFileSync(urlsFile, `from django.urls import path from .views import ${model}ListView urlpatterns = [ path("${routeName}/", ${model}ListView.as_view(), name="${routeName}-list"), ] `); files.push(urlsFile); fs.writeFileSync(adminFile, `from django.contrib import admin from .models import ${model} admin.site.register(${model}) `); files.push(adminFile); fs.writeFileSync(appsFile, `from django.apps import AppConfig class ${pascalCase(appName)}Config(AppConfig): default_auto_field = "django.db.models.BigAutoField" name = "${appName}" `); files.push(appsFile); return jsonResult({ kind: 'django', model, app: appName, route: `/${routeName}/`, files, nextSteps: [ `Add "${appName}" to INSTALLED_APPS.`, `Include path("${appName}/", include("${appName}.urls")) in the project urls.py.`, 'Run python manage.py makemigrations && python manage.py migrate.', ], }); } catch (error) { return errorResult(toErrorMessage(error)); } }); } export function registerGenerateRailsResourceTool(server) { server.registerTool('generate_rails_resource', { title: 'Generate Rails Resource', description: 'Scaffold Rails model + controller stubs and append a resources route hint to config/routes.rb when present. ' + 'File-based (does not invoke `rails generate`).', inputSchema: { name: z.string().min(1).describe('Resource name, e.g. "Post" or "blog_post".'), api: z.boolean().optional().default(true).describe('If true, generate an API-style controller under app/controllers/api.'), overwrite: z.boolean().optional().default(false), }, }, async ({ name, api, overwrite }) => { try { const detected = detectProject(config.workspaceRoot); if (detected.backend?.kind !== 'rails') { return errorResult('No Rails project detected. Expected a Gemfile with rails and/or bin/rails.'); } const root = detected.backend.root; const model = pascalCase(name); const plural = pluralize(model); const controllerName = `${plural}Controller`; const table = kebabCase(plural).replace(/-/g, '_'); const modelFile = resolveWorkspacePath(root, path.join('app', 'models', `${kebabCase(model).replace(/-/g, '_')}.rb`)); const controllerRel = api ? path.join('app', 'controllers', 'api', `${kebabCase(plural).replace(/-/g, '_')}_controller.rb`) : path.join('app', 'controllers', `${kebabCase(plural).replace(/-/g, '_')}_controller.rb`); const controllerFile = resolveWorkspacePath(root, controllerRel); assertWritable(modelFile, overwrite ?? false); assertWritable(controllerFile, overwrite ?? false); fs.mkdirSync(path.dirname(modelFile), { recursive: true }); fs.mkdirSync(path.dirname(controllerFile), { recursive: true }); const files = []; fs.writeFileSync(modelFile, `class ${model} < ApplicationRecord # self.table_name = "${table}" end `); files.push(modelFile); const parent = api ? 'ApplicationController' : 'ApplicationController'; fs.writeFileSync(controllerFile, api ? `module Api class ${controllerName} < ${parent} def index render json: ${model}.all end def show render json: ${model}.find(params[:id]) end end end ` : `class ${controllerName} < ${parent} def index @${camelCase(plural)} = ${model}.all end def show @${camelCase(model)} = ${model}.find(params[:id]) end end `); files.push(controllerFile); const routesFile = path.join(root, 'config', 'routes.rb'); let routeWired = false; const routeLine = api ? ` namespace :api do\n resources :${table}, only: %i[index show]\n end` : ` resources :${table}, only: %i[index show]`; if (fs.existsSync(routesFile)) { const text = fs.readFileSync(routesFile, 'utf8'); if (!text.includes(`resources :${table}`)) { const wired = text.includes('Rails.application.routes.draw') ? text.replace(/Rails\.application\.routes\.draw do\s*\n/, (match) => `${match}${routeLine}\n`) : `${text.trimEnd()}\n${routeLine}\n`; fs.writeFileSync(routesFile, wired); routeWired = true; files.push(routesFile); } } return jsonResult({ kind: 'rails', model, controller: controllerName, api: Boolean(api), route: api ? `/api/${table}` : `/${table}`, routeWired, files, nextSteps: [ 'Add a migration for the model (bin/rails g migration Create...).', routeWired ? 'Review config/routes.rb for the new resources entry.' : 'Add a resources route in config/routes.rb.', ], }); } catch (error) { return errorResult(toErrorMessage(error)); } }); } export function registerGenerateSymfonyResourceTool(server) { server.registerTool('generate_symfony_resource', { title: 'Generate Symfony Resource', description: 'Scaffold a Symfony Entity + Repository + Controller stub under src/. File-based (does not invoke MakerBundle).', inputSchema: { name: z.string().min(1).describe('Resource/entity name, e.g. "Post" or "blog_post".'), overwrite: z.boolean().optional().default(false), }, }, async ({ name, overwrite }) => { try { const detected = detectProject(config.workspaceRoot); if (detected.backend?.kind !== 'symfony') { return errorResult('No Symfony project detected. Expected symfony/framework-bundle or bin/console.'); } const root = detected.backend.root; const entity = pascalCase(name); const route = kebabCase(pluralize(entity)); const entityFile = resolveWorkspacePath(root, path.join('src', 'Entity', `${entity}.php`)); const repoFile = resolveWorkspacePath(root, path.join('src', 'Repository', `${entity}Repository.php`)); const controllerFile = resolveWorkspacePath(root, path.join('src', 'Controller', `${entity}Controller.php`)); for (const file of [entityFile, repoFile, controllerFile]) { assertWritable(file, overwrite ?? false); fs.mkdirSync(path.dirname(file), { recursive: true }); } fs.writeFileSync(entityFile, `id; } public function getTitle(): ?string { return $this->title; } public function setTitle(string $title): static { $this->title = $title; return $this; } } `); fs.writeFileSync(repoFile, ` */ class ${entity}Repository extends ServiceEntityRepository { public function __construct(ManagerRegistry $registry) { parent::__construct($registry, ${entity}::class); } } `); fs.writeFileSync(controllerFile, ` ['id' => $item->getId(), 'title' => $item->getTitle()], $repository->findAll(), ); return $this->json(['results' => $items]); } } `); return jsonResult({ kind: 'symfony', entity, route: `/${route}`, files: [entityFile, repoFile, controllerFile], nextSteps: [ 'Run doctrine migrations if Doctrine is configured (php bin/console make:migration / doctrine:migrations:migrate).', 'Ensure Doctrine ORM and attributes are enabled for the Entity mapping.', ], }); } catch (error) { return errorResult(toErrorMessage(error)); } }); } //# sourceMappingURL=generateResources.js.map