Files
2026-07-31 13:12:54 -04:00

346 lines
13 KiB
JavaScript

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, `<?php
namespace App\\Entity;
use App\\Repository\\${entity}Repository;
use Doctrine\\ORM\\Mapping as ORM;
#[ORM\\Entity(repositoryClass: ${entity}Repository::class)]
class ${entity}
{
#[ORM\\Id]
#[ORM\\GeneratedValue]
#[ORM\\Column]
private ?int $id = null;
#[ORM\\Column(length: 255)]
private ?string $title = null;
public function getId(): ?int
{
return $this->id;
}
public function getTitle(): ?string
{
return $this->title;
}
public function setTitle(string $title): static
{
$this->title = $title;
return $this;
}
}
`);
fs.writeFileSync(repoFile, `<?php
namespace App\\Repository;
use App\\Entity\\${entity};
use Doctrine\\Bundle\\DoctrineBundle\\Repository\\ServiceEntityRepository;
use Doctrine\\Persistence\\ManagerRegistry;
/**
* @extends ServiceEntityRepository<${entity}>
*/
class ${entity}Repository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, ${entity}::class);
}
}
`);
fs.writeFileSync(controllerFile, `<?php
namespace App\\Controller;
use App\\Repository\\${entity}Repository;
use Symfony\\Bundle\\FrameworkBundle\\Controller\\AbstractController;
use Symfony\\Component\\HttpFoundation\\JsonResponse;
use Symfony\\Component\\Routing\\Attribute\\Route;
class ${entity}Controller extends AbstractController
{
#[Route('/${route}', name: 'app_${route}_index', methods: ['GET'])]
public function index(${entity}Repository $repository): JsonResponse
{
$items = array_map(
static fn (${entity} $item) => ['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