Files
idea.llm.gitea.repo.docker.…/cli/commands/batch.js
2026-01-26 22:33:55 -06:00

279 lines
7.5 KiB
JavaScript

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}`));
}