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 downloadLudusDocumentation(logger: Logger): Promise<void> {
const repoUrl = 'https://gitlab.com/badsectorlabs/ludus.git';
const targetPath = 'docs/docs';
const ludusConfigDir = path.join(os.homedir(), '.ludus-mcp');
const docsDir = path.join(ludusConfigDir, 'docs');
const tempDir = path.join(ludusConfigDir, 'temp-docs-download');
try {
logger.info('Downloading fresh Ludus documentation...');
// Ensure .ludus-mcp directory exists
await fs.mkdir(ludusConfigDir, { recursive: true });
// Clean up any existing temp and docs directories (Windows-safe)
await removeDirectorySafe(tempDir);
await removeDirectorySafe(docsDir);
// Clone only the specific docs folder
logger.info('Cloning documentation from GitLab 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 docs to final location (Windows-safe approach)
const sourceDocsPath = path.join(tempDir, targetPath);
await copyDirectoryRecursive(sourceDocsPath, docsDir);
// Verify we got the important subdirectories
const importantDirs = ['environment-guides', 'quick-start', 'troubleshooting'];
const foundDirs: string[] = [];
const missingDirs: string[] = [];
for (const dirName of importantDirs) {
const dirPath = path.join(docsDir, dirName);
try {
const stat = await fs.stat(dirPath);
if (stat.isDirectory()) {
const files = await findMarkdownFiles(dirPath);
foundDirs.push(`${dirName} (${files.length} files)`);
} else {
missingDirs.push(dirName);
}
} catch {
missingDirs.push(dirName);
}
}
// Clean up temp directory
await removeDirectorySafe(tempDir);
// Get total file count and structure
const allFiles = await findAllFiles(docsDir);
const markdownFiles = await findMarkdownFiles(docsDir);
logger.info(`Successfully downloaded Ludus documentation to ~/.ludus-mcp/docs/`, {
totalFiles: allFiles.length,
markdownFiles: markdownFiles.length,
foundDirectories: foundDirs,
missingDirectories: missingDirs.length > 0 ? missingDirs : undefined
});
if (foundDirs.length > 0) {
logger.info(`📂 Key documentation sections available: ${foundDirs.join(', ')}`);
}
if (missingDirs.length > 0) {
logger.warn(` Some expected directories were not found: ${missingDirs.join(', ')}`);
}
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
logger.error('Failed to download Ludus documentation', { error: errorMessage });
// Clean up on error
await removeDirectorySafe(tempDir);
throw new Error(`Documentation download failed: ${errorMessage}`);
}
}
async function findAllFiles(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 findAllFiles(fullPath);
files.push(...subFiles);
} else if (item.isFile()) {
files.push(fullPath);
}
}
} catch (error) {
// Directory doesn't exist or can't be read
}
return files.sort();
}
async function findMarkdownFiles(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 findMarkdownFiles(fullPath);
files.push(...subFiles);
} else if (item.isFile() && (item.name.endsWith('.md') || item.name.endsWith('.mdx'))) {
files.push(fullPath);
}
}
} catch (error) {
// Directory doesn't exist or can't be read
}
return files.sort();
}
/**
* Get directory structure for documentation
*/
export async function getDocsStructure(logger: Logger): Promise<any> {
const ludusConfigDir = path.join(os.homedir(), '.ludus-mcp');
const docsDir = path.join(ludusConfigDir, 'docs');
try {
return await buildDirectoryTree(docsDir, docsDir);
} catch (error) {
logger.error('Failed to get docs structure', { error });
return null;
}
}
async function buildDirectoryTree(dirPath: string, basePath: string): Promise<any> {
const items = await fs.readdir(dirPath, { withFileTypes: true });
const tree: any = {
directories: {},
files: []
};
for (const item of items) {
const fullPath = path.join(dirPath, item.name);
const relativePath = path.relative(basePath, fullPath);
if (item.isDirectory()) {
tree.directories[item.name] = await buildDirectoryTree(fullPath, basePath);
} else if (item.isFile() && (item.name.endsWith('.md') || item.name.endsWith('.mdx'))) {
tree.files.push({
name: item.name,
path: relativePath,
fullPath: fullPath
});
}
}
return tree;
}
/**
* 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
}
}