232 lines
7.9 KiB
JavaScript
232 lines
7.9 KiB
JavaScript
import fs from 'node:fs';
|
|
import { z } from 'zod';
|
|
import { config } from '../../config.js';
|
|
import { detectProject, resolvePackage } from '../../lib/frameworks/detect.js';
|
|
import { resolveWorkspacePath } from '../../lib/sandbox.js';
|
|
import { runCommand } from '../../lib/runCommand.js';
|
|
import { requireLaravel } from '../laravel/requireLaravel.js';
|
|
import { errorResult, jsonResult, packageSchema, toErrorMessage } from '../shared.js';
|
|
function nodeDockerfile() {
|
|
return `# Generated by web-dev-mcp
|
|
FROM node:24-alpine AS builder
|
|
WORKDIR /app
|
|
COPY package*.json ./
|
|
RUN npm ci
|
|
COPY . .
|
|
RUN npm run build
|
|
|
|
FROM node:24-alpine
|
|
WORKDIR /app
|
|
ENV NODE_ENV=production
|
|
COPY package*.json ./
|
|
RUN npm ci --omit=dev && npm cache clean --force
|
|
COPY --from=builder /app/dist ./dist
|
|
EXPOSE 3000
|
|
CMD ["node", "dist/index.js"]
|
|
`;
|
|
}
|
|
function phpDockerfile(kind) {
|
|
const publicDir = kind === 'laravel' ? 'public' : 'public';
|
|
return `# Generated by web-dev-mcp for ${kind}
|
|
FROM php:8.3-cli-alpine AS vendor
|
|
WORKDIR /app
|
|
COPY composer.json composer.lock* ./
|
|
RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer \\
|
|
&& composer install --no-dev --optimize-autoloader --no-interaction --prefer-dist || composer install --no-interaction --prefer-dist
|
|
|
|
FROM php:8.3-apache
|
|
WORKDIR /var/www/html
|
|
RUN docker-php-ext-install pdo pdo_mysql
|
|
COPY --from=vendor /app/vendor ./vendor
|
|
COPY . .
|
|
ENV APACHE_DOCUMENT_ROOT=/var/www/html/${publicDir}
|
|
RUN sed -ri -e 's!/var/www/html!\\\${APACHE_DOCUMENT_ROOT}!g' /etc/apache2/sites-available/*.conf \\
|
|
&& a2enmod rewrite
|
|
EXPOSE 80
|
|
`;
|
|
}
|
|
function nginxNodeCompose() {
|
|
return `# Generated by web-dev-mcp
|
|
services:
|
|
app:
|
|
build: .
|
|
ports:
|
|
- "3000:3000"
|
|
environment:
|
|
NODE_ENV: production
|
|
depends_on:
|
|
- redis
|
|
db:
|
|
image: postgres:16-alpine
|
|
environment:
|
|
POSTGRES_USER: app
|
|
POSTGRES_PASSWORD: app
|
|
POSTGRES_DB: app
|
|
ports:
|
|
- "5432:5432"
|
|
volumes:
|
|
- db_data:/var/lib/postgresql/data
|
|
redis:
|
|
image: redis:7-alpine
|
|
ports:
|
|
- "6379:6379"
|
|
volumes:
|
|
db_data:
|
|
`;
|
|
}
|
|
function phpCompose() {
|
|
return `# Generated by web-dev-mcp
|
|
services:
|
|
app:
|
|
build: .
|
|
ports:
|
|
- "8080:80"
|
|
environment:
|
|
APP_ENV: local
|
|
DB_HOST: db
|
|
DB_DATABASE: app
|
|
DB_USERNAME: app
|
|
DB_PASSWORD: app
|
|
REDIS_HOST: redis
|
|
depends_on:
|
|
- db
|
|
- redis
|
|
db:
|
|
image: mysql:8.0
|
|
environment:
|
|
MYSQL_DATABASE: app
|
|
MYSQL_USER: app
|
|
MYSQL_PASSWORD: app
|
|
MYSQL_ROOT_PASSWORD: root
|
|
ports:
|
|
- "3306:3306"
|
|
volumes:
|
|
- db_data:/var/lib/mysql
|
|
redis:
|
|
image: redis:7-alpine
|
|
ports:
|
|
- "6379:6379"
|
|
volumes:
|
|
db_data:
|
|
`;
|
|
}
|
|
export function registerGenerateDockerfileTool(server) {
|
|
server.registerTool('generate_dockerfile', {
|
|
title: 'Generate Dockerfile',
|
|
description: 'Write a stack-aware Dockerfile at the workspace (or package) root based on detected framework (Node multi-stage or PHP/Apache for Laravel/CodeIgniter).',
|
|
inputSchema: {
|
|
package: packageSchema,
|
|
overwrite: z.boolean().optional().default(false),
|
|
},
|
|
}, async ({ package: packageName, overwrite }) => {
|
|
try {
|
|
const root = packageName
|
|
? resolvePackage(config.workspaceRoot, packageName).pkg.absolutePath
|
|
: config.workspaceRoot;
|
|
const detected = detectProject(root);
|
|
const file = resolveWorkspacePath(root, 'Dockerfile');
|
|
if (!overwrite && fs.existsSync(file)) {
|
|
return errorResult(`"${file}" already exists. Pass overwrite: true to replace it.`);
|
|
}
|
|
let contents;
|
|
let stack;
|
|
if (detected.backend?.kind === 'laravel' ||
|
|
detected.backend?.kind === 'codeigniter' ||
|
|
detected.backend?.kind === 'symfony') {
|
|
stack = detected.backend.kind;
|
|
contents = phpDockerfile(detected.backend.kind === 'codeigniter' ? 'codeigniter' : 'laravel');
|
|
}
|
|
else {
|
|
const front = detected.frontend?.kind;
|
|
stack =
|
|
front === 'react' || front === 'vue' || front === 'nuxt'
|
|
? `${front}/node`
|
|
: detected.backend?.kind === 'django' || detected.backend?.kind === 'rails'
|
|
? detected.backend.kind
|
|
: 'node';
|
|
contents = nodeDockerfile();
|
|
}
|
|
fs.writeFileSync(file, contents);
|
|
return jsonResult({ file, stack, package: packageName ?? null });
|
|
}
|
|
catch (error) {
|
|
return errorResult(toErrorMessage(error));
|
|
}
|
|
});
|
|
}
|
|
export function registerGenerateComposeTool(server) {
|
|
server.registerTool('generate_compose', {
|
|
title: 'Generate docker-compose.yml',
|
|
description: 'Write a docker-compose.yml with app + db + redis presets inferred from the detected stack (Postgres for Node, MySQL for PHP).',
|
|
inputSchema: {
|
|
package: packageSchema,
|
|
overwrite: z.boolean().optional().default(false),
|
|
},
|
|
}, async ({ package: packageName, overwrite }) => {
|
|
try {
|
|
const root = packageName
|
|
? resolvePackage(config.workspaceRoot, packageName).pkg.absolutePath
|
|
: config.workspaceRoot;
|
|
const detected = detectProject(root);
|
|
const file = resolveWorkspacePath(root, 'docker-compose.yml');
|
|
if (!overwrite && fs.existsSync(file)) {
|
|
return errorResult(`"${file}" already exists. Pass overwrite: true to replace it.`);
|
|
}
|
|
const isPhp = detected.backend?.kind === 'laravel' ||
|
|
detected.backend?.kind === 'codeigniter' ||
|
|
detected.backend?.kind === 'symfony';
|
|
const contents = isPhp ? phpCompose() : nginxNodeCompose();
|
|
fs.writeFileSync(file, contents);
|
|
return jsonResult({
|
|
file,
|
|
stack: isPhp
|
|
? detected.backend?.kind
|
|
: (detected.frontend?.kind ?? detected.backend?.kind ?? 'node'),
|
|
services: ['app', 'db', 'redis'],
|
|
package: packageName ?? null,
|
|
});
|
|
}
|
|
catch (error) {
|
|
return errorResult(toErrorMessage(error));
|
|
}
|
|
});
|
|
}
|
|
export function registerGenerateSailTool(server) {
|
|
server.registerTool('generate_sail', {
|
|
title: 'Install Laravel Sail',
|
|
description: 'Run `php artisan sail:install` in a detected Laravel project. Requires confirm: true because it modifies docker-compose and related files.',
|
|
inputSchema: {
|
|
with: z
|
|
.array(z.string())
|
|
.optional()
|
|
.describe('Optional Sail services, e.g. ["mysql", "redis", "mailpit"].'),
|
|
confirm: z.boolean().optional().describe('Required as true to run sail:install.'),
|
|
},
|
|
}, async ({ with: services, confirm }) => {
|
|
try {
|
|
if (confirm !== true) {
|
|
return errorResult('generate_sail requires confirm: true.');
|
|
}
|
|
const laravel = requireLaravel();
|
|
const args = ['artisan', 'sail:install', '--no-interaction'];
|
|
if (services && services.length > 0) {
|
|
args.push(`--with=${services.join(',')}`);
|
|
}
|
|
const result = await runCommand('php', args, {
|
|
cwd: laravel.root,
|
|
timeoutMs: config.scaffoldCommandTimeoutMs,
|
|
});
|
|
return jsonResult({
|
|
services: services ?? null,
|
|
exitCode: result.exitCode,
|
|
failed: result.failed,
|
|
output: result.output,
|
|
cwd: result.cwd,
|
|
});
|
|
}
|
|
catch (error) {
|
|
return errorResult(toErrorMessage(error));
|
|
}
|
|
});
|
|
}
|
|
//# sourceMappingURL=generate.js.map
|