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}`));
|
||||
}
|
||||
77
cli/commands/detect.js
Normal file
77
cli/commands/detect.js
Normal file
@@ -0,0 +1,77 @@
|
||||
import chalk from 'chalk';
|
||||
import { resolve } from 'path';
|
||||
import { existsSync } from 'fs';
|
||||
import { detectProject, getProjectInfo } from '../detectors/index.js';
|
||||
|
||||
/**
|
||||
* Detect command - identify project type and show deployment recommendations
|
||||
*/
|
||||
export async function detectCommand(path, options) {
|
||||
const projectPath = resolve(path);
|
||||
|
||||
if (!existsSync(projectPath)) {
|
||||
throw new Error(`Path does not exist: ${projectPath}`);
|
||||
}
|
||||
|
||||
console.log(chalk.blue('Scanning project...'), projectPath);
|
||||
console.log();
|
||||
|
||||
const detection = await detectProject(projectPath);
|
||||
|
||||
if (options.json) {
|
||||
console.log(JSON.stringify(detection, null, 2));
|
||||
return detection;
|
||||
}
|
||||
|
||||
// Display results
|
||||
if (detection.dockerizable) {
|
||||
console.log(chalk.green('✓ Detected:'), chalk.bold(detection.description || detection.type));
|
||||
console.log();
|
||||
console.log(chalk.gray(' Project: '), detection.projectName);
|
||||
console.log(chalk.gray(' Type: '), detection.type);
|
||||
console.log(chalk.gray(' Template: '), detection.template);
|
||||
console.log(chalk.gray(' Port: '), detection.port || 'N/A');
|
||||
console.log(chalk.gray(' Entry: '), detection.entryPoint || 'N/A');
|
||||
|
||||
if (detection.buildCommand) {
|
||||
console.log(chalk.gray(' Build: '), detection.buildCommand);
|
||||
}
|
||||
|
||||
if (detection.note) {
|
||||
console.log();
|
||||
console.log(chalk.yellow(' Note:'), detection.note);
|
||||
}
|
||||
|
||||
console.log();
|
||||
console.log(chalk.gray('Dockerizable:'), chalk.green('Yes'));
|
||||
|
||||
// Get additional info
|
||||
const info = await getProjectInfo(projectPath, detection.type);
|
||||
if (info) {
|
||||
console.log();
|
||||
console.log(chalk.gray('Additional Info:'));
|
||||
if (info.dependencies) {
|
||||
console.log(chalk.gray(' Dependencies:'), info.dependencies.length);
|
||||
}
|
||||
if (info.scripts) {
|
||||
console.log(chalk.gray(' Scripts:'), info.scripts.join(', '));
|
||||
}
|
||||
if (info.packages) {
|
||||
console.log(chalk.gray(' Packages:'), info.packages.length);
|
||||
}
|
||||
}
|
||||
|
||||
console.log();
|
||||
console.log(chalk.blue('Next step:'), `npm run docker-deploy -- init "${path}"`);
|
||||
|
||||
} else {
|
||||
console.log(chalk.red('✗ Not Dockerizable:'), chalk.bold(detection.type));
|
||||
console.log();
|
||||
console.log(chalk.gray(' Project:'), detection.projectName);
|
||||
console.log(chalk.gray(' Reason: '), detection.reason);
|
||||
console.log();
|
||||
console.log(chalk.gray('Dockerizable:'), chalk.red('No'));
|
||||
}
|
||||
|
||||
return detection;
|
||||
}
|
||||
706
cli/commands/init.js
Normal file
706
cli/commands/init.js
Normal file
@@ -0,0 +1,706 @@
|
||||
import chalk from 'chalk';
|
||||
import inquirer from 'inquirer';
|
||||
import { resolve, join, basename } from 'path';
|
||||
import { existsSync, writeFileSync, mkdirSync } from 'fs';
|
||||
import { detectProject } from '../detectors/index.js';
|
||||
import { createProjectConfig, saveProjectConfig, loadGlobalConfig } from '../utils/config-manager.js';
|
||||
import { processTemplate, getTemplateFiles, buildTemplateContext, loadTemplate, renderTemplate, TEMPLATES_DIR } from '../utils/template-engine.js';
|
||||
|
||||
/**
|
||||
* Init command - initialize Docker configuration for a project
|
||||
*/
|
||||
export async function initCommand(path, options) {
|
||||
const projectPath = resolve(path);
|
||||
|
||||
if (!existsSync(projectPath)) {
|
||||
throw new Error(`Path does not exist: ${projectPath}`);
|
||||
}
|
||||
|
||||
const projectName = basename(projectPath);
|
||||
console.log(chalk.blue('Initializing Docker configuration for:'), projectName);
|
||||
console.log();
|
||||
|
||||
// Detect project type
|
||||
let detection;
|
||||
if (options.type) {
|
||||
// Force specific type
|
||||
detection = {
|
||||
type: options.type,
|
||||
dockerizable: true,
|
||||
template: options.type.replace('-', '/'),
|
||||
port: options.port || 3000,
|
||||
projectName,
|
||||
projectPath
|
||||
};
|
||||
console.log(chalk.yellow('Using forced type:'), options.type);
|
||||
} else {
|
||||
detection = await detectProject(projectPath);
|
||||
}
|
||||
|
||||
if (!detection.dockerizable) {
|
||||
console.log(chalk.red('✗ Project is not dockerizable:'), detection.reason);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(chalk.green('✓ Detected:'), detection.description || detection.type);
|
||||
console.log();
|
||||
|
||||
// Check for existing files
|
||||
const existingFiles = checkExistingFiles(projectPath);
|
||||
if (existingFiles.length > 0 && !options.overwrite) {
|
||||
console.log(chalk.yellow('Existing Docker files found:'));
|
||||
existingFiles.forEach(f => console.log(chalk.gray(` - ${f}`)));
|
||||
console.log();
|
||||
|
||||
if (options.interactive !== false) {
|
||||
const { proceed } = await inquirer.prompt([{
|
||||
type: 'confirm',
|
||||
name: 'proceed',
|
||||
message: 'Overwrite existing files?',
|
||||
default: false
|
||||
}]);
|
||||
|
||||
if (!proceed) {
|
||||
console.log(chalk.yellow('Cancelled.'));
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
console.log(chalk.yellow('Use --overwrite to replace existing files.'));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Interactive configuration
|
||||
let config;
|
||||
if (options.interactive !== false) {
|
||||
config = await interactiveConfig(projectName, detection, options);
|
||||
} else {
|
||||
config = createProjectConfig(projectName, detection, {
|
||||
port: options.port,
|
||||
name: options.name
|
||||
});
|
||||
}
|
||||
|
||||
// Dry run - just show what would be generated
|
||||
if (options.dryRun) {
|
||||
console.log(chalk.blue('Dry run - would generate:'));
|
||||
console.log();
|
||||
const files = getFilesToGenerate(detection, config);
|
||||
files.forEach(f => console.log(chalk.gray(` - ${f.output}`)));
|
||||
console.log();
|
||||
console.log(chalk.gray('Configuration:'));
|
||||
console.log(JSON.stringify(config, null, 2));
|
||||
return;
|
||||
}
|
||||
|
||||
// Generate files
|
||||
console.log();
|
||||
console.log(chalk.blue('Generating files...'));
|
||||
|
||||
const generatedFiles = await generateFiles(projectPath, detection, config);
|
||||
|
||||
console.log();
|
||||
generatedFiles.forEach(file => {
|
||||
console.log(chalk.green('✓'), file);
|
||||
});
|
||||
|
||||
console.log();
|
||||
console.log(chalk.green('Docker configuration initialized successfully!'));
|
||||
console.log();
|
||||
console.log(chalk.blue('Next steps:'));
|
||||
console.log(chalk.gray(' 1. Review generated files'));
|
||||
console.log(chalk.gray(' 2. Create .env file from .env.example (if needed)'));
|
||||
console.log(chalk.gray(' 3. Build and test locally:'), 'docker compose up --build');
|
||||
console.log(chalk.gray(' 4. Deploy using:'), '.\\deploy-docker-auto.ps1', chalk.gray('or'), '.\\build-image-tar.ps1');
|
||||
}
|
||||
|
||||
/**
|
||||
* Interactive configuration prompts
|
||||
*/
|
||||
async function interactiveConfig(projectName, detection, options) {
|
||||
const globalConfig = loadGlobalConfig();
|
||||
|
||||
const sanitizedName = projectName.toLowerCase().replace(/[^a-z0-9-]/g, '-');
|
||||
|
||||
const answers = await inquirer.prompt([
|
||||
{
|
||||
type: 'input',
|
||||
name: 'containerName',
|
||||
message: 'Container name:',
|
||||
default: options.name || sanitizedName
|
||||
},
|
||||
{
|
||||
type: 'number',
|
||||
name: 'port',
|
||||
message: 'Application port:',
|
||||
default: options.port || detection.port || 3000
|
||||
},
|
||||
{
|
||||
type: 'confirm',
|
||||
name: 'useEnvFile',
|
||||
message: 'Use .env file for configuration?',
|
||||
default: true
|
||||
},
|
||||
{
|
||||
type: 'confirm',
|
||||
name: 'configureSSH',
|
||||
message: 'Configure SSH deployment?',
|
||||
default: true
|
||||
}
|
||||
]);
|
||||
|
||||
let sshConfig = {};
|
||||
if (answers.configureSSH) {
|
||||
sshConfig = await inquirer.prompt([
|
||||
{
|
||||
type: 'input',
|
||||
name: 'sshHost',
|
||||
message: 'SSH host:',
|
||||
default: globalConfig.deployment?.sshHost || '192.168.8.178'
|
||||
},
|
||||
{
|
||||
type: 'input',
|
||||
name: 'sshUser',
|
||||
message: 'SSH user:',
|
||||
default: globalConfig.deployment?.sshUser || 'deployer'
|
||||
}
|
||||
]);
|
||||
}
|
||||
|
||||
// Ask about volumes for projects that might need persistence
|
||||
let volumes = [];
|
||||
if (detection.type.includes('express') || detection.type.includes('python')) {
|
||||
const { needsVolumes } = await inquirer.prompt([{
|
||||
type: 'confirm',
|
||||
name: 'needsVolumes',
|
||||
message: 'Does this project need persistent data volumes?',
|
||||
default: false
|
||||
}]);
|
||||
|
||||
if (needsVolumes) {
|
||||
const { volumePath } = await inquirer.prompt([{
|
||||
type: 'input',
|
||||
name: 'volumePath',
|
||||
message: 'Volume mount (local:container):',
|
||||
default: './data:/app/data'
|
||||
}]);
|
||||
volumes = [volumePath];
|
||||
}
|
||||
}
|
||||
|
||||
return createProjectConfig(projectName, detection, {
|
||||
name: answers.containerName,
|
||||
port: answers.port,
|
||||
envFile: answers.useEnvFile,
|
||||
sshHost: sshConfig.sshHost,
|
||||
sshUser: sshConfig.sshUser,
|
||||
volumes
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Check for existing Docker files
|
||||
*/
|
||||
function checkExistingFiles(projectPath) {
|
||||
const filesToCheck = [
|
||||
'Dockerfile',
|
||||
'docker-compose.yml',
|
||||
'docker-compose.yaml',
|
||||
'.dockerignore',
|
||||
'docker-deployment.json'
|
||||
];
|
||||
|
||||
return filesToCheck.filter(f => existsSync(join(projectPath, f)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get list of files to generate
|
||||
*/
|
||||
function getFilesToGenerate(detection, config) {
|
||||
const files = [
|
||||
{ output: 'Dockerfile' },
|
||||
{ output: 'docker-compose.yml' },
|
||||
{ output: '.dockerignore' },
|
||||
{ output: 'docker-deployment.json' },
|
||||
{ output: '.env.example' },
|
||||
{ output: 'deploy-docker-auto.ps1' },
|
||||
{ output: 'build-image-tar.ps1' },
|
||||
{ output: 'README.DOCKER.md' }
|
||||
];
|
||||
|
||||
// Add nginx.conf for static sites
|
||||
if (detection.type === 'static-nginx' || detection.type === 'flutter-web') {
|
||||
files.push({ output: 'nginx.conf' });
|
||||
}
|
||||
|
||||
return files;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate all Docker files for the project
|
||||
*/
|
||||
async function generateFiles(projectPath, detection, config) {
|
||||
const context = buildTemplateContext(config, detection);
|
||||
const generatedFiles = [];
|
||||
|
||||
// 1. Generate Dockerfile
|
||||
try {
|
||||
const dockerfileTemplate = loadTemplate(`${detection.template}/Dockerfile.template`);
|
||||
const dockerfile = renderTemplate(dockerfileTemplate, context);
|
||||
writeFileSync(join(projectPath, 'Dockerfile'), dockerfile);
|
||||
generatedFiles.push('Dockerfile');
|
||||
} catch (error) {
|
||||
console.log(chalk.yellow(`Warning: Could not generate Dockerfile: ${error.message}`));
|
||||
// Use fallback template
|
||||
const fallbackDockerfile = generateFallbackDockerfile(detection, context);
|
||||
writeFileSync(join(projectPath, 'Dockerfile'), fallbackDockerfile);
|
||||
generatedFiles.push('Dockerfile (fallback)');
|
||||
}
|
||||
|
||||
// 2. Generate docker-compose.yml
|
||||
try {
|
||||
const composeTemplate = loadTemplate(`${detection.template}/docker-compose.yml.template`);
|
||||
const compose = renderTemplate(composeTemplate, context);
|
||||
writeFileSync(join(projectPath, 'docker-compose.yml'), compose);
|
||||
generatedFiles.push('docker-compose.yml');
|
||||
} catch (error) {
|
||||
const fallbackCompose = generateFallbackCompose(detection, context);
|
||||
writeFileSync(join(projectPath, 'docker-compose.yml'), fallbackCompose);
|
||||
generatedFiles.push('docker-compose.yml (fallback)');
|
||||
}
|
||||
|
||||
// 3. Generate .dockerignore
|
||||
try {
|
||||
const ignoreTemplate = loadTemplate(`${detection.template}/.dockerignore.template`);
|
||||
const dockerignore = renderTemplate(ignoreTemplate, context);
|
||||
writeFileSync(join(projectPath, '.dockerignore'), dockerignore);
|
||||
generatedFiles.push('.dockerignore');
|
||||
} catch (error) {
|
||||
const fallbackIgnore = generateFallbackDockerignore(detection);
|
||||
writeFileSync(join(projectPath, '.dockerignore'), fallbackIgnore);
|
||||
generatedFiles.push('.dockerignore (fallback)');
|
||||
}
|
||||
|
||||
// 4. Generate nginx.conf for static sites
|
||||
if (detection.type === 'static-nginx' || detection.type === 'flutter-web') {
|
||||
try {
|
||||
const nginxTemplate = loadTemplate(`${detection.template}/nginx.conf.template`);
|
||||
const nginx = renderTemplate(nginxTemplate, context);
|
||||
writeFileSync(join(projectPath, 'nginx.conf'), nginx);
|
||||
generatedFiles.push('nginx.conf');
|
||||
} catch (error) {
|
||||
const fallbackNginx = generateFallbackNginxConf();
|
||||
writeFileSync(join(projectPath, 'nginx.conf'), fallbackNginx);
|
||||
generatedFiles.push('nginx.conf (fallback)');
|
||||
}
|
||||
}
|
||||
|
||||
// 5. Save project config
|
||||
saveProjectConfig(projectPath, config);
|
||||
generatedFiles.push('docker-deployment.json');
|
||||
|
||||
// 6. Generate .env.example
|
||||
const envExample = generateEnvExample(detection, context);
|
||||
writeFileSync(join(projectPath, '.env.example'), envExample);
|
||||
generatedFiles.push('.env.example');
|
||||
|
||||
// 7. Generate PowerShell scripts
|
||||
const deployScript = generateDeployScript(context);
|
||||
writeFileSync(join(projectPath, 'deploy-docker-auto.ps1'), deployScript);
|
||||
generatedFiles.push('deploy-docker-auto.ps1');
|
||||
|
||||
const buildScript = generateBuildScript(context);
|
||||
writeFileSync(join(projectPath, 'build-image-tar.ps1'), buildScript);
|
||||
generatedFiles.push('build-image-tar.ps1');
|
||||
|
||||
// 8. Generate README.DOCKER.md
|
||||
const readme = generateDockerReadme(detection, context, config);
|
||||
writeFileSync(join(projectPath, 'README.DOCKER.md'), readme);
|
||||
generatedFiles.push('README.DOCKER.md');
|
||||
|
||||
return generatedFiles;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate fallback Dockerfile when template is not found
|
||||
*/
|
||||
function generateFallbackDockerfile(detection, context) {
|
||||
if (detection.type.startsWith('nodejs')) {
|
||||
return `FROM node:${context.NODE_VERSION}-alpine
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY package.json package-lock.json ./
|
||||
RUN npm ci --omit=dev
|
||||
|
||||
COPY . .
|
||||
|
||||
ENV NODE_ENV=production
|
||||
ENV PORT=${context.PORT}
|
||||
|
||||
EXPOSE ${context.PORT}
|
||||
|
||||
CMD ["node", "${context.ENTRY_POINT}"]
|
||||
`;
|
||||
}
|
||||
|
||||
if (detection.type.startsWith('python')) {
|
||||
return `FROM python:${context.PYTHON_VERSION}-slim
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY requirements.txt .
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
COPY . .
|
||||
|
||||
ENV PYTHONUNBUFFERED=1
|
||||
|
||||
EXPOSE ${context.PORT}
|
||||
|
||||
CMD ["python", "${context.ENTRY_POINT}"]
|
||||
`;
|
||||
}
|
||||
|
||||
if (detection.type.startsWith('dotnet')) {
|
||||
return `FROM mcr.microsoft.com/dotnet/sdk:${context.DOTNET_VERSION} AS build
|
||||
WORKDIR /src
|
||||
COPY . .
|
||||
RUN dotnet restore
|
||||
RUN dotnet publish -c Release -o /app/publish
|
||||
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:${context.DOTNET_VERSION}
|
||||
WORKDIR /app
|
||||
COPY --from=build /app/publish .
|
||||
ENV ASPNETCORE_URLS=http://+:${context.PORT}
|
||||
EXPOSE ${context.PORT}
|
||||
ENTRYPOINT ["dotnet", "${context.ENTRY_POINT}"]
|
||||
`;
|
||||
}
|
||||
|
||||
// Static site fallback
|
||||
return `FROM nginx:alpine
|
||||
COPY . /usr/share/nginx/html
|
||||
EXPOSE 80
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate fallback docker-compose.yml
|
||||
*/
|
||||
function generateFallbackCompose(detection, context) {
|
||||
let compose = `services:
|
||||
${context.PROJECT_NAME}:
|
||||
build: .
|
||||
container_name: ${context.PROJECT_NAME}
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "\${HOST_PORT:-${context.PORT}}:${context.PORT}"
|
||||
`;
|
||||
|
||||
if (context.USE_ENV_FILE) {
|
||||
compose += ` env_file:
|
||||
- .env
|
||||
`;
|
||||
}
|
||||
|
||||
compose += ` environment:
|
||||
NODE_ENV: production
|
||||
`;
|
||||
|
||||
if (context.HAS_VOLUMES && context.VOLUMES.length > 0) {
|
||||
compose += ` volumes:\n`;
|
||||
context.VOLUMES.forEach(vol => {
|
||||
compose += ` - ${vol}\n`;
|
||||
});
|
||||
}
|
||||
|
||||
if (context.EXTRA_HOSTS) {
|
||||
compose += ` extra_hosts:
|
||||
- "host.docker.internal:host-gateway"
|
||||
`;
|
||||
}
|
||||
|
||||
return compose;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate fallback .dockerignore
|
||||
*/
|
||||
function generateFallbackDockerignore(detection) {
|
||||
return `node_modules
|
||||
npm-debug.log
|
||||
.git
|
||||
.gitignore
|
||||
.env
|
||||
.env.local
|
||||
*.tar
|
||||
*.tar.gz
|
||||
Dockerfile
|
||||
docker-compose.yml
|
||||
docker-compose.yaml
|
||||
.dockerignore
|
||||
README.md
|
||||
README.DOCKER.md
|
||||
.vscode
|
||||
.idea
|
||||
coverage
|
||||
.nyc_output
|
||||
*.log
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate fallback nginx.conf
|
||||
*/
|
||||
function generateFallbackNginxConf() {
|
||||
return `server {
|
||||
listen 80;
|
||||
server_name localhost;
|
||||
|
||||
root /usr/share/nginx/html;
|
||||
index index.html;
|
||||
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
|
||||
location ~* \\.(jpg|jpeg|png|gif|ico|css|js|svg|woff|woff2|ttf|eot)$ {
|
||||
expires 1y;
|
||||
add_header Cache-Control "public, immutable";
|
||||
}
|
||||
|
||||
gzip on;
|
||||
gzip_vary on;
|
||||
gzip_types text/plain text/css application/json application/javascript text/xml application/xml;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate .env.example file
|
||||
*/
|
||||
function generateEnvExample(detection, context) {
|
||||
let env = `# Application Configuration
|
||||
PORT=${context.PORT}
|
||||
NODE_ENV=production
|
||||
|
||||
# Docker Host Port (change to avoid conflicts)
|
||||
HOST_PORT=${context.PORT}
|
||||
`;
|
||||
|
||||
if (context.HAS_SSH) {
|
||||
env += `
|
||||
# SSH Deployment (used by deploy-docker-auto.ps1)
|
||||
SSH_HOST=${context.SSH_HOST}
|
||||
SSH_USER=${context.SSH_USER}
|
||||
`;
|
||||
}
|
||||
|
||||
return env;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate deploy-docker-auto.ps1 (SSH automation)
|
||||
*/
|
||||
function generateDeployScript(context) {
|
||||
return `# Docker Deploy Script (SSH Automation)
|
||||
# Generated by docker-deployment-manager
|
||||
|
||||
param(
|
||||
[string]$Platform = "linux/amd64",
|
||||
[string]$ImageTag = "${context.PROJECT_NAME}:latest",
|
||||
[string]$TarFile = "${context.PROJECT_NAME}.tar",
|
||||
[switch]$SkipBuild,
|
||||
[switch]$SkipDeploy
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
# Load config if exists
|
||||
$config = $null
|
||||
if (Test-Path "docker-deployment.json") {
|
||||
$config = Get-Content "docker-deployment.json" | ConvertFrom-Json
|
||||
}
|
||||
|
||||
if (-not $SkipBuild) {
|
||||
Write-Host "Building Docker image..." -ForegroundColor Cyan
|
||||
docker buildx build --platform $Platform -t $ImageTag --load .
|
||||
if ($LASTEXITCODE -ne 0) { throw "Docker build failed" }
|
||||
|
||||
Write-Host "Saving image to tar..." -ForegroundColor Cyan
|
||||
docker save -o $TarFile $ImageTag
|
||||
if ($LASTEXITCODE -ne 0) { throw "Docker save failed" }
|
||||
|
||||
Write-Host "Created: $TarFile" -ForegroundColor Green
|
||||
}
|
||||
|
||||
if (-not $SkipDeploy) {
|
||||
$sshHost = if ($config) { $config.deployment.sshHost } else { "${context.SSH_HOST}" }
|
||||
$sshUser = if ($config) { $config.deployment.sshUser } else { "${context.SSH_USER}" }
|
||||
$targetPath = if ($config) { $config.deployment.targetPath } else { "${context.TARGET_PATH}" }
|
||||
|
||||
if (-not $sshHost -or -not $sshUser) {
|
||||
Write-Host "SSH not configured. Use build-image-tar.ps1 for manual deployment." -ForegroundColor Yellow
|
||||
exit 0
|
||||
}
|
||||
|
||||
$sshTarget = "\${sshUser}@\${sshHost}"
|
||||
|
||||
Write-Host "Creating target directory on server..." -ForegroundColor Cyan
|
||||
ssh $sshTarget "mkdir -p $targetPath"
|
||||
|
||||
Write-Host "Copying files to server..." -ForegroundColor Cyan
|
||||
scp $TarFile "\${sshTarget}:\${targetPath}/"
|
||||
scp docker-compose.yml "\${sshTarget}:\${targetPath}/"
|
||||
if (Test-Path ".env") {
|
||||
scp .env "\${sshTarget}:\${targetPath}/"
|
||||
}
|
||||
|
||||
Write-Host "Loading and starting container on server..." -ForegroundColor Cyan
|
||||
ssh $sshTarget "cd $targetPath && docker load -i $TarFile && docker compose down 2>/dev/null; docker compose up -d"
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "Deployment complete!" -ForegroundColor Green
|
||||
Write-Host "View logs: ssh $sshTarget 'cd $targetPath && docker compose logs -f'" -ForegroundColor Gray
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate build-image-tar.ps1 (manual workflow)
|
||||
*/
|
||||
function generateBuildScript(context) {
|
||||
return `# Docker Build Script (Manual Workflow)
|
||||
# Generated by docker-deployment-manager
|
||||
|
||||
param(
|
||||
[string]$Platform = "linux/amd64",
|
||||
[string]$ImageTag = "${context.PROJECT_NAME}:latest",
|
||||
[string]$TarFile = "${context.PROJECT_NAME}.tar"
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
Write-Host "Building Docker image..." -ForegroundColor Cyan
|
||||
docker buildx build --platform $Platform -t $ImageTag --load .
|
||||
if ($LASTEXITCODE -ne 0) { throw "Docker build failed" }
|
||||
|
||||
Write-Host "Saving image to tar..." -ForegroundColor Cyan
|
||||
docker save -o $TarFile $ImageTag
|
||||
if ($LASTEXITCODE -ne 0) { throw "Docker save failed" }
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "Build complete!" -ForegroundColor Green
|
||||
Write-Host "Created: $TarFile" -ForegroundColor Green
|
||||
Write-Host ""
|
||||
Write-Host "Manual deployment instructions:" -ForegroundColor Yellow
|
||||
Write-Host "1. scp $TarFile user@server:~/containers/${context.PROJECT_NAME}/files/" -ForegroundColor Gray
|
||||
Write-Host "2. scp docker-compose.yml user@server:~/containers/${context.PROJECT_NAME}/files/" -ForegroundColor Gray
|
||||
Write-Host "3. ssh user@server" -ForegroundColor Gray
|
||||
Write-Host "4. cd ~/containers/${context.PROJECT_NAME}/files" -ForegroundColor Gray
|
||||
Write-Host "5. docker load -i $TarFile" -ForegroundColor Gray
|
||||
Write-Host "6. docker compose up -d" -ForegroundColor Gray
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate README.DOCKER.md
|
||||
*/
|
||||
function generateDockerReadme(detection, context, config) {
|
||||
return `# Docker Deployment
|
||||
|
||||
This project is configured for Docker deployment.
|
||||
|
||||
## Project Info
|
||||
|
||||
- **Type:** ${detection.description || detection.type}
|
||||
- **Port:** ${context.PORT}
|
||||
- **Image:** ${context.PROJECT_NAME}:latest
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Build and Run Locally
|
||||
|
||||
\`\`\`bash
|
||||
docker compose up --build
|
||||
\`\`\`
|
||||
|
||||
Then visit: http://localhost:${context.PORT}
|
||||
|
||||
### Deploy to Server
|
||||
|
||||
**Option 1: Automated SSH Deployment**
|
||||
|
||||
\`\`\`powershell
|
||||
.\\deploy-docker-auto.ps1
|
||||
\`\`\`
|
||||
|
||||
**Option 2: Manual Deployment**
|
||||
|
||||
\`\`\`powershell
|
||||
# Build
|
||||
.\\build-image-tar.ps1
|
||||
|
||||
# Copy to server
|
||||
scp ${context.PROJECT_NAME}.tar user@server:~/containers/${context.PROJECT_NAME}/files/
|
||||
scp docker-compose.yml user@server:~/containers/${context.PROJECT_NAME}/files/
|
||||
|
||||
# On server
|
||||
ssh user@server
|
||||
cd ~/containers/${context.PROJECT_NAME}/files
|
||||
docker load -i ${context.PROJECT_NAME}.tar
|
||||
docker compose up -d
|
||||
\`\`\`
|
||||
|
||||
## Configuration
|
||||
|
||||
### Environment Variables
|
||||
|
||||
Copy \`.env.example\` to \`.env\` and configure:
|
||||
|
||||
\`\`\`bash
|
||||
cp .env.example .env
|
||||
\`\`\`
|
||||
|
||||
Key variables:
|
||||
- \`PORT\` - Application port (default: ${context.PORT})
|
||||
- \`HOST_PORT\` - Docker host port mapping
|
||||
|
||||
### Deployment Settings
|
||||
|
||||
Edit \`docker-deployment.json\` to configure:
|
||||
- SSH host and user
|
||||
- Target deployment path
|
||||
- Build settings
|
||||
|
||||
## Files
|
||||
|
||||
- \`Dockerfile\` - Container build instructions
|
||||
- \`docker-compose.yml\` - Container runtime configuration
|
||||
- \`.dockerignore\` - Files excluded from image
|
||||
- \`docker-deployment.json\` - Deployment configuration
|
||||
- \`deploy-docker-auto.ps1\` - Automated SSH deployment script
|
||||
- \`build-image-tar.ps1\` - Manual build script
|
||||
|
||||
## Useful Commands
|
||||
|
||||
\`\`\`bash
|
||||
# View logs
|
||||
docker compose logs -f
|
||||
|
||||
# Stop container
|
||||
docker compose down
|
||||
|
||||
# Rebuild and restart
|
||||
docker compose up --build -d
|
||||
|
||||
# Shell into container
|
||||
docker compose exec ${context.PROJECT_NAME} sh
|
||||
\`\`\`
|
||||
|
||||
---
|
||||
Generated by docker-deployment-manager
|
||||
`;
|
||||
}
|
||||
Reference in New Issue
Block a user