@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
JavaScript
// 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, "&")
.replace(/</g, "<")
.replace(/>/g, ">")
.replace(/"/g, """)
.replace(/'/g, "'");
}
/**
* 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