const path = require('path');
const yargs = require('yargs');
const { hideBin } = require('yargs/helpers');
const chalk = require('chalk');
const fs = require('fs').promises;
const ConfigManager = require('../config/configManager');
const IgnoreProcessor = require('./ignoreProcessor');
const FileScanner = require('./fileScanner');
const ContentFormatter = require('./contentFormatter');
const OutputAggregator = require('./outputAggregator');
const ContentSizeManager = require('./contentSizeManager');
const ProgressTracker = require('./progressTracker');
const StreamProcessor = require('./streamProcessor');
const ErrorHandler = require('./errorHandler');
/**
* Runs the llm-pack CLI tool.
* @param {string[]} argv - Command-line arguments.
*/
/**
* Executes the llm-pack CLI application.
*
* Processes command-line arguments, loads configuration, scans files,
* formats content, and outputs aggregated results based on the specified options.
*
* @async
* @param {string[]} [argv=process.argv.slice(2)] - Command-line arguments array.
* @returns {Promise<void>} Resolves when the application has finished running.
*/
async function run(argv = process.argv.slice(2)) {
const parsedArgv = yargs(hideBin(argv))
.scriptName('llm-pack')
.usage('$0 [options]')
.option('format', {
alias: 'f',
describe: 'Output format (markdown or json)',
choices: ['markdown', 'json'],
default: 'markdown',
})
.option('output', {
alias: 'o',
describe: 'Output file path',
type: 'string',
default: null,
})
.option('ignore', {
alias: 'i',
describe: 'Custom ignore files',
type: 'array',
default: [],
})
.option('extensions', {
alias: 'e',
describe: 'Additional file extensions to include',
type: 'array',
default: [],
})
.option('max-files', {
alias: 'm',
describe: 'Maximum number of files to include in the output',
type: 'number',
default: null,
})
.option('max-file-size', {
alias: 's',
describe:
'Maximum file size in kilobytes. Files larger than this will be skipped or compacted.',
type: 'number',
default: null,
})
.option('config', {
alias: 'c',
describe: 'Path to configuration file',
type: 'string',
default: null,
})
.help()
.alias('help', 'h')
.version()
.alias('version', 'v').argv;
const errorHandler = new ErrorHandler();
try {
// Load configuration
const configManager = new ConfigManager(parsedArgv.config);
const config = await configManager.loadConfig();
// Merge CLI arguments with config
const options = {
...config,
format: parsedArgv.format || config.output.format,
outputPath: parsedArgv.output || config.output.path,
maxFiles: parsedArgv['max-files'] || config.limits.maxFiles,
maxFileSize: parsedArgv['max-file-size'] || config.limits.maxFileSize,
ignoreFiles: parsedArgv.ignore || config.ignore.customPatterns,
extensions: parsedArgv.extensions || config.extensions.include,
};
// Initialize components
const rootDir = process.cwd();
const ignoreProcessor = new IgnoreProcessor(options.ignoreFiles);
const contentSizeManager = new ContentSizeManager(
options.maxFileSize,
options.maxFiles,
options.extensions
);
const fileScanner = new FileScanner(rootDir, ignoreProcessor, options.extensions);
const streamProcessor = new StreamProcessor();
// Estimate project size
console.log(chalk.blue('Estimating project size...'));
const sizeEstimate = await contentSizeManager.estimateProjectSize(rootDir, ignoreProcessor);
// Initialize progress tracking
const progressTracker = new ProgressTracker(sizeEstimate);
console.log(
chalk.blue(`Found ${sizeEstimate.totalFiles} files (${Math.round(sizeEstimate.totalSize)}KB)`)
);
// Scan files
let files = await fileScanner.scan();
console.log(chalk.blue(`Processing ${files.length} files...`));
// Apply file limits
if (options.maxFiles !== null && files.length > options.maxFiles) {
console.log(chalk.yellow(`Limiting output to the first ${options.maxFiles} files.`));
files = files.slice(0, options.maxFiles);
}
if (files.length === 0) {
console.log(chalk.yellow('No files found to aggregate.'));
process.exit(0);
}
const contentFormatter = new ContentFormatter(options.format);
const formattedContents = await Promise.all(
files.map(async (file) => {
const filePath = path.join(rootDir, file);
let content;
let compacted = false;
try {
const stats = await fs.stat(filePath);
// const fileSizeKB = stats.size / 1024; // Removed unused variable
const processDecision = contentSizeManager.shouldProcessFile(file, stats.size);
if (!processDecision.shouldProcess) {
errorHandler.addWarning(`Skipping ${file}: ${processDecision.reason}`, 'FileProcessor');
return null;
}
if (processDecision.shouldCompact) {
content = await streamProcessor.processLargeFile(filePath);
compacted = true;
} else {
content = await fs.readFile(filePath, 'utf-8');
}
progressTracker.updateProgress(stats.size);
return {
filePath: file,
formattedContent: contentFormatter.formatContent(file, content, compacted),
};
} catch (error) {
errorHandler.addWarning(`Error processing ${file}: ${error.message}`, 'FileProcessor');
return null;
}
})
);
// Filter out null results from failed processing
const validContents = formattedContents.filter((content) => content !== null);
const outputPath = options.outputPath
? path.isAbsolute(options.outputPath)
? options.outputPath
: path.join(rootDir, options.outputPath)
: path.join(
rootDir,
options.format === 'markdown' ? 'llm-pack-output.md' : 'llm-pack-output.json'
);
const outputAggregator = new OutputAggregator(options.format, outputPath);
const aggregatedContent = outputAggregator.aggregateContents(validContents);
await outputAggregator.saveOutput(aggregatedContent);
progressTracker.complete();
if (errorHandler.hasWarnings()) {
console.log(
chalk.yellow('\nWarnings occurred during processing. Check the output for details.')
);
}
} catch (error) {
errorHandler.handleError(error, 'Main', true);
}
}
module.exports = { run };