MCP_CLIENTE_LATEX_V3 / project-manager.ts
C2MV's picture
πŸš€ Deploy LaTeX MCP Server v2.0 β€” 7 tools (Modules A-F)
8cd7606 verified
import * as fs from 'fs/promises';
import * as path from 'path';
import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
// ─── Project Directory ─────────────────────────────────────────
export function getTempProjectsDir(): string {
return path.join(__dirname, '..', 'temp_projects');
}
// ─── List all compiled projects with metadata ──────────────────
export interface ProjectInfo {
name: string;
path: string;
createdAt: string;
hasPdf: boolean;
pdfSizeBytes: number;
sections: string[];
}
export async function listProjects(): Promise<ProjectInfo[]> {
const baseDir = getTempProjectsDir();
const projects: ProjectInfo[] = [];
try {
const entries = await fs.readdir(baseDir, { withFileTypes: true });
for (const entry of entries) {
if (!entry.isDirectory()) continue;
const projectPath = path.join(baseDir, entry.name);
const info: ProjectInfo = {
name: entry.name,
path: projectPath,
createdAt: '',
hasPdf: false,
pdfSizeBytes: 0,
sections: [],
};
// Check for PDF in output/
const pdfPath = path.join(projectPath, 'output', 'main.pdf');
try {
const pdfStat = await fs.stat(pdfPath);
info.hasPdf = true;
info.pdfSizeBytes = pdfStat.size;
info.createdAt = pdfStat.mtime.toISOString();
} catch {
// No PDF found
try {
const dirStat = await fs.stat(projectPath);
info.createdAt = dirStat.mtime.toISOString();
} catch {}
}
// List sections
const seccionesDir = path.join(projectPath, 'secciones');
try {
const secFiles = await fs.readdir(seccionesDir);
info.sections = secFiles.filter(f => f.endsWith('.tex'));
} catch {
// No secciones dir
}
projects.push(info);
}
// Sort by creation date, newest first
projects.sort((a, b) => b.createdAt.localeCompare(a.createdAt));
return projects;
} catch {
return [];
}
}
// ─── Get latest version of a project by base name ──────────────
export async function getLatestVersion(projectBaseName: string): Promise<string | null> {
const projects = await listProjects();
const matching = projects.filter(p => p.name.startsWith(projectBaseName));
if (matching.length === 0) return null;
// Already sorted newest first
return matching[0].path;
}
// ─── Create versioned project name ─────────────────────────────
export function createVersionedName(
baseName: string,
version: number,
operation: string,
section?: string
): string {
// Strip any existing version suffix starting from _v... to prevent recursive growth
// If the user named their project "tesis_v2", it will be safely stripped to "tesis"
const vMatch = baseName.match(/_v\d+_/);
const rootName = vMatch ? baseName.substring(0, vMatch.index) : baseName;
let sanitized = rootName.replace(/[^a-zA-Z0-9_-]/g, '_');
// Enforce a hard limit on the root name length to prevent Windows MAX_PATH issues
if (sanitized.length > 50) sanitized = sanitized.substring(0, 50);
// Shorten operation names (e.g., "patch_section" -> "patch", "restructure" -> "restructur")
const op = operation.split('_')[0].substring(0, 10).replace(/[^a-zA-Z0-9_-]/g, '_');
// Shorten section filename
const sec = section ? `_${section.replace(/\.tex$/, '').substring(0, 15).replace(/[^a-zA-Z0-9_-]/g, '_')}` : '';
// Use base36 for a shorter timestamp string (e.g. 'lwqxvz')
const shortTime = Math.floor(Date.now() / 1000).toString(36);
return `${sanitized}_v${version}_${op}${sec}_${shortTime}`;
}
// ─── Copy a project for creating a new version ─────────────────
export async function copyProjectForVersion(
sourceDir: string,
newName: string
): Promise<string> {
const destDir = path.join(getTempProjectsDir(), newName);
await fs.mkdir(destDir, { recursive: true });
// Copy main.tex
try {
await fs.copyFile(
path.join(sourceDir, 'main.tex'),
path.join(destDir, 'main.tex')
);
} catch {}
// Copy referencias.bib
try {
await fs.copyFile(
path.join(sourceDir, 'referencias.bib'),
path.join(destDir, 'referencias.bib')
);
} catch {}
// Copy secciones/
const srcSecciones = path.join(sourceDir, 'secciones');
const destSecciones = path.join(destDir, 'secciones');
try {
const files = await fs.readdir(srcSecciones);
await fs.mkdir(destSecciones, { recursive: true });
for (const file of files) {
await fs.copyFile(
path.join(srcSecciones, file),
path.join(destSecciones, file)
);
}
} catch {}
return destDir;
}
// ─── Read all files from an existing project ───────────────────
export interface ProjectContent {
mainTex: string;
referencesBib: string;
sections: { filename: string; content: string }[];
projectPath: string;
}
export async function readProject(projectDir: string): Promise<ProjectContent> {
const result: ProjectContent = {
mainTex: '',
referencesBib: '',
sections: [],
projectPath: projectDir,
};
// Read main.tex
try {
result.mainTex = await fs.readFile(path.join(projectDir, 'main.tex'), 'utf8');
} catch {
throw new Error(`main.tex not found in ${projectDir}`);
}
// Read referencias.bib
try {
result.referencesBib = await fs.readFile(path.join(projectDir, 'referencias.bib'), 'utf8');
} catch {
// Optional β€” may not exist
}
// Read all sections
const seccionesDir = path.join(projectDir, 'secciones');
try {
const files = await fs.readdir(seccionesDir);
for (const file of files) {
if (file.endsWith('.tex')) {
const content = await fs.readFile(path.join(seccionesDir, file), 'utf8');
result.sections.push({ filename: file, content });
}
}
// Sort alphabetically for consistency
result.sections.sort((a, b) => a.filename.localeCompare(b.filename));
} catch {
// No sections directory
}
return result;
}
// ─── Generate standalone .tex from section content ─────────────
export function generateStandaloneTex(sectionContent: string, referencesBib?: string): string {
const hasBib = referencesBib && referencesBib.trim().length > 0;
const bibBlock = hasBib
? `\\usepackage[backend=biber,style=apa]{biblatex}
\\addbibresource{referencias.bib}`
: '';
const printBib = hasBib ? '\n\\printbibliography' : '';
return `\\documentclass[12pt]{article}
\\usepackage[utf8]{inputenc}
\\usepackage[T1]{fontenc}
\\usepackage[english,spanish]{babel}
\\usepackage{booktabs,adjustbox,threeparttable,multirow}
\\usepackage{times}
\\usepackage{geometry}
\\geometry{a4paper, margin=2.5cm}
\\usepackage{setspace}
\\onehalfspacing
${bibBlock}
\\begin{document}
${sectionContent}
${printBib}
\\end{document}
`;
}
// ─── Resolve a project directory (supports name or full path) ──
export async function resolveProjectDir(projectDirOrName: string): Promise<string> {
// If it's already an absolute path and exists, use it directly
if (path.isAbsolute(projectDirOrName)) {
try {
await fs.access(projectDirOrName);
return projectDirOrName;
} catch {}
}
// Try as a project name inside temp_projects
const inTempProjects = path.join(getTempProjectsDir(), projectDirOrName);
try {
await fs.access(inTempProjects);
return inTempProjects;
} catch {}
throw new Error(
`Project not found: "${projectDirOrName}". Provide an absolute path or the name of a project in temp_projects/.`
);
}