UNPKG

@johnlindquist/file-forge

Version:

File Forge is a powerful CLI tool for deep analysis of codebases, generating markdown reports to feed AI reasoning models.

162 lines 7.46 kB
// import { format } from "date-fns"; import { PROP_SUMMARY, PROP_TREE, PROP_CONTENT } from "./constants.js"; import { getTemplateByName, applyTemplate } from "./templates.js"; import { existsSync } from "node:fs"; import { execSync } from "node:child_process"; import { join } from "node:path"; import { formatDebugMessage } from "./formatter.js"; /** Converts Windows backslashes to POSIX forward slashes */ const toPosixPath = (p) => p.replace(/\\/g, "/"); /** * Escapes special characters in XML content */ function escapeXML(str) { return str .replace(/&/g, "&amp;") .replace(/</g, "&lt;") .replace(/>/g, "&gt;") .replace(/"/g, "&quot;") .replace(/'/g, "&apos;"); } /** * Gets Git information for the current directory */ function getGitInfo(source) { const gitInfo = {}; const gitDirPath = join(source, ".git"); // Only proceed if source appears to be a git repository if (!existsSync(gitDirPath)) { if (process.env['DEBUG']) { console.log(formatDebugMessage(`No .git directory found in ${source}, skipping Git info.`)); } return gitInfo; } try { // Use --quiet to reduce stderr noise and || true to prevent non-zero exit codes from throwing gitInfo['branch'] = execSync('git rev-parse --abbrev-ref HEAD 2>/dev/null || true', { cwd: source }).toString().trim(); gitInfo['remote'] = execSync('git remote get-url origin 2>/dev/null || true', { cwd: source }).toString().trim(); gitInfo['commit'] = execSync('git rev-parse HEAD 2>/dev/null || true', { cwd: source }).toString().trim(); gitInfo['commitDate'] = execSync('git log -1 --format=%cd 2>/dev/null || true', { cwd: source }).toString().trim(); // Clean up empty strings if commands failed silently for (const key in gitInfo) { if (!gitInfo[key]) { delete gitInfo[key]; } } } catch (error) { // Log error only in debug mode if (process.env['DEBUG']) { console.log(formatDebugMessage(`Failed to get Git information for ${source}: ${error instanceof Error ? error.message : String(error)}`)); } // Return whatever info was successfully gathered, or empty object } return gitInfo; } /** * Builds XML output for File Forge analysis */ export async function buildXMLOutput(digest, source, timestamp, options) { // const projectName = options.name || "FileForgeAnalysis"; // const generatedDate = format(new Date(), "yyyy-MM-dd'T'HH:mm:ss.SSSxxx"); // let xml = `<analysis name="${escapeXML(projectName)}" generated="${generatedDate}">\n`; let xml = ``; // Use conditional indentation const indent = options.whitespace ? " " : ""; const childIndent = options.whitespace ? " " : " "; const contentIndent = options.whitespace ? " " : " "; // Project metadata xml += `${indent}<project>\n`; xml += `${childIndent}<source>${escapeXML(source)}</source>\n`; xml += `${childIndent}<timestamp>${escapeXML(timestamp)}</timestamp>\n`; if (options.command) { xml += `${childIndent}<command>${escapeXML(options.command)}</command>\n`; } // Add Git information ONLY if it was a repo analysis or explicitly requested if (options.isRepoAnalysis) { const gitInfo = getGitInfo(source); if (Object.keys(gitInfo).length > 0) { xml += `${childIndent}<git>\n`; if (gitInfo['branch']) xml += `${contentIndent}<branch>${escapeXML(gitInfo['branch'])}</branch>\n`; if (gitInfo['remote']) xml += `${contentIndent}<remote>${escapeXML(gitInfo['remote'])}</remote>\n`; if (gitInfo['commit']) xml += `${contentIndent}<commit>${escapeXML(gitInfo['commit'])}</commit>\n`; if (gitInfo['commitDate']) xml += `${contentIndent}<commitDate>${escapeXML(gitInfo['commitDate'])}</commitDate>\n`; xml += `${childIndent}</git>\n`; } } xml += `${indent}</project>\n`; // Summary section xml += `${indent}<summary>\n`; xml += `${childIndent}${digest[PROP_SUMMARY] || ""}\n`; xml += `${indent}</summary>\n`; // Directory structure xml += `${indent}<directoryTree>\n`; xml += `${digest[PROP_TREE] || ""}\n`; xml += `${indent}</directoryTree>\n`; // File contents (if verbose or saving to file) if (options.verbose) { xml += `${indent}<files>\n`; // Split content by file sections and wrap each in a file tag const fileContents = (digest[PROP_CONTENT] || "").split(/(?=^=+\nFile: .*\n=+\n)/m); if (process.env["DEBUG"]) { console.log(`[DEBUG] XML Formatter: Found ${fileContents.length} file content blocks`); } for (const fileContent of fileContents) { if (!fileContent.trim()) continue; // Extract filename from the content using the actual header format const headerMatch = fileContent.match(/^=+\nFile: (.*)\n=+\n/); if (!headerMatch || !headerMatch[1]) { if (process.env["DEBUG"]) { console.log(`[DEBUG] XML Formatter: No header match found in content: ${fileContent.substring(0, 100)}...`); } continue; } const fullPath = headerMatch[1].trim(); const posixPath = toPosixPath(fullPath); // Convert to POSIX path if (process.env["DEBUG"]) { console.log(`[DEBUG] XML Formatter: Processing file path: ${posixPath}`); // Log posixPath } // Remove the header to get the file content const content = fileContent.replace(/^=+\nFile: .*\n=+\n/, ""); xml += `${childIndent}<file path="${escapeXML(posixPath)}">\n`; // Use posixPath xml += `${content}`; xml += `${childIndent}</file>\n`; } xml += `${indent}</files>\n`; } // AI instructions if bulk mode is enabled if (options.bulk) { xml += `${indent}<aiInstructions>\n`; xml += `${childIndent}<instruction>When I provide a set of files with paths and content, please return **one single shell script**</instruction>\n`; xml += `${childIndent}<instruction>Use \`#!/usr/bin/env bash\` at the start</instruction>\n`; xml += `${indent}</aiInstructions>\n`; } // Template section if specified if (options.template) { const template = getTemplateByName(options.template); let templateResult = ''; if (template) { try { // Ensure digestContent is passed and is a string const contentToApply = typeof options.digestContent === 'string' ? options.digestContent : ''; templateResult = await applyTemplate(template.templateContent, contentToApply); } catch (error) { templateResult = `Error applying template: ${error instanceof Error ? error.message : String(error)}`; } } else { templateResult = 'Template not found. Use --list-templates to see available templates.'; } // Append the template result (raw, no wrappers) to the XML output xml += `\n${templateResult}`; } // xml += `</analysis>`; return xml; } //# sourceMappingURL=xmlFormatter.js.map