const { app, BrowserWindow, ipcMain } = require('electron'); const path = require('path'); const fs = require('fs'); const { SSHService } = require('./ssh-service'); const { ProjectScanner } = require('./project-scanner'); const { ServerScanner } = require('./server-scanner'); let mainWindow; let config = { servers: [], projectsRoot: 'C:\\.bucket\\repos.gitea', projects: {} }; const CONFIG_PATH = path.join(__dirname, '..', '..', 'deployment-config.json'); const ENV_PATH = path.join(__dirname, '..', '.env'); function loadConfig() { try { if (fs.existsSync(CONFIG_PATH)) { config = JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf-8')); } } catch (err) { console.error('Failed to load config:', err); } } function saveConfig() { try { fs.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2)); } catch (err) { console.error('Failed to save config:', err); } } function loadEnv() { try { if (fs.existsSync(ENV_PATH)) { const envContent = fs.readFileSync(ENV_PATH, 'utf-8'); const lines = envContent.split('\n'); const env = {}; for (const line of lines) { const [key, ...valueParts] = line.split('='); if (key && valueParts.length > 0) { env[key.trim()] = valueParts.join('=').trim(); } } return env; } } catch (err) { console.error('Failed to load .env:', err); } return {}; } // Load project's docker-deployment.json config function loadProjectDeployConfig(projectPath) { const configPath = path.join(projectPath, 'docker-deployment.json'); try { if (fs.existsSync(configPath)) { return JSON.parse(fs.readFileSync(configPath, 'utf-8')); } } catch (err) { console.error('Failed to load project config:', err); } return null; } // Get list of files to upload for a project function getUploadFiles(projectPath, projectConfig) { const projectName = path.basename(projectPath); // Default files - always upload these if they exist const defaultFiles = [ { local: `${projectName}.tar`, remote: `${projectName}.tar`, type: 'file' }, { local: 'docker-compose.yml', remote: 'docker-compose.yml', type: 'file' }, { local: '.env', remote: '.env', type: 'file' } ]; // Additional files from project config const additionalFiles = projectConfig?.deployment?.uploadFiles || []; const customFiles = additionalFiles.map(f => { if (typeof f === 'string') { // Simple string path - detect if directory const isDir = f.endsWith('/'); return { local: f.replace(/\/$/, ''), remote: f.replace(/\/$/, ''), type: isDir ? 'directory' : 'file' }; } return f; // Already an object with local/remote/type }); return [...defaultFiles, ...customFiles]; } function createWindow() { mainWindow = new BrowserWindow({ width: 1400, height: 900, webPreferences: { preload: path.join(__dirname, '..', 'preload.js'), contextIsolation: true, nodeIntegration: false }, title: 'Docker Deployment Manager' }); if (process.argv.includes('--dev')) { mainWindow.loadURL('http://localhost:5173'); mainWindow.webContents.openDevTools(); } else { mainWindow.loadFile(path.join(__dirname, '..', 'renderer', 'dist', 'index.html')); } } app.whenReady().then(() => { loadConfig(); createWindow(); app.on('activate', () => { if (BrowserWindow.getAllWindows().length === 0) { createWindow(); } }); }); app.on('window-all-closed', () => { if (process.platform !== 'darwin') { app.quit(); } }); // IPC Handlers // Get configuration ipcMain.handle('get-config', () => { return config; }); // Save configuration ipcMain.handle('save-config', (event, newConfig) => { config = { ...config, ...newConfig }; saveConfig(); return { success: true }; }); // Get servers ipcMain.handle('get-servers', () => { return config.servers || []; }); // Add/update server ipcMain.handle('save-server', (event, server) => { const index = config.servers.findIndex(s => s.id === server.id); if (index >= 0) { config.servers[index] = server; } else { server.id = Date.now().toString(); config.servers.push(server); } saveConfig(); return { success: true, server }; }); // Delete server ipcMain.handle('delete-server', (event, serverId) => { config.servers = config.servers.filter(s => s.id !== serverId); saveConfig(); return { success: true }; }); // Scan local projects (deep scan to find nested Dockerfiles) ipcMain.handle('scan-local-projects', async () => { const scanner = new ProjectScanner(config.projectsRoot); const projects = await scanner.scanDeep(2); return projects; }); // Scan remote server for deployed containers ipcMain.handle('scan-server', async (event, serverId) => { const server = config.servers.find(s => s.id === serverId); if (!server) { return { error: 'Server not found' }; } const env = loadEnv(); const sshConfig = { host: server.host, username: env.SSH_USERNAME || server.username, password: env.SSH_PASSWORD || server.password }; const scanner = new ServerScanner(sshConfig); try { const deployed = await scanner.scan(); return { success: true, deployed }; } catch (err) { return { error: err.message }; } }); // Compare local vs deployed ipcMain.handle('compare-project', async (event, { projectPath, serverId, remotePath }) => { const server = config.servers.find(s => s.id === serverId); if (!server) { return { error: 'Server not found' }; } const env = loadEnv(); const sshConfig = { host: server.host, username: env.SSH_USERNAME || server.username, password: env.SSH_PASSWORD || server.password }; // Load project config to get additional files to compare const projectConfig = loadProjectDeployConfig(projectPath); const additionalFiles = projectConfig?.deployment?.uploadFiles || []; const scanner = new ServerScanner(sshConfig); try { const diff = await scanner.compareFiles(projectPath, remotePath, additionalFiles); return { success: true, diff }; } catch (err) { return { error: err.message }; } }); // Build tar for project ipcMain.handle('build-tar', async (event, projectPath) => { const { exec } = require('child_process'); const projectName = path.basename(projectPath); return new Promise((resolve) => { // Check if build-image-tar.ps1 exists const scriptPath = path.join(projectPath, 'build-image-tar.ps1'); if (!fs.existsSync(scriptPath)) { resolve({ error: 'No build-image-tar.ps1 found in project' }); return; } exec(`powershell -ExecutionPolicy Bypass -File "${scriptPath}"`, { cwd: projectPath }, (error, stdout, stderr) => { if (error) { resolve({ error: error.message, stderr }); } else { resolve({ success: true, output: stdout }); } }); }); }); // Deploy project to server ipcMain.handle('deploy-project', async (event, { projectPath, serverId, remotePath }) => { const server = config.servers.find(s => s.id === serverId); if (!server) { return { error: 'Server not found' }; } const env = loadEnv(); const sshConfig = { host: server.host, username: env.SSH_USERNAME || server.username, password: env.SSH_PASSWORD || server.password }; const ssh = new SSHService(sshConfig); const projectName = path.basename(projectPath); // Sudo prefix for servers that need elevated permissions const password = env.SSH_PASSWORD || server.password; const sudoPrefix = server.useSudo ? `echo '${password}' | sudo -S ` : ''; // Load project config and get files to upload const projectConfig = loadProjectDeployConfig(projectPath); const uploadFiles = getUploadFiles(projectPath, projectConfig); const uploadedFiles = []; try { await ssh.connect(); // Ensure remote directory exists await ssh.exec(`mkdir -p ${remotePath}`); // Delete old tar file with sudo if needed (may be owned by root) if (server.useSudo) { await ssh.exec(`echo '${password}' | sudo -S rm -f ${remotePath}/${projectName}.tar 2>/dev/null || true`); } // Upload all configured files for (const fileSpec of uploadFiles) { const localPath = path.join(projectPath, fileSpec.local); if (!fs.existsSync(localPath)) { continue; // Skip if file doesn't exist locally } const remoteDest = `${remotePath}/${fileSpec.remote}`; if (fileSpec.type === 'directory') { // Create remote directory and upload all contents await ssh.exec(`mkdir -p ${remoteDest}`); // Get all files in directory recursively const uploadDir = async (dirPath, remoteDir) => { const entries = fs.readdirSync(dirPath, { withFileTypes: true }); for (const entry of entries) { const entryLocalPath = path.join(dirPath, entry.name); const entryRemotePath = `${remoteDir}/${entry.name}`; if (entry.isDirectory()) { await ssh.exec(`mkdir -p ${entryRemotePath}`); await uploadDir(entryLocalPath, entryRemotePath); } else { await ssh.uploadFile(entryLocalPath, entryRemotePath); } } }; await uploadDir(localPath, remoteDest); uploadedFiles.push(`${fileSpec.local}/ (directory)`); } else { // Regular file await ssh.uploadFile(localPath, remoteDest); uploadedFiles.push(fileSpec.local); } } // Load image, stop existing container, and start new container await ssh.exec(`cd ${remotePath} && ${sudoPrefix}docker load -i ${projectName}.tar && ${sudoPrefix}docker compose down 2>/dev/null; ${sudoPrefix}docker compose up -d`); // Health check - poll for container status let healthy = false; let status = ''; for (let i = 0; i < 10; i++) { await new Promise(resolve => setTimeout(resolve, 2000)); // Wait 2s try { status = await ssh.exec(`cd ${remotePath} && ${sudoPrefix}docker compose ps --format "{{.Name}}|{{.Status}}" 2>/dev/null || ${sudoPrefix}docker compose ps`); if (status.includes('Up') || status.includes('healthy')) { healthy = true; break; } } catch (err) { // Ignore errors during health check polling } } ssh.disconnect(); return { success: true, healthy, status, uploadedFiles, message: healthy ? 'Container started successfully' : 'Container started but health check pending' }; } catch (err) { return { error: err.message }; } }); // Get docker ps from server ipcMain.handle('get-running-containers', async (event, serverId) => { const server = config.servers.find(s => s.id === serverId); if (!server) { return { error: 'Server not found' }; } const env = loadEnv(); const sshConfig = { host: server.host, username: env.SSH_USERNAME || server.username, password: env.SSH_PASSWORD || server.password }; const ssh = new SSHService(sshConfig); try { await ssh.connect(); const result = await ssh.exec('docker ps --format "{{.Names}}|{{.Status}}|{{.Ports}}"'); ssh.disconnect(); const containers = result.split('\n').filter(Boolean).map(line => { const [name, status, ports] = line.split('|'); return { name, status, ports }; }); return { success: true, containers }; } catch (err) { return { error: err.message }; } }); // Pull file from server (sync back) ipcMain.handle('pull-file', async (event, { serverId, remotePath, localPath, isDirectory }) => { const server = config.servers.find(s => s.id === serverId); if (!server) { return { error: 'Server not found' }; } const env = loadEnv(); const sshConfig = { host: server.host, username: env.SSH_USERNAME || server.username, password: env.SSH_PASSWORD || server.password }; const ssh = new SSHService(sshConfig); try { await ssh.connect(); if (isDirectory) { // Pull entire directory recursively const pullDir = async (remoteDir, localDir) => { // Ensure local directory exists if (!fs.existsSync(localDir)) { fs.mkdirSync(localDir, { recursive: true }); } // List remote directory contents const result = await ssh.exec(`ls -la ${remoteDir} 2>/dev/null | tail -n +4 || echo ""`); const lines = result.split('\n').filter(Boolean); for (const line of lines) { const parts = line.split(/\s+/); if (parts.length < 9) continue; const isDir = line.startsWith('d'); const fileName = parts.slice(8).join(' '); if (fileName === '.' || fileName === '..') continue; const remoteFilePath = `${remoteDir}/${fileName}`; const localFilePath = path.join(localDir, fileName); if (isDir) { await pullDir(remoteFilePath, localFilePath); } else { await ssh.downloadFile(remoteFilePath, localFilePath); } } }; await pullDir(remotePath, localPath); } else { // Pull single file await ssh.downloadFile(remotePath, localPath); } ssh.disconnect(); return { success: true }; } catch (err) { return { error: err.message }; } }); // Pull multiple files from server ipcMain.handle('pull-files', async (event, { serverId, files }) => { const server = config.servers.find(s => s.id === serverId); if (!server) { return { error: 'Server not found' }; } const env = loadEnv(); const sshConfig = { host: server.host, username: env.SSH_USERNAME || server.username, password: env.SSH_PASSWORD || server.password }; const ssh = new SSHService(sshConfig); const pulled = []; const errors = []; try { await ssh.connect(); for (const file of files) { try { if (file.type === 'directory') { // Pull directory recursively (same logic as above) const pullDir = async (remoteDir, localDir) => { if (!fs.existsSync(localDir)) { fs.mkdirSync(localDir, { recursive: true }); } const result = await ssh.exec(`ls -la ${remoteDir} 2>/dev/null | tail -n +4 || echo ""`); const lines = result.split('\n').filter(Boolean); for (const line of lines) { const parts = line.split(/\s+/); if (parts.length < 9) continue; const isDir = line.startsWith('d'); const fileName = parts.slice(8).join(' '); if (fileName === '.' || fileName === '..') continue; const remoteFilePath = `${remoteDir}/${fileName}`; const localFilePath = path.join(localDir, fileName); if (isDir) { await pullDir(remoteFilePath, localFilePath); } else { await ssh.downloadFile(remoteFilePath, localFilePath); } } }; await pullDir(file.remotePath, file.localPath); pulled.push(file.name); } else { // Ensure parent directory exists const parentDir = path.dirname(file.localPath); if (!fs.existsSync(parentDir)) { fs.mkdirSync(parentDir, { recursive: true }); } await ssh.downloadFile(file.remotePath, file.localPath); pulled.push(file.name); } } catch (err) { errors.push({ name: file.name, error: err.message }); } } ssh.disconnect(); return { success: true, pulled, errors }; } catch (err) { return { error: err.message }; } }); // Initialize project with CLI tool ipcMain.handle('init-project', async (event, projectPath) => { const { exec } = require('child_process'); const cliPath = path.join(__dirname, '..', '..'); return new Promise((resolve) => { const cmd = `node cli/index.js init "${projectPath}" --no-interactive`; exec(cmd, { cwd: cliPath }, (error, stdout, stderr) => { if (error) { resolve({ error: error.message, stderr }); } else { resolve({ success: true, output: stdout + (stderr || '') }); } }); }); }); // Get container logs from server ipcMain.handle('get-container-logs', async (event, { serverId, containerName, remotePath, lines = 100 }) => { const server = config.servers.find(s => s.id === serverId); if (!server) { return { error: 'Server not found' }; } const env = loadEnv(); const sshConfig = { host: server.host, username: env.SSH_USERNAME || server.username, password: env.SSH_PASSWORD || server.password }; const ssh = new SSHService(sshConfig); const password = env.SSH_PASSWORD || server.password; const sudoPrefix = server.useSudo ? `echo '${password}' | sudo -S ` : ''; try { await ssh.connect(); // Use docker compose logs if remotePath is provided, otherwise docker logs let logs; if (remotePath) { logs = await ssh.exec(`cd ${remotePath} && ${sudoPrefix}docker compose logs --tail ${lines} 2>&1`); } else { logs = await ssh.exec(`${sudoPrefix}docker logs ${containerName} --tail ${lines} 2>&1`); } ssh.disconnect(); return { success: true, logs }; } catch (err) { return { error: err.message }; } }); // Open VS Code diff for file comparison ipcMain.handle('open-vscode-diff', async (event, { localPath, remotePath, serverId, remoteFilePath }) => { const { exec } = require('child_process'); const server = config.servers.find(s => s.id === serverId); if (!server) { return { error: 'Server not found' }; } const env = loadEnv(); const sshConfig = { host: server.host, username: env.SSH_USERNAME || server.username, password: env.SSH_PASSWORD || server.password }; const ssh = new SSHService(sshConfig); try { // Download remote file to temp const tempDir = path.join(require('os').tmpdir(), 'docker-deploy-diff'); if (!fs.existsSync(tempDir)) { fs.mkdirSync(tempDir, { recursive: true }); } const tempFile = path.join(tempDir, `remote-${path.basename(localPath)}`); await ssh.connect(); await ssh.downloadFile(remoteFilePath, tempFile); ssh.disconnect(); // Open VS Code diff return new Promise((resolve) => { exec(`code --diff "${tempFile}" "${localPath}"`, (error) => { if (error) { resolve({ error: error.message }); } else { resolve({ success: true }); } }); }); } catch (err) { return { error: err.message }; } });