init project

This commit is contained in:
root
2026-07-31 13:12:54 -04:00
parent 0da92d5e02
commit f3863f760c
7215 changed files with 1860260 additions and 1 deletions
+144
View File
@@ -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
View File
@@ -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
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"generateShieldAuth.js","sourceRoot":"","sources":["../../../src/tools/codeigniter/generateShieldAuth.ts"],"names":[],"mappings":"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"}
+13
View File
@@ -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
+1
View File
@@ -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
View File
@@ -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
+1
View File
@@ -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
View File
@@ -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
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"runCodeIgniterTests.js","sourceRoot":"","sources":["../../../src/tools/codeigniter/runCodeIgniterTests.ts"],"names":[],"mappings":"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"}
+57
View File
@@ -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
+1
View File
@@ -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
View File
@@ -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
+1
View File
@@ -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
View File
@@ -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
View File
@@ -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"}