import fs from 'node:fs'; import path from 'node:path'; import { z } from 'zod'; import { config } from '../../config.js'; import { runCommand } from '../../lib/runCommand.js'; import { kebabCase, pascalCase } from '../../lib/strings.js'; import { errorResult, jsonResult, toErrorMessage } from '../shared.js'; import { requireLaravel } from './requireLaravel.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`; } export function registerGenerateLaravelResourceTool(server) { server.registerTool('generate_laravel_resource', { title: 'Generate Laravel Resource', description: 'One-shot scaffold of a full Laravel resource: model + migration + factory + seeder + controller, ' + 'then wire an apiResource/resource route. Wraps the relevant `php artisan make:*` calls.', inputSchema: { name: z .string() .min(1) .describe('Resource name, e.g. "Post" or "blog_post". Converted to PascalCase for the model/controller.'), api: z .boolean() .optional() .default(true) .describe('If true (default), generate an API controller (--api) and register Route::apiResource in routes/api.php. If false, generate a web resource controller and register Route::resource in routes/web.php.'), migrate: z .boolean() .optional() .default(false) .describe('If true, run `php artisan migrate` after scaffolding. Requires a configured database.'), }, }, async ({ name, api, migrate }) => { try { const laravel = requireLaravel(); const model = pascalCase(name); const controller = `${model}Controller`; const routeName = kebabCase(pluralize(model)); const steps = []; // Model + migration + factory + seeder in one go. const modelResult = await runCommand('php', ['artisan', 'make:model', model, '-mfs', '--no-interaction'], { cwd: laravel.root, timeoutMs: config.scaffoldCommandTimeoutMs }); steps.push({ step: 'make:model -mfs', ...modelResult }); const controllerArgs = api ? ['artisan', 'make:controller', controller, '--api', '--model=' + model, '--no-interaction'] : ['artisan', 'make:controller', controller, '--resource', '--model=' + model, '--no-interaction']; const controllerResult = await runCommand('php', controllerArgs, { cwd: laravel.root, timeoutMs: config.scaffoldCommandTimeoutMs, }); steps.push({ step: 'make:controller', ...controllerResult }); const routesFile = path.join(laravel.root, 'routes', api ? 'api.php' : 'web.php'); const routeLine = api ? `Route::apiResource('${routeName}', \\App\\Http\\Controllers\\${controller}::class);` : `Route::resource('${routeName}', \\App\\Http\\Controllers\\${controller}::class);`; const routeWired = wireRoute(routesFile, routeLine, api); steps.push({ step: 'wire-route', routesFile, routeLine, wired: routeWired }); let migrateResult = null; if (migrate) { migrateResult = await runCommand('php', ['artisan', 'migrate', '--no-interaction'], { cwd: laravel.root, timeoutMs: config.scaffoldCommandTimeoutMs, }); steps.push({ step: 'migrate', ...migrateResult }); } return jsonResult({ model, controller, route: `${api ? 'apiResource' : 'resource'} /${routeName}`, routesFile, routeWired, steps, }); } catch (error) { return errorResult(toErrorMessage(error)); } }); } function wireRoute(routesFile, routeLine, api) { fs.mkdirSync(path.dirname(routesFile), { recursive: true }); if (!fs.existsSync(routesFile)) { const header = api ? `