init project
This commit is contained in:
+346
@@ -0,0 +1,346 @@
|
||||
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
|
||||
Reference in New Issue
Block a user