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