Skip to main content
Glama
mac-installer-old.ts10.9 kB
/** * Mac Setup Installer - Auto-installs ComfyUI Desktop 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`; // D&D Battlemaps SDXL model const MODEL_NAME = 'dnd_battlemaps_sdxl.safetensors'; const MODEL_URL = 'https://huggingface.co/Darchi/dnd_battlemaps_sdxl/resolve/main/dnd_battlemaps_sdxl.safetensors'; const MODEL_SIZE = 2.5 * 1024 * 1024 * 1024; // 2.5GB export interface SetupProgress { stage: 'idle' | 'checking' | 'downloading_comfyui' | 'installing_comfyui' | 'downloading_model' | 'complete' | 'error'; progress: number; // 0-100 message: 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 SDXL model is installed */ isModelInstalled(): boolean { try { const modelPath = this.getModelPath(); return fs.existsSync(modelPath); } catch { return false; } } /** * Get the path where the SDXL model should be installed */ getModelPath(): string { // Check if ComfyUI Desktop is installed if (fs.existsSync(COMFYUI_RESOURCES_PATH)) { return path.join(COMFYUI_RESOURCES_PATH, 'models', 'checkpoints', MODEL_NAME); } // Fallback to user's home directory ComfyUI const home = process.env.HOME || '/tmp'; return path.join(home, 'Library', 'Application Support', 'ComfyUI', 'models', 'checkpoints', MODEL_NAME); } /** * 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 SDXL model */ async downloadModel(): Promise<void> { const modelPath = this.getModelPath(); const modelDir = path.dirname(modelPath); // Create model directory if it doesn't exist if (!fs.existsSync(modelDir)) { fs.mkdirSync(modelDir, { recursive: true }); } this.updateProgress({ stage: 'downloading_model', progress: 0, message: 'Downloading D&D Battlemaps SDXL model (2.5GB)...' }); try { const response = await axios({ method: 'GET', url: MODEL_URL, responseType: 'stream', maxRedirects: 5 }); const totalSize = parseInt(response.headers['content-length'] || '0', 10); let downloadedSize = 0; const writer = fs.createWriteStream(modelPath); response.data.on('data', (chunk: Buffer) => { downloadedSize += chunk.length; const progress = totalSize > 0 ? Math.round((downloadedSize / totalSize) * 100) : 0; this.updateProgress({ stage: 'downloading_model', progress, message: `Downloading SDXL model... ${(downloadedSize / 1024 / 1024 / 1024).toFixed(2)}GB / ${(totalSize / 1024 / 1024 / 1024).toFixed(2)}GB` }); }); response.data.pipe(writer); await new Promise<void>((resolve, reject) => { writer.on('finish', () => resolve()); writer.on('error', reject); }); this.logger.info('SDXL model downloaded successfully', { path: modelPath }); this.updateProgress({ stage: 'downloading_model', progress: 100, message: 'Model installed successfully' }); } catch (error) { const message = error instanceof Error ? error.message : 'Unknown error'; this.logger.error('Failed to download model', { error: message }); throw new Error(`Failed to download model: ${message}`); } } /** * Run complete setup process */ async runSetup(options: { skipComfyUI?: boolean; skipModel?: 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 ComfyUI const comfyUIInstalled = this.isComfyUIInstalled(); const modelInstalled = this.isModelInstalled(); this.logger.info('Setup status', { comfyUIInstalled, modelInstalled }); // 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 model if needed if (!modelInstalled && !options.skipModel) { await this.downloadModel(); } else if (modelInstalled) { this.logger.info('Model already installed, skipping'); } 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; modelInstalled: boolean; ready: boolean; } { const { canRun, reason } = this.canRunComfyUI(); const comfyUIInstalled = this.isComfyUIInstalled(); const modelInstalled = this.isModelInstalled(); return { canRun, reason: reason || undefined, comfyUIInstalled, modelInstalled, ready: canRun && comfyUIInstalled && modelInstalled }; } }

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