init project
This commit is contained in:
@@ -0,0 +1,144 @@
|
||||
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
|
||||
File diff suppressed because one or more lines are too long
+61
@@ -0,0 +1,61 @@
|
||||
import { z } from 'zod';
|
||||
import { config } from '../../config.js';
|
||||
import { hasComposerDependency, readComposerJson } from '../../lib/frameworks/composer.js';
|
||||
import { runCommand } from '../../lib/runCommand.js';
|
||||
import { errorResult, jsonResult, toErrorMessage } from '../shared.js';
|
||||
import { requireCodeIgniter } from './requireCodeIgniter.js';
|
||||
export function registerGenerateCodeIgniterShieldAuthTool(server) {
|
||||
server.registerTool('generate_codeigniter_shield_auth', {
|
||||
title: 'Scaffold CodeIgniter Shield Auth',
|
||||
description: 'Install/setup CodeIgniter Shield auth helpers. Requires codeigniter4/shield. ' +
|
||||
'Runs `php spark shield:setup` (and optionally composer require). Requires confirm: true.',
|
||||
inputSchema: {
|
||||
installPackage: z
|
||||
.boolean()
|
||||
.optional()
|
||||
.default(false)
|
||||
.describe('If true and Shield is missing, run composer require codeigniter4/shield first.'),
|
||||
confirm: z.boolean().optional().describe('Required as true — Shield setup modifies project files.'),
|
||||
},
|
||||
}, async ({ installPackage, confirm }) => {
|
||||
try {
|
||||
if (confirm !== true) {
|
||||
return errorResult('generate_codeigniter_shield_auth requires confirm: true.');
|
||||
}
|
||||
const ci = requireCodeIgniter();
|
||||
const composer = readComposerJson(ci.root);
|
||||
const steps = [];
|
||||
let hasShield = hasComposerDependency(composer, 'codeigniter4/shield');
|
||||
if (!hasShield && installPackage) {
|
||||
const requireResult = await runCommand('composer', ['require', 'codeigniter4/shield', '--no-interaction'], { cwd: ci.root, timeoutMs: config.scaffoldCommandTimeoutMs });
|
||||
steps.push({
|
||||
step: 'composer-require',
|
||||
exitCode: requireResult.exitCode,
|
||||
failed: requireResult.failed,
|
||||
output: requireResult.output,
|
||||
});
|
||||
if (requireResult.failed) {
|
||||
return errorResult(`Failed to install codeigniter4/shield:\n${requireResult.output}`);
|
||||
}
|
||||
hasShield = true;
|
||||
}
|
||||
if (!hasShield) {
|
||||
return errorResult('codeigniter4/shield is not installed. Pass installPackage: true (with confirm: true) ' +
|
||||
'or run composer_require with package "codeigniter4/shield" first.');
|
||||
}
|
||||
const setup = await runCommand('php', ['spark', 'shield:setup', '--no-interaction'], {
|
||||
cwd: ci.root,
|
||||
timeoutMs: config.scaffoldCommandTimeoutMs,
|
||||
});
|
||||
steps.push({ step: 'shield:setup', exitCode: setup.exitCode, failed: setup.failed, output: setup.output });
|
||||
return jsonResult({
|
||||
package: 'codeigniter4/shield',
|
||||
steps,
|
||||
});
|
||||
}
|
||||
catch (error) {
|
||||
return errorResult(toErrorMessage(error));
|
||||
}
|
||||
});
|
||||
}
|
||||
//# sourceMappingURL=generateShieldAuth.js.map
|
||||
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"generateShieldAuth.js","sourceRoot":"","sources":["../../../src/tools/codeigniter/generateShieldAuth.ts"],"names":[],"mappings":"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"}
|
||||
Vendored
+13
@@ -0,0 +1,13 @@
|
||||
import { registerSparkTool } from './spark.js';
|
||||
import { registerGenerateCodeIgniterResourceTool } from './generateCodeIgniterResource.js';
|
||||
import { registerCodeIgniterTestTool } from './runCodeIgniterTests.js';
|
||||
import { registerGenerateCodeIgniterShieldAuthTool } from './generateShieldAuth.js';
|
||||
import { registerSparkDiscoverTool } from './sparkDiscover.js';
|
||||
export function registerCodeIgniterTools(server) {
|
||||
registerSparkTool(server);
|
||||
registerGenerateCodeIgniterResourceTool(server);
|
||||
registerCodeIgniterTestTool(server);
|
||||
registerGenerateCodeIgniterShieldAuthTool(server);
|
||||
registerSparkDiscoverTool(server);
|
||||
}
|
||||
//# sourceMappingURL=index.js.map
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/tools/codeigniter/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAC/C,OAAO,EAAE,uCAAuC,EAAE,MAAM,kCAAkC,CAAC;AAC3F,OAAO,EAAE,2BAA2B,EAAE,MAAM,0BAA0B,CAAC;AACvE,OAAO,EAAE,yCAAyC,EAAE,MAAM,yBAAyB,CAAC;AACpF,OAAO,EAAE,yBAAyB,EAAE,MAAM,oBAAoB,CAAC;AAE/D,MAAM,UAAU,wBAAwB,CAAC,MAAiB;IACxD,iBAAiB,CAAC,MAAM,CAAC,CAAC;IAC1B,uCAAuC,CAAC,MAAM,CAAC,CAAC;IAChD,2BAA2B,CAAC,MAAM,CAAC,CAAC;IACpC,yCAAyC,CAAC,MAAM,CAAC,CAAC;IAClD,yBAAyB,CAAC,MAAM,CAAC,CAAC;AACpC,CAAC"}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
import { config } from '../../config.js';
|
||||
import { detectProject } from '../../lib/frameworks/detect.js';
|
||||
import { CodeIgniterAdapter } from '../../lib/frameworks/codeigniter.js';
|
||||
export function requireCodeIgniter() {
|
||||
const detected = detectProject(config.workspaceRoot);
|
||||
if (detected.backend instanceof CodeIgniterAdapter) {
|
||||
return detected.backend;
|
||||
}
|
||||
throw new Error('No CodeIgniter 4 project detected in WORKSPACE_ROOT. ' +
|
||||
'Expected composer.json with codeigniter4/framework (or a spark file) at the workspace root.');
|
||||
}
|
||||
//# sourceMappingURL=requireCodeIgniter.js.map
|
||||
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"requireCodeIgniter.js","sourceRoot":"","sources":["../../../src/tools/codeigniter/requireCodeIgniter.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAC;AACzC,OAAO,EAAE,aAAa,EAAE,MAAM,gCAAgC,CAAC;AAC/D,OAAO,EAAE,kBAAkB,EAAE,MAAM,qCAAqC,CAAC;AAEzE,MAAM,UAAU,kBAAkB;IAChC,MAAM,QAAQ,GAAG,aAAa,CAAC,MAAM,CAAC,aAAa,CAAC,CAAC;IACrD,IAAI,QAAQ,CAAC,OAAO,YAAY,kBAAkB,EAAE,CAAC;QACnD,OAAO,QAAQ,CAAC,OAAO,CAAC;IAC1B,CAAC;IACD,MAAM,IAAI,KAAK,CACb,uDAAuD;QACrD,6FAA6F,CAChG,CAAC;AACJ,CAAC"}
|
||||
+71
@@ -0,0 +1,71 @@
|
||||
import path from 'node:path';
|
||||
import { z } from 'zod';
|
||||
import { config } from '../../config.js';
|
||||
import { vendorBinExists } from '../../lib/frameworks/composer.js';
|
||||
import { runCommand } from '../../lib/runCommand.js';
|
||||
import { errorResult, jsonResult, toErrorMessage } from '../shared.js';
|
||||
import { requireCodeIgniter } from './requireCodeIgniter.js';
|
||||
function vendorBin(root, bin) {
|
||||
const suffix = process.platform === 'win32' ? '.bat' : '';
|
||||
return path.join(root, 'vendor', 'bin', `${bin}${suffix}`);
|
||||
}
|
||||
function parsePhpUnitSummary(output) {
|
||||
const ok = output.match(/OK\s*\((\d+)\s+tests?,\s*(\d+)\s+assertions?\)/i);
|
||||
if (ok) {
|
||||
return { passed: true, tests: Number(ok[1]), assertions: Number(ok[2]), failures: 0, errors: 0 };
|
||||
}
|
||||
const summary = output.match(/Tests:\s*(\d+),\s*Assertions:\s*(\d+)(?:,\s*Errors:\s*(\d+))?(?:,\s*Failures:\s*(\d+))?(?:,\s*Skipped:\s*(\d+))?/i);
|
||||
if (summary) {
|
||||
return {
|
||||
passed: !/FAILURES!|ERRORS!/i.test(output),
|
||||
tests: Number(summary[1]),
|
||||
assertions: Number(summary[2]),
|
||||
errors: Number(summary[3] ?? 0),
|
||||
failures: Number(summary[4] ?? 0),
|
||||
skipped: Number(summary[5] ?? 0),
|
||||
};
|
||||
}
|
||||
return { passed: null, note: 'Could not parse a pass/fail summary from the runner output.' };
|
||||
}
|
||||
export function registerCodeIgniterTestTool(server) {
|
||||
server.registerTool('run_codeigniter_tests', {
|
||||
title: 'Run CodeIgniter Tests',
|
||||
description: "Run the CodeIgniter 4 project's PHPUnit-based test suite (CIUnitTestCase) via vendor/bin/phpunit, " +
|
||||
'or `php spark test` as a fallback. Returns parsed pass/fail counts when possible.',
|
||||
inputSchema: {
|
||||
extraArgs: z
|
||||
.array(z.string())
|
||||
.optional()
|
||||
.describe('Extra CLI args, e.g. ["--filter", "UserTest"].'),
|
||||
},
|
||||
}, async ({ extraArgs }) => {
|
||||
try {
|
||||
const ci = requireCodeIgniter();
|
||||
const args = extraArgs ?? [];
|
||||
let command;
|
||||
let cmdArgs;
|
||||
if (vendorBinExists(ci.root, 'phpunit')) {
|
||||
command = vendorBin(ci.root, 'phpunit');
|
||||
cmdArgs = args;
|
||||
}
|
||||
else if (vendorBinExists(ci.root, 'spark')) {
|
||||
// CI4 does not ship a built-in `spark test` command out of the box,
|
||||
// but projects sometimes add one. If phpunit is missing, try it.
|
||||
command = 'php';
|
||||
cmdArgs = ['spark', 'test', ...args];
|
||||
}
|
||||
else {
|
||||
return errorResult(`No vendor/bin/phpunit found at ${ci.root}. Run composer install (or run_install) first.`);
|
||||
}
|
||||
const result = await runCommand(command, cmdArgs, {
|
||||
cwd: ci.root,
|
||||
timeoutMs: config.scaffoldCommandTimeoutMs,
|
||||
});
|
||||
return jsonResult({ runner: 'phpunit', summary: parsePhpUnitSummary(result.output), ...result });
|
||||
}
|
||||
catch (error) {
|
||||
return errorResult(toErrorMessage(error));
|
||||
}
|
||||
});
|
||||
}
|
||||
//# sourceMappingURL=runCodeIgniterTests.js.map
|
||||
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"runCodeIgniterTests.js","sourceRoot":"","sources":["../../../src/tools/codeigniter/runCodeIgniterTests.ts"],"names":[],"mappings":"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"}
|
||||
Vendored
+57
@@ -0,0 +1,57 @@
|
||||
import { z } from 'zod';
|
||||
import { config } from '../../config.js';
|
||||
import { runCommand } from '../../lib/runCommand.js';
|
||||
import { errorResult, jsonResult, toErrorMessage } from '../shared.js';
|
||||
import { isAllowedSparkCommand, SPARK_ALLOWED, SPARK_DESTRUCTIVE } from './sparkAllowList.js';
|
||||
import { requireCodeIgniter } from './requireCodeIgniter.js';
|
||||
export function registerSparkTool(server) {
|
||||
server.registerTool('spark', {
|
||||
title: 'Run Spark Command',
|
||||
description: 'Run a whitelisted `php spark` subcommand in the detected CodeIgniter 4 project and return stdout/stderr. ' +
|
||||
`Allowed commands: ${SPARK_ALLOWED.join(', ')}. ` +
|
||||
'Destructive command (migrate:refresh) requires confirm: true.',
|
||||
inputSchema: {
|
||||
command: z
|
||||
.string()
|
||||
.min(1)
|
||||
.describe('Spark subcommand, e.g. "make:controller" or "migrate".'),
|
||||
args: z
|
||||
.array(z.string())
|
||||
.optional()
|
||||
.default([])
|
||||
.describe('Arguments forwarded after the subcommand, e.g. ["PostController", "--resource"].'),
|
||||
confirm: z
|
||||
.boolean()
|
||||
.optional()
|
||||
.describe('Required as true for destructive command (migrate:refresh).'),
|
||||
},
|
||||
}, async ({ command, args, confirm }) => {
|
||||
try {
|
||||
if (!isAllowedSparkCommand(command)) {
|
||||
return errorResult(`Spark command "${command}" is not on the allow-list. Allowed: ${SPARK_ALLOWED.join(', ')}.`);
|
||||
}
|
||||
if (SPARK_DESTRUCTIVE.has(command) && confirm !== true) {
|
||||
return errorResult(`Spark command "${command}" is destructive and requires confirm: true.`);
|
||||
}
|
||||
const ci = requireCodeIgniter();
|
||||
const phpArgs = ['spark', command, ...(args ?? [])];
|
||||
const result = await runCommand('php', phpArgs, {
|
||||
cwd: ci.root,
|
||||
timeoutMs: config.scaffoldCommandTimeoutMs,
|
||||
});
|
||||
return jsonResult({
|
||||
sparkCommand: command,
|
||||
sparkArgs: args ?? [],
|
||||
exitCode: result.exitCode,
|
||||
timedOut: result.timedOut,
|
||||
failed: result.failed,
|
||||
output: result.output,
|
||||
cwd: result.cwd,
|
||||
});
|
||||
}
|
||||
catch (error) {
|
||||
return errorResult(toErrorMessage(error));
|
||||
}
|
||||
});
|
||||
}
|
||||
//# sourceMappingURL=spark.js.map
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"spark.js","sourceRoot":"","sources":["../../../src/tools/codeigniter/spark.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,OAAO,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAC;AAEzC,OAAO,EAAE,UAAU,EAAE,MAAM,yBAAyB,CAAC;AACrD,OAAO,EAAE,WAAW,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AACvE,OAAO,EAAE,qBAAqB,EAAE,aAAa,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AAC9F,OAAO,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAC;AAE7D,MAAM,UAAU,iBAAiB,CAAC,MAAiB;IACjD,MAAM,CAAC,YAAY,CACjB,OAAO,EACP;QACE,KAAK,EAAE,mBAAmB;QAC1B,WAAW,EACT,2GAA2G;YAC3G,qBAAqB,aAAa,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI;YACjD,+DAA+D;QACjE,WAAW,EAAE;YACX,OAAO,EAAE,CAAC;iBACP,MAAM,EAAE;iBACR,GAAG,CAAC,CAAC,CAAC;iBACN,QAAQ,CAAC,wDAAwD,CAAC;YACrE,IAAI,EAAE,CAAC;iBACJ,KAAK,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC;iBACjB,QAAQ,EAAE;iBACV,OAAO,CAAC,EAAE,CAAC;iBACX,QAAQ,CAAC,kFAAkF,CAAC;YAC/F,OAAO,EAAE,CAAC;iBACP,OAAO,EAAE;iBACT,QAAQ,EAAE;iBACV,QAAQ,CAAC,6DAA6D,CAAC;SAC3E;KACF,EACD,KAAK,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,EAAE,EAAE;QACnC,IAAI,CAAC;YACH,IAAI,CAAC,qBAAqB,CAAC,OAAO,CAAC,EAAE,CAAC;gBACpC,OAAO,WAAW,CAChB,kBAAkB,OAAO,wCAAwC,aAAa,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAC7F,CAAC;YACJ,CAAC;YACD,IAAI,iBAAiB,CAAC,GAAG,CAAC,OAAO,CAAC,IAAI,OAAO,KAAK,IAAI,EAAE,CAAC;gBACvD,OAAO,WAAW,CAChB,kBAAkB,OAAO,8CAA8C,CACxE,CAAC;YACJ,CAAC;YAED,MAAM,EAAE,GAAG,kBAAkB,EAAE,CAAC;YAChC,MAAM,OAAO,GAAG,CAAC,OAAO,EAAE,OAAO,EAAE,GAAG,CAAC,IAAI,IAAI,EAAE,CAAC,CAAC,CAAC;YAEpD,MAAM,MAAM,GAAG,MAAM,UAAU,CAAC,KAAK,EAAE,OAAO,EAAE;gBAC9C,GAAG,EAAE,EAAE,CAAC,IAAI;gBACZ,SAAS,EAAE,MAAM,CAAC,wBAAwB;aAC3C,CAAC,CAAC;YACH,OAAO,UAAU,CAAC;gBAChB,YAAY,EAAE,OAAO;gBACrB,SAAS,EAAE,IAAI,IAAI,EAAE;gBACrB,QAAQ,EAAE,MAAM,CAAC,QAAQ;gBACzB,QAAQ,EAAE,MAAM,CAAC,QAAQ;gBACzB,MAAM,EAAE,MAAM,CAAC,MAAM;gBACrB,MAAM,EAAE,MAAM,CAAC,MAAM;gBACrB,GAAG,EAAE,MAAM,CAAC,GAAG;aAChB,CAAC,CAAC;QACL,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,OAAO,WAAW,CAAC,cAAc,CAAC,KAAK,CAAC,CAAC,CAAC;QAC5C,CAAC;IACH,CAAC,CACF,CAAC;AACJ,CAAC"}
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
/**
|
||||
* Spark subcommands the `spark` tool is willing to run.
|
||||
* Anything not on this list is rejected so the agent cannot pass arbitrary
|
||||
* spark commands (e.g. `spark make:migration` with a bad path, destructive
|
||||
* migrations without confirm, etc.).
|
||||
*/
|
||||
export const SPARK_ALLOWED = [
|
||||
'make:controller',
|
||||
'make:model',
|
||||
'make:entity',
|
||||
'make:filter',
|
||||
'make:command',
|
||||
'make:validation',
|
||||
'make:migration',
|
||||
'make:seeder',
|
||||
'make:test',
|
||||
'make:config',
|
||||
'migrate',
|
||||
'migrate:rollback',
|
||||
'migrate:refresh',
|
||||
'migrate:status',
|
||||
'db:seed',
|
||||
'routes',
|
||||
'cache:clear',
|
||||
'optimize',
|
||||
'list',
|
||||
'about',
|
||||
];
|
||||
export const SPARK_DESTRUCTIVE = new Set(['migrate:refresh']);
|
||||
export function isAllowedSparkCommand(command) {
|
||||
return SPARK_ALLOWED.includes(command);
|
||||
}
|
||||
//# sourceMappingURL=sparkAllowList.js.map
|
||||
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"sparkAllowList.js","sourceRoot":"","sources":["../../../src/tools/codeigniter/sparkAllowList.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,MAAM,CAAC,MAAM,aAAa,GAAG;IAC3B,iBAAiB;IACjB,YAAY;IACZ,aAAa;IACb,aAAa;IACb,cAAc;IACd,iBAAiB;IACjB,gBAAgB;IAChB,aAAa;IACb,WAAW;IACX,aAAa;IACb,SAAS;IACT,kBAAkB;IAClB,iBAAiB;IACjB,gBAAgB;IAChB,SAAS;IACT,QAAQ;IACR,aAAa;IACb,UAAU;IACV,MAAM;IACN,OAAO;CACC,CAAC;AAIX,MAAM,CAAC,MAAM,iBAAiB,GAAG,IAAI,GAAG,CAAS,CAAC,iBAAiB,CAAC,CAAC,CAAC;AAEtE,MAAM,UAAU,qBAAqB,CAAC,OAAe;IACnD,OAAQ,aAAmC,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC;AAChE,CAAC"}
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
import { config } from '../../config.js';
|
||||
import { runCommand } from '../../lib/runCommand.js';
|
||||
import { errorResult, jsonResult, toErrorMessage } from '../shared.js';
|
||||
import { isAllowedSparkCommand, SPARK_ALLOWED } from './sparkAllowList.js';
|
||||
import { requireCodeIgniter } from './requireCodeIgniter.js';
|
||||
export function registerSparkDiscoverTool(server) {
|
||||
server.registerTool('spark_discover', {
|
||||
title: 'Discover Spark Commands',
|
||||
description: 'Run `php spark list` and return only commands that are on the server allow-list (intersected with SPARK_ALLOWED).',
|
||||
inputSchema: {},
|
||||
}, async () => {
|
||||
try {
|
||||
const ci = requireCodeIgniter();
|
||||
const result = await runCommand('php', ['spark', 'list', '--no-interaction'], {
|
||||
cwd: ci.root,
|
||||
timeoutMs: config.defaultCommandTimeoutMs,
|
||||
});
|
||||
const discovered = new Set();
|
||||
for (const line of result.output.split(/\r?\n/)) {
|
||||
const match = line.match(/^\s*([a-z][\w:-]*)\s+/i);
|
||||
const command = match?.[1];
|
||||
if (command && isAllowedSparkCommand(command)) {
|
||||
discovered.add(command);
|
||||
}
|
||||
}
|
||||
// Also include allow-listed commands even if spark list formatting hid them.
|
||||
for (const allowed of SPARK_ALLOWED) {
|
||||
if (result.output.includes(allowed))
|
||||
discovered.add(allowed);
|
||||
}
|
||||
return jsonResult({
|
||||
exitCode: result.exitCode,
|
||||
failed: result.failed,
|
||||
allowed: [...SPARK_ALLOWED],
|
||||
discovered: [...discovered].sort(),
|
||||
note: 'Only allow-listed spark commands are returned. Use the spark tool to run one.',
|
||||
});
|
||||
}
|
||||
catch (error) {
|
||||
return errorResult(toErrorMessage(error));
|
||||
}
|
||||
});
|
||||
}
|
||||
//# sourceMappingURL=sparkDiscover.js.map
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"sparkDiscover.js","sourceRoot":"","sources":["../../../src/tools/codeigniter/sparkDiscover.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAC;AACzC,OAAO,EAAE,UAAU,EAAE,MAAM,yBAAyB,CAAC;AACrD,OAAO,EAAE,WAAW,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AACvE,OAAO,EAAE,qBAAqB,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAC3E,OAAO,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAC;AAE7D,MAAM,UAAU,yBAAyB,CAAC,MAAiB;IACzD,MAAM,CAAC,YAAY,CACjB,gBAAgB,EAChB;QACE,KAAK,EAAE,yBAAyB;QAChC,WAAW,EACT,mHAAmH;QACrH,WAAW,EAAE,EAAE;KAChB,EACD,KAAK,IAAI,EAAE;QACT,IAAI,CAAC;YACH,MAAM,EAAE,GAAG,kBAAkB,EAAE,CAAC;YAChC,MAAM,MAAM,GAAG,MAAM,UAAU,CAAC,KAAK,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,kBAAkB,CAAC,EAAE;gBAC5E,GAAG,EAAE,EAAE,CAAC,IAAI;gBACZ,SAAS,EAAE,MAAM,CAAC,uBAAuB;aAC1C,CAAC,CAAC;YAEH,MAAM,UAAU,GAAG,IAAI,GAAG,EAAU,CAAC;YACrC,KAAK,MAAM,IAAI,IAAI,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC,EAAE,CAAC;gBAChD,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,wBAAwB,CAAC,CAAC;gBACnD,MAAM,OAAO,GAAG,KAAK,EAAE,CAAC,CAAC,CAAC,CAAC;gBAC3B,IAAI,OAAO,IAAI,qBAAqB,CAAC,OAAO,CAAC,EAAE,CAAC;oBAC9C,UAAU,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;gBAC1B,CAAC;YACH,CAAC;YAED,6EAA6E;YAC7E,KAAK,MAAM,OAAO,IAAI,aAAa,EAAE,CAAC;gBACpC,IAAI,MAAM,CAAC,MAAM,CAAC,QAAQ,CAAC,OAAO,CAAC;oBAAE,UAAU,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;YAC/D,CAAC;YAED,OAAO,UAAU,CAAC;gBAChB,QAAQ,EAAE,MAAM,CAAC,QAAQ;gBACzB,MAAM,EAAE,MAAM,CAAC,MAAM;gBACrB,OAAO,EAAE,CAAC,GAAG,aAAa,CAAC;gBAC3B,UAAU,EAAE,CAAC,GAAG,UAAU,CAAC,CAAC,IAAI,EAAE;gBAClC,IAAI,EAAE,+EAA+E;aACtF,CAAC,CAAC;QACL,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,OAAO,WAAW,CAAC,cAAc,CAAC,KAAK,CAAC,CAAC,CAAC;QAC5C,CAAC;IACH,CAAC,CACF,CAAC;AACJ,CAAC"}
|
||||
Reference in New Issue
Block a user