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