migrationAction.ts•9.96 kB
import * as fs from 'node:fs/promises';
import path from 'node:path';
import * as prompts from '@clack/prompts';
import pc from 'picocolors';
import { getGlobalDirectory } from '../../config/globalDirectory.js';
import { logger } from '../../shared/logger.js';
interface MigrationPaths {
oldConfigPath: string;
newConfigPath: string;
oldIgnorePath: string;
newIgnorePath: string;
oldInstructionPath: string;
newInstructionPath: string;
oldOutputPaths: string[];
newOutputPaths: string[];
oldGlobalConfigPath: string;
newGlobalConfigPath: string;
}
interface MigrationResult {
configMigrated: boolean;
ignoreMigrated: boolean;
instructionMigrated: boolean;
outputFilesMigrated: string[];
globalConfigMigrated: boolean;
error?: Error;
}
/**
* Check if a file exists at the given path
*/
const fileExists = async (filePath: string): Promise<boolean> => {
try {
await fs.access(filePath);
return true;
} catch {
return false;
}
};
/**
* Replace all occurrences of 'repopack' with 'repomix' in a string
*/
const replaceRepopackString = (content: string): string => {
return content.replace(/repopack/g, 'repomix').replace(/Repopack/g, 'Repomix');
};
/**
* Update file content by replacing 'repopack' with 'repomix'
*/
const updateFileContent = async (filePath: string): Promise<boolean> => {
const content = await fs.readFile(filePath, 'utf8');
const updatedContent = replaceRepopackString(content);
// Check if content needs to be updated
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;
};
/**
* Parse JSON content, update instructionFilePath if exists
*/
const updateInstructionPath = (content: string): string => {
try {
const config = JSON.parse(content);
if (config.output?.instructionFilePath) {
config.output.instructionFilePath = config.output.instructionFilePath.replace('repopack', 'repomix');
}
// Also update output.filePath if it exists
if (config.output?.filePath) {
config.output.filePath = config.output.filePath.replace('repopack', 'repomix');
}
return JSON.stringify(config, null, 2);
} catch {
return content;
}
};
/**
* Get output file paths pairs
*/
const getOutputFilePaths = (rootDir: string): { oldPaths: string[]; newPaths: string[] } => {
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 };
};
/**
* Migrate a single file from old path to new path
*/
const migrateFile = async (
oldPath: string,
newPath: string,
description: string,
isConfig = false,
): Promise<boolean> => {
if (!(await fileExists(oldPath))) {
return false;
}
const exists = await fileExists(newPath);
if (exists) {
const shouldOverwrite = await prompts.confirm({
message: `${description} already exists at ${newPath}. Do you want to overwrite it?`,
});
if (prompts.isCancel(shouldOverwrite) || !shouldOverwrite) {
logger.info(`Skipping migration of ${description}`);
return false;
}
}
try {
// Read and update content
let content = await fs.readFile(oldPath, 'utf8');
content = replaceRepopackString(content);
// For config files, also update instructionFilePath and output.filePath
if (isConfig) {
content = updateInstructionPath(content);
}
// Ensure the target directory exists
await fs.mkdir(path.dirname(newPath), { recursive: true });
// Write to new file
await fs.writeFile(newPath, content, 'utf8');
// Remove old file
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;
}
};
/**
* Update content of gitignore and repomixignore files
*/
const updateIgnoreFiles = async (rootDir: string): Promise<void> => {
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');
}
}
};
/**
* Get all migration related file paths
*/
const getMigrationPaths = (rootDir: string): MigrationPaths => {
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'),
};
};
/**
* Migrate output files
*/
const migrateOutputFiles = async (oldPaths: string[], newPaths: string[]): Promise<string[]> => {
const migratedFiles: string[] = [];
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: string): Promise<MigrationResult> => {
const result: MigrationResult = {
configMigrated: false,
ignoreMigrated: false,
instructionMigrated: false,
outputFilesMigrated: [],
globalConfigMigrated: false,
};
try {
const paths = getMigrationPaths(rootDir);
// Check if migration is needed
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;
}
// Show migration notice based on what needs to be migrated
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')}?`;
// Confirm migration with user
const shouldMigrate = await prompts.confirm({
message: migrationMessage,
});
if (prompts.isCancel(shouldMigrate) || !shouldMigrate) {
logger.info('Migration cancelled.');
return result;
}
// Show migration notice
logger.info(pc.cyan('\nMigrating from Repopack to Repomix...'));
logger.log('');
// Migrate config file
if (hasOldConfig) {
result.configMigrated = await migrateFile(paths.oldConfigPath, paths.newConfigPath, 'Configuration file', true);
}
// Migrate global config file
if (hasOldGlobalConfig) {
result.globalConfigMigrated = await migrateFile(
paths.oldGlobalConfigPath,
paths.newGlobalConfigPath,
'Global configuration file',
true,
);
}
// Migrate ignore file
if (hasOldIgnore) {
result.ignoreMigrated = await migrateFile(paths.oldIgnorePath, paths.newIgnorePath, 'Ignore file');
}
// Migrate instruction file
if (hasOldInstruction) {
result.instructionMigrated = await migrateFile(
paths.oldInstructionPath,
paths.newInstructionPath,
'Instruction file',
);
}
// Migrate output files
if (hasOldOutput) {
result.outputFilesMigrated = await migrateOutputFiles(paths.oldOutputPaths, paths.newOutputPaths);
}
// Update content in gitignore and repomixignore
await updateIgnoreFiles(rootDir);
// Show success message
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;
}
};