UNPKG

ludus-mcp

Version:

MCP server for managing Ludus cybersecurity training environments through natural language commands

203 lines (168 loc) 7.17 kB
import fs from 'fs/promises'; import path from 'path'; import os from 'os'; import { execSync } from 'child_process'; import { Logger } from './logger.js'; export async function downloadBaseConfigs(logger: Logger): Promise<void> { const repoUrl = 'https://github.com/NocteDefensor/LudusMCP.git'; const targetPath = 'base-configs'; const ludusConfigDir = path.join(os.homedir(), '.ludus-mcp'); const rangeConfigTemplatesDir = path.join(ludusConfigDir, 'range-config-templates'); const baseConfigsDir = path.join(rangeConfigTemplatesDir, 'base-configs'); const tempDir = path.join(ludusConfigDir, 'temp-base-configs-download'); try { logger.info('Downloading base configurations from GitHub...'); // Ensure .ludus-mcp and range-config-templates directories exist await fs.mkdir(rangeConfigTemplatesDir, { recursive: true }); // Check if base-configs directory exists const dirExists = await fs.access(baseConfigsDir).then(() => true).catch(() => false); if (dirExists) { logger.info('base-configs directory exists, updating contents...'); await updateBaseConfigsContents(baseConfigsDir, tempDir, repoUrl, targetPath, logger); } else { logger.info('base-configs directory not found, performing full clone...'); await cloneBaseConfigs(baseConfigsDir, tempDir, repoUrl, targetPath, logger); } // Get file count and structure verification const configFiles = await findConfigFiles(baseConfigsDir); logger.info(`Successfully synchronized base configurations to ~/.ludus-mcp/range-config-templates/base-configs/`, { configFiles: configFiles.length, files: configFiles.map(f => path.basename(f)) }); if (configFiles.length > 0) { logger.info(`Configuration templates available: ${configFiles.map(f => path.basename(f)).join(', ')}`); } } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); logger.warn('Failed to download base configurations (continuing without update)', { error: errorMessage }); // Clean up on error await removeDirectorySafe(tempDir); // Don't throw - fail gracefully as requested } } /** * UPDATE MODE: Directory exists, update contents while preserving custom files */ async function updateBaseConfigsContents( baseConfigsDir: string, tempDir: string, repoUrl: string, targetPath: string, logger: Logger ): Promise<void> { // Clean up any existing temp directory await removeDirectorySafe(tempDir); // Clone to temp location logger.info('Cloning base-configs from GitHub repository...'); execSync(`git clone --filter=blob:none --sparse --depth 1 "${repoUrl}" "${tempDir}"`, { stdio: 'pipe' }); execSync(`git -C "${tempDir}" sparse-checkout set "${targetPath}"`, { stdio: 'pipe' }); // Get source configs const sourceConfigsPath = path.join(tempDir, targetPath); const sourceFiles = await findConfigFiles(sourceConfigsPath); // Copy/overwrite GitHub config files, preserve user files let updatedCount = 0; let preservedCount = 0; // Get existing files for comparison const existingFiles = await findConfigFiles(baseConfigsDir); const existingFileNames = new Set(existingFiles.map(f => path.basename(f))); // Copy source files to destination (overwrite GitHub templates) for (const sourceFile of sourceFiles) { const fileName = path.basename(sourceFile); const destFile = path.join(baseConfigsDir, fileName); await fs.copyFile(sourceFile, destFile); updatedCount++; logger.debug('Updated config file', { fileName }); } // Count preserved user files (files that exist but aren't from GitHub) const sourceFileNames = new Set(sourceFiles.map(f => path.basename(f))); for (const existingFileName of existingFileNames) { if (!sourceFileNames.has(existingFileName)) { preservedCount++; logger.debug('Preserved user config file', { fileName: existingFileName }); } } // Clean up temp directory await removeDirectorySafe(tempDir); logger.info('Base configs update completed', { updated: updatedCount, preserved: preservedCount, total: updatedCount + preservedCount }); } /** * CLONE MODE: Directory doesn't exist, full clone */ async function cloneBaseConfigs( baseConfigsDir: string, tempDir: string, repoUrl: string, targetPath: string, logger: Logger ): Promise<void> { // Clean up any existing temp directory await removeDirectorySafe(tempDir); // Clone only the specific base-configs folder logger.info('Cloning base-configs from GitHub repository...'); execSync(`git clone --filter=blob:none --sparse --depth 1 "${repoUrl}" "${tempDir}"`, { stdio: 'pipe' }); execSync(`git -C "${tempDir}" sparse-checkout set "${targetPath}"`, { stdio: 'pipe' }); // Move base-configs to final location (Windows-safe approach) const sourceConfigsPath = path.join(tempDir, targetPath); await copyDirectoryRecursive(sourceConfigsPath, baseConfigsDir); // Clean up temp directory await removeDirectorySafe(tempDir); const configFiles = await findConfigFiles(baseConfigsDir); logger.info('Base configs clone completed', { cloned: configFiles.length, files: configFiles.map(f => path.basename(f)) }); } /** * Find all configuration files (.yml, .yaml) in a directory */ async function findConfigFiles(dir: string): Promise<string[]> { const files: string[] = []; try { const items = await fs.readdir(dir, { withFileTypes: true }); for (const item of items) { const fullPath = path.join(dir, item.name); if (item.isDirectory()) { const subFiles = await findConfigFiles(fullPath); files.push(...subFiles); } else if (item.isFile() && (item.name.endsWith('.yml') || item.name.endsWith('.yaml'))) { files.push(fullPath); } } } catch (error) { // Directory doesn't exist or can't be read } return files.sort(); } /** * Windows-safe recursive directory copy */ async function copyDirectoryRecursive(source: string, destination: string): Promise<void> { // Ensure destination directory exists await fs.mkdir(destination, { recursive: true }); const items = await fs.readdir(source, { withFileTypes: true }); for (const item of items) { const sourcePath = path.join(source, item.name); const destPath = path.join(destination, item.name); if (item.isDirectory()) { await copyDirectoryRecursive(sourcePath, destPath); } else if (item.isFile()) { await fs.copyFile(sourcePath, destPath); } } } /** * Windows-safe directory removal */ async function removeDirectorySafe(dirPath: string): Promise<void> { try { await fs.access(dirPath); // Directory exists, try to remove it await fs.rm(dirPath, { recursive: true, force: true }); } catch (error) { // Directory doesn't exist or couldn't be removed - that's fine for cleanup } }