repomix
Version:
A tool to pack repository contents to single file for AI consumption
205 lines (204 loc) • 8.5 kB
JavaScript
import * as fs from 'node:fs/promises';
import path from 'node:path';
import pc from 'picocolors';
import { getGlobalDirectory } from '../../config/globalDirectory.js';
import { logger } from '../../shared/logger.js';
const fileExists = async (filePath) => {
try {
await fs.access(filePath);
return true;
}
catch {
return false;
}
};
const replaceRepopackString = (content) => {
return content.replace(/repopack/g, 'repomix').replace(/Repopack/g, 'Repomix');
};
const updateFileContent = async (filePath) => {
const content = await fs.readFile(filePath, 'utf8');
const updatedContent = replaceRepopackString(content);
if (content !== updatedContent) {
await fs.writeFile(filePath, updatedContent, 'utf8');
const relativePath = path.relative(process.cwd(), filePath);
logger.log(`Updated repopack references in ${pc.cyan(relativePath)}`);
return true;
}
return false;
};
const updateInstructionPath = (content) => {
try {
const config = JSON.parse(content);
if (config.output?.instructionFilePath) {
config.output.instructionFilePath = config.output.instructionFilePath.replace('repopack', 'repomix');
}
if (config.output?.filePath) {
config.output.filePath = config.output.filePath.replace('repopack', 'repomix');
}
return JSON.stringify(config, null, 2);
}
catch {
return content;
}
};
const getOutputFilePaths = (rootDir) => {
const extensions = ['.txt', '.xml', '.md'];
const oldPaths = extensions.map((ext) => path.join(rootDir, `repopack-output${ext}`));
const newPaths = extensions.map((ext) => path.join(rootDir, `repomix-output${ext}`));
return { oldPaths, newPaths };
};
const migrateFile = async (oldPath, newPath, description, isConfig = false) => {
if (!(await fileExists(oldPath))) {
return false;
}
const exists = await fileExists(newPath);
if (exists) {
const p = await import('@clack/prompts');
const shouldOverwrite = await p.confirm({
message: `${description} already exists at ${newPath}. Do you want to overwrite it?`,
});
if (p.isCancel(shouldOverwrite) || !shouldOverwrite) {
logger.info(`Skipping migration of ${description}`);
return false;
}
}
try {
let content = await fs.readFile(oldPath, 'utf8');
content = replaceRepopackString(content);
if (isConfig) {
content = updateInstructionPath(content);
}
await fs.mkdir(path.dirname(newPath), { recursive: true });
await fs.writeFile(newPath, content, 'utf8');
await fs.unlink(oldPath);
const relativeOldPath = path.relative(process.cwd(), oldPath);
const relativeNewPath = path.relative(process.cwd(), newPath);
logger.log(`Renamed ${description} from ${relativeOldPath} to ${relativeNewPath}`);
return true;
}
catch (error) {
logger.error(`Failed to migrate ${description}:`, error);
return false;
}
};
const updateIgnoreFiles = async (rootDir) => {
const gitignorePath = path.join(rootDir, '.gitignore');
const repomixignorePath = path.join(rootDir, '.repomixignore');
if (await fileExists(gitignorePath)) {
const updated = await updateFileContent(gitignorePath);
if (!updated) {
logger.debug('No changes needed in .gitignore');
}
}
if (await fileExists(repomixignorePath)) {
const updated = await updateFileContent(repomixignorePath);
if (!updated) {
logger.debug('No changes needed in .repomixignore');
}
}
};
const getMigrationPaths = (rootDir) => {
const { oldPaths: oldOutputPaths, newPaths: newOutputPaths } = getOutputFilePaths(rootDir);
const oldGlobalDirectory = path.join(process.env.HOME || '', '.config', 'repopack');
const newGlobalDirectory = getGlobalDirectory();
return {
oldConfigPath: path.join(rootDir, 'repopack.config.json'),
newConfigPath: path.join(rootDir, 'repomix.config.json'),
oldIgnorePath: path.join(rootDir, '.repopackignore'),
newIgnorePath: path.join(rootDir, '.repomixignore'),
oldInstructionPath: path.join(rootDir, 'repopack-instruction.md'),
newInstructionPath: path.join(rootDir, 'repomix-instruction.md'),
oldOutputPaths,
newOutputPaths,
oldGlobalConfigPath: path.join(oldGlobalDirectory, 'repopack.config.json'),
newGlobalConfigPath: path.join(newGlobalDirectory, 'repomix.config.json'),
};
};
const migrateOutputFiles = async (oldPaths, newPaths) => {
const migratedFiles = [];
for (let i = 0; i < oldPaths.length; i++) {
const oldPath = oldPaths[i];
const newPath = newPaths[i];
const ext = path.extname(oldPath);
if (await migrateFile(oldPath, newPath, `Output file (${ext})`)) {
migratedFiles.push(newPath);
}
}
return migratedFiles;
};
export const runMigrationAction = async (rootDir) => {
const result = {
configMigrated: false,
ignoreMigrated: false,
instructionMigrated: false,
outputFilesMigrated: [],
globalConfigMigrated: false,
};
try {
const paths = getMigrationPaths(rootDir);
const hasOldConfig = await fileExists(paths.oldConfigPath);
const hasOldIgnore = await fileExists(paths.oldIgnorePath);
const hasOldInstruction = await fileExists(paths.oldInstructionPath);
const hasOldGlobalConfig = await fileExists(paths.oldGlobalConfigPath);
const hasOldOutput = await Promise.all(paths.oldOutputPaths.map(fileExists)).then((results) => results.some((exists) => exists));
if (!hasOldConfig && !hasOldIgnore && !hasOldInstruction && !hasOldOutput && !hasOldGlobalConfig) {
logger.debug('No Repopack files found to migrate.');
return result;
}
let migrationMessage = `Found ${pc.green('Repopack')} `;
const items = [];
if (hasOldConfig || hasOldIgnore || hasOldInstruction || hasOldOutput)
items.push('local configuration');
if (hasOldGlobalConfig)
items.push('global configuration');
migrationMessage += `${items.join(' and ')}. Would you like to migrate to ${pc.green('Repomix')}?`;
const p = await import('@clack/prompts');
const shouldMigrate = await p.confirm({
message: migrationMessage,
});
if (p.isCancel(shouldMigrate) || !shouldMigrate) {
logger.info('Migration cancelled.');
return result;
}
logger.info(pc.cyan('\nMigrating from Repopack to Repomix...'));
logger.log('');
if (hasOldConfig) {
result.configMigrated = await migrateFile(paths.oldConfigPath, paths.newConfigPath, 'Configuration file', true);
}
if (hasOldGlobalConfig) {
result.globalConfigMigrated = await migrateFile(paths.oldGlobalConfigPath, paths.newGlobalConfigPath, 'Global configuration file', true);
}
if (hasOldIgnore) {
result.ignoreMigrated = await migrateFile(paths.oldIgnorePath, paths.newIgnorePath, 'Ignore file');
}
if (hasOldInstruction) {
result.instructionMigrated = await migrateFile(paths.oldInstructionPath, paths.newInstructionPath, 'Instruction file');
}
if (hasOldOutput) {
result.outputFilesMigrated = await migrateOutputFiles(paths.oldOutputPaths, paths.newOutputPaths);
}
await updateIgnoreFiles(rootDir);
if (result.configMigrated ||
result.ignoreMigrated ||
result.instructionMigrated ||
result.outputFilesMigrated.length > 0 ||
result.globalConfigMigrated) {
logger.log('');
logger.success('✔ Migration completed successfully!');
logger.log('');
logger.info('You can now use Repomix commands as usual. The old Repopack files have been migrated to the new format.');
logger.log('');
}
return result;
}
catch (error) {
if (error instanceof Error) {
result.error = error;
}
else {
result.error = new Error(String(error));
}
logger.error('An error occurred during migration:', error);
return result;
}
};