init project
This commit is contained in:
+166
@@ -0,0 +1,166 @@
|
||||
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 { camelCase, kebabCase, pascalCase } from '../../lib/strings.js';
|
||||
import { errorResult, jsonResult, targetSchema, toErrorMessage } from '../shared.js';
|
||||
const METHODS = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'];
|
||||
export function registerGenerateApiRouteTool(server) {
|
||||
server.registerTool('generate_api_route', {
|
||||
title: 'Generate API Route',
|
||||
description: 'Scaffold a backend route + handler for the detected framework: controller + routes/api.php entry for Laravel, controller + Routes.php entry for CodeIgniter, an app/pages API route file for Next.js, or a standalone Express router file otherwise.',
|
||||
inputSchema: {
|
||||
name: z.string().min(1).describe('Resource/route name, e.g. "posts" or "UserProfile".'),
|
||||
method: z.enum(METHODS).optional().default('GET'),
|
||||
target: targetSchema,
|
||||
},
|
||||
}, async ({ name, method, target }) => {
|
||||
try {
|
||||
const detected = detectProject(config.workspaceRoot);
|
||||
const adapter = target === 'frontend'
|
||||
? detected.frontend
|
||||
: target === 'backend'
|
||||
? detected.backend
|
||||
: (detected.backend ?? detected.frontend ?? detected.primary);
|
||||
if (!adapter)
|
||||
return errorResult(`target "${target}" was requested but no such project was detected.`);
|
||||
switch (adapter.kind) {
|
||||
case 'laravel':
|
||||
return jsonResult(generateLaravelRoute(adapter.root, name, method));
|
||||
case 'codeigniter':
|
||||
return jsonResult(generateCodeIgniterRoute(adapter.root, name, method));
|
||||
case 'react':
|
||||
return jsonResult(generateNextApiRoute(adapter.root, name, method));
|
||||
default:
|
||||
return jsonResult(generateExpressRoute(adapter.root, name, method));
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
return errorResult(toErrorMessage(error));
|
||||
}
|
||||
});
|
||||
}
|
||||
function appendUnlessPresent(file, header, line) {
|
||||
fs.mkdirSync(path.dirname(file), { recursive: true });
|
||||
if (!fs.existsSync(file)) {
|
||||
fs.writeFileSync(file, header);
|
||||
}
|
||||
const existing = fs.readFileSync(file, 'utf8');
|
||||
if (!existing.includes(line.trim())) {
|
||||
fs.appendFileSync(file, `${line}\n`);
|
||||
}
|
||||
}
|
||||
function writeIfMissing(file, contents) {
|
||||
fs.mkdirSync(path.dirname(file), { recursive: true });
|
||||
if (fs.existsSync(file))
|
||||
return false;
|
||||
fs.writeFileSync(file, contents);
|
||||
return true;
|
||||
}
|
||||
function generateLaravelRoute(root, name, method) {
|
||||
const controllerName = `${pascalCase(name)}Controller`;
|
||||
const controllerFile = path.join(root, 'app', 'Http', 'Controllers', `${controllerName}.php`);
|
||||
const controllerCreated = writeIfMissing(controllerFile, `<?php
|
||||
|
||||
namespace App\\Http\\Controllers;
|
||||
|
||||
use Illuminate\\Http\\Request;
|
||||
|
||||
class ${controllerName}
|
||||
{
|
||||
public function __invoke(Request $request)
|
||||
{
|
||||
return response()->json([]);
|
||||
}
|
||||
}
|
||||
`);
|
||||
const routesFile = path.join(root, 'routes', 'api.php');
|
||||
appendUnlessPresent(routesFile, `<?php
|
||||
|
||||
use Illuminate\\Support\\Facades\\Route;
|
||||
|
||||
`, `Route::${method.toLowerCase()}('/${kebabCase(name)}', \\App\\Http\\Controllers\\${controllerName}::class);`);
|
||||
return { kind: 'laravel', controllerFile, controllerCreated, routesFile, route: `${method} /api/${kebabCase(name)}` };
|
||||
}
|
||||
function generateCodeIgniterRoute(root, name, method) {
|
||||
const controllerName = pascalCase(name);
|
||||
const controllerFile = path.join(root, 'app', 'Controllers', `${controllerName}.php`);
|
||||
const controllerCreated = writeIfMissing(controllerFile, `<?php
|
||||
|
||||
namespace App\\Controllers;
|
||||
|
||||
class ${controllerName} extends BaseController
|
||||
{
|
||||
public function index()
|
||||
{
|
||||
return $this->response->setJSON([]);
|
||||
}
|
||||
}
|
||||
`);
|
||||
const routesFile = path.join(root, 'app', 'Config', 'Routes.php');
|
||||
appendUnlessPresent(routesFile, `<?php
|
||||
|
||||
use CodeIgniter\\Router\\RouteCollection;
|
||||
|
||||
/** @var RouteCollection $routes */
|
||||
|
||||
`, `$routes->${method.toLowerCase()}('${kebabCase(name)}', '${controllerName}::index');`);
|
||||
return { kind: 'codeigniter', controllerFile, controllerCreated, routesFile, route: `${method} /${kebabCase(name)}` };
|
||||
}
|
||||
function generateNextApiRoute(root, name, method) {
|
||||
const appDir = path.join(root, 'app');
|
||||
const srcAppDir = path.join(root, 'src', 'app');
|
||||
const pagesDir = path.join(root, 'pages');
|
||||
const srcPagesDir = path.join(root, 'src', 'pages');
|
||||
const appRoot = fs.existsSync(appDir) ? appDir : fs.existsSync(srcAppDir) ? srcAppDir : null;
|
||||
const pagesRoot = fs.existsSync(pagesDir) ? pagesDir : fs.existsSync(srcPagesDir) ? srcPagesDir : null;
|
||||
if (appRoot) {
|
||||
const file = path.join(appRoot, 'api', kebabCase(name), 'route.ts');
|
||||
const created = writeIfMissing(file, `import { NextResponse } from 'next/server';
|
||||
|
||||
export async function ${method}() {
|
||||
return NextResponse.json({});
|
||||
}
|
||||
`);
|
||||
return { kind: 'next-app-router', file, created, route: `${method} /api/${kebabCase(name)}` };
|
||||
}
|
||||
if (pagesRoot) {
|
||||
const file = path.join(pagesRoot, 'api', `${kebabCase(name)}.ts`);
|
||||
const created = writeIfMissing(file, `import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
|
||||
export default function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
if (req.method !== '${method}') {
|
||||
res.status(405).end();
|
||||
return;
|
||||
}
|
||||
res.status(200).json({});
|
||||
}
|
||||
`);
|
||||
return { kind: 'next-pages-router', file, created, route: `${method} /api/${kebabCase(name)}` };
|
||||
}
|
||||
throw new Error('The detected React project has no "app" or "pages" directory, so it has no built-in API routes (this looks like a plain Vite React app, not Next.js). ' +
|
||||
'Scaffold a separate backend (Laravel/CodeIgniter/express-api) instead, or use target: "backend" if one already exists.');
|
||||
}
|
||||
function generateExpressRoute(root, name, method) {
|
||||
const file = path.join(root, 'routes', `${kebabCase(name)}.js`);
|
||||
const created = writeIfMissing(file, `import { Router } from 'express';
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.${method.toLowerCase()}('/', (req, res) => {
|
||||
res.json({});
|
||||
});
|
||||
|
||||
export default router;
|
||||
`);
|
||||
const varName = `${camelCase(name)}Router`;
|
||||
return {
|
||||
kind: 'express',
|
||||
file,
|
||||
created,
|
||||
route: `${method} /api/${kebabCase(name)}`,
|
||||
wiring: `Not auto-mounted. Add to your app entry point: import ${varName} from './routes/${kebabCase(name)}.js'; app.use('/api/${kebabCase(name)}', ${varName});`,
|
||||
};
|
||||
}
|
||||
//# sourceMappingURL=generateApiRoute.js.map
|
||||
Reference in New Issue
Block a user