Files
dashcaddy/dashcaddy-installer/src/main/index.js
T
Sami f61e85d9a7 Initial commit: DashCaddy v1.0
Full codebase including API server (32 modules + routes), dashboard frontend,
DashCA certificate distribution, installer script, and deployment skills.
2026-03-05 02:26:12 -08:00

1048 lines
30 KiB
JavaScript

const { app, BrowserWindow, ipcMain, dialog } = require('electron');
const path = require('path');
// Disable GPU acceleration to prevent crashes
app.disableHardwareAcceleration();
// Add error handling
process.on('uncaughtException', (error) => {
console.error('Uncaught Exception:', error);
});
let mainWindow;
function createWindow() {
mainWindow = new BrowserWindow({
width: 900,
height: 700,
minWidth: 800,
minHeight: 600,
icon: path.join(__dirname, '../../assets/favicon.ico'),
webPreferences: {
preload: path.join(__dirname, '../preload/index.js'),
nodeIntegration: false,
contextIsolation: true,
sandbox: false
},
autoHideMenuBar: true,
resizable: true,
show: false // Don't show until ready
});
mainWindow.loadFile(path.join(__dirname, '../renderer/index.html'));
// Show window when ready
mainWindow.once('ready-to-show', () => {
mainWindow.show();
});
// Open DevTools in development mode
if (process.argv.includes('--dev')) {
mainWindow.webContents.openDevTools();
}
mainWindow.on('closed', () => {
mainWindow = null;
});
}
// App lifecycle handlers
app.whenReady().then(() => {
createWindow();
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) {
createWindow();
}
});
});
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit();
}
});
// Import utilities
const { DEFAULT_PORTS } = require('../shared/constants');
const { getPlatformInfo } = require('../shared/platform-utils');
const DependencyChecker = require('./dependency-checker');
const ConfigManager = require('./config-manager');
const FileDeployer = require('./file-deployer');
const CaddyfileGenerator = require('./caddyfile-generator');
const BrowserLauncher = require('./browser-launcher');
const ServiceManager = require('./service-manager');
// Create instances
const dependencyChecker = new DependencyChecker();
const configManager = new ConfigManager();
const fileDeployer = new FileDeployer();
const caddyfileGenerator = new CaddyfileGenerator();
const browserLauncher = new BrowserLauncher();
const serviceManager = new ServiceManager();
// IPC handlers
ipcMain.handle('check-platform', async () => {
try {
return {
success: true,
data: getPlatformInfo()
};
} catch (error) {
return {
success: false,
error: error.message
};
}
});
ipcMain.handle('check-dependencies', async (event, options) => {
try {
// Accept optional search paths for Caddy (e.g. user's configured caddy folder)
const additionalPaths = options?.caddySearchPaths || [];
const [docker, caddy, platform] = await Promise.all([
dependencyChecker.checkDocker(),
dependencyChecker.checkCaddy(additionalPaths),
dependencyChecker.checkPlatform()
]);
return {
success: true,
data: {
docker,
caddy,
platform
}
};
} catch (error) {
return {
success: false,
error: error.message
};
}
});
ipcMain.handle('check-docker', async () => {
try {
const result = await dependencyChecker.checkDocker();
return {
success: true,
data: result
};
} catch (error) {
return {
success: false,
error: error.message
};
}
});
ipcMain.handle('check-caddy', async (event, options) => {
try {
const additionalPaths = options?.caddySearchPaths || [];
const result = await dependencyChecker.checkCaddy(additionalPaths);
return {
success: true,
data: result
};
} catch (error) {
return {
success: false,
error: error.message
};
}
});
ipcMain.handle('install-docker', async () => {
try {
const platform = getPlatformInfo().os;
const result = await dependencyChecker.installDocker(platform);
return {
success: result.success,
data: result
};
} catch (error) {
return {
success: false,
error: error.message
};
}
});
ipcMain.handle('install-caddy', async (event, options) => {
try {
const platform = getPlatformInfo().os;
const targetPath = options?.targetPath || null;
const result = await dependencyChecker.installCaddy(platform, null, targetPath);
return {
success: result.success,
data: result
};
} catch (error) {
return {
success: false,
error: error.message
};
}
});
// Configuration management handlers
ipcMain.handle('validate-path', async (event, testPath) => {
try {
const result = await configManager.validatePath(testPath);
return {
success: result.valid,
data: result
};
} catch (error) {
return {
success: false,
error: error.message
};
}
});
ipcMain.handle('save-config', async (event, config) => {
try {
const result = await configManager.saveConfig(config, config.installPath);
return {
success: result.success,
data: result
};
} catch (error) {
return {
success: false,
error: error.message
};
}
});
ipcMain.handle('load-config', async (event, installPath) => {
try {
const result = await configManager.loadConfig(installPath);
return {
success: result.exists,
data: result
};
} catch (error) {
return {
success: false,
error: error.message
};
}
});
// File deployment handlers
ipcMain.handle('validate-sources', async () => {
try {
const result = await fileDeployer.validateSources();
return {
success: result.valid,
data: result
};
} catch (error) {
return {
success: false,
error: error.message
};
}
});
ipcMain.handle('deploy-dashboard', async (event, installPath) => {
try {
const result = await fileDeployer.deployDashboard(installPath, (progress) => {
event.sender.send('deployment-progress', progress);
});
return {
success: result.success,
data: result
};
} catch (error) {
return {
success: false,
error: error.message
};
}
});
ipcMain.handle('deploy-api', async (event, installPath) => {
try {
const result = await fileDeployer.deployAPI(installPath, (progress) => {
event.sender.send('deployment-progress', progress);
});
return {
success: result.success,
data: result
};
} catch (error) {
return {
success: false,
error: error.message
};
}
});
ipcMain.handle('deploy-complete', async (event, installPath) => {
try {
const result = await fileDeployer.deployComplete(installPath, (progress) => {
event.sender.send('deployment-progress', progress);
});
return {
success: result.success,
data: result
};
} catch (error) {
return {
success: false,
error: error.message
};
}
});
// Caddyfile generation handlers
ipcMain.handle('create-caddyfile', async (event, installPath, options) => {
try {
const result = await caddyfileGenerator.createCaddyfileSetup(installPath, options);
return {
success: result.success,
data: result
};
} catch (error) {
return {
success: false,
error: error.message
};
}
});
ipcMain.handle('create-docker-compose', async (event, installPath) => {
try {
const result = await caddyfileGenerator.createDockerCompose(installPath);
return {
success: result.success,
data: result
};
} catch (error) {
return {
success: false,
error: error.message
};
}
});
// Browser launcher handlers
ipcMain.handle('open-dashboard', async (event, port, hostname) => {
try {
const result = await browserLauncher.openDashboardWhenReady(port, hostname);
return {
success: result.success,
data: result
};
} catch (error) {
return {
success: false,
error: error.message
};
}
});
// Folder selection handler
ipcMain.handle('select-folder', async () => {
try {
const result = await dialog.showOpenDialog(mainWindow, {
properties: ['openDirectory', 'createDirectory'],
title: 'Select Installation Folder'
});
if (result.canceled) {
return { success: false, canceled: true };
}
return {
success: true,
path: result.filePaths[0]
};
} catch (error) {
return {
success: false,
error: error.message
};
}
});
// File selection handler (for logo, etc.)
ipcMain.handle('select-file', async (event, options) => {
try {
const result = await dialog.showOpenDialog(mainWindow, {
properties: ['openFile'],
filters: options?.filters || [
{ name: 'Images', extensions: ['png', 'jpg', 'jpeg', 'svg', 'ico'] }
],
title: options?.title || 'Select File'
});
if (result.canceled) {
return { success: false, canceled: true };
}
return {
success: true,
path: result.filePaths[0]
};
} catch (error) {
return {
success: false,
error: error.message
};
}
});
// Installation orchestration handler (tier-aware)
ipcMain.handle('run-installation', async (event, config) => {
try {
const tier = config.tier || 'basic';
const isDocker = tier !== 'basic'; // intermediate or advanced needs Docker
// Build steps dynamically based on tier
const steps = [
{ name: 'Creating directories', weight: 5 },
{ name: 'Creating seed files', weight: 3 }
];
steps.push({ name: 'Deploying dashboard files', weight: 20 });
if (isDocker) {
steps.push({ name: 'Deploying API server', weight: 15 });
}
steps.push({ name: 'Generating Caddyfile', weight: 10 });
if (isDocker) {
steps.push({ name: 'Creating docker-compose.yml', weight: 8 });
}
steps.push({ name: 'Setting up branding', weight: 5 });
if (config.dns) {
steps.push({ name: 'Saving DNS credentials', weight: 5 });
}
steps.push({ name: 'Saving configuration', weight: 5 });
steps.push({ name: 'Starting services', weight: 10 });
let completedWeight = 0;
const totalWeight = steps.reduce((sum, s) => sum + s.weight, 0);
const sendProgress = (stepName, stepProgress = 100) => {
const overallProgress = Math.round((completedWeight / totalWeight) * 100);
event.sender.send('step-progress', {
task: stepName,
progress: overallProgress,
stepProgress
});
};
const completeStep = (stepName) => {
const step = steps.find(s => s.name === stepName);
if (step) completedWeight += step.weight;
event.sender.send('step-complete', { step: stepName });
};
// Step: Create directories
sendProgress('Creating directories', 0);
await configManager.createDirectories(config.installPath);
completeStep('Creating directories');
// Step: Restore preserved settings from a previous uninstall (if any)
try {
const restoreResult = await configManager.restoreUserSettings(config.installPath);
if (restoreResult.success && restoreResult.restored.length > 0) {
console.log('[Installer] Restored preserved settings:', restoreResult.restored);
event.sender.send('step-progress', {
task: `Restored ${restoreResult.restored.length} preserved file(s) from previous install`,
progress: 0,
stepProgress: 100
});
}
} catch (err) {
console.warn('Settings restore check:', err.message);
}
// Step: Create seed files (services.json, credentials.json, etc.)
sendProgress('Creating seed files', 0);
const seedResult = await configManager.seedFiles(config.installPath);
if (!seedResult.success) {
console.warn('Seed file creation warning:', seedResult.error);
}
completeStep('Creating seed files');
// Step: Deploy dashboard
sendProgress('Deploying dashboard files', 0);
await fileDeployer.deployDashboard(config.installPath, (progress) => {
sendProgress('Deploying dashboard files', progress.percent || 0);
});
completeStep('Deploying dashboard files');
// Step: Deploy API (intermediate/advanced only)
if (isDocker) {
sendProgress('Deploying API server', 0);
await fileDeployer.deployAPI(config.installPath, (progress) => {
sendProgress('Deploying API server', progress.percent || 0);
});
completeStep('Deploying API server');
}
// Step: Generate Caddyfile
sendProgress('Generating Caddyfile', 0);
const caddyfileOptions = {
port: config.dashboardPort || 8080,
apiPort: config.apiPort || 3001,
tier: tier,
domainMode: config.domainMode || 'local'
};
if (config.domainMode === 'public' && config.domain) {
caddyfileOptions.publicDomain = config.domain.publicDomain;
caddyfileOptions.email = config.domain.email;
} else if (config.domainMode === 'custom-tld' && config.domain) {
caddyfileOptions.tld = config.domain.tld;
caddyfileOptions.caName = config.domain.caName || 'DashCaddy Local CA';
}
await caddyfileGenerator.createCaddyfileSetup(config.installPath, caddyfileOptions);
completeStep('Generating Caddyfile');
// Step: Create docker-compose (intermediate/advanced only)
if (isDocker) {
sendProgress('Creating docker-compose.yml', 0);
await caddyfileGenerator.createDockerCompose(config.installPath, {
apiPort: config.apiPort || 3001,
domainMode: config.domainMode || 'local',
lanIP: config.lanIP || '',
tailscaleIP: config.tailscaleIP || ''
});
completeStep('Creating docker-compose.yml');
}
// Step: Setup branding
sendProgress('Setting up branding', 0);
if (config.branding) {
await configManager.saveBranding(config.branding, config.installPath);
}
completeStep('Setting up branding');
// Step: Save DNS credentials (if provided)
if (config.dns) {
sendProgress('Saving DNS credentials', 0);
await configManager.saveDNSCredentials(config.dns, config.installPath);
completeStep('Saving DNS credentials');
}
// Step: Save configuration
sendProgress('Saving configuration', 0);
// Derive dashboardHost and tld from domain mode
let dashboardHost, tld = null;
if (config.domainMode === 'public' && config.domain) {
dashboardHost = config.domain.publicDomain;
} else if (config.domainMode === 'custom-tld' && config.domain) {
tld = config.domain.tld;
dashboardHost = `dashcaddy${tld}`;
} else {
dashboardHost = `localhost:${config.dashboardPort || 8080}`;
}
await configManager.saveConfig({
setupComplete: true,
configurationType: config.domainMode === 'custom-tld' ? 'homelab' : (config.domainMode === 'public' ? 'public' : 'local'),
tier: tier,
domainMode: config.domainMode || 'local',
tld: tld,
dashboardHost: dashboardHost,
dns: config.dns ? {
provider: 'technitium',
ip: config.dns.server ? config.dns.server.replace(/^https?:\/\//, '').replace(/:\d+$/, '') : '',
port: '5380'
} : undefined,
installedAt: new Date().toISOString(),
version: '1.0.0'
}, config.installPath);
completeStep('Saving configuration');
// Step: Start services
sendProgress('Starting services', 0);
if (config.autoStart) {
const caddyfilePath = path.join(config.installPath, 'Caddyfile');
const caddyBinaryPath = config.caddyBinaryPath || null;
// Start Caddy
sendProgress('Starting Caddy...', 30);
const caddyResult = await serviceManager.startCaddy(caddyfilePath, caddyBinaryPath);
if (!caddyResult.success) {
console.warn('Caddy start warning:', caddyResult.error);
}
// Start Docker Compose only for intermediate/advanced tiers
if (isDocker) {
sendProgress('Starting Docker containers...', 60);
const dockerResult = await serviceManager.startDockerCompose(config.installPath);
if (!dockerResult.success) {
console.warn('Docker Compose start warning:', dockerResult.error);
}
}
}
completeStep('Starting services');
// Installation complete - determine dashboard URL
let dashboardUrl;
if (config.domainMode === 'public' && config.domain) {
dashboardUrl = `https://${config.domain.publicDomain}`;
} else if (config.domainMode === 'custom-tld' && config.domain) {
dashboardUrl = `https://dashcaddy${config.domain.tld}`;
} else {
dashboardUrl = `http://localhost:${config.dashboardPort || 8080}`;
}
// Quick health check (non-blocking, just informational)
let healthResults = null;
if (config.autoStart) {
sendProgress('Verifying services...', 95);
// Wait a moment for services to start up
await new Promise(r => setTimeout(r, 3000));
try {
const http = require('http');
const caddyOk = await new Promise((resolve) => {
const req = http.get(`http://localhost:${DEFAULT_PORTS.CADDY_ADMIN}/config/`, { timeout: 5000 }, (res) => {
resolve(res.statusCode === 200);
});
req.on('error', () => resolve(false));
req.on('timeout', () => { req.destroy(); resolve(false); });
});
healthResults = { caddy: caddyOk };
} catch {
healthResults = { caddy: false };
}
}
sendProgress('Installation complete', 100);
event.sender.send('installation-complete', {
success: true,
installPath: config.installPath,
dashboardUrl,
health: healthResults
});
return {
success: true,
data: {
installPath: config.installPath,
dashboardUrl
}
};
} catch (error) {
event.sender.send('step-error', { error: error.message });
return {
success: false,
error: error.message
};
}
});
// DNS connection test handler
ipcMain.handle('test-dns-connection', async (event, credentials) => {
try {
// Test connectivity to the DNS server
if (!credentials?.server) {
return {
success: false,
error: 'No server URL provided'
};
}
const http = require('http');
const https = require('https');
const { URL } = require('url');
const url = new URL(credentials.server);
const protocol = url.protocol === 'https:' ? https : http;
const connected = await new Promise((resolve) => {
const req = protocol.get(url.href, { timeout: 10000 }, (res) => {
resolve(true);
});
req.on('error', () => resolve(false));
req.on('timeout', () => {
req.destroy();
resolve(false);
});
});
return {
success: true,
data: {
connected,
message: connected ? 'Successfully connected to DNS server' : 'Could not reach DNS server'
}
};
} catch (error) {
return {
success: false,
error: error.message
};
}
});
// Network detection handler - detects LAN and Tailscale IPs
ipcMain.handle('detect-network', async () => {
try {
const os = require('os');
const interfaces = os.networkInterfaces();
let lanIP = '';
let tailscaleIP = '';
for (const [name, addrs] of Object.entries(interfaces)) {
for (const addr of addrs) {
if (addr.family !== 'IPv4' || addr.internal) continue;
// Tailscale interfaces: typically 100.x.x.x range
if (addr.address.startsWith('100.') && (name.toLowerCase().includes('tailscale') || name.toLowerCase().includes('utun'))) {
tailscaleIP = addr.address;
}
// LAN: common private ranges (not 100.x Tailscale, not 172.17+ Docker)
else if (
addr.address.startsWith('192.168.') ||
addr.address.startsWith('10.') ||
(addr.address.startsWith('172.') && !addr.address.startsWith('172.17.'))
) {
if (!lanIP) lanIP = addr.address;
}
}
}
// Fallback: try to find Tailscale IP from 100.x range even without name match
if (!tailscaleIP) {
for (const addrs of Object.values(interfaces)) {
for (const addr of addrs) {
if (addr.family === 'IPv4' && addr.address.startsWith('100.') && !addr.internal) {
tailscaleIP = addr.address;
break;
}
}
if (tailscaleIP) break;
}
}
return {
success: true,
data: { lanIP, tailscaleIP }
};
} catch (error) {
return { success: false, error: error.message };
}
});
// Health check handler - verify services are running after install
ipcMain.handle('health-check', async (event, config) => {
try {
const http = require('http');
const results = {};
// Check Caddy admin API
results.caddy = await new Promise((resolve) => {
const req = http.get(`http://localhost:${DEFAULT_PORTS.CADDY_ADMIN}/config/`, { timeout: 5000 }, (res) => {
resolve({ running: res.statusCode === 200, statusCode: res.statusCode });
});
req.on('error', () => resolve({ running: false }));
req.on('timeout', () => { req.destroy(); resolve({ running: false }); });
});
// Check API server (only for non-basic tiers)
const apiPort = config?.apiPort || DEFAULT_PORTS.API;
results.api = await new Promise((resolve) => {
const req = http.get(`http://localhost:${apiPort}/api/health`, { timeout: 5000 }, (res) => {
resolve({ running: res.statusCode === 200, statusCode: res.statusCode });
});
req.on('error', () => resolve({ running: false }));
req.on('timeout', () => { req.destroy(); resolve({ running: false }); });
});
// Check dashboard is reachable
let dashboardPort = config?.dashboardPort || 8080;
let dashboardProto = 'http';
if (config?.domainMode === 'custom-tld' || config?.domainMode === 'public') {
dashboardPort = 443;
dashboardProto = 'https';
}
results.dashboard = await new Promise((resolve) => {
const mod = dashboardProto === 'https' ? require('https') : http;
const opts = { timeout: 5000 };
if (dashboardProto === 'https') opts.rejectUnauthorized = false;
const req = mod.get(`${dashboardProto}://localhost:${dashboardPort}/`, opts, (res) => {
resolve({ running: res.statusCode >= 200 && res.statusCode < 400, statusCode: res.statusCode });
});
req.on('error', () => resolve({ running: false }));
req.on('timeout', () => { req.destroy(); resolve({ running: false }); });
});
return { success: true, data: results };
} catch (error) {
return { success: false, error: error.message };
}
});
// DNS credential handlers
ipcMain.handle('save-dns-credentials', async (event, credentials, installPath) => {
try {
const result = await configManager.saveDNSCredentials(credentials, installPath);
return {
success: result.success,
data: result
};
} catch (error) {
return {
success: false,
error: error.message
};
}
});
// Branding handler
ipcMain.handle('save-branding', async (event, branding, installPath) => {
try {
const result = await configManager.saveBranding(branding, installPath);
return {
success: result.success,
data: result
};
} catch (error) {
return {
success: false,
error: error.message
};
}
});
// Service management handlers
ipcMain.handle('start-services', async (event, config) => {
try {
const result = await serviceManager.startAll(config.installPath, {
caddyfilePath: config.caddyfilePath,
caddyBinaryPath: config.caddyBinaryPath
});
return { success: result.success, data: result };
} catch (error) {
return { success: false, error: error.message };
}
});
ipcMain.handle('stop-services', async (event, config) => {
try {
const result = await serviceManager.stopAll(config.installPath);
return { success: result.success, data: result };
} catch (error) {
return { success: false, error: error.message };
}
});
ipcMain.handle('check-service-status', async (event, config) => {
try {
const [caddyStatus, dockerStatus] = await Promise.all([
serviceManager.checkCaddyStatus(),
serviceManager.checkDockerComposeStatus(config.installPath)
]);
return {
success: true,
data: { caddy: caddyStatus, docker: dockerStatus }
};
} catch (error) {
return { success: false, error: error.message };
}
});
// Detect existing installation
ipcMain.handle('detect-installation', async () => {
try {
const { DEFAULT_PATHS } = require('../shared/constants');
const platformInfo = getPlatformInfo();
const defaultPath = DEFAULT_PATHS[platformInfo.platform] || DEFAULT_PATHS.win32;
// Check default path first
const exists = await configManager.installationExists(defaultPath);
if (exists) {
const configResult = await configManager.loadConfig(defaultPath);
return {
success: true,
data: { found: true, installPath: defaultPath, config: configResult.config }
};
}
// Check common alternative paths
const alternatives = ['C:\\DashCaddy', 'C:\\caddy', '/opt/dashcaddy', '/Applications/DashCaddy'];
for (const altPath of alternatives) {
if (altPath === defaultPath) continue;
const altExists = await configManager.installationExists(altPath);
if (altExists) {
const configResult = await configManager.loadConfig(altPath);
return {
success: true,
data: { found: true, installPath: altPath, config: configResult.config }
};
}
}
return { success: true, data: { found: false } };
} catch (error) {
return { success: false, error: error.message };
}
});
// Check for preserved settings from previous uninstall
ipcMain.handle('check-preserved-settings', async (event, installPath) => {
try {
const settingsDir = path.join(installPath, '.dashcaddy-settings');
try {
await require('fs').promises.access(settingsDir);
const files = await require('fs').promises.readdir(settingsDir);
return { success: true, data: { found: true, files, settingsPath: settingsDir } };
} catch {
return { success: true, data: { found: false } };
}
} catch (error) {
return { success: false, error: error.message };
}
});
// Restore preserved settings during reinstall
ipcMain.handle('restore-preserved-settings', async (event, installPath) => {
try {
const result = await configManager.restoreUserSettings(installPath);
return { success: result.success, data: result };
} catch (error) {
return { success: false, error: error.message };
}
});
// Uninstall orchestration handler with progress tracking
ipcMain.handle('run-uninstallation', async (event, options) => {
try {
const { installPath, preserveSettings, preserveCA } = options;
// Verify installation exists
const exists = await configManager.installationExists(installPath);
if (!exists) {
return { success: false, error: 'No DashCaddy installation found at this path' };
}
// Load config to understand what was installed
const configResult = await configManager.loadConfig(installPath);
const config = configResult.config || {};
const isDocker = config.tier && config.tier !== 'basic';
// Build uninstall steps
const uninstallSteps = [];
uninstallSteps.push({ name: 'Stopping Caddy', weight: 10 });
if (isDocker) {
uninstallSteps.push({ name: 'Stopping Docker containers', weight: 15 });
uninstallSteps.push({ name: 'Removing Docker containers', weight: 10 });
}
if (preserveSettings || preserveCA) {
uninstallSteps.push({ name: 'Backing up preserved data', weight: 10 });
}
uninstallSteps.push({ name: 'Removing installation files', weight: 40 });
let completedWeight = 0;
const totalWeight = uninstallSteps.reduce((sum, s) => sum + s.weight, 0);
const sendProgress = (stepName, stepProgress = 0) => {
const overallProgress = Math.round((completedWeight / totalWeight) * 100);
event.sender.send('uninstall-progress', {
task: stepName,
progress: overallProgress,
stepProgress
});
};
const completeStep = (stepName) => {
const step = uninstallSteps.find(s => s.name === stepName);
if (step) completedWeight += step.weight;
event.sender.send('uninstall-step-complete', { step: stepName });
};
// Step: Stop Caddy
sendProgress('Stopping Caddy');
try {
await serviceManager.stopCaddy();
} catch (err) {
console.warn('Caddy stop warning:', err.message);
}
completeStep('Stopping Caddy');
// Step: Stop & remove Docker containers (if applicable)
if (isDocker) {
sendProgress('Stopping Docker containers');
try {
await serviceManager.stopDockerCompose(installPath);
} catch (err) {
console.warn('Docker stop warning:', err.message);
}
completeStep('Stopping Docker containers');
sendProgress('Removing Docker containers');
try {
const execPromise = require('util').promisify(require('child_process').exec);
const composePath = path.join(installPath, 'sites', 'dashcaddy-api', 'docker-compose.yml');
await execPromise(`docker compose -f "${composePath}" down --rmi local --volumes`, {
cwd: path.dirname(composePath),
timeout: 120000
});
} catch (err) {
console.warn('Docker cleanup warning:', err.message);
}
completeStep('Removing Docker containers');
}
// Step: Back up preserved data
if (preserveSettings || preserveCA) {
sendProgress('Backing up preserved data');
}
// Step: Remove files (config-manager handles backup internally)
sendProgress('Removing installation files');
const removeResult = await configManager.removeInstallation(installPath, {
preserveSettings,
preserveCA
});
if (!removeResult.success) {
event.sender.send('uninstall-error', { error: removeResult.error || removeResult.message });
return { success: false, error: removeResult.message };
}
if (preserveSettings || preserveCA) {
completeStep('Backing up preserved data');
}
completeStep('Removing installation files');
sendProgress('Uninstall complete', 100);
event.sender.send('uninstall-complete', {
success: true,
preservedSettings: preserveSettings,
preservedCA: preserveCA,
settingsPath: removeResult.settingsPath || null
});
return {
success: true,
data: {
preservedSettings: preserveSettings,
preservedCA: preserveCA,
settingsPath: removeResult.settingsPath || null
}
};
} catch (error) {
event.sender.send('uninstall-error', { error: error.message });
return { success: false, error: error.message };
}
});
module.exports = { mainWindow };