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

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