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 { requireCodeIgniter } from './requireCodeIgniter.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 registerGenerateCodeIgniterResourceTool(server) { server.registerTool('generate_codeigniter_resource', { title: 'Generate CodeIgniter Resource', description: 'One-shot scaffold of a full CodeIgniter 4 resource: model + migration + seeder + ResourceController, ' + 'then wire the route in app/Config/Routes.php. Supports an optional API version path prefix.', inputSchema: { name: z .string() .min(1) .describe('Resource name, e.g. "Post" or "blog_post". Converted to PascalCase for the model/controller.'), migrate: z .boolean() .optional() .default(false) .describe('If true, run `php spark migrate` after scaffolding. Requires a configured database.'), apiVersion: z .string() .optional() .describe('Optional API version prefix for the route, e.g. "v1" → resource at /v1/{name}.'), resourceController: z .boolean() .optional() .default(true) .describe('If true (default), generate a ResourceController via spark --resource.'), }, }, async ({ name, migrate, apiVersion, resourceController }) => { try { const ci = requireCodeIgniter(); const model = pascalCase(name); const controller = `${model}Controller`; const routeName = kebabCase(pluralize(model)); const versionPrefix = apiVersion?.replace(/^\/+|\/+$/g, '') || ''; const resourcePath = versionPrefix ? `${versionPrefix}/${routeName}` : routeName; const steps = []; const modelResult = await runCommand('php', ['spark', 'make:model', model, '--no-interaction'], { cwd: ci.root, timeoutMs: config.scaffoldCommandTimeoutMs, }); steps.push({ step: 'make:model', exitCode: modelResult.exitCode, failed: modelResult.failed, output: modelResult.output }); const migrationResult = await runCommand('php', ['spark', 'make:migration', `create_${kebabCase(pluralize(model))}_table`, '--no-interaction'], { cwd: ci.root, timeoutMs: config.scaffoldCommandTimeoutMs }); steps.push({ step: 'make:migration', exitCode: migrationResult.exitCode, failed: migrationResult.failed, output: migrationResult.output, }); const seederResult = await runCommand('php', ['spark', 'make:seeder', `${model}Seeder`, '--no-interaction'], { cwd: ci.root, timeoutMs: config.scaffoldCommandTimeoutMs, }); steps.push({ step: 'make:seeder', exitCode: seederResult.exitCode, failed: seederResult.failed, output: seederResult.output, }); const controllerArgs = ['spark', 'make:controller', controller, '--no-interaction']; if (resourceController !== false) controllerArgs.splice(3, 0, '--resource'); const controllerResult = await runCommand('php', controllerArgs, { cwd: ci.root, timeoutMs: config.scaffoldCommandTimeoutMs, }); steps.push({ step: 'make:controller', exitCode: controllerResult.exitCode, failed: controllerResult.failed, output: controllerResult.output, }); const routesFile = path.join(ci.root, 'app', 'Config', 'Routes.php'); const routeLine = `$routes->resource('${resourcePath}', ['controller' => '\\App\\Controllers\\${controller}']);`; const routeWired = wireRoute(routesFile, routeLine); steps.push({ step: 'wire-route', routesFile, routeLine, wired: routeWired }); let migrateResult = null; if (migrate) { migrateResult = await runCommand('php', ['spark', 'migrate', '--no-interaction'], { cwd: ci.root, timeoutMs: config.scaffoldCommandTimeoutMs, }); steps.push({ step: 'migrate', exitCode: migrateResult.exitCode, failed: migrateResult.failed, output: migrateResult.output, }); } return jsonResult({ model, controller, apiVersion: versionPrefix || null, route: `resource /${resourcePath}`, resourceController: resourceController !== false, routesFile, routeWired, steps, }); } catch (error) { return errorResult(toErrorMessage(error)); } }); } function wireRoute(routesFile, routeLine) { fs.mkdirSync(path.dirname(routesFile), { recursive: true }); if (!fs.existsSync(routesFile)) { fs.writeFileSync(routesFile, `