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
+43
View File
@@ -0,0 +1,43 @@
import { z } from 'zod';
import { config } from '../../config.js';
import { detectProject } from '../../lib/frameworks/detect.js';
import { resolveTarget } from '../../lib/frameworks/resolve.js';
import { addDependencyCommandFor, detectJsPackageManager } from '../../lib/frameworks/packageManager.js';
import { runCommand } from '../../lib/runCommand.js';
import { errorResult, jsonResult, targetSchema, toErrorMessage } from '../shared.js';
export function registerAddDependencyTool(server) {
server.registerTool('add_dependency', {
title: 'Add / Remove Dependency',
description: 'Add or remove a dependency, auto-detecting npm/pnpm/yarn/bun (package.json) vs. Composer (composer.json) from the resolved project. Pass `ecosystem` explicitly to override the auto-detection.',
inputSchema: {
packages: z.array(z.string().min(1)).min(1).describe('Package names to add/remove, e.g. ["axios"] or ["laravel/sanctum"].'),
ecosystem: z.enum(['npm', 'composer']).optional(),
dev: z.boolean().optional().default(false).describe('Install as a dev dependency (ignored for composer remove).'),
remove: z.boolean().optional().default(false),
target: targetSchema,
},
}, async ({ packages, ecosystem, dev, remove, target }) => {
try {
const detected = detectProject(config.workspaceRoot);
const { adapter, target: resolvedTarget } = resolveTarget(detected, target);
const resolvedEcosystem = ecosystem ?? (adapter.packageManager === 'composer' ? 'composer' : 'npm');
const { command, args } = resolvedEcosystem === 'composer'
? {
command: 'composer',
args: remove
? ['remove', ...packages]
: ['require', ...(dev ? ['--dev'] : []), ...packages],
}
: addDependencyCommandFor(detectJsPackageManager(adapter.root), packages, { dev, remove });
const result = await runCommand(command, args, {
cwd: adapter.root,
timeoutMs: config.scaffoldCommandTimeoutMs,
});
return jsonResult({ target: resolvedTarget, ecosystem: resolvedEcosystem, ...result });
}
catch (error) {
return errorResult(toErrorMessage(error));
}
});
}
//# sourceMappingURL=addDependency.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"addDependency.js","sourceRoot":"","sources":["../../../src/tools/scaffold/addDependency.ts"],"names":[],"mappings":"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"}
+166
View File
@@ -0,0 +1,166 @@
import fs from 'node:fs';
import path from 'node:path';
import { z } from 'zod';
import { config } from '../../config.js';
import { detectProject } from '../../lib/frameworks/detect.js';
import { camelCase, kebabCase, pascalCase } from '../../lib/strings.js';
import { errorResult, jsonResult, targetSchema, toErrorMessage } from '../shared.js';
const METHODS = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'];
export function registerGenerateApiRouteTool(server) {
server.registerTool('generate_api_route', {
title: 'Generate API Route',
description: 'Scaffold a backend route + handler for the detected framework: controller + routes/api.php entry for Laravel, controller + Routes.php entry for CodeIgniter, an app/pages API route file for Next.js, or a standalone Express router file otherwise.',
inputSchema: {
name: z.string().min(1).describe('Resource/route name, e.g. "posts" or "UserProfile".'),
method: z.enum(METHODS).optional().default('GET'),
target: targetSchema,
},
}, async ({ name, method, target }) => {
try {
const detected = detectProject(config.workspaceRoot);
const adapter = target === 'frontend'
? detected.frontend
: target === 'backend'
? detected.backend
: (detected.backend ?? detected.frontend ?? detected.primary);
if (!adapter)
return errorResult(`target "${target}" was requested but no such project was detected.`);
switch (adapter.kind) {
case 'laravel':
return jsonResult(generateLaravelRoute(adapter.root, name, method));
case 'codeigniter':
return jsonResult(generateCodeIgniterRoute(adapter.root, name, method));
case 'react':
return jsonResult(generateNextApiRoute(adapter.root, name, method));
default:
return jsonResult(generateExpressRoute(adapter.root, name, method));
}
}
catch (error) {
return errorResult(toErrorMessage(error));
}
});
}
function appendUnlessPresent(file, header, line) {
fs.mkdirSync(path.dirname(file), { recursive: true });
if (!fs.existsSync(file)) {
fs.writeFileSync(file, header);
}
const existing = fs.readFileSync(file, 'utf8');
if (!existing.includes(line.trim())) {
fs.appendFileSync(file, `${line}\n`);
}
}
function writeIfMissing(file, contents) {
fs.mkdirSync(path.dirname(file), { recursive: true });
if (fs.existsSync(file))
return false;
fs.writeFileSync(file, contents);
return true;
}
function generateLaravelRoute(root, name, method) {
const controllerName = `${pascalCase(name)}Controller`;
const controllerFile = path.join(root, 'app', 'Http', 'Controllers', `${controllerName}.php`);
const controllerCreated = writeIfMissing(controllerFile, `<?php
namespace App\\Http\\Controllers;
use Illuminate\\Http\\Request;
class ${controllerName}
{
public function __invoke(Request $request)
{
return response()->json([]);
}
}
`);
const routesFile = path.join(root, 'routes', 'api.php');
appendUnlessPresent(routesFile, `<?php
use Illuminate\\Support\\Facades\\Route;
`, `Route::${method.toLowerCase()}('/${kebabCase(name)}', \\App\\Http\\Controllers\\${controllerName}::class);`);
return { kind: 'laravel', controllerFile, controllerCreated, routesFile, route: `${method} /api/${kebabCase(name)}` };
}
function generateCodeIgniterRoute(root, name, method) {
const controllerName = pascalCase(name);
const controllerFile = path.join(root, 'app', 'Controllers', `${controllerName}.php`);
const controllerCreated = writeIfMissing(controllerFile, `<?php
namespace App\\Controllers;
class ${controllerName} extends BaseController
{
public function index()
{
return $this->response->setJSON([]);
}
}
`);
const routesFile = path.join(root, 'app', 'Config', 'Routes.php');
appendUnlessPresent(routesFile, `<?php
use CodeIgniter\\Router\\RouteCollection;
/** @var RouteCollection $routes */
`, `$routes->${method.toLowerCase()}('${kebabCase(name)}', '${controllerName}::index');`);
return { kind: 'codeigniter', controllerFile, controllerCreated, routesFile, route: `${method} /${kebabCase(name)}` };
}
function generateNextApiRoute(root, name, method) {
const appDir = path.join(root, 'app');
const srcAppDir = path.join(root, 'src', 'app');
const pagesDir = path.join(root, 'pages');
const srcPagesDir = path.join(root, 'src', 'pages');
const appRoot = fs.existsSync(appDir) ? appDir : fs.existsSync(srcAppDir) ? srcAppDir : null;
const pagesRoot = fs.existsSync(pagesDir) ? pagesDir : fs.existsSync(srcPagesDir) ? srcPagesDir : null;
if (appRoot) {
const file = path.join(appRoot, 'api', kebabCase(name), 'route.ts');
const created = writeIfMissing(file, `import { NextResponse } from 'next/server';
export async function ${method}() {
return NextResponse.json({});
}
`);
return { kind: 'next-app-router', file, created, route: `${method} /api/${kebabCase(name)}` };
}
if (pagesRoot) {
const file = path.join(pagesRoot, 'api', `${kebabCase(name)}.ts`);
const created = writeIfMissing(file, `import type { NextApiRequest, NextApiResponse } from 'next';
export default function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method !== '${method}') {
res.status(405).end();
return;
}
res.status(200).json({});
}
`);
return { kind: 'next-pages-router', file, created, route: `${method} /api/${kebabCase(name)}` };
}
throw new Error('The detected React project has no "app" or "pages" directory, so it has no built-in API routes (this looks like a plain Vite React app, not Next.js). ' +
'Scaffold a separate backend (Laravel/CodeIgniter/express-api) instead, or use target: "backend" if one already exists.');
}
function generateExpressRoute(root, name, method) {
const file = path.join(root, 'routes', `${kebabCase(name)}.js`);
const created = writeIfMissing(file, `import { Router } from 'express';
const router = Router();
router.${method.toLowerCase()}('/', (req, res) => {
res.json({});
});
export default router;
`);
const varName = `${camelCase(name)}Router`;
return {
kind: 'express',
file,
created,
route: `${method} /api/${kebabCase(name)}`,
wiring: `Not auto-mounted. Add to your app entry point: import ${varName} from './routes/${kebabCase(name)}.js'; app.use('/api/${kebabCase(name)}', ${varName});`,
};
}
//# sourceMappingURL=generateApiRoute.js.map
File diff suppressed because one or more lines are too long
+162
View File
@@ -0,0 +1,162 @@
import fs from 'node:fs';
import path from 'node:path';
import { z } from 'zod';
import { config } from '../../config.js';
import { detectProject } from '../../lib/frameworks/detect.js';
import { resolveWorkspacePath } from '../../lib/sandbox.js';
import { errorResult, jsonResult, toErrorMessage } from '../shared.js';
/**
* UI component generator. Prefers React (TSX/JSX) or Vue/Nuxt SFCs when a
* matching frontend is detected; otherwise writes a plain HTML/CSS/JS snippet.
*/
export function registerGenerateComponentTool(server) {
server.registerTool('generate_component', {
title: 'Generate Component',
description: 'Scaffold a UI component. Generates a React function component (TSX/JSX) when React is detected, ' +
'a Vue SFC (.vue) when Vue or Nuxt is detected, or a plain HTML/CSS/JS snippet otherwise.',
inputSchema: {
name: z.string().min(1).describe('Component name, e.g. "UserCard" (PascalCase recommended).'),
directory: z
.string()
.optional()
.describe('Directory relative to the frontend project root. Defaults to "src/components".'),
style: z.enum(['css-module', 'tailwind', 'none']).optional().default('css-module'),
withTest: z.boolean().optional().default(false),
clientComponent: z
.boolean()
.optional()
.default(false)
.describe('If true (Next.js App Router), prepends "use client" for Client Components. Ignored for Vue/Nuxt.'),
overwrite: z.boolean().optional().default(false),
},
}, async ({ name, directory, style, withTest, clientComponent, overwrite }) => {
try {
const detected = detectProject(config.workspaceRoot);
const frontend = detected.frontend;
if (frontend?.kind === 'react') {
return jsonResult(generateReactComponent(frontend.root, name, directory, style, withTest, clientComponent ?? false, overwrite));
}
if (frontend?.kind === 'vue' || frontend?.kind === 'nuxt') {
return jsonResult(generateVueSfc(frontend.root, frontend.kind, name, directory, style, withTest, overwrite));
}
return jsonResult(generatePlainComponent(config.workspaceRoot, name, directory, overwrite));
}
catch (error) {
return errorResult(toErrorMessage(error));
}
});
}
function generateReactComponent(frontendRoot, name, directory, style, withTest, clientComponent, overwrite) {
const typescript = fs.existsSync(path.join(frontendRoot, 'tsconfig.json'));
const ext = typescript ? 'tsx' : 'jsx';
const dir = resolveWorkspacePath(frontendRoot, directory ?? 'src/components');
const componentFile = path.join(dir, `${name}.${ext}`);
const files = [];
assertNotExists(componentFile, overwrite);
fs.mkdirSync(dir, { recursive: true });
const useClient = clientComponent ? `'use client';\n\n` : '';
const cssImport = style === 'css-module' ? `import styles from './${name}.module.css';\n` : '';
const rootClass = style === 'css-module' ? ' className={styles.root}' : style === 'tailwind' ? ' className="p-4"' : '';
fs.writeFileSync(componentFile, `${useClient}${cssImport}export function ${name}() {
return (
<div${rootClass}>
<p>${name}</p>
</div>
);
}
`);
files.push(componentFile);
if (style === 'css-module') {
const cssFile = path.join(dir, `${name}.module.css`);
assertNotExists(cssFile, overwrite);
fs.writeFileSync(cssFile, `.root {\n}\n`);
files.push(cssFile);
}
if (withTest) {
const testFile = path.join(dir, `${name}.test.${ext}`);
assertNotExists(testFile, overwrite);
fs.writeFileSync(testFile, `import { render, screen } from '@testing-library/react';
import { describe, expect, it } from 'vitest';
import { ${name} } from './${name}';
describe('${name}', () => {
it('renders', () => {
render(<${name} />);
expect(screen.getByText('${name}')).toBeInTheDocument();
});
});
`);
files.push(testFile);
}
return { kind: 'react', name, files, clientComponent };
}
function generateVueSfc(frontendRoot, kind, name, directory, style, withTest, overwrite) {
const typescript = fs.existsSync(path.join(frontendRoot, 'tsconfig.json'));
const dir = resolveWorkspacePath(frontendRoot, directory ?? 'src/components');
const componentFile = path.join(dir, `${name}.vue`);
const files = [];
assertNotExists(componentFile, overwrite);
fs.mkdirSync(dir, { recursive: true });
const langAttr = typescript ? ' lang="ts"' : '';
const rootClass = style === 'tailwind' ? ' class="p-4"' : style === 'none' ? '' : ' class="root"';
const styleBlock = style === 'none'
? ''
: style === 'tailwind'
? ''
: `\n<style scoped>\n.root {\n}\n</style>\n`;
fs.writeFileSync(componentFile, `<script setup${langAttr}>
</script>
<template>
<div${rootClass}>
<p>${name}</p>
</div>
</template>
${styleBlock}`);
files.push(componentFile);
if (withTest) {
const testExt = typescript ? 'ts' : 'js';
const testFile = path.join(dir, `${name}.test.${testExt}`);
assertNotExists(testFile, overwrite);
fs.writeFileSync(testFile, `import { mount } from '@vue/test-utils';
import { describe, expect, it } from 'vitest';
import ${name} from './${name}.vue';
describe('${name}', () => {
it('renders', () => {
const wrapper = mount(${name});
expect(wrapper.text()).toContain('${name}');
});
});
`);
files.push(testFile);
}
return { kind, name, files };
}
function generatePlainComponent(root, name, directory, overwrite) {
const dir = resolveWorkspacePath(root, directory ?? path.join('components', name));
fs.mkdirSync(dir, { recursive: true });
const htmlFile = path.join(dir, `${name}.html`);
const cssFile = path.join(dir, `${name}.css`);
const jsFile = path.join(dir, `${name}.js`);
for (const file of [htmlFile, cssFile, jsFile])
assertNotExists(file, overwrite);
fs.writeFileSync(htmlFile, `<div class="${name.toLowerCase()}">
<p>${name}</p>
</div>
`);
fs.writeFileSync(cssFile, `.${name.toLowerCase()} {\n}\n`);
fs.writeFileSync(jsFile, `// Behaviour for the ${name} component.\n`);
return {
kind: 'generic',
name,
files: [htmlFile, cssFile, jsFile],
note: 'No React/Vue/Nuxt frontend was detected, so a plain HTML/CSS/JS snippet was generated instead.',
};
}
function assertNotExists(file, overwrite) {
if (!overwrite && fs.existsSync(file)) {
throw new Error(`"${file}" already exists. Pass overwrite: true to replace it.`);
}
}
//# sourceMappingURL=generateComponent.js.map
File diff suppressed because one or more lines are too long
+11
View File
@@ -0,0 +1,11 @@
import { registerScaffoldProjectTool } from './scaffoldProject.js';
import { registerGenerateComponentTool } from './generateComponent.js';
import { registerGenerateApiRouteTool } from './generateApiRoute.js';
import { registerAddDependencyTool } from './addDependency.js';
export function registerScaffoldTools(server) {
registerScaffoldProjectTool(server);
registerGenerateComponentTool(server);
registerGenerateApiRouteTool(server);
registerAddDependencyTool(server);
}
//# sourceMappingURL=index.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/tools/scaffold/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,2BAA2B,EAAE,MAAM,sBAAsB,CAAC;AACnE,OAAO,EAAE,6BAA6B,EAAE,MAAM,wBAAwB,CAAC;AACvE,OAAO,EAAE,4BAA4B,EAAE,MAAM,uBAAuB,CAAC;AACrE,OAAO,EAAE,yBAAyB,EAAE,MAAM,oBAAoB,CAAC;AAE/D,MAAM,UAAU,qBAAqB,CAAC,MAAiB;IACrD,2BAA2B,CAAC,MAAM,CAAC,CAAC;IACpC,6BAA6B,CAAC,MAAM,CAAC,CAAC;IACtC,4BAA4B,CAAC,MAAM,CAAC,CAAC;IACrC,yBAAyB,CAAC,MAAM,CAAC,CAAC;AACpC,CAAC"}
+122
View File
@@ -0,0 +1,122 @@
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
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"scaffoldProject.js","sourceRoot":"","sources":["../../../src/tools/scaffold/scaffoldProject.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,SAAS,CAAC;AACzB,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,OAAO,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAC;AACzC,OAAO,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC;AAC5D,OAAO,EAAE,UAAU,EAAE,MAAM,yBAAyB,CAAC;AACrD,OAAO,EAAE,WAAW,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AACvE,OAAO,EAAE,uBAAuB,EAAE,MAAM,2BAA2B,CAAC;AACpE,OAAO,EAAE,uBAAuB,EAAE,MAAM,2BAA2B,CAAC;AAEpE,MAAM,SAAS,GAAG;IAChB,YAAY;IACZ,UAAU;IACV,MAAM;IACN,aAAa;IACb,aAAa;IACb,SAAS;IACT,aAAa;IACb,eAAe;IACf,mBAAmB;CACX,CAAC;AAIX,MAAM,UAAU,2BAA2B,CAAC,MAAiB;IAC3D,MAAM,CAAC,YAAY,CACjB,kBAAkB,EAClB;QACE,KAAK,EAAE,kBAAkB;QACzB,WAAW,EAAE,8DAA8D,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,6IAA6I;QAC5O,WAAW,EAAE;YACX,QAAQ,EAAE,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC;YAC3B,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,gCAAgC,CAAC;YAClE,SAAS,EAAE,CAAC;iBACT,MAAM,EAAE;iBACR,QAAQ,EAAE;iBACV,QAAQ,CAAC,6GAA6G,CAAC;SAC3H;KACF,EACD,KAAK,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,SAAS,EAAE,EAAE,EAAE;QACtC,IAAI,CAAC;YACH,MAAM,SAAS,GAAG,oBAAoB,CAAC,MAAM,CAAC,aAAa,EAAE,SAAS,IAAI,GAAG,CAAC,CAAC;YAC/E,EAAE,CAAC,SAAS,CAAC,SAAS,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;YAE7C,MAAM,SAAS,GAAG,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,IAAI,CAAC,CAAC;YAC7C,IAAI,EAAE,CAAC,UAAU,CAAC,SAAS,CAAC,IAAI,EAAE,CAAC,WAAW,CAAC,SAAS,CAAC,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;gBACrE,OAAO,WAAW,CAAC,IAAI,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,aAAa,EAAE,SAAS,CAAC,oCAAoC,CAAC,CAAC;YAC7G,CAAC;YAED,MAAM,MAAM,GAAG,MAAM,WAAW,CAAC,QAAQ,EAAE,IAAI,EAAE,SAAS,CAAC,CAAC;YAC5D,OAAO,UAAU,CAAC,MAAM,CAAC,CAAC;QAC5B,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,OAAO,WAAW,CAAC,cAAc,CAAC,KAAK,CAAC,CAAC,CAAC;QAC5C,CAAC;IACH,CAAC,CACF,CAAC;AACJ,CAAC;AAED,KAAK,UAAU,WAAW,CAAC,QAAkB,EAAE,IAAY,EAAE,SAAiB;IAC5E,QAAQ,QAAQ,EAAE,CAAC;QACjB,KAAK,YAAY;YACf,OAAO,YAAY,CAAC,IAAI,EAAE,SAAS,EAAE,UAAU,CAAC,CAAC;QACnD,KAAK,UAAU;YACb,OAAO,YAAY,CAAC,IAAI,EAAE,SAAS,EAAE,QAAQ,CAAC,CAAC;QACjD,KAAK,MAAM;YACT,OAAO,YAAY,CAAC,IAAI,EAAE,SAAS,CAAC,CAAC;QACvC,KAAK,aAAa;YAChB,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,uBAAuB,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,IAAI,CAAC,EAAE,IAAI,CAAC,EAAE,CAAC;QACvF,KAAK,aAAa;YAChB,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,uBAAuB,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,IAAI,CAAC,EAAE,IAAI,CAAC,EAAE,CAAC;QACvF,KAAK,SAAS;YACZ,OAAO,uBAAuB,CAAC,iBAAiB,EAAE,IAAI,EAAE,SAAS,EAAE,QAAQ,CAAC,CAAC;QAC/E,KAAK,aAAa;YAChB,OAAO,uBAAuB,CAAC,yBAAyB,EAAE,IAAI,EAAE,SAAS,EAAE,QAAQ,CAAC,CAAC;QACvF,KAAK,eAAe;YAClB,OAAO,gBAAgB,CAAC,iBAAiB,EAAE,IAAI,EAAE,SAAS,EAAE,QAAQ,CAAC,CAAC;QACxE,KAAK,mBAAmB;YACtB,OAAO,gBAAgB,CAAC,yBAAyB,EAAE,IAAI,EAAE,SAAS,EAAE,QAAQ,CAAC,CAAC;QAChF,OAAO,CAAC,CAAC,CAAC;YACR,MAAM,UAAU,GAAU,QAAQ,CAAC;YACnC,MAAM,IAAI,KAAK,CAAC,uBAAuB,UAAoB,EAAE,CAAC,CAAC;QACjE,CAAC;IACH,CAAC;AACH,CAAC;AAED,KAAK,UAAU,YAAY,CAAC,IAAY,EAAE,SAAiB,EAAE,YAAmC;IAC9F,MAAM,MAAM,GAAG,MAAM,UAAU,CAAC,KAAK,EAAE,CAAC,QAAQ,EAAE,aAAa,EAAE,IAAI,EAAE,IAAI,EAAE,YAAY,EAAE,YAAY,CAAC,EAAE;QACxG,GAAG,EAAE,SAAS;QACd,SAAS,EAAE,MAAM,CAAC,wBAAwB;KAC3C,CAAC,CAAC;IACH,OAAO,EAAE,QAAQ,EAAE,YAAY,KAAK,UAAU,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,UAAU,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,IAAI,CAAC,EAAE,GAAG,MAAM,EAAE,CAAC;AAC5H,CAAC;AAED,KAAK,UAAU,YAAY,CAAC,IAAY,EAAE,SAAiB;IACzD,MAAM,MAAM,GAAG,MAAM,UAAU,CAC7B,KAAK,EACL;QACE,OAAO;QACP,wBAAwB;QACxB,IAAI;QACJ,MAAM;QACN,UAAU;QACV,OAAO;QACP,WAAW;QACX,gBAAgB;QAChB,KAAK;QACL,WAAW;QACX,eAAe;QACf,OAAO;KACR,EACD,EAAE,GAAG,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,CAAC,wBAAwB,EAAE,CAC/D,CAAC;IACF,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,IAAI,CAAC,EAAE,GAAG,MAAM,EAAE,CAAC;AAC3E,CAAC;AAED,KAAK,UAAU,uBAAuB,CAAC,eAAuB,EAAE,IAAY,EAAE,SAAiB,EAAE,QAAkB;IACjH,MAAM,MAAM,GAAG,MAAM,UAAU,CAAC,UAAU,EAAE,CAAC,gBAAgB,EAAE,eAAe,EAAE,IAAI,CAAC,EAAE;QACrF,GAAG,EAAE,SAAS;QACd,SAAS,EAAE,MAAM,CAAC,wBAAwB;KAC3C,CAAC,CAAC;IACH,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,IAAI,CAAC,EAAE,GAAG,MAAM,EAAE,CAAC;AACnE,CAAC;AAED,KAAK,UAAU,gBAAgB,CAAC,eAAuB,EAAE,IAAY,EAAE,SAAiB,EAAE,QAAkB;IAC1G,MAAM,UAAU,GAAG,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,IAAI,CAAC,CAAC;IAC9C,EAAE,CAAC,SAAS,CAAC,UAAU,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAE9C,MAAM,aAAa,GAAG,MAAM,UAAU,CAAC,UAAU,EAAE,CAAC,gBAAgB,EAAE,eAAe,EAAE,SAAS,CAAC,EAAE;QACjG,GAAG,EAAE,UAAU;QACf,SAAS,EAAE,MAAM,CAAC,wBAAwB;KAC3C,CAAC,CAAC;IACH,MAAM,cAAc,GAAG,MAAM,UAAU,CACrC,KAAK,EACL,CAAC,QAAQ,EAAE,aAAa,EAAE,UAAU,EAAE,IAAI,EAAE,YAAY,EAAE,UAAU,CAAC,EACrE,EAAE,GAAG,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,CAAC,wBAAwB,EAAE,CAChE,CAAC;IAEF,OAAO;QACL,QAAQ;QACR,IAAI,EAAE,UAAU;QAChB,OAAO,EAAE,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,SAAS,CAAC,EAAE,GAAG,aAAa,EAAE;QACrE,QAAQ,EAAE,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,UAAU,CAAC,EAAE,GAAG,cAAc,EAAE;QACxE,IAAI,EAAE,+JAA+J;KACtK,CAAC;AACJ,CAAC"}
+44
View File
@@ -0,0 +1,44 @@
import fs from 'node:fs';
import path from 'node:path';
export function writeExpressApiTemplate(targetDir, name) {
fs.mkdirSync(path.join(targetDir, 'routes'), { recursive: true });
fs.writeFileSync(path.join(targetDir, 'package.json'), `${JSON.stringify({
name,
version: '0.1.0',
private: true,
type: 'module',
scripts: {
dev: 'node --watch index.js',
start: 'node index.js',
test: 'echo "no tests yet" && exit 0',
},
dependencies: {
express: '^5.0.0',
},
}, null, 2)}\n`);
fs.writeFileSync(path.join(targetDir, 'index.js'), `import express from 'express';
import healthRouter from './routes/health.js';
const app = express();
app.use(express.json());
app.use('/api/health', healthRouter);
const port = process.env.PORT ?? 3000;
app.listen(port, () => {
console.log(\`${name} listening on http://localhost:\${port}\`);
});
`);
fs.writeFileSync(path.join(targetDir, 'routes', 'health.js'), `import { Router } from 'express';
const router = Router();
router.get('/', (_req, res) => {
res.json({ status: 'ok' });
});
export default router;
`);
fs.writeFileSync(path.join(targetDir, '.gitignore'), 'node_modules/\n');
return targetDir;
}
//# sourceMappingURL=expressApi.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"expressApi.js","sourceRoot":"","sources":["../../../../src/tools/scaffold/templates/expressApi.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,SAAS,CAAC;AACzB,OAAO,IAAI,MAAM,WAAW,CAAC;AAE7B,MAAM,UAAU,uBAAuB,CAAC,SAAiB,EAAE,IAAY;IACrE,EAAE,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,QAAQ,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAElE,EAAE,CAAC,aAAa,CACd,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,cAAc,CAAC,EACpC,GAAG,IAAI,CAAC,SAAS,CACf;QACE,IAAI;QACJ,OAAO,EAAE,OAAO;QAChB,OAAO,EAAE,IAAI;QACb,IAAI,EAAE,QAAQ;QACd,OAAO,EAAE;YACP,GAAG,EAAE,uBAAuB;YAC5B,KAAK,EAAE,eAAe;YACtB,IAAI,EAAE,+BAA+B;SACtC;QACD,YAAY,EAAE;YACZ,OAAO,EAAE,QAAQ;SAClB;KACF,EACD,IAAI,EACJ,CAAC,CACF,IAAI,CACN,CAAC;IAEF,EAAE,CAAC,aAAa,CACd,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,UAAU,CAAC,EAChC;;;;;;;;;kBASc,IAAI;;CAErB,CACE,CAAC;IAEF,EAAE,CAAC,aAAa,CACd,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,QAAQ,EAAE,WAAW,CAAC,EAC3C;;;;;;;;;CASH,CACE,CAAC;IAEF,EAAE,CAAC,aAAa,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,YAAY,CAAC,EAAE,iBAAiB,CAAC,CAAC;IAExE,OAAO,SAAS,CAAC;AACnB,CAAC"}
+45
View File
@@ -0,0 +1,45 @@
import fs from 'node:fs';
import path from 'node:path';
export function writeStaticHtmlTemplate(targetDir, name) {
fs.mkdirSync(targetDir, { recursive: true });
fs.writeFileSync(path.join(targetDir, 'index.html'), `<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>${name}</title>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<main>
<h1>${name}</h1>
<p>Edit <code>index.html</code>, <code>style.css</code>, and <code>script.js</code> to get started.</p>
</main>
<script src="script.js"></script>
</body>
</html>
`);
fs.writeFileSync(path.join(targetDir, 'style.css'), `:root {
color-scheme: light dark;
font-family: system-ui, sans-serif;
}
body {
margin: 0;
display: grid;
place-items: center;
min-height: 100vh;
}
main {
max-width: 40rem;
padding: 2rem;
text-align: center;
}
`);
fs.writeFileSync(path.join(targetDir, 'script.js'), `// Entry point for ${name}.
console.log('${name} loaded');
`);
return targetDir;
}
//# sourceMappingURL=staticHtml.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"staticHtml.js","sourceRoot":"","sources":["../../../../src/tools/scaffold/templates/staticHtml.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,SAAS,CAAC;AACzB,OAAO,IAAI,MAAM,WAAW,CAAC;AAE7B,MAAM,UAAU,uBAAuB,CAAC,SAAiB,EAAE,IAAY;IACrE,EAAE,CAAC,SAAS,CAAC,SAAS,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAE7C,EAAE,CAAC,aAAa,CACd,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,YAAY,CAAC,EAClC;;;;;aAKS,IAAI;;;;;YAKL,IAAI;;;;;;CAMf,CACE,CAAC;IAEF,EAAE,CAAC,aAAa,CACd,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,WAAW,CAAC,EACjC;;;;;;;;;;;;;;;;;CAiBH,CACE,CAAC;IAEF,EAAE,CAAC,aAAa,CACd,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,WAAW,CAAC,EACjC,sBAAsB,IAAI;eACf,IAAI;CAClB,CACE,CAAC;IAEF,OAAO,SAAS,CAAC;AACnB,CAAC"}