144 lines
6.5 KiB
JavaScript
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
|