repomix
Version:
A tool to pack repository contents to single file for AI consumption
299 lines (298 loc) • 11.1 kB
JavaScript
import path from 'node:path';
import * as v from 'valibot';
import { loadFileConfig, mergeConfigs } from '../../config/configLoad.js';
import { repomixConfigCliSchema, } from '../../config/configSchema.js';
import { readFilePathsFromStdin } from '../../core/file/fileStdin.js';
import { pack } from '../../core/packager.js';
import { generateDefaultSkillName } from '../../core/skill/skillUtils.js';
import { RepomixError, rethrowValidationErrorIfSchemaError } from '../../shared/errorHandle.js';
import { logger } from '../../shared/logger.js';
import { splitPatterns } from '../../shared/patternUtils.js';
import { reportResults } from '../cliReport.js';
import { Spinner } from '../cliSpinner.js';
import { promptSkillLocation, resolveAndPrepareSkillDir } from '../prompts/skillPrompts.js';
import { runMigrationAction } from './migrationAction.js';
export const runDefaultAction = async (directories, cwd, cliOptions, progressCallback) => {
logger.trace('Loaded CLI options:', cliOptions);
await runMigrationAction(cwd);
const fileConfig = await loadFileConfig(cwd, cliOptions.config ?? null, {
skipLocalConfig: cliOptions.skipLocalConfig,
});
logger.trace('Loaded file config:', fileConfig);
const cliConfig = buildCliConfig(cliOptions);
logger.trace('CLI config:', cliConfig);
const config = mergeConfigs(cwd, fileConfig, cliConfig);
logger.trace('Merged config:', config);
validateConflictingOptions(config);
if (cliOptions.skillOutput && config.skillGenerate === undefined) {
throw new RepomixError('--skill-output can only be used with --skill-generate');
}
if (cliOptions.force && config.skillGenerate === undefined) {
throw new RepomixError('--force can only be used with --skill-generate');
}
if (cliOptions.skillOutput !== undefined && !cliOptions.skillOutput.trim()) {
throw new RepomixError('--skill-output path cannot be empty');
}
if (config.skillGenerate !== undefined) {
cliOptions.skillName ??=
typeof config.skillGenerate === 'string'
? config.skillGenerate
: generateDefaultSkillName(directories.map((d) => path.resolve(cwd, d)));
if (cliOptions.skillOutput && !cliOptions.skillDir) {
cliOptions.skillDir = await resolveAndPrepareSkillDir(cliOptions.skillOutput, cwd, cliOptions.force ?? false);
}
else if (!cliOptions.skillDir) {
const promptResult = await promptSkillLocation(cliOptions.skillName, cwd);
cliOptions.skillDir = promptResult.skillDir;
}
}
let stdinFilePaths;
if (cliOptions.stdin) {
const firstDir = directories[0] ?? '.';
if (directories.length > 1 || firstDir !== '.') {
throw new RepomixError('When using --stdin, do not specify directory arguments. File paths will be read from stdin.');
}
const stdinResult = await readFilePathsFromStdin(cwd);
stdinFilePaths = stdinResult.filePaths;
logger.trace(`Read ${stdinFilePaths.length} file paths from stdin`);
}
const spinner = new Spinner('Initializing...', cliOptions);
spinner.start();
let packResult;
try {
const { skillName, skillDir, skillProjectName, skillSourceUrl } = cliOptions;
const packOptions = { skillName, skillDir, skillProjectName, skillSourceUrl };
const targetPaths = stdinFilePaths ? [cwd] : directories.map((directory) => path.resolve(cwd, directory));
const handleProgress = (message) => {
spinner.update(message);
if (progressCallback) {
try {
Promise.resolve(progressCallback(message)).catch((error) => {
logger.trace('progressCallback error:', error);
});
}
catch (error) {
logger.trace('progressCallback error:', error);
}
}
};
packResult = await pack(targetPaths, config, handleProgress, {}, stdinFilePaths, packOptions);
spinner.succeed('Packing completed successfully!');
}
catch (error) {
spinner.fail('Error during packing');
throw error;
}
reportResults(cwd, packResult, config, cliOptions);
return {
packResult,
config,
};
};
export const buildCliConfig = (options) => {
const cliConfig = {};
if (options.output) {
cliConfig.output = { filePath: options.output };
}
if (options.include) {
cliConfig.include = splitPatterns(options.include);
}
if (options.ignore) {
cliConfig.ignore = { customPatterns: splitPatterns(options.ignore) };
}
if (options.gitignore === false) {
cliConfig.ignore = { ...cliConfig.ignore, useGitignore: options.gitignore };
}
if (options.dotIgnore === false) {
cliConfig.ignore = { ...cliConfig.ignore, useDotIgnore: options.dotIgnore };
}
if (options.defaultPatterns === false) {
cliConfig.ignore = {
...cliConfig.ignore,
useDefaultPatterns: options.defaultPatterns,
};
}
if (options.topFilesLen !== undefined) {
cliConfig.output = {
...cliConfig.output,
topFilesLength: options.topFilesLen,
};
}
if (options.outputShowLineNumbers !== undefined) {
cliConfig.output = {
...cliConfig.output,
showLineNumbers: options.outputShowLineNumbers,
};
}
if (options.copy) {
cliConfig.output = { ...cliConfig.output, copyToClipboard: options.copy };
}
if (options.style) {
cliConfig.output = {
...cliConfig.output,
style: options.style.toLowerCase(),
};
}
if (options.parsableStyle !== undefined) {
cliConfig.output = {
...cliConfig.output,
parsableStyle: options.parsableStyle,
};
}
if (options.stdout) {
cliConfig.output = {
...cliConfig.output,
stdout: true,
};
}
if (options.securityCheck === false) {
cliConfig.security = { enableSecurityCheck: options.securityCheck };
}
if (options.fileSummary === false) {
cliConfig.output = {
...cliConfig.output,
fileSummary: false,
};
}
if (options.directoryStructure === false) {
cliConfig.output = {
...cliConfig.output,
directoryStructure: false,
};
}
if (options.files === false) {
cliConfig.output = {
...cliConfig.output,
files: false,
};
}
if (options.removeComments !== undefined) {
cliConfig.output = {
...cliConfig.output,
removeComments: options.removeComments,
};
}
if (options.removeEmptyLines !== undefined) {
cliConfig.output = {
...cliConfig.output,
removeEmptyLines: options.removeEmptyLines,
};
}
if (options.truncateBase64 !== undefined) {
cliConfig.output = {
...cliConfig.output,
truncateBase64: options.truncateBase64,
};
}
if (options.headerText !== undefined) {
cliConfig.output = { ...cliConfig.output, headerText: options.headerText };
}
if (options.compress !== undefined) {
cliConfig.output = { ...cliConfig.output, compress: options.compress };
}
if (options.tokenCountEncoding) {
cliConfig.tokenCount = { encoding: options.tokenCountEncoding };
}
if (options.instructionFilePath) {
cliConfig.output = {
...cliConfig.output,
instructionFilePath: options.instructionFilePath,
};
}
if (options.includeEmptyDirectories) {
cliConfig.output = {
...cliConfig.output,
includeEmptyDirectories: options.includeEmptyDirectories,
};
}
if (options.includeFullDirectoryStructure) {
cliConfig.output = {
...cliConfig.output,
includeFullDirectoryStructure: options.includeFullDirectoryStructure,
};
}
if (options.splitOutput !== undefined) {
cliConfig.output = {
...cliConfig.output,
splitOutput: options.splitOutput,
};
}
if (options.gitSortByChanges === false) {
cliConfig.output = {
...cliConfig.output,
git: {
...cliConfig.output?.git,
sortByChanges: false,
},
};
}
if (options.includeDiffs) {
cliConfig.output = {
...cliConfig.output,
git: {
...cliConfig.output?.git,
includeDiffs: true,
},
};
}
if (options.includeLogs || options.includeLogsCount !== undefined) {
const gitLogConfig = {
...cliConfig.output?.git,
...(options.includeLogs && { includeLogs: true }),
...(options.includeLogsCount !== undefined && { includeLogsCount: options.includeLogsCount }),
};
cliConfig.output = {
...cliConfig.output,
git: gitLogConfig,
};
}
if (options.tokenCountTree !== undefined) {
cliConfig.output = {
...cliConfig.output,
tokenCountTree: options.tokenCountTree,
};
}
if (options.skillGenerate !== undefined) {
cliConfig.skillGenerate = options.skillGenerate;
}
try {
return v.parse(repomixConfigCliSchema, cliConfig);
}
catch (error) {
rethrowValidationErrorIfSchemaError(error, 'Invalid cli arguments');
throw error;
}
};
const validateConflictingOptions = (config) => {
const isStdoutMode = config.output.stdout || config.output.filePath === '-';
const options = {
splitOutput: {
enabled: config.output.splitOutput !== undefined,
name: '--split-output',
},
skillGenerate: {
enabled: config.skillGenerate !== undefined,
name: '--skill-generate',
},
stdout: {
enabled: isStdoutMode,
name: '--stdout',
},
copy: {
enabled: config.output.copyToClipboard,
name: '--copy',
},
};
const conflicts = [
['splitOutput', 'stdout', 'Split output requires writing to filesystem.'],
['splitOutput', 'skillGenerate', 'Skill output is a directory.'],
['splitOutput', 'copy', 'Split output generates multiple files.'],
['skillGenerate', 'stdout', 'Skill output requires writing to filesystem.'],
['skillGenerate', 'copy', 'Skill output is a directory and cannot be copied to clipboard.'],
];
for (const [optionA, optionB, message] of conflicts) {
if (options[optionA].enabled && options[optionB].enabled) {
throw new RepomixError(`${options[optionA].name} cannot be used with ${options[optionB].name}. ${message}`);
}
}
};