Skip to main content
Glama
MIT License
27,120
19,787
  • Linux
  • Apple
outputGenerate.ts13.5 kB
import fs from 'node:fs/promises'; import path from 'node:path'; import { XMLBuilder } from 'fast-xml-parser'; import Handlebars from 'handlebars'; import type { RepomixConfigMerged } from '../../config/configSchema.js'; import { RepomixError } from '../../shared/errorHandle.js'; import { type FileSearchResult, listDirectories, listFiles, searchFiles } from '../file/fileSearch.js'; import { generateTreeString } from '../file/fileTreeGenerate.js'; import type { ProcessedFile } from '../file/fileTypes.js'; import type { GitDiffResult } from '../git/gitDiffHandle.js'; import type { GitLogResult } from '../git/gitLogHandle.js'; import type { OutputGeneratorContext, RenderContext } from './outputGeneratorTypes.js'; import { sortOutputFiles } from './outputSort.js'; import { generateHeader, generateSummaryFileFormat, generateSummaryFileFormatJson, generateSummaryNotes, generateSummaryPurpose, generateSummaryUsageGuidelines, } from './outputStyleDecorate.js'; import { getMarkdownTemplate } from './outputStyles/markdownStyle.js'; import { getPlainTemplate } from './outputStyles/plainStyle.js'; import { getXmlTemplate } from './outputStyles/xmlStyle.js'; const calculateMarkdownDelimiter = (files: ReadonlyArray<ProcessedFile>): string => { const maxBackticks = files .flatMap((file) => file.content.match(/`+/g) ?? []) .reduce((max, match) => Math.max(max, match.length), 0); return '`'.repeat(Math.max(3, maxBackticks + 1)); }; const createRenderContext = (outputGeneratorContext: OutputGeneratorContext): RenderContext => { return { generationHeader: generateHeader(outputGeneratorContext.config, outputGeneratorContext.generationDate), summaryPurpose: generateSummaryPurpose(outputGeneratorContext.config), summaryFileFormat: generateSummaryFileFormat(), summaryUsageGuidelines: generateSummaryUsageGuidelines( outputGeneratorContext.config, outputGeneratorContext.instruction, ), summaryNotes: generateSummaryNotes(outputGeneratorContext.config), headerText: outputGeneratorContext.config.output.headerText, instruction: outputGeneratorContext.instruction, treeString: outputGeneratorContext.treeString, processedFiles: outputGeneratorContext.processedFiles, fileSummaryEnabled: outputGeneratorContext.config.output.fileSummary, directoryStructureEnabled: outputGeneratorContext.config.output.directoryStructure, filesEnabled: outputGeneratorContext.config.output.files, escapeFileContent: outputGeneratorContext.config.output.parsableStyle, markdownCodeBlockDelimiter: calculateMarkdownDelimiter(outputGeneratorContext.processedFiles), gitDiffEnabled: outputGeneratorContext.config.output.git?.includeDiffs, gitDiffWorkTree: outputGeneratorContext.gitDiffResult?.workTreeDiffContent, gitDiffStaged: outputGeneratorContext.gitDiffResult?.stagedDiffContent, gitLogEnabled: outputGeneratorContext.config.output.git?.includeLogs, gitLogContent: outputGeneratorContext.gitLogResult?.logContent, gitLogCommits: outputGeneratorContext.gitLogResult?.commits, }; }; const generateParsableXmlOutput = async (renderContext: RenderContext): Promise<string> => { const xmlBuilder = new XMLBuilder({ ignoreAttributes: false }); const xmlDocument = { repomix: { file_summary: renderContext.fileSummaryEnabled ? { '#text': renderContext.generationHeader, purpose: renderContext.summaryPurpose, file_format: `${renderContext.summaryFileFormat} 5. Repository files, each consisting of: - File path as an attribute - Full contents of the file`, usage_guidelines: renderContext.summaryUsageGuidelines, notes: renderContext.summaryNotes, } : undefined, user_provided_header: renderContext.headerText, directory_structure: renderContext.directoryStructureEnabled ? renderContext.treeString : undefined, files: renderContext.filesEnabled ? { '#text': "This section contains the contents of the repository's files.", file: renderContext.processedFiles.map((file) => ({ '#text': file.content, '@_path': file.path, })), } : undefined, git_diffs: renderContext.gitDiffEnabled ? { git_diff_work_tree: renderContext.gitDiffWorkTree, git_diff_staged: renderContext.gitDiffStaged, } : undefined, git_logs: renderContext.gitLogEnabled ? { git_log_commit: renderContext.gitLogCommits?.map((commit) => ({ date: commit.date, message: commit.message, files: commit.files.map((file) => ({ '#text': file })), })), } : undefined, instruction: renderContext.instruction ? renderContext.instruction : undefined, }, }; try { return xmlBuilder.build(xmlDocument); } catch (error) { throw new RepomixError( `Failed to generate XML output: ${error instanceof Error ? error.message : 'Unknown error'}`, error instanceof Error ? { cause: error } : undefined, ); } }; const generateParsableJsonOutput = async (renderContext: RenderContext): Promise<string> => { const jsonDocument = { ...(renderContext.fileSummaryEnabled && { fileSummary: { generationHeader: renderContext.generationHeader, purpose: renderContext.summaryPurpose, fileFormat: generateSummaryFileFormatJson(), usageGuidelines: renderContext.summaryUsageGuidelines, notes: renderContext.summaryNotes, }, }), ...(renderContext.headerText && { userProvidedHeader: renderContext.headerText, }), ...(renderContext.directoryStructureEnabled && { directoryStructure: renderContext.treeString, }), ...(renderContext.filesEnabled && { files: renderContext.processedFiles.reduce( (acc, file) => { acc[file.path] = file.content; return acc; }, {} as Record<string, string>, ), }), ...(renderContext.gitDiffEnabled && { gitDiffs: { workTree: renderContext.gitDiffWorkTree, staged: renderContext.gitDiffStaged, }, }), ...(renderContext.gitLogEnabled && { gitLogs: renderContext.gitLogCommits?.map((commit) => ({ date: commit.date, message: commit.message, files: commit.files, })), }), ...(renderContext.instruction && { instruction: renderContext.instruction, }), }; try { return JSON.stringify(jsonDocument, null, 2); } catch (error) { throw new RepomixError( `Failed to generate JSON output: ${error instanceof Error ? error.message : 'Unknown error'}`, error instanceof Error ? { cause: error } : undefined, ); } }; const generateHandlebarOutput = async ( config: RepomixConfigMerged, renderContext: RenderContext, processedFiles?: ProcessedFile[], ): Promise<string> => { let template: string; switch (config.output.style) { case 'xml': template = getXmlTemplate(); break; case 'markdown': template = getMarkdownTemplate(); break; case 'plain': template = getPlainTemplate(); break; default: throw new RepomixError(`Unsupported output style for handlebars template: ${config.output.style}`); } try { const compiledTemplate = Handlebars.compile(template); return `${compiledTemplate(renderContext).trim()}\n`; } catch (error) { if (error instanceof RangeError && error.message === 'Invalid string length') { let largeFilesInfo = ''; if (processedFiles && processedFiles.length > 0) { const topFiles = processedFiles .sort((a, b) => b.content.length - a.content.length) .slice(0, 5) .map((f) => ` - ${f.path} (${(f.content.length / 1024 / 1024).toFixed(1)} MB)`) .join('\n'); largeFilesInfo = `\n\nLargest files in this repository:\n${topFiles}`; } throw new RepomixError( `Output size exceeds JavaScript string limit. The repository contains files that are too large to process. Please try: - Use --ignore to exclude large files (e.g., --ignore "docs/**" or --ignore "*.html") - Use --include to process only specific files - Process smaller portions of the repository at a time${largeFilesInfo}`, { cause: error }, ); } throw new RepomixError( `Failed to compile template: ${error instanceof Error ? error.message : 'Unknown error'}`, error instanceof Error ? { cause: error } : undefined, ); } }; export const generateOutput = async ( rootDirs: string[], config: RepomixConfigMerged, processedFiles: ProcessedFile[], allFilePaths: string[], gitDiffResult: GitDiffResult | undefined = undefined, gitLogResult: GitLogResult | undefined = undefined, deps = { buildOutputGeneratorContext, generateHandlebarOutput, generateParsableXmlOutput, generateParsableJsonOutput, sortOutputFiles, }, ): Promise<string> => { // Sort processed files by git change count if enabled const sortedProcessedFiles = await deps.sortOutputFiles(processedFiles, config); const outputGeneratorContext = await deps.buildOutputGeneratorContext( rootDirs, config, allFilePaths, sortedProcessedFiles, gitDiffResult, gitLogResult, ); const renderContext = createRenderContext(outputGeneratorContext); switch (config.output.style) { case 'xml': return config.output.parsableStyle ? deps.generateParsableXmlOutput(renderContext) : deps.generateHandlebarOutput(config, renderContext, sortedProcessedFiles); case 'json': return deps.generateParsableJsonOutput(renderContext); case 'markdown': case 'plain': return deps.generateHandlebarOutput(config, renderContext, sortedProcessedFiles); default: throw new RepomixError(`Unsupported output style: ${config.output.style}`); } }; export const buildOutputGeneratorContext = async ( rootDirs: string[], config: RepomixConfigMerged, allFilePaths: string[], processedFiles: ProcessedFile[], gitDiffResult: GitDiffResult | undefined = undefined, gitLogResult: GitLogResult | undefined = undefined, deps = { listDirectories, listFiles, searchFiles, }, ): Promise<OutputGeneratorContext> => { let repositoryInstruction = ''; if (config.output.instructionFilePath) { const instructionPath = path.resolve(config.cwd, config.output.instructionFilePath); try { repositoryInstruction = await fs.readFile(instructionPath, 'utf-8'); } catch { throw new RepomixError(`Instruction file not found at ${instructionPath}`); } } // Determine if full-tree mode applies (only when directory structure is rendered) const shouldUseFullTree = config.output.directoryStructure === true && !!config.output.includeFullDirectoryStructure && (config.include?.length ?? 0) > 0; // Paths to include in the directory tree visualization let directoryPathsForTree: string[] = []; let filePathsForTree: string[] = allFilePaths; if (shouldUseFullTree) { try { // Collect all directories and all files from all roots const [allDirectoriesByRoot, allFilesByRoot] = await Promise.all([ Promise.all(rootDirs.map((rootDir) => deps.listDirectories(rootDir, config))), Promise.all(rootDirs.map((rootDir) => deps.listFiles(rootDir, config))), ]); // Merge, deduplicate, and sort for deterministic output const allDirectories = Array.from(new Set(allDirectoriesByRoot.flat())).sort(); const allRepoFiles = Array.from(new Set(allFilesByRoot.flat())); // Merge in any files that weren't part of the included files so they appear in the tree const includedSet = new Set(allFilePaths); const additionalFiles = allRepoFiles.filter((p) => !includedSet.has(p)); directoryPathsForTree = allDirectories; filePathsForTree = Array.from(new Set([...allFilePaths, ...additionalFiles])); } catch (error) { throw new RepomixError( `Failed to build full directory structure: ${error instanceof Error ? error.message : String(error)}`, error instanceof Error ? { cause: error } : undefined, ); } } else if (config.output.directoryStructure && config.output.includeEmptyDirectories) { // Default behavior: include empty directories only try { const merged = (await Promise.all(rootDirs.map((rootDir) => deps.searchFiles(rootDir, config)))).reduce( (acc: FileSearchResult, curr: FileSearchResult) => ({ filePaths: [...acc.filePaths, ...curr.filePaths], emptyDirPaths: [...acc.emptyDirPaths, ...curr.emptyDirPaths], }) as FileSearchResult, { filePaths: [], emptyDirPaths: [] }, ).emptyDirPaths; directoryPathsForTree = [...new Set(merged)].sort(); } catch (error) { throw new RepomixError( `Failed to search for empty directories: ${error instanceof Error ? error.message : String(error)}`, error instanceof Error ? { cause: error } : undefined, ); } } return { generationDate: new Date().toISOString(), treeString: generateTreeString(filePathsForTree, directoryPathsForTree), processedFiles, config, instruction: repositoryInstruction, gitDiffResult, gitLogResult, }; };

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/yamadashy/repomix'

If you have feedback or need assistance with the MCP directory API, please join our Discord server