122 lines
5.4 KiB
JavaScript
122 lines
5.4 KiB
JavaScript
import fs from 'node:fs';
|
|
import path from 'node:path';
|
|
import { z } from 'zod';
|
|
import { config } from '../../config.js';
|
|
import { resolveWorkspacePath } from '../../lib/sandbox.js';
|
|
import { runCommand } from '../../lib/runCommand.js';
|
|
import { errorResult, jsonResult, toErrorMessage } from '../shared.js';
|
|
import { writeStaticHtmlTemplate } from './templates/staticHtml.js';
|
|
import { writeExpressApiTemplate } from './templates/expressApi.js';
|
|
const TEMPLATES = [
|
|
'vite-react',
|
|
'vite-vue',
|
|
'next',
|
|
'express-api',
|
|
'static-html',
|
|
'laravel',
|
|
'codeigniter',
|
|
'laravel-react',
|
|
'codeigniter-react',
|
|
];
|
|
export function registerScaffoldProjectTool(server) {
|
|
server.registerTool('scaffold_project', {
|
|
title: 'Scaffold Project',
|
|
description: `Create a new project from a template. Supported templates: ${TEMPLATES.join(', ')}. "laravel-react" and "codeigniter-react" create a PHP backend in "<name>/backend" and a separate Vite+React frontend in "<name>/frontend".`,
|
|
inputSchema: {
|
|
template: z.enum(TEMPLATES),
|
|
name: z.string().min(1).describe('Project/folder name to create.'),
|
|
directory: z
|
|
.string()
|
|
.optional()
|
|
.describe('Parent directory (relative to the workspace root) to create the project in. Defaults to the workspace root.'),
|
|
},
|
|
}, async ({ template, name, directory }) => {
|
|
try {
|
|
const parentDir = resolveWorkspacePath(config.workspaceRoot, directory ?? '.');
|
|
fs.mkdirSync(parentDir, { recursive: true });
|
|
const targetDir = path.join(parentDir, name);
|
|
if (fs.existsSync(targetDir) && fs.readdirSync(targetDir).length > 0) {
|
|
return errorResult(`"${path.relative(config.workspaceRoot, targetDir)}" already exists and is not empty.`);
|
|
}
|
|
const result = await runScaffold(template, name, parentDir);
|
|
return jsonResult(result);
|
|
}
|
|
catch (error) {
|
|
return errorResult(toErrorMessage(error));
|
|
}
|
|
});
|
|
}
|
|
async function runScaffold(template, name, parentDir) {
|
|
switch (template) {
|
|
case 'vite-react':
|
|
return scaffoldVite(name, parentDir, 'react-ts');
|
|
case 'vite-vue':
|
|
return scaffoldVite(name, parentDir, 'vue-ts');
|
|
case 'next':
|
|
return scaffoldNext(name, parentDir);
|
|
case 'express-api':
|
|
return { template, path: writeExpressApiTemplate(path.join(parentDir, name), name) };
|
|
case 'static-html':
|
|
return { template, path: writeStaticHtmlTemplate(path.join(parentDir, name), name) };
|
|
case 'laravel':
|
|
return scaffoldComposerProject('laravel/laravel', name, parentDir, template);
|
|
case 'codeigniter':
|
|
return scaffoldComposerProject('codeigniter4/appstarter', name, parentDir, template);
|
|
case 'laravel-react':
|
|
return scaffoldCombined('laravel/laravel', name, parentDir, template);
|
|
case 'codeigniter-react':
|
|
return scaffoldCombined('codeigniter4/appstarter', name, parentDir, template);
|
|
default: {
|
|
const exhaustive = template;
|
|
throw new Error(`Unhandled template: ${exhaustive}`);
|
|
}
|
|
}
|
|
}
|
|
async function scaffoldVite(name, parentDir, viteTemplate) {
|
|
const result = await runCommand('npm', ['create', 'vite@latest', name, '--', '--template', viteTemplate], {
|
|
cwd: parentDir,
|
|
timeoutMs: config.scaffoldCommandTimeoutMs,
|
|
});
|
|
return { template: viteTemplate === 'react-ts' ? 'vite-react' : 'vite-vue', path: path.join(parentDir, name), ...result };
|
|
}
|
|
async function scaffoldNext(name, parentDir) {
|
|
const result = await runCommand('npx', [
|
|
'--yes',
|
|
'create-next-app@latest',
|
|
name,
|
|
'--ts',
|
|
'--eslint',
|
|
'--app',
|
|
'--src-dir',
|
|
'--import-alias',
|
|
'@/*',
|
|
'--use-npm',
|
|
'--no-tailwind',
|
|
'--yes',
|
|
], { cwd: parentDir, timeoutMs: config.scaffoldCommandTimeoutMs });
|
|
return { template: 'next', path: path.join(parentDir, name), ...result };
|
|
}
|
|
async function scaffoldComposerProject(composerPackage, name, parentDir, template) {
|
|
const result = await runCommand('composer', ['create-project', composerPackage, name], {
|
|
cwd: parentDir,
|
|
timeoutMs: config.scaffoldCommandTimeoutMs,
|
|
});
|
|
return { template, path: path.join(parentDir, name), ...result };
|
|
}
|
|
async function scaffoldCombined(composerPackage, name, parentDir, template) {
|
|
const projectDir = path.join(parentDir, name);
|
|
fs.mkdirSync(projectDir, { recursive: true });
|
|
const backendResult = await runCommand('composer', ['create-project', composerPackage, 'backend'], {
|
|
cwd: projectDir,
|
|
timeoutMs: config.scaffoldCommandTimeoutMs,
|
|
});
|
|
const frontendResult = await runCommand('npm', ['create', 'vite@latest', 'frontend', '--', '--template', 'react-ts'], { cwd: projectDir, timeoutMs: config.scaffoldCommandTimeoutMs });
|
|
return {
|
|
template,
|
|
path: projectDir,
|
|
backend: { path: path.join(projectDir, 'backend'), ...backendResult },
|
|
frontend: { path: path.join(projectDir, 'frontend'), ...frontendResult },
|
|
note: 'Backend and frontend were scaffolded as independent siblings (not wired together via Inertia). Point the frontend at the backend API base URL as a next step.',
|
|
};
|
|
}
|
|
//# sourceMappingURL=scaffoldProject.js.map
|