/** * Docker management for Memgraph * Handles starting/stopping the Memgraph container */ import { spawn, execSync } from 'child_process'; import * as path from 'path'; import * as fs from 'fs'; import { getSettings, getConfigDir, } from './settings.js'; const CONTAINER_NAME = 'cgr-memgraph'; // ============================================================================= // Docker Compose Generation // ============================================================================= /** * Get the path to the docker directory */ function getDockerComposeDir(): string { return path.join(getConfigDir(), 'docker'); } /** * Get the docker-compose.yml file path */ function getDockerComposeFilePath(): string { return path.join(getDockerComposeDir(), 'docker-compose.yml'); } /** * Generate docker-compose.yml with current settings */ function generateDockerCompose(): string { const settings = getSettings(); return `# Auto-generated by pi-code-graph # Do not edit manually - use /cgs config to change settings services: memgraph: image: memgraph/memgraph-mage container_name: ${CONTAINER_NAME} ports: - "${settings.memgraphPort}:7687" # Bolt protocol - "27444:7444" # HTTP API volumes: - cgr-memgraph-data:/var/lib/memgraph entrypoint: ["/usr/lib/memgraph/memgraph", "--bolt-server-name-for-init=Neo4j/", "--log-level=WARNING"] restart: unless-stopped lab: image: memgraph/lab container_name: ${CONTAINER_NAME}-lab ports: - "23000:3000" # Lab frontend environment: QUICK_CONNECT_MG_HOST: memgraph depends_on: - memgraph restart: unless-stopped volumes: cgr-memgraph-data: `; } /** * Ensure docker-compose.yml exists with current settings * Only regenerates if settings have changed or file doesn't exist * Returns the path to the compose file */ function ensureDockerCompose(): string { const composeDir = getDockerComposeDir(); const composePath = getDockerComposeFilePath(); // Ensure directory exists if (!fs.existsSync(composeDir)) { fs.mkdirSync(composeDir, { recursive: true }); } // Generate expected compose content const composeContent = generateDockerCompose(); // Only write if file doesn't exist or content changed if (fs.existsSync(composePath)) { const existingContent = fs.readFileSync(composePath, 'utf-8'); if (existingContent === composeContent) { return composePath; // No changes needed } } // Write new/updated compose file fs.writeFileSync(composePath, composeContent, 'utf-8'); return composePath; } // ============================================================================= // Types // ============================================================================= export interface DockerStatus { installed: boolean; composeInstalled: boolean; memgraphRunning: boolean; memgraphHealthy: boolean; containerId?: string; error?: string; } // ============================================================================= // Docker Detection // ============================================================================= /** * Check if Docker is installed */ export function isDockerInstalled(): boolean { try { execSync('docker --version', { stdio: 'ignore' }); return true; } catch { return false; } } /** * Check if Docker Compose is installed */ export function isDockerComposeInstalled(): boolean { try { // Try docker compose (v2) execSync('docker compose version', { stdio: 'ignore' }); return true; } catch { try { // Try docker-compose (v1) execSync('docker-compose --version', { stdio: 'ignore' }); return true; } catch { return false; } } } /** * Get the docker compose command (v1 or v2) */ function getComposeCommand(): string[] { try { execSync('docker compose version', { stdio: 'ignore' }); return ['docker', 'compose']; } catch { return ['docker-compose']; } } /** * Check if Memgraph container is running */ export function isMemgraphRunning(): { running: boolean; healthy: boolean; containerId?: string } { try { const result = execSync( `docker ps --filter "name=${CONTAINER_NAME}" --format "{{.ID}}\t{{.Status}}"`, { encoding: 'utf-8' } ).trim(); if (!result) { return { running: false, healthy: false }; } const [containerId, status] = result.split('\t'); const healthy = status.includes('healthy'); return { running: true, healthy, containerId }; } catch { return { running: false, healthy: false }; } } /** * Check if Memgraph container exists (running or stopped) */ export function memgraphContainerExists(): boolean { try { const result = execSync( `docker ps -a --filter "name=${CONTAINER_NAME}" --format "{{.ID}}"`, { encoding: 'utf-8' } ).trim(); return !!result; } catch { return false; } } /** * Get full Docker status */ export function getDockerStatus(): DockerStatus { const installed = isDockerInstalled(); if (!installed) { return { installed: false, composeInstalled: false, memgraphRunning: false, memgraphHealthy: false, error: 'Docker is not installed', }; } const composeInstalled = isDockerComposeInstalled(); if (!composeInstalled) { return { installed: true, composeInstalled: false, memgraphRunning: false, memgraphHealthy: false, error: 'Docker Compose is not installed', }; } const { running, healthy, containerId } = isMemgraphRunning(); return { installed: true, composeInstalled: true, memgraphRunning: running, memgraphHealthy: healthy, containerId, }; } // ============================================================================= // Container Management // ============================================================================= /** * Start Memgraph using Docker Compose */ export async function startMemgraph(): Promise<{ success: boolean; error?: string; passwordGenerated?: boolean }> { const status = getDockerStatus(); if (!status.installed) { return { success: false, error: 'Docker is not installed' }; } if (!status.composeInstalled) { return { success: false, error: 'Docker Compose is not installed' }; } if (status.memgraphRunning) { return { success: true }; // Already running } // Ensure docker-compose.yml exists with password const composePath = ensureDockerCompose(); return new Promise((resolve) => { const composeCmd = getComposeCommand(); const args = [...composeCmd.slice(1), '-f', composePath, 'up', '-d']; const proc = spawn(composeCmd[0], args, { stdio: 'pipe', }); let stderr = ''; proc.stderr?.on('data', (data) => { stderr += data.toString(); }); proc.on('close', (code) => { if (code === 0) { resolve({ success: true }); } else { resolve({ success: false, error: stderr || `Exit code: ${code}` }); } }); proc.on('error', (err) => { resolve({ success: false, error: err.message }); }); }); } /** * Stop Memgraph */ export async function stopMemgraph(): Promise<{ success: boolean; error?: string }> { const status = getDockerStatus(); if (!status.memgraphRunning) { return { success: true }; // Already stopped } const composePath = getDockerComposeFilePath(); if (!fs.existsSync(composePath)) { // Try to stop by container name directly try { execSync(`docker stop ${CONTAINER_NAME}`, { stdio: 'ignore' }); return { success: true }; } catch { return { success: false, error: 'Could not stop container' }; } } return new Promise((resolve) => { const composeCmd = getComposeCommand(); const args = [...composeCmd.slice(1), '-f', composePath, 'down']; const proc = spawn(composeCmd[0], args, { stdio: 'pipe', }); let stderr = ''; proc.stderr?.on('data', (data) => { stderr += data.toString(); }); proc.on('close', (code) => { if (code === 0) { resolve({ success: true }); } else { resolve({ success: false, error: stderr || `Exit code: ${code}` }); } }); proc.on('error', (err) => { resolve({ success: false, error: err.message }); }); }); } /** * Restart Memgraph */ export async function restartMemgraph(): Promise<{ success: boolean; error?: string }> { const stopResult = await stopMemgraph(); if (!stopResult.success) { return stopResult; } return startMemgraph(); } /** * Get Memgraph logs */ export function getMemgraphLogs(lines: number = 50): string { const composePath = getDockerComposeFilePath(); if (!fs.existsSync(composePath)) { // Try to get logs by container name directly try { return execSync(`docker logs --tail=${lines} ${CONTAINER_NAME}`, { encoding: 'utf-8' }); } catch (err) { return `Failed to get logs: ${(err as Error).message}`; } } try { const composeCmd = getComposeCommand(); const result = execSync( `${composeCmd.join(' ')} -f "${composePath}" logs --tail=${lines}`, { encoding: 'utf-8' } ); return result; } catch (err) { return `Failed to get logs: ${(err as Error).message}`; } } /** * Wait for Memgraph to be healthy */ export async function waitForMemgraph( timeoutMs: number = 30000, intervalMs: number = 1000 ): Promise { const startTime = Date.now(); while (Date.now() - startTime < timeoutMs) { const { running, healthy } = isMemgraphRunning(); if (running && healthy) { return true; } await new Promise((resolve) => setTimeout(resolve, intervalMs)); } return false; } /** * Get the docker-compose.yml path for users to view */ export function getDockerComposePath(): string { return getDockerComposeFilePath(); }