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

144 lines
6.5 KiB
JavaScript

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, `<?php\n\nuse CodeIgniter\\Router\\RouteCollection;\n\n/**\n * @var RouteCollection $routes\n */\n\n${routeLine}\n`);
return true;
}
const existing = fs.readFileSync(routesFile, 'utf8');
if (existing.includes(routeLine.trim()))
return false;
let next = existing;
if (!/use\s+CodeIgniter\\Router\\RouteCollection\s*;/.test(next)) {
if (next.startsWith('<?php')) {
next = next.replace('<?php', "<?php\n\nuse CodeIgniter\\Router\\RouteCollection;");
}
else {
next = `<?php\n\nuse CodeIgniter\\Router\\RouteCollection;\n\n${next}`;
}
}
fs.writeFileSync(routesFile, `${next.trimEnd()}\n${routeLine}\n`);
return true;
}
//# sourceMappingURL=generateCodeIgniterResource.js.map