Skip to main content
Glama
mac-installer.ts17 kB
/** * Mac Setup Installer - Auto-installs ComfyUI Desktop + Models + Foundry Module on Apple Silicon */ import * as fs from 'fs'; import * as path from 'path'; import { execSync } from 'child_process'; import axios from 'axios'; import { Logger } from '../logger.js'; import { isAppleSilicon, isIntelMac } from '../utils/platform.js'; const COMFYUI_DOWNLOAD_URL = 'https://download.comfy.org/mac/dmg/arm64'; const COMFYUI_APP_PATH = '/Applications/ComfyUI.app'; const COMFYUI_RESOURCES_PATH = `${COMFYUI_APP_PATH}/Contents/Resources/ComfyUI`; // Model downloads (total ~13.3GB) interface ModelFile { url: string; path: string; size: number; name: string; } const MODELS: Record<string, ModelFile> = { yaml: { url: 'https://huggingface.co/AdamDooley/dnd-battlemaps-sdxl-1.0-mirror/resolve/main/dDBattlemapsSDXL10_upscaleV10.yaml', path: 'models/configs/dDBattlemapsSDXL10_upscaleV10.yaml', size: 1 * 1024, // ~1KB name: 'YAML Config' }, sdxlBase: { url: 'https://huggingface.co/stabilityai/stable-diffusion-xl-base-1.0/resolve/main/sd_xl_base_1.0.safetensors', path: 'models/checkpoints/sd_xl_base_1.0.safetensors', size: 6.5 * 1024 * 1024 * 1024, // 6.5GB name: 'SDXL Base Model' }, vae: { url: 'https://huggingface.co/stabilityai/sdxl-vae/resolve/main/sdxl_vae.safetensors', path: 'models/vae/sdxl_vae.safetensors', size: 335 * 1024 * 1024, // 335MB name: 'SDXL VAE' }, dndCheckpoint: { url: 'https://huggingface.co/AdamDooley/dnd-battlemaps-sdxl-1.0-mirror/resolve/main/dDBattlemapsSDXL10_upscaleV10.safetensors', path: 'models/checkpoints/dDBattlemapsSDXL10_upscaleV10.safetensors', size: 6.5 * 1024 * 1024 * 1024, // 6.5GB name: 'D&D Battlemaps Checkpoint' }, license: { url: 'https://huggingface.co/AdamDooley/dnd-battlemaps-sdxl-1.0-mirror/resolve/main/LICENSE.txt', path: 'models/checkpoints/dDBattlemapsSDXL10_LICENSE.txt', size: 1 * 1024, // ~1KB name: 'License File' } }; export interface SetupProgress { stage: 'idle' | 'checking' | 'downloading_comfyui' | 'installing_comfyui' | 'downloading_models' | 'installing_foundry_module' | 'complete' | 'error'; progress: number; // 0-100 message: string; currentFile?: string; error?: string; } export class MacInstaller { private logger: Logger; private progressCallback?: (progress: SetupProgress) => void; constructor(logger: Logger) { this.logger = logger.child({ component: 'MacInstaller' }); } setProgressCallback(callback: (progress: SetupProgress) => void) { this.progressCallback = callback; } private updateProgress(progress: SetupProgress) { if (this.progressCallback) { this.progressCallback(progress); } this.logger.info('Setup progress', progress); } /** * Check if ComfyUI is already installed */ isComfyUIInstalled(): boolean { try { return fs.existsSync(COMFYUI_APP_PATH) && fs.existsSync(`${COMFYUI_RESOURCES_PATH}/main.py`); } catch { return false; } } /** * Check if all models are installed */ areModelsInstalled(): boolean { try { for (const model of Object.values(MODELS)) { const modelPath = this.getModelFullPath(model.path); if (!fs.existsSync(modelPath)) { return false; } } return true; } catch { return false; } } /** * Get the full path for a model file */ private getModelFullPath(relativePath: string): string { if (fs.existsSync(COMFYUI_RESOURCES_PATH)) { return path.join(COMFYUI_RESOURCES_PATH, relativePath); } const home = process.env.HOME || '/tmp'; return path.join(home, 'Library', 'Application Support', 'ComfyUI', relativePath); } /** * Detect Foundry VTT installation */ detectFoundryInstallation(): string | null { const home = process.env.HOME; if (!home) return null; const possiblePaths = [ path.join(home, 'Library', 'Application Support', 'FoundryVTT', 'Data', 'modules'), path.join(home, 'FoundryVTT', 'Data', 'modules'), '/Applications/FoundryVTT/Data/modules' ]; for (const foundryPath of possiblePaths) { if (fs.existsSync(foundryPath)) { this.logger.info('Found Foundry VTT installation', { path: foundryPath }); return foundryPath; } } return null; } /** * Install Foundry MCP Bridge module */ async installFoundryModule(): Promise<{ success: boolean; message: string }> { this.updateProgress({ stage: 'installing_foundry_module', progress: 0, message: 'Detecting Foundry VTT installation...' }); const foundryModulesPath = this.detectFoundryInstallation(); if (!foundryModulesPath) { return { success: false, message: 'Foundry VTT not found. Please install Foundry VTT from https://foundryvtt.com/ and rerun setup.' }; } const moduleDestPath = path.join(foundryModulesPath, 'foundry-mcp-bridge'); // Check if module already exists if (fs.existsSync(path.join(moduleDestPath, 'module.json'))) { this.logger.info('Foundry module already installed', { path: moduleDestPath }); return { success: true, message: 'Foundry module already installed' }; } try { this.updateProgress({ stage: 'installing_foundry_module', progress: 50, message: 'Installing Foundry MCP Bridge module...' }); // Find the bundled module (should be in Resources) // @ts-ignore - resourcesPath is added by Electron but not in Node types const resourcesPath = process.resourcesPath || path.join(__dirname, '..', '..', '..', 'Resources'); const moduleSourcePath = path.join(resourcesPath, 'foundry-module'); if (!fs.existsSync(moduleSourcePath)) { throw new Error(`Module source not found at ${moduleSourcePath}`); } // Copy module files this.copyRecursiveSync(moduleSourcePath, moduleDestPath); this.updateProgress({ stage: 'installing_foundry_module', progress: 100, message: 'Foundry module installed successfully' }); return { success: true, message: `Module installed to ${moduleDestPath}` }; } catch (error) { const message = error instanceof Error ? error.message : 'Unknown error'; this.logger.error('Failed to install Foundry module', { error: message }); return { success: false, message: `Failed to install module: ${message}` }; } } /** * Recursively copy directory */ private copyRecursiveSync(src: string, dest: string) { const exists = fs.existsSync(src); const stats = exists && fs.statSync(src); const isDirectory = exists && stats && stats.isDirectory(); if (isDirectory) { if (!fs.existsSync(dest)) { fs.mkdirSync(dest, { recursive: true }); } fs.readdirSync(src).forEach((childItemName) => { this.copyRecursiveSync(path.join(src, childItemName), path.join(dest, childItemName)); }); } else { fs.copyFileSync(src, dest); } } /** * Check if this Mac can run ComfyUI (Apple Silicon only) */ canRunComfyUI(): { canRun: boolean; reason?: string } { if (isIntelMac()) { return { canRun: false, reason: 'ComfyUI requires Apple Silicon (M1/M2/M3/M4). Intel Macs are not supported.' }; } if (!isAppleSilicon()) { return { canRun: false, reason: 'ComfyUI is only available for macOS with Apple Silicon.' }; } return { canRun: true }; } /** * Download ComfyUI Desktop DMG */ async downloadComfyUI(downloadPath: string): Promise<void> { this.updateProgress({ stage: 'downloading_comfyui', progress: 0, message: 'Downloading ComfyUI Desktop (200MB)...' }); try { const response = await axios({ method: 'GET', url: COMFYUI_DOWNLOAD_URL, responseType: 'stream', maxRedirects: 5 }); const totalSize = parseInt(response.headers['content-length'] || '0', 10); let downloadedSize = 0; const writer = fs.createWriteStream(downloadPath); response.data.on('data', (chunk: Buffer) => { downloadedSize += chunk.length; const progress = totalSize > 0 ? Math.round((downloadedSize / totalSize) * 100) : 0; this.updateProgress({ stage: 'downloading_comfyui', progress, message: `Downloading ComfyUI Desktop... ${Math.round(downloadedSize / 1024 / 1024)}MB / ${Math.round(totalSize / 1024 / 1024)}MB` }); }); response.data.pipe(writer); await new Promise<void>((resolve, reject) => { writer.on('finish', () => resolve()); writer.on('error', reject); }); this.logger.info('ComfyUI Desktop downloaded successfully'); } catch (error) { const message = error instanceof Error ? error.message : 'Unknown error'; this.logger.error('Failed to download ComfyUI', { error: message }); throw new Error(`Failed to download ComfyUI: ${message}`); } } /** * Install ComfyUI from DMG */ async installComfyUI(dmgPath: string): Promise<void> { this.updateProgress({ stage: 'installing_comfyui', progress: 0, message: 'Installing ComfyUI Desktop...' }); try { // Mount the DMG this.logger.info('Mounting DMG', { path: dmgPath }); const mountOutput = execSync(`hdiutil attach "${dmgPath}" -nobrowse -noverify`, { encoding: 'utf8' }); // Parse mount output to find volume path const lines = mountOutput.split('\n'); let volumePath = ''; for (const line of lines) { if (line.includes('/Volumes/')) { const match = line.match(/\/Volumes\/[^\s]+/); if (match) { volumePath = match[0]; break; } } } if (!volumePath) { throw new Error('Failed to find mounted volume path'); } this.logger.info('DMG mounted', { volume: volumePath }); this.updateProgress({ stage: 'installing_comfyui', progress: 50, message: 'Copying ComfyUI to Applications...' }); // Find ComfyUI.app in mounted volume const appPath = `${volumePath}/ComfyUI.app`; if (!fs.existsSync(appPath)) { throw new Error(`ComfyUI.app not found in mounted volume: ${volumePath}`); } // Copy to /Applications execSync(`cp -R "${appPath}" /Applications/`, { encoding: 'utf8' }); this.logger.info('ComfyUI copied to Applications'); this.updateProgress({ stage: 'installing_comfyui', progress: 90, message: 'Cleaning up...' }); // Unmount DMG execSync(`hdiutil detach "${volumePath}"`, { encoding: 'utf8' }); this.logger.info('DMG unmounted'); this.updateProgress({ stage: 'installing_comfyui', progress: 100, message: 'ComfyUI installed successfully' }); } catch (error) { const message = error instanceof Error ? error.message : 'Unknown error'; this.logger.error('Failed to install ComfyUI', { error: message }); throw new Error(`Failed to install ComfyUI: ${message}`); } } /** * Download a single model file */ private async downloadFile(url: string, destPath: string, displayName: string, modelSize: number): Promise<void> { // Create directory if it doesn't exist const destDir = path.dirname(destPath); if (!fs.existsSync(destDir)) { fs.mkdirSync(destDir, { recursive: true }); } try { const response = await axios({ method: 'GET', url, responseType: 'stream', maxRedirects: 5 }); const totalSize = parseInt(response.headers['content-length'] || '0', 10) || modelSize; let downloadedSize = 0; const writer = fs.createWriteStream(destPath); response.data.on('data', (chunk: Buffer) => { downloadedSize += chunk.length; const progress = totalSize > 0 ? Math.round((downloadedSize / totalSize) * 100) : 0; const sizeFormatted = totalSize > 1024 * 1024 * 1024 ? `${(downloadedSize / 1024 / 1024 / 1024).toFixed(2)}GB / ${(totalSize / 1024 / 1024 / 1024).toFixed(2)}GB` : `${Math.round(downloadedSize / 1024 / 1024)}MB / ${Math.round(totalSize / 1024 / 1024)}MB`; this.updateProgress({ stage: 'downloading_models', progress, message: `Downloading ${displayName}...`, currentFile: `${displayName} - ${sizeFormatted}` }); }); response.data.pipe(writer); await new Promise<void>((resolve, reject) => { writer.on('finish', () => resolve()); writer.on('error', reject); }); this.logger.info('File downloaded successfully', { file: displayName, path: destPath }); } catch (error) { const message = error instanceof Error ? error.message : 'Unknown error'; this.logger.error('Failed to download file', { file: displayName, error: message }); throw new Error(`Failed to download ${displayName}: ${message}`); } } /** * Download all required models */ async downloadModels(): Promise<void> { const modelEntries = Object.entries(MODELS); const totalModels = modelEntries.length; for (let i = 0; i < totalModels; i++) { const [key, model] = modelEntries[i]; const modelPath = this.getModelFullPath(model.path); // Skip if already exists if (fs.existsSync(modelPath)) { this.logger.info('Model already exists, skipping', { model: model.name }); continue; } this.updateProgress({ stage: 'downloading_models', progress: Math.round((i / totalModels) * 100), message: `Downloading ${model.name} (${i + 1}/${totalModels})...`, currentFile: model.name }); await this.downloadFile(model.url, modelPath, model.name, model.size); } this.updateProgress({ stage: 'downloading_models', progress: 100, message: 'All models downloaded successfully' }); } /** * Run complete setup process */ async runSetup(options: { skipComfyUI?: boolean; skipModels?: boolean; skipFoundryModule?: boolean } = {}): Promise<void> { try { this.updateProgress({ stage: 'checking', progress: 0, message: 'Checking system compatibility...' }); // Check if Apple Silicon const { canRun, reason } = this.canRunComfyUI(); if (!canRun) { throw new Error(reason); } // Check current state const comfyUIInstalled = this.isComfyUIInstalled(); const modelsInstalled = this.areModelsInstalled(); this.logger.info('Setup status', { comfyUIInstalled, modelsInstalled }); // Install ComfyUI if needed if (!comfyUIInstalled && !options.skipComfyUI) { const tmpDir = process.env.TMPDIR || '/tmp'; const dmgPath = path.join(tmpDir, 'ComfyUI.dmg'); await this.downloadComfyUI(dmgPath); await this.installComfyUI(dmgPath); // Clean up DMG if (fs.existsSync(dmgPath)) { fs.unlinkSync(dmgPath); } } else if (comfyUIInstalled) { this.logger.info('ComfyUI already installed, skipping'); } // Download models if needed if (!modelsInstalled && !options.skipModels) { await this.downloadModels(); } else if (modelsInstalled) { this.logger.info('Models already installed, skipping'); } // Install Foundry module if needed if (!options.skipFoundryModule) { const moduleResult = await this.installFoundryModule(); this.logger.info('Foundry module installation result', moduleResult); } this.updateProgress({ stage: 'complete', progress: 100, message: 'Setup complete! AI map generation is ready.' }); } catch (error) { const message = error instanceof Error ? error.message : 'Unknown error'; this.updateProgress({ stage: 'error', progress: 0, message: 'Setup failed', error: message }); throw error; } } /** * Get setup status */ getSetupStatus(): { canRun: boolean; reason?: string | undefined; comfyUIInstalled: boolean; modelsInstalled: boolean; foundryDetected: boolean; ready: boolean; } { const { canRun, reason } = this.canRunComfyUI(); const comfyUIInstalled = this.isComfyUIInstalled(); const modelsInstalled = this.areModelsInstalled(); const foundryDetected = this.detectFoundryInstallation() !== null; return { canRun, reason: reason || undefined, comfyUIInstalled, modelsInstalled, foundryDetected, ready: canRun && comfyUIInstalled && modelsInstalled }; } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/adambdooley/foundry-vtt-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server