UNPKG

codecrucible-synth

Version:

Production-Ready AI Development Platform with Multi-Voice Synthesis, Smithery MCP Integration, Enterprise Security, and Zero-Timeout Reliability

604 lines (516 loc) 18.4 kB
import { z } from 'zod'; import { BaseTool } from './base-tool.js'; import { promises as fs } from 'fs'; import { join, isAbsolute } from 'path'; import { exec } from 'child_process'; import { promisify } from 'util'; const execAsync = promisify(exec); const BuildProjectSchema = z.object({ buildTool: z .enum(['npm', 'yarn', 'webpack', 'vite', 'rollup', 'tsc', 'esbuild']) .default('npm') .describe('Build tool to use'), buildScript: z.string().optional().describe('Specific build script to run (overrides default)'), environment: z .enum(['development', 'production', 'test']) .default('production') .describe('Build environment'), watch: z.boolean().default(false).describe('Whether to run build in watch mode'), optimize: z.boolean().default(true).describe('Whether to optimize the build'), sourceMaps: z.boolean().default(false).describe('Whether to generate source maps'), timeout: z.number().default(300000).describe('Build timeout in milliseconds'), }); export class BuildAutomatorTool extends BaseTool { constructor(private agentContext: { workingDirectory: string }) { super({ name: 'buildProject', description: 'Builds the project using various build tools and configurations', category: 'Build Automation', parameters: BuildProjectSchema, }); } async execute(args: z.infer<typeof BuildProjectSchema>): Promise<string> { try { const { buildTool, buildScript, environment, watch, optimize, sourceMaps, timeout } = args; // Check if package.json exists to understand the project structure const packageJsonPath = join(this.agentContext.workingDirectory, 'package.json'); let packageJson: any = {}; try { const packageContent = await fs.readFile(packageJsonPath, 'utf-8'); packageJson = JSON.parse(packageContent); } catch { // No package.json found, will use basic commands } const command = this.buildCommand( buildTool, buildScript, packageJson, environment, watch, optimize, sourceMaps ); console.log(`Building project with command: ${command}`); const startTime = Date.now(); const { stdout, stderr } = await execAsync(command, { cwd: this.agentContext.workingDirectory, timeout, maxBuffer: 1024 * 1024 * 20, // 20MB buffer for build output env: { ...process.env, NODE_ENV: environment, OPTIMIZE: optimize.toString(), SOURCE_MAPS: sourceMaps.toString(), }, }); const duration = ((Date.now() - startTime) / 1000).toFixed(2); let result = `🚀 Build completed successfully in ${duration}s\n\n`; if (stdout) { result += `Build Output:\n${stdout}\n\n`; } if (stderr) { result += `Build Warnings:\n${stderr}\n\n`; } // Check for build artifacts const artifacts = await this.findBuildArtifacts(); if (artifacts.length > 0) { result += `📦 Build Artifacts Generated:\n${artifacts.join('\n')}\n\n`; } // Analyze build size if possible const sizeAnalysis = await this.analyzeBuildSize(); if (sizeAnalysis) { result += `📊 Build Size Analysis:\n${sizeAnalysis}`; } return result; } catch (error: any) { const duration = error.signal === 'SIGTERM' ? 'timed out' : 'failed'; return ( `❌ Build ${duration}:\n` + `Error: ${error.message}\n` + `${error.stdout ? `STDOUT:\n${error.stdout}\n` : ''}` + `${error.stderr ? `STDERR:\n${error.stderr}\n` : ''}` ); } } private buildCommand( buildTool: string, buildScript: string | undefined, packageJson: any, environment: string, watch: boolean, optimize: boolean, sourceMaps: boolean ): string { // If custom build script is provided, use it if (buildScript) { return buildScript; } // Check package.json scripts const scripts = packageJson.scripts || {}; switch (buildTool) { case 'npm': { if (scripts.build) return `npm run build${watch ? ':watch' : ''}`; if (scripts.compile) return 'npm run compile'; return 'npm run build'; // Will fail if no build script } case 'yarn': { if (scripts.build) return `yarn build${watch ? ':watch' : ''}`; if (scripts.compile) return 'yarn compile'; return 'yarn build'; } case 'webpack': { let webpackCmd = 'npx webpack'; if (environment === 'production') webpackCmd += ' --mode=production'; if (environment === 'development') webpackCmd += ' --mode=development'; if (watch) webpackCmd += ' --watch'; if (optimize) webpackCmd += ' --optimize-minimize'; if (sourceMaps) webpackCmd += ' --devtool source-map'; return webpackCmd; } case 'vite': { if (watch) return 'npx vite'; return 'npx vite build'; } case 'rollup': { let rollupCmd = 'npx rollup -c'; if (watch) rollupCmd += ' --watch'; if (environment === 'production') rollupCmd += ' --environment NODE_ENV:production'; return rollupCmd; } case 'tsc': { let tscCmd = 'npx tsc'; if (watch) tscCmd += ' --watch'; if (sourceMaps) tscCmd += ' --sourceMap'; return tscCmd; } case 'esbuild': { let esbuildCmd = 'npx esbuild src/index.ts --bundle --outdir=dist'; if (environment === 'production') esbuildCmd += ' --minify'; if (sourceMaps) esbuildCmd += ' --sourcemap'; if (watch) esbuildCmd += ' --watch'; return esbuildCmd; } default: return 'npm run build'; } } private async findBuildArtifacts(): Promise<string[]> { const commonBuildDirs = ['dist', 'build', 'out', 'lib', 'public']; const artifacts = []; for (const dir of commonBuildDirs) { try { const fullPath = join(this.agentContext.workingDirectory, dir); const stats = await fs.stat(fullPath); if (stats.isDirectory()) { const files = await fs.readdir(fullPath); if (files.length > 0) { artifacts.push(`${dir}/ (${files.length} files)`); } } } catch { // Directory doesn't exist, continue } } return artifacts; } private async analyzeBuildSize(): Promise<string | null> { const buildDirs = ['dist', 'build', 'out']; for (const dir of buildDirs) { try { const fullPath = join(this.agentContext.workingDirectory, dir); const size = await this.getDirectorySize(fullPath); if (size > 0) { return `${dir}: ${this.formatFileSize(size)}`; } } catch { // Directory doesn't exist or can't read } } return null; } private async getDirectorySize(dirPath: string): Promise<number> { let totalSize = 0; try { const items = await fs.readdir(dirPath); for (const item of items) { const itemPath = join(dirPath, item); const stats = await fs.stat(itemPath); if (stats.isDirectory()) { totalSize += await this.getDirectorySize(itemPath); } else { totalSize += stats.size; } } } catch { // Error reading directory } return totalSize; } private formatFileSize(bytes: number): string { if (bytes === 0) return '0 Bytes'; const k = 1024; const sizes = ['Bytes', 'KB', 'MB', 'GB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; } } const PackageManagerSchema = z.object({ action: z .enum(['install', 'update', 'remove', 'list', 'audit', 'outdated', 'init', 'publish']) .describe('Package management action'), packages: z.array(z.string()).optional().describe('Package names (for install/remove/update)'), packageManager: z.enum(['npm', 'yarn', 'pnpm']).default('npm').describe('Package manager to use'), flags: z.array(z.string()).optional().describe('Additional flags (e.g., --save-dev, --global)'), force: z.boolean().default(false).describe('Force the operation'), timeout: z.number().default(120000).describe('Operation timeout in milliseconds'), }); export class PackageManagerTool extends BaseTool { constructor(private agentContext: { workingDirectory: string }) { super({ name: 'managePackages', description: 'Manages project dependencies and packages', category: 'Package Management', parameters: PackageManagerSchema, }); } async execute(args: z.infer<typeof PackageManagerSchema>): Promise<string> { try { const { action, packages, packageManager, flags, force, timeout } = args; const command = this.buildPackageCommand(action, packages, packageManager, flags, force); console.log(`Running package management command: ${command}`); const startTime = Date.now(); const { stdout, stderr } = await execAsync(command, { cwd: this.agentContext.workingDirectory, timeout, maxBuffer: 1024 * 1024 * 10, // 10MB buffer }); const duration = ((Date.now() - startTime) / 1000).toFixed(2); let result = `📦 Package ${action} completed in ${duration}s\n\n`; if (stdout) { result += `Output:\n${stdout}\n\n`; } if (stderr) { result += `Warnings/Info:\n${stderr}\n\n`; } // Add specific analysis based on action if (action === 'install' || action === 'update') { const analysis = await this.analyzePackageChanges(); if (analysis) { result += `Package Analysis:\n${analysis}`; } } return result; } catch (error: any) { return ( `❌ Package ${args.action} failed:\n` + `Error: ${error.message}\n` + `${error.stdout ? `STDOUT:\n${error.stdout}\n` : ''}` + `${error.stderr ? `STDERR:\n${error.stderr}\n` : ''}` ); } } private buildPackageCommand( action: string, packages: string[] | undefined, packageManager: string, flags: string[] | undefined, force: boolean ): string { let command = packageManager; // Add action if (packageManager === 'npm') { switch (action) { case 'install': command += packages && packages.length > 0 ? ` install ${packages.join(' ')}` : ' install'; break; case 'update': command += packages && packages.length > 0 ? ` update ${packages.join(' ')}` : ' update'; break; case 'remove': command += ` uninstall ${packages?.join(' ') || ''}`; break; case 'list': command += ' list'; break; case 'audit': command += ' audit'; if (force) command += ' --audit-level moderate'; break; case 'outdated': command += ' outdated'; break; case 'init': command += ' init'; if (force) command += ' -y'; break; case 'publish': command += ' publish'; break; } } else if (packageManager === 'yarn') { switch (action) { case 'install': command += packages && packages.length > 0 ? ` add ${packages.join(' ')}` : ' install'; break; case 'update': command += packages && packages.length > 0 ? ` upgrade ${packages.join(' ')}` : ' upgrade'; break; case 'remove': command += ` remove ${packages?.join(' ') || ''}`; break; case 'list': command += ' list'; break; case 'audit': command += ' audit'; break; case 'outdated': command += ' outdated'; break; case 'init': command += ' init'; if (force) command += ' -y'; break; case 'publish': command += ' publish'; break; } } // Add flags if (flags && flags.length > 0) { command += ` ${flags.join(' ')}`; } return command; } private async analyzePackageChanges(): Promise<string | null> { try { const packageJsonPath = join(this.agentContext.workingDirectory, 'package.json'); const lockfilePath = join(this.agentContext.workingDirectory, 'package-lock.json'); const [packageJson, lockfileExists] = await Promise.all([ fs .readFile(packageJsonPath, 'utf-8') .then(JSON.parse) .catch(() => null), fs .access(lockfilePath) .then(() => true) .catch(() => false), ]); if (!packageJson) return null; let analysis = ''; // Count dependencies const deps = Object.keys(packageJson.dependencies || {}).length; const devDeps = Object.keys(packageJson.devDependencies || {}).length; analysis += `Dependencies: ${deps} production, ${devDeps} development\n`; if (lockfileExists) { analysis += `✅ Lockfile present (package-lock.json)\n`; } else { analysis += `⚠️ No lockfile found\n`; } return analysis; } catch { return null; } } } const DeploySchema = z.object({ deploymentTarget: z .enum(['vercel', 'netlify', 'heroku', 'aws', 'docker', 'github-pages', 'firebase']) .describe('Deployment target platform'), buildBeforeDeploy: z .boolean() .default(true) .describe('Whether to build the project before deployment'), environment: z .enum(['staging', 'production']) .default('production') .describe('Deployment environment'), environmentVariables: z .record(z.string()) .optional() .describe('Environment variables for deployment'), deploymentConfig: z.string().optional().describe('Path to deployment configuration file'), dryRun: z .boolean() .default(false) .describe('Whether to perform a dry run without actual deployment'), }); export class DeploymentTool extends BaseTool { constructor(private agentContext: { workingDirectory: string }) { super({ name: 'deployProject', description: 'Deploys the project to various hosting platforms', category: 'Deployment', parameters: DeploySchema, }); } async execute(args: z.infer<typeof DeploySchema>): Promise<string> { try { const { deploymentTarget, buildBeforeDeploy, environment, environmentVariables, deploymentConfig, dryRun, } = args; let result = `🚀 Starting deployment to ${deploymentTarget} (${environment})\n\n`; // Build before deployment if requested if (buildBeforeDeploy) { result += '📦 Building project...\n'; try { const { stdout } = await execAsync('npm run build', { cwd: this.agentContext.workingDirectory, timeout: 300000, }); result += `Build completed successfully\n\n`; } catch (buildError: any) { return `❌ Build failed before deployment:\n${buildError.message}`; } } // Generate deployment command const deployCommand = this.buildDeploymentCommand( deploymentTarget, environment, environmentVariables, deploymentConfig, dryRun ); if (dryRun) { result += `🔍 Dry run - would execute: ${deployCommand}\n`; result += await this.validateDeploymentSetup(deploymentTarget); return result; } console.log(`Deploying with command: ${deployCommand}`); const { stdout, stderr } = await execAsync(deployCommand, { cwd: this.agentContext.workingDirectory, timeout: 600000, // 10 minutes for deployment maxBuffer: 1024 * 1024 * 20, // 20MB buffer env: { ...process.env, ...environmentVariables, }, }); if (stdout) { result += `Deployment Output:\n${stdout}\n\n`; } if (stderr) { result += `Deployment Info:\n${stderr}\n\n`; } result += `✅ Deployment to ${deploymentTarget} completed successfully!`; return result; } catch (error: any) { return ( `❌ Deployment failed:\n` + `Error: ${error.message}\n` + `${error.stdout ? `STDOUT:\n${error.stdout}\n` : ''}` + `${error.stderr ? `STDERR:\n${error.stderr}\n` : ''}` ); } } private buildDeploymentCommand( target: string, environment: string, envVars?: Record<string, string>, configPath?: string, dryRun?: boolean ): string { const commands = { vercel: `npx vercel${environment === 'production' ? ' --prod' : ''}${dryRun ? ' --dry-run' : ''}`, netlify: `npx netlify deploy${environment === 'production' ? ' --prod' : ''}${dryRun ? ' --dry-run' : ''}`, heroku: `git push heroku ${environment === 'production' ? 'main' : 'staging'}`, 'github-pages': `npx gh-pages -d dist`, firebase: `npx firebase deploy${configPath ? ` --config ${configPath}` : ''}`, aws: `npx aws s3 sync dist/ s3://your-bucket-name${dryRun ? ' --dryrun' : ''}`, docker: `docker build -t app . && docker run -p 3000:3000 app${dryRun ? ' --dry-run' : ''}`, }; return ( commands[target as keyof typeof commands] || `echo "Deployment target ${target} not configured"` ); } private async validateDeploymentSetup(target: string): Promise<string> { let validation = 'Deployment Setup Validation:\n'; const requiredFiles = { vercel: ['vercel.json', 'package.json'], netlify: ['netlify.toml', 'package.json'], heroku: ['Procfile', 'package.json'], 'github-pages': ['package.json'], firebase: ['firebase.json', '.firebaserc'], aws: ['package.json'], docker: ['Dockerfile', 'package.json'], }; const files = requiredFiles[target as keyof typeof requiredFiles] || []; for (const file of files) { try { await fs.access(join(this.agentContext.workingDirectory, file)); validation += `✅ ${file} found\n`; } catch { validation += `❌ ${file} missing\n`; } } return validation; } }