first pass
This commit is contained in:
278
cli/commands/batch.js
Normal file
278
cli/commands/batch.js
Normal file
@@ -0,0 +1,278 @@
|
||||
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}`));
|
||||
}
|
||||
Reference in New Issue
Block a user