Files
dashcaddy/dashcaddy-installer/src/main/dependency-checker.js
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

793 lines
25 KiB
JavaScript

const { exec, spawn } = require('child_process');
const { promisify } = require('util');
const path = require('path');
const platformUtils = require('../shared/platform-utils');
const DownloadManager = require('./download-manager');
const execAsync = promisify(exec);
class DependencyChecker {
/**
* Checks if Docker is installed and running
* @returns {Promise<Object>} { installed: boolean, running: boolean, version: string }
*/
async checkDocker() {
try {
// Check if Docker is installed
const versionResult = await this.executeCommand('docker --version');
if (!versionResult.success) {
return {
installed: false,
running: false,
version: null
};
}
// Extract version from output (e.g., "Docker version 24.0.7, build afdd53b")
const versionMatch = versionResult.stdout.match(/Docker version ([\d.]+)/);
const version = versionMatch ? versionMatch[1] : 'unknown';
// Check if Docker daemon is running
const infoResult = await this.executeCommand('docker info');
return {
installed: true,
running: infoResult.success,
version: version
};
} catch (error) {
return {
installed: false,
running: false,
version: null,
error: error.message
};
}
}
/**
* Checks if Caddy is installed
* @param {string[]} [additionalPaths] - Additional paths to search for caddy binary
* @returns {Promise<Object>} { installed: boolean, version: string, path: string }
*/
async checkCaddy(additionalPaths) {
try {
// First try caddy in PATH
const result = await this.executeCommand('caddy version');
if (result.success) {
const versionMatch = result.stdout.match(/v([\d.]+)/);
const version = versionMatch ? versionMatch[1] : 'unknown';
// Try to get Caddy path
let caddyPath = null;
if (platformUtils.isWindows()) {
const whereResult = await this.executeCommand('where caddy');
if (whereResult.success) {
caddyPath = whereResult.stdout.trim().split('\n')[0];
}
} else {
const whichResult = await this.executeCommand('which caddy');
if (whichResult.success) {
caddyPath = whichResult.stdout.trim();
}
}
return { installed: true, version, path: caddyPath };
}
// Not in PATH - search common locations
const searchResult = await this.findCaddyBinary(additionalPaths);
if (searchResult) {
return searchResult;
}
return { installed: false, version: null, path: null };
} catch (error) {
return {
installed: false,
version: null,
path: null,
error: error.message
};
}
}
/**
* Searches common locations for the Caddy binary
* @param {string[]} [additionalPaths] - Additional paths to search
* @returns {Promise<Object|null>} Caddy info if found, null otherwise
*/
async findCaddyBinary(additionalPaths) {
const fs = require('fs');
const binaryName = platformUtils.isWindows() ? 'caddy.exe' : 'caddy';
// Build list of candidate paths
const searchPaths = [];
if (platformUtils.isWindows()) {
searchPaths.push(
'C:\\caddy',
'C:\\Program Files\\Caddy',
'C:\\Program Files (x86)\\Caddy',
path.join(process.env.LOCALAPPDATA || '', 'Caddy'),
path.join(process.env.USERPROFILE || '', 'caddy'),
'C:\\DashCaddy\\caddy'
);
} else {
searchPaths.push(
'/usr/local/bin',
'/usr/bin',
'/opt/caddy',
'/opt/dashcaddy/caddy',
path.join(process.env.HOME || '', '.local/bin')
);
}
// Add any user-specified paths
if (additionalPaths) {
searchPaths.push(...additionalPaths);
}
for (const dir of searchPaths) {
if (!dir) continue;
const fullPath = path.join(dir, binaryName);
try {
fs.accessSync(fullPath, fs.constants.X_OK);
// Found the binary - verify it works
const result = await this.executeCommand(`"${fullPath}" version`);
if (result.success) {
const versionMatch = result.stdout.match(/v([\d.]+)/);
const version = versionMatch ? versionMatch[1] : 'unknown';
return { installed: true, version, path: fullPath };
}
} catch {
// Not found here, continue searching
}
}
return null;
}
/**
* Checks platform information
* @returns {Promise<Object>} Platform details
*/
async checkPlatform() {
return platformUtils.getPlatformInfo();
}
/**
* Provides installation instructions for Docker
* @param {string} platform - Platform identifier (windows, macos, linux)
* @returns {Object} Installation instructions
*/
getDockerInstallInstructions(platform) {
const instructions = {
windows: {
title: 'Install Docker Desktop for Windows',
steps: [
'Download Docker Desktop from https://www.docker.com/products/docker-desktop',
'Run the installer and follow the setup wizard',
'Restart your computer if prompted',
'Launch Docker Desktop from the Start menu',
'Wait for Docker to start (you\'ll see the whale icon in the system tray)',
'Click "Retry" in the installer once Docker is running'
],
url: 'https://docs.docker.com/desktop/install/windows-install/',
requiresWSL2: true,
wsl2Instructions: [
'Docker Desktop requires WSL 2',
'The installer will guide you through enabling WSL 2 if needed',
'You may need to enable virtualization in your BIOS'
]
},
macos: {
title: 'Install Docker Desktop for Mac',
steps: [
'Download Docker Desktop from https://www.docker.com/products/docker-desktop',
'Open the .dmg file and drag Docker to Applications',
'Launch Docker from Applications',
'Grant necessary permissions when prompted',
'Wait for Docker to start (you\'ll see the whale icon in the menu bar)',
'Click "Retry" in the installer once Docker is running'
],
url: 'https://docs.docker.com/desktop/install/mac-install/',
requiresWSL2: false
},
linux: {
title: 'Install Docker Engine for Linux',
steps: [
'Open a terminal',
'Run: curl -fsSL https://get.docker.com -o get-docker.sh',
'Run: sudo sh get-docker.sh',
'Run: sudo systemctl start docker',
'Run: sudo systemctl enable docker',
'Run: sudo usermod -aG docker $USER',
'Log out and back in for group changes to take effect',
'Click "Retry" in the installer'
],
url: 'https://docs.docker.com/engine/install/',
requiresWSL2: false,
packageManagers: {
ubuntu: 'sudo apt-get update && sudo apt-get install docker.io',
fedora: 'sudo dnf install docker',
arch: 'sudo pacman -S docker'
}
}
};
return instructions[platform] || instructions.linux;
}
/**
* Provides installation instructions for Caddy
* @param {string} platform - Platform identifier (windows, macos, linux)
* @returns {Object} Installation instructions
*/
getCaddyInstallInstructions(platform) {
const instructions = {
windows: {
title: 'Install Caddy for Windows',
steps: [
'Download Caddy from https://caddyserver.com/download',
'Extract the caddy.exe file',
'Move caddy.exe to C:\\Windows\\System32 (requires admin)',
'Or add the Caddy folder to your PATH environment variable',
'Open a new PowerShell window',
'Run: caddy version',
'Click "Retry" in the installer'
],
url: 'https://caddyserver.com/docs/install#windows',
automated: false
},
macos: {
title: 'Install Caddy for macOS',
steps: [
'Open Terminal',
'Install Homebrew if not already installed: /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"',
'Run: brew install caddy',
'Run: caddy version',
'Click "Retry" in the installer'
],
url: 'https://caddyserver.com/docs/install#mac',
automated: true,
command: 'brew install caddy'
},
linux: {
title: 'Install Caddy for Linux',
steps: [
'Open a terminal',
'Run the appropriate command for your distribution (see below)',
'Run: caddy version',
'Click "Retry" in the installer'
],
url: 'https://caddyserver.com/docs/install#debian-ubuntu-raspbian',
automated: true,
packageManagers: {
ubuntu: 'sudo apt install -y debian-keyring debian-archive-keyring apt-transport-https && curl -1sLf "https://dl.cloudsmith.io/public/caddy/stable/gpg.key" | sudo gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg && curl -1sLf "https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt" | sudo tee /etc/apt/sources.list.d/caddy-stable.list && sudo apt update && sudo apt install caddy',
fedora: 'dnf install "dnf-command(copr)" && dnf copr enable @caddy/caddy && dnf install caddy',
arch: 'pacman -S caddy'
}
}
};
return instructions[platform] || instructions.linux;
}
/**
* Attempts automated installation of Docker (platform-specific)
* @param {string} platform - Platform identifier
* @param {Function} progressCallback - Progress callback
* @returns {Promise<Object>} Installation result
*/
async installDocker(platform, progressCallback) {
const downloadManager = new DownloadManager();
const platformInfo = platformUtils.getPlatformInfo();
try {
// Download Docker Desktop
progressCallback?.({ status: 'downloading', message: 'Downloading Docker Desktop...' });
const downloadResult = await downloadManager.downloadDocker(
platform,
platformInfo.arch,
progressCallback
);
if (!downloadResult.success) {
// Fallback to instructions if download fails
return {
success: false,
automated: false,
message: downloadResult.message || 'Download failed',
instructions: this.getDockerInstallInstructions(platform)
};
}
// Run the installer
progressCallback?.({ status: 'installing', message: 'Running Docker installer...' });
if (platform === 'windows') {
// Run Docker Desktop installer silently
const installerPath = downloadResult.path;
try {
// The Docker Desktop installer supports silent install
await this.runInstallerWindows(installerPath);
// Wait for Docker to be ready
progressCallback?.({ status: 'waiting', message: 'Waiting for Docker to start...' });
await this.waitForDocker(120000); // 2 minute timeout
// Verify installation
const verification = await this.checkDocker();
if (verification.installed) {
await downloadManager.cleanup();
return {
success: true,
automated: true,
message: 'Docker Desktop installed successfully',
version: verification.version
};
}
} catch (err) {
// Installation might require user interaction or admin
return {
success: false,
automated: true,
message: 'Docker installer launched. Please complete the installation and restart.',
instructions: this.getDockerInstallInstructions(platform)
};
}
} else if (platform === 'macos') {
// Mount and install DMG
const dmgPath = downloadResult.path;
try {
await this.runInstallerMacOS(dmgPath);
progressCallback?.({ status: 'waiting', message: 'Waiting for Docker to start...' });
await this.waitForDocker(120000);
const verification = await this.checkDocker();
if (verification.installed) {
await downloadManager.cleanup();
return {
success: true,
automated: true,
message: 'Docker Desktop installed successfully',
version: verification.version
};
}
} catch (err) {
return {
success: false,
automated: true,
message: 'Docker installation started. Please complete any prompts.',
instructions: this.getDockerInstallInstructions(platform)
};
}
} else {
// Linux - use package manager
return await this.installDockerLinux(progressCallback);
}
return {
success: false,
automated: false,
message: 'Docker installation requires manual completion',
instructions: this.getDockerInstallInstructions(platform)
};
} catch (error) {
return {
success: false,
automated: false,
message: error.message,
instructions: this.getDockerInstallInstructions(platform)
};
}
}
/**
* Run Docker installer on Windows
*/
async runInstallerWindows(installerPath) {
return new Promise((resolve, reject) => {
// Start installer with install flag
const installer = spawn(installerPath, ['install', '--quiet', '--accept-license'], {
detached: true,
stdio: 'ignore'
});
installer.unref();
// Give the installer time to start
setTimeout(() => resolve(), 5000);
});
}
/**
* Run Docker installer on macOS
*/
async runInstallerMacOS(dmgPath) {
// Mount the DMG
await this.executeCommand(`hdiutil attach "${dmgPath}" -nobrowse`);
// Copy to Applications
await this.executeCommand('cp -R "/Volumes/Docker/Docker.app" /Applications/');
// Unmount
await this.executeCommand('hdiutil detach "/Volumes/Docker"');
// Launch Docker
await this.executeCommand('open /Applications/Docker.app');
}
/**
* Install Docker on Linux using package manager
*/
async installDockerLinux(progressCallback) {
const distro = await this.detectLinuxDistro();
progressCallback?.({ status: 'installing', message: `Installing Docker for ${distro}...` });
try {
if (distro === 'ubuntu' || distro === 'debian') {
await this.executeCommand('curl -fsSL https://get.docker.com -o /tmp/get-docker.sh');
await this.executeCommand('sudo sh /tmp/get-docker.sh');
await this.executeCommand('sudo systemctl enable docker');
await this.executeCommand('sudo systemctl start docker');
} else if (distro === 'fedora') {
await this.executeCommand('sudo dnf install -y docker-ce docker-ce-cli containerd.io');
await this.executeCommand('sudo systemctl enable docker');
await this.executeCommand('sudo systemctl start docker');
} else if (distro === 'arch') {
await this.executeCommand('sudo pacman -S --noconfirm docker');
await this.executeCommand('sudo systemctl enable docker');
await this.executeCommand('sudo systemctl start docker');
} else {
return {
success: false,
automated: false,
message: 'Unsupported Linux distribution',
instructions: this.getDockerInstallInstructions('linux')
};
}
// Add user to docker group
await this.executeCommand('sudo usermod -aG docker $USER');
const verification = await this.checkDocker();
return {
success: verification.installed,
automated: true,
message: verification.installed ?
'Docker installed successfully. You may need to log out and back in for group changes.' :
'Docker installed but may require restart',
version: verification.version
};
} catch (error) {
return {
success: false,
automated: true,
message: error.message,
instructions: this.getDockerInstallInstructions('linux')
};
}
}
/**
* Wait for Docker to become available
*/
async waitForDocker(timeout = 60000) {
const startTime = Date.now();
const pollInterval = 3000;
while (Date.now() - startTime < timeout) {
const status = await this.checkDocker();
if (status.installed && status.running) {
return true;
}
await new Promise(resolve => setTimeout(resolve, pollInterval));
}
throw new Error('Docker did not become available within timeout');
}
/**
* Attempts automated installation of Caddy (platform-specific)
* @param {string} platform - Platform identifier
* @param {Function} progressCallback - Progress callback
* @param {string} targetPath - Optional target path for Caddy binary
* @returns {Promise<Object>} Installation result
*/
async installCaddy(platform, progressCallback, targetPath) {
const downloadManager = new DownloadManager();
const platformInfo = platformUtils.getPlatformInfo();
const instructions = this.getCaddyInstallInstructions(platform);
try {
// Download Caddy from GitHub
progressCallback?.({ status: 'downloading', message: 'Downloading Caddy...' });
const downloadResult = await downloadManager.downloadCaddy(
platform,
platformInfo.arch,
progressCallback
);
if (!downloadResult.success) {
// Try package manager as fallback
return await this.installCaddyWithPackageManager(platform, progressCallback);
}
// Extract the archive
progressCallback?.({ status: 'extracting', message: 'Extracting Caddy...' });
// Determine target directory
let extractTarget;
if (targetPath) {
extractTarget = targetPath;
} else if (platform === 'windows') {
extractTarget = 'C:\\Program Files\\Caddy';
} else {
extractTarget = '/usr/local/bin';
}
const extractResult = await downloadManager.extractCaddy(
downloadResult.path,
extractTarget,
platform
);
if (!extractResult.success) {
return {
success: false,
automated: true,
message: 'Failed to extract Caddy',
error: extractResult.error,
instructions
};
}
// On Windows, add to PATH if needed
if (platform === 'windows') {
progressCallback?.({ status: 'configuring', message: 'Adding Caddy to PATH...' });
await this.addToWindowsPath(extractTarget);
}
// Verify installation
progressCallback?.({ status: 'verifying', message: 'Verifying installation...' });
// Give a moment for PATH changes to take effect
await new Promise(resolve => setTimeout(resolve, 1000));
const verification = await this.checkCaddy([extractTarget]);
await downloadManager.cleanup();
if (verification.installed) {
return {
success: true,
automated: true,
message: 'Caddy installed successfully',
version: downloadResult.version || verification.version,
path: verification.path || extractResult.binaryPath
};
} else {
// Binary exists but not in PATH yet
return {
success: true,
automated: true,
message: 'Caddy installed. Restart terminal for PATH changes.',
version: downloadResult.version,
path: extractResult.binaryPath
};
}
} catch (error) {
// Fallback to package manager
return await this.installCaddyWithPackageManager(platform, progressCallback);
}
}
/**
* Install Caddy using system package manager
*/
async installCaddyWithPackageManager(platform, progressCallback) {
const instructions = this.getCaddyInstallInstructions(platform);
try {
let command;
if (platform === 'macos') {
// Check if Homebrew is available
const brewCheck = await this.executeCommand('which brew');
if (brewCheck.success) {
progressCallback?.({ status: 'installing', message: 'Installing Caddy via Homebrew...' });
command = 'brew install caddy';
} else {
return {
success: false,
automated: false,
message: 'Homebrew not found. Please install Homebrew first.',
instructions
};
}
} else if (platform === 'linux') {
const distro = await this.detectLinuxDistro();
progressCallback?.({ status: 'installing', message: `Installing Caddy for ${distro}...` });
if (distro === 'ubuntu' || distro === 'debian') {
// Add Caddy repository and install
await this.executeCommand('sudo apt install -y debian-keyring debian-archive-keyring apt-transport-https');
await this.executeCommand('curl -1sLf "https://dl.cloudsmith.io/public/caddy/stable/gpg.key" | sudo gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg');
await this.executeCommand('curl -1sLf "https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt" | sudo tee /etc/apt/sources.list.d/caddy-stable.list');
await this.executeCommand('sudo apt update');
command = 'sudo apt install -y caddy';
} else if (distro === 'fedora') {
await this.executeCommand('sudo dnf install -y "dnf-command(copr)"');
await this.executeCommand('sudo dnf copr enable -y @caddy/caddy');
command = 'sudo dnf install -y caddy';
} else if (distro === 'arch') {
command = 'sudo pacman -S --noconfirm caddy';
} else {
return {
success: false,
automated: false,
message: 'Unsupported Linux distribution',
instructions
};
}
} else {
return {
success: false,
automated: false,
message: 'Automated installation not supported',
instructions
};
}
const result = await this.executeCommand(command);
if (result.success) {
const verification = await this.checkCaddy();
return {
success: verification.installed,
automated: true,
message: verification.installed ? 'Caddy installed successfully' : 'Installation completed but verification failed',
version: verification.version
};
} else {
return {
success: false,
automated: true,
message: 'Installation command failed',
error: result.stderr,
instructions
};
}
} catch (error) {
return {
success: false,
automated: true,
message: 'Installation failed',
error: error.message,
instructions
};
}
}
/**
* Add directory to Windows PATH
*/
async addToWindowsPath(directory) {
try {
// Add to user PATH using PowerShell
const script = `
$currentPath = [Environment]::GetEnvironmentVariable('Path', 'User')
if ($currentPath -notlike '*${directory.replace(/\\/g, '\\\\')}*') {
[Environment]::SetEnvironmentVariable('Path', "$currentPath;${directory.replace(/\\/g, '\\\\')}", 'User')
}
`;
await this.executeCommand(`powershell -Command "${script}"`);
return { success: true };
} catch (error) {
return { success: false, error: error.message };
}
}
/**
* Detects Linux distribution
* @returns {Promise<string>} Distribution name (ubuntu, fedora, arch, etc.)
*/
async detectLinuxDistro() {
try {
const result = await this.executeCommand('cat /etc/os-release');
if (result.success) {
const output = result.stdout.toLowerCase();
if (output.includes('ubuntu') || output.includes('debian')) {
return 'ubuntu';
} else if (output.includes('fedora') || output.includes('rhel') || output.includes('centos')) {
return 'fedora';
} else if (output.includes('arch')) {
return 'arch';
}
}
return 'unknown';
} catch (error) {
return 'unknown';
}
}
/**
* Executes a shell command
* @param {string} command - Command to execute
* @returns {Promise<Object>} { success: boolean, stdout: string, stderr: string }
*/
async executeCommand(command) {
try {
const { stdout, stderr } = await execAsync(command, {
timeout: 30000, // 30 second timeout
maxBuffer: 1024 * 1024 // 1MB buffer
});
return {
success: true,
stdout: stdout,
stderr: stderr
};
} catch (error) {
return {
success: false,
stdout: error.stdout || '',
stderr: error.stderr || error.message,
error: error.message
};
}
}
/**
* Checks if WSL2 is available on Windows
* @returns {Promise<Object>} WSL2 status
*/
async checkWSL2() {
if (!platformUtils.isWindows()) {
return {
available: false,
required: false,
message: 'WSL2 is only relevant on Windows'
};
}
try {
const result = await this.executeCommand('wsl --status');
return {
available: result.success,
required: true,
message: result.success ? 'WSL2 is available' : 'WSL2 is not installed',
details: result.stdout
};
} catch (error) {
return {
available: false,
required: true,
message: 'WSL2 is not installed or not accessible',
error: error.message
};
}
}
}
module.exports = DependencyChecker;