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
}
}