Source: outputAggregator.js

const fs = require('fs/promises');

/**
 * Class representing a content formatter.
 */
class ContentFormatter {
  constructor(format) {
    this.format = format;
  }

  /**
   * Aggregates the provided contents based on the specified format.
   * @param {Array} contents - An array of content objects to aggregate.
   * @returns {string|Object} The aggregated content in the specified format.
   */
  aggregate(contents) {
    if (this.format === 'markdown') {
      return this.aggregateMarkdown(contents);
    } else if (this.format === 'json') {
      return this.aggregateJson(contents);
    }
  }

  /**
   * Aggregates contents into Markdown format, including a Table of Contents.
   * @param {Array} contents - An array of content objects to aggregate.
   * @returns {string} The aggregated Markdown content.
   */
  aggregateMarkdown(contents) {
    // Generate ToC
    let toc = '# Table of Contents\n\n';
    contents.forEach((item) => {
      const title = item.filePath.replace(/\//g, '/');
      const anchor = this.sanitizeAnchor(title);
      toc += `- [${title}](#${anchor})\n`;
    });

    // Combine ToC and content
    let result = toc + '\n# Project Content\n\n';
    result += contents.map((item) => item.formattedContent.trim()).join('\n\n');
    return result;
  }

  /**
   * Aggregates contents into JSON format.
   * @param {Array} contents - An array of content objects to aggregate.
   * @returns {string} The aggregated JSON string.
   */
  aggregateJson(contents) {
    const jsonContents = contents.map((item) => item.formattedContent);
    return JSON.stringify(jsonContents, null, 2);
  }

  /**
   * Sanitizes a file path to create a markdown anchor.
   * @param {string} filePath - The file path to sanitize.
   * @returns {string} The sanitized anchor string.
   */
  sanitizeAnchor(filePath) {
    return filePath
      .toLowerCase()
      .replace(/\//g, '-') // Replace forward slashes with hyphens
      .replace(/\./g, '-') // Replace dots with hyphens
      .replace(/[^\w\s-]/g, '') // Remove any other special characters
      .replace(/\s+/g, '-') // Replace whitespace with hyphens
      .replace(/-+/g, '-') // Replace multiple hyphens with single hyphen
      .trim() // Trim any leading or trailing hyphens
      .replace(/^-+|-+$/g, ''); // Remove leading/trailing hyphens after trim
  }
}

/**
 * Class representing an output aggregator.
 */
class OutputAggregator {
  /**
   * Creates an instance of OutputAggregator.
   * @param {string} format - The format to aggregate content into ('markdown' or 'json').
   * @param {string} outputPath - The path where the aggregated output will be saved.
   */
  constructor(format = 'markdown', outputPath = 'llm-pack-output') {
    this.format = format.toLowerCase();
    if (!['markdown', 'json'].includes(this.format)) {
      throw new Error('Unsupported format. Choose either "markdown" or "json".');
    }
    this.outputPath = outputPath;
    if (this.format === 'markdown' && !this.outputPath.endsWith('.md')) {
      this.outputPath += '.md';
    }
    if (this.format === 'json' && !this.outputPath.endsWith('.json')) {
      this.outputPath += '.json';
    }
    this.formatter = new ContentFormatter(this.format);
  }

  /**
   * Aggregates contents.
   * @param {Array} contents - An array of content objects to aggregate.
   * @returns {string|Object} The aggregated content.
   */
  aggregateContents(contents) {
    return this.formatter.aggregate(contents);
  }

  /**
   * Saves the aggregated content to the output file.
   * @param {string|Object} content - The aggregated content to save.
   */
  async saveOutput(content) {
    await fs.writeFile(this.outputPath, content, 'utf-8');
    console.log(`Output saved to ${this.outputPath}`);
  }
}

module.exports = OutputAggregator;