import chalk from 'chalk'; import { resolve, join, basename } from 'path'; import { existsSync, readdirSync, statSync } from 'fs'; import { detectProject, isValidProject } from '../detectors/index.js'; import { initCommand } from './init.js'; /** * Batch command - operations across multiple projects */ export async function batchCommand(action, options) { const rootPath = resolve(options.root); if (!existsSync(rootPath)) { throw new Error(`Root path does not exist: ${rootPath}`); } console.log(chalk.blue('Batch operation:'), action); console.log(chalk.gray('Root directory:'), rootPath); console.log(); // Get all project directories const projects = getProjectDirectories(rootPath, options); console.log(chalk.gray(`Found ${projects.length} projects`)); console.log(); switch (action) { case 'detect': await batchDetect(projects, options); break; case 'init': await batchInit(projects, options); break; default: throw new Error(`Unknown batch action: ${action}. Supported: detect, init`); } } /** * Get all project directories from root */ function getProjectDirectories(rootPath, options) { const entries = readdirSync(rootPath); let projects = []; for (const entry of entries) { const fullPath = join(rootPath, entry); // Skip if not a directory try { if (!statSync(fullPath).isDirectory()) continue; } catch { continue; } // Skip invalid projects if (!isValidProject(fullPath)) continue; // Apply filter pattern if (options.filter && !entry.includes(options.filter)) continue; // Apply exclusions if (options.exclude) { const excludeList = options.exclude.split(',').map(e => e.trim()); if (excludeList.some(exc => entry.includes(exc))) continue; } projects.push({ name: entry, path: fullPath }); } return projects.sort((a, b) => a.name.localeCompare(b.name)); } /** * Batch detect - scan all projects and report types */ async function batchDetect(projects, options) { const results = { dockerizable: [], notDockerizable: [], byType: {} }; console.log(chalk.blue('Scanning projects...')); console.log(); for (const project of projects) { process.stdout.write(chalk.gray(` ${project.name}... `)); try { const detection = await detectProject(project.path); if (detection.dockerizable) { results.dockerizable.push({ ...project, ...detection }); // Group by type if (!results.byType[detection.type]) { results.byType[detection.type] = []; } results.byType[detection.type].push(project.name); console.log(chalk.green(`✓ ${detection.type}`)); } else { results.notDockerizable.push({ ...project, ...detection }); console.log(chalk.yellow(`✗ ${detection.type} - ${detection.reason}`)); } } catch (error) { results.notDockerizable.push({ ...project, type: 'error', reason: error.message }); console.log(chalk.red(`✗ Error: ${error.message}`)); } } // Print report if (options.report) { printReport(results, projects.length); } return results; } /** * Print batch detect report */ function printReport(results, totalCount) { console.log(); console.log(chalk.blue('═'.repeat(60))); console.log(chalk.blue.bold(' BATCH DETECTION REPORT')); console.log(chalk.blue('═'.repeat(60))); console.log(); // Summary console.log(chalk.white.bold('Summary')); console.log(chalk.gray('─'.repeat(40))); console.log(` Total projects: ${totalCount}`); console.log(` Dockerizable: ${chalk.green(results.dockerizable.length)}`); console.log(` Not dockerizable: ${chalk.yellow(results.notDockerizable.length)}`); console.log(); // By type console.log(chalk.white.bold('Projects by Type')); console.log(chalk.gray('─'.repeat(40))); const typeOrder = [ 'nodejs-express', 'nodejs-vite-react', 'nodejs-vite-react-ssr', 'nodejs-generic', 'python-standard', 'python-ml-pytorch', 'dotnet-blazor', 'dotnet-webapi', 'static-nginx', 'flutter-web' ]; for (const type of typeOrder) { if (results.byType[type] && results.byType[type].length > 0) { console.log(); console.log(chalk.cyan(` ${type} (${results.byType[type].length}):`)); results.byType[type].forEach(name => { console.log(chalk.gray(` - ${name}`)); }); } } // Other types not in the order list for (const [type, projects] of Object.entries(results.byType)) { if (!typeOrder.includes(type) && projects.length > 0) { console.log(); console.log(chalk.cyan(` ${type} (${projects.length}):`)); projects.forEach(name => { console.log(chalk.gray(` - ${name}`)); }); } } // Not dockerizable if (results.notDockerizable.length > 0) { console.log(); console.log(chalk.white.bold('Not Dockerizable')); console.log(chalk.gray('─'.repeat(40))); results.notDockerizable.forEach(p => { console.log(chalk.yellow(` - ${p.name}`), chalk.gray(`(${p.reason || p.type})`)); }); } console.log(); console.log(chalk.blue('═'.repeat(60))); // Suggested ports if (results.dockerizable.length > 0) { console.log(); console.log(chalk.white.bold('Suggested Port Mapping')); console.log(chalk.gray('─'.repeat(40))); let port = 3000; results.dockerizable.forEach(p => { const suggestedPort = p.port || port; console.log(chalk.gray(` ${p.name}:`), `${suggestedPort}`); port = Math.max(port, suggestedPort) + 1; }); } } /** * Batch init - initialize Docker config for multiple projects */ async function batchInit(projects, options) { console.log(chalk.blue('Initializing Docker configuration...')); console.log(); const parallel = options.parallel || 4; let completed = 0; let failed = 0; let skipped = 0; // Process in parallel batches for (let i = 0; i < projects.length; i += parallel) { const batch = projects.slice(i, i + parallel); const promises = batch.map(async (project) => { try { // Check if already initialized const hasDockerfile = existsSync(join(project.path, 'Dockerfile')); const hasConfig = existsSync(join(project.path, 'docker-deployment.json')); if ((hasDockerfile || hasConfig) && !options.force) { console.log(chalk.yellow(` ${project.name}: Skipped (already initialized)`)); skipped++; return; } // Detect first const detection = await detectProject(project.path); if (!detection.dockerizable) { console.log(chalk.yellow(` ${project.name}: Skipped (${detection.reason})`)); skipped++; return; } // Initialize with non-interactive mode await initCommand(project.path, { interactive: false, overwrite: options.force }); console.log(chalk.green(` ${project.name}: ✓ Initialized`)); completed++; } catch (error) { console.log(chalk.red(` ${project.name}: ✗ Failed - ${error.message}`)); failed++; } }); await Promise.all(promises); } // Summary console.log(); console.log(chalk.blue('Batch init complete:')); console.log(chalk.green(` Initialized: ${completed}`)); console.log(chalk.yellow(` Skipped: ${skipped}`)); console.log(chalk.red(` Failed: ${failed}`)); }