smart-renamer
Version:
š Intelligent file naming suggestions based on project-specific naming conventions. Interactive CLI tool that asks yes/no for each file renaming.
384 lines ⢠17.2 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.createProgram = createProgram;
const commander_1 = require("commander");
const config_manager_1 = require("../config/config-manager");
const naming_validator_1 = require("../core/naming-validator");
const suggestion_engine_1 = require("../core/suggestion-engine");
const file_watcher_1 = require("../core/file-watcher");
const inquirer_1 = __importDefault(require("inquirer"));
const fs_1 = require("fs");
const path_1 = require("path");
function createProgram() {
const program = new commander_1.Command();
program
.name('renamer')
.description('Intelligent file naming suggestions based on project conventions')
.version('1.0.0');
program
.command('init')
.description('Initialize naming convention for the current project')
.action(initCommand);
program
.command('set-convention <convention>')
.description('Set the naming convention for the project')
.action(setConventionCommand);
program
.command('watch')
.description('Monitor for new files and suggest names')
.option('-p, --patterns <patterns>', 'File patterns to watch (comma-separated)', '**/*')
.action(watchCommand);
program
.command('validate')
.description('Validate existing file names against the project convention')
.option('-f, --fix', 'Show suggested fixes for invalid names')
.action(validateCommand);
program
.command('suggest <filename>')
.description('Get naming suggestions for a specific file')
.action(suggestCommand);
program
.command('analyze')
.description('Analyze project structure and naming patterns')
.action(analyzeCommand);
program
.command('add-exception <name>')
.description('Add a file name to the exceptions list (excluded from convention checks)')
.action(addExceptionCommand);
program
.command('rename')
.description('Rename files to follow the project naming convention')
.option('-d, --dry-run', 'Show what would be renamed without actually renaming')
.option('-f, --force', 'Rename all files without individual prompts')
.option('-i, --interactive', 'Ask yes/no for each file individually (default)')
.option('--keep <files>', 'Comma-separated list of files to keep unchanged')
.action(renameCommand);
return program;
}
async function initCommand() {
console.log('š Initializing Renamer for your project...\n');
const configManager = new config_manager_1.ConfigManager();
if (configManager.configExists()) {
const { overwrite } = await inquirer_1.default.prompt([{
type: 'confirm',
name: 'overwrite',
message: 'naming.config already exists. Do you want to overwrite it?',
default: false
}]);
if (!overwrite) {
console.log('ā
Keeping existing configuration.');
return;
}
}
const suggestionEngine = new suggestion_engine_1.SuggestionEngine();
const detectedConvention = suggestionEngine.detectProjectConvention();
const questions = [
{
type: 'list',
name: 'convention',
message: 'Choose your preferred naming convention:',
choices: [
'camelCase',
'snake_case',
'kebab-case',
'PascalCase',
'UPPER_SNAKE_CASE'
],
default: detectedConvention || 'camelCase'
},
{
type: 'input',
name: 'files',
message: 'File patterns to apply convention to:',
default: '*.ts,*.js'
},
{
type: 'list',
name: 'folders',
message: 'Folder naming convention:',
choices: [
'camelCase',
'snake_case',
'kebab-case',
'PascalCase',
'UPPER_SNAKE_CASE'
],
default: 'kebab-case'
},
{
type: 'input',
name: 'exceptions',
message: 'Files to exclude from convention (comma-separated):',
default: 'index,main,app'
}
];
const answers = await inquirer_1.default.prompt(questions);
const config = {
convention: answers.convention,
files: answers.files.split(',').map((s) => s.trim()),
folders: answers.folders,
exceptions: answers.exceptions.split(',').map((s) => s.trim()).filter(Boolean)
};
configManager.saveConfig(config);
console.log('\nā
Configuration saved to naming.config');
console.log(`š Primary convention: ${config.convention}`);
console.log(`š Folder convention: ${config.folders}`);
if (detectedConvention && detectedConvention !== config.convention) {
console.log(`\nš” Note: Detected '${detectedConvention}' in existing files, but you chose '${config.convention}'`);
}
}
async function setConventionCommand(convention) {
const validConventions = ['camelCase', 'snake_case', 'kebab-case', 'PascalCase', 'UPPER_SNAKE_CASE'];
if (!validConventions.includes(convention)) {
console.error(`ā Invalid convention '${convention}'. Valid options: ${validConventions.join(', ')}`);
process.exit(1);
}
const configManager = new config_manager_1.ConfigManager();
const config = configManager.loadConfig();
config.convention = convention;
configManager.saveConfig(config);
console.log(`ā
Convention set to '${convention}'`);
}
async function watchCommand(options) {
const configManager = new config_manager_1.ConfigManager();
const config = configManager.loadConfig();
const suggestionEngine = new suggestion_engine_1.SuggestionEngine();
const patterns = options.patterns.split(',').map(p => p.trim());
const watcher = new file_watcher_1.FileWatcher(process.cwd(), patterns);
console.log(`š Watching for new files with convention: ${config.convention}`);
console.log(`š Patterns: ${patterns.join(', ')}\n`);
watcher.on('fileAdded', (fileInfo) => {
const suggestion = suggestionEngine.suggestName(fileInfo.name, config.convention, config.exceptions);
if (suggestion.original !== suggestion.suggested) {
console.log(`\nš New file detected: ${fileInfo.name}`);
console.log(`š” Suggested name: ${suggestion.suggested}`);
console.log(`šÆ Convention: ${suggestion.convention}`);
console.log(`š Confidence: ${(suggestion.confidence * 100).toFixed(1)}%`);
}
});
watcher.on('error', (error) => {
console.error('ā File watcher error:', error);
});
watcher.start();
process.on('SIGINT', () => {
console.log('\nš Stopping file watcher...');
watcher.stop();
process.exit(0);
});
}
async function validateCommand(options) {
const configManager = new config_manager_1.ConfigManager();
const config = configManager.loadConfig();
const suggestionEngine = new suggestion_engine_1.SuggestionEngine();
console.log(`š Validating files against '${config.convention}' convention...\n`);
const files = suggestionEngine['getAllProjectFiles']();
const invalidFiles = [];
for (const file of files) {
if (file.isDirectory)
continue;
const validation = naming_validator_1.NamingValidator.validateName(file.name, config.convention, config.exceptions);
if (!validation.isValid && validation.expectedName) {
invalidFiles.push({
name: file.name,
suggested: validation.expectedName,
path: file.path
});
console.log(`ā ${file.name}`);
console.log(` š” Should be: ${validation.expectedName}`);
console.log(` š Path: ${file.path}\n`);
}
}
if (invalidFiles.length === 0) {
console.log('ā
All files follow the naming convention!');
}
else {
console.log(`\nš Found ${invalidFiles.length} files that don't follow the convention.`);
if (options.fix) {
console.log('\nš§ Suggested fixes:');
for (const file of invalidFiles) {
console.log(`mv "${file.name}" "${file.suggested}"`);
}
}
}
}
async function suggestCommand(filename) {
const configManager = new config_manager_1.ConfigManager();
const config = configManager.loadConfig();
const suggestionEngine = new suggestion_engine_1.SuggestionEngine();
console.log(`š Generating suggestions for: ${filename}\n`);
const suggestions = suggestionEngine.suggestMultipleNames(filename, config.convention, config.exceptions);
for (const suggestion of suggestions) {
const icon = suggestion.convention === config.convention ? 'ā' : ' ';
console.log(`${icon} ${suggestion.convention.padEnd(20)} ${suggestion.suggested.padEnd(30)} (${(suggestion.confidence * 100).toFixed(1)}%)`);
}
}
async function analyzeCommand() {
const suggestionEngine = new suggestion_engine_1.SuggestionEngine();
const analysis = suggestionEngine.analyzeProjectStructure();
console.log('š Project Analysis\n');
console.log(`š Total files: ${analysis.totalFiles}`);
console.log(`šÆ Most common convention: ${analysis.mostCommon || 'None detected'}`);
console.log(`š Consistency: ${(analysis.consistency * 100).toFixed(1)}%\n`);
console.log('š Convention breakdown:');
for (const [convention, count] of Object.entries(analysis.conventions)) {
const percentage = analysis.totalFiles > 0 ? (count / analysis.totalFiles * 100).toFixed(1) : '0.0';
console.log(` ${convention.padEnd(20)} ${count.toString().padStart(3)} files (${percentage}%)`);
}
}
async function renameCommand(options) {
const configManager = new config_manager_1.ConfigManager();
const config = configManager.loadConfig();
const suggestionEngine = new suggestion_engine_1.SuggestionEngine();
// Parse keep list
const keepFiles = options.keep ? options.keep.split(',').map(f => f.trim()) : [];
// Default exclusions - system files, config files, and common files
const defaultExclusions = [
'package.json', 'tsconfig.json', 'bun.lockb', 'naming.config',
'package-lock.json', 'yarn.lock', '.gitignore', 'LICENSE',
// Config files
'.eslintrc.js', '.eslintrc.json', '.prettierrc', '.prettierrc.json',
'babel.config.js', 'webpack.config.js', 'vite.config.js', 'rollup.config.js',
'jest.config.js', 'vitest.config.js', '.env.example', 'Dockerfile',
'docker-compose.yml', 'docker-compose.yaml'
];
// File extensions to exclude by default
const excludedExtensions = [
// Image files
'.jpg', '.jpeg', '.png', '.gif', '.bmp', '.svg', '.webp', '.ico', '.tiff',
// Markdown files
'.md', '.markdown',
// Declaration files (all languages)
'.d.ts', '.d.mts', '.d.cts', // TypeScript declarations
'.h', '.hpp', '.hxx', // C/C++ headers
'.hi', // Haskell interface files
'.pyi', // Python stub files
'.rbi', // Ruby interface files
'.rei', // ReasonML interface files
'.mli', // OCaml interface files
'.sig', // Standard ML signature files
'.fsi', // F# signature files
'.spec', // RPM spec files
'.def', // Definition files (various languages)
// Config file extensions
'.config.js', '.config.ts', '.config.json', '.yml', '.yaml', '.toml', '.ini'
];
console.log(`š Finding files to rename to '${config.convention}' convention...\n`);
const files = suggestionEngine['getAllProjectFiles']();
const filesToRename = [];
for (const file of files) {
if (file.isDirectory)
continue;
// Skip files that should be ignored
if (file.name.startsWith('.') && file.name !== '.gitignore')
continue;
if (defaultExclusions.includes(file.name))
continue;
if (keepFiles.includes(file.name))
continue;
// Check if file extension should be excluded
const fileExtension = file.extension.toLowerCase();
const hasExcludedExtension = excludedExtensions.some(ext => file.name.toLowerCase().endsWith(ext.toLowerCase()));
if (hasExcludedExtension) {
const fileType = fileExtension.match(/\.(jpg|jpeg|png|gif|bmp|svg|webp|ico|tiff)$/i) ? 'image' :
fileExtension.match(/\.(md|markdown)$/i) ? 'markdown' :
file.name.match(/\.d\.(ts|mts|cts)$/i) ? 'TypeScript declaration' :
fileExtension.match(/\.(h|hpp|hxx)$/i) ? 'C/C++ header' :
fileExtension.match(/\.(hi|pyi|rbi|rei|mli|sig|fsi|spec|def)$/i) ? 'declaration' : 'config';
console.log(`š Skipping ${file.name} (${fileType} files excluded by default)`);
continue;
}
// Skip files containing 'config' in the filename
if (file.name.toLowerCase().includes('config')) {
console.log(`š Skipping ${file.name} (config files excluded by default)`);
continue;
}
const validation = naming_validator_1.NamingValidator.validateName(file.name, config.convention, config.exceptions);
if (!validation.isValid && validation.expectedName) {
const newPath = (0, path_1.join)((0, path_1.dirname)(file.path), validation.expectedName);
filesToRename.push({
oldPath: file.path,
newPath,
oldName: file.name,
newName: validation.expectedName
});
}
}
if (filesToRename.length === 0) {
console.log('ā
All files already follow the naming convention!');
return;
}
if (options.dryRun) {
console.log(`š Would rename ${filesToRename.length} files:\n`);
for (const file of filesToRename) {
console.log(` ${file.oldName} ā ${file.newName}`);
}
console.log('\nš Dry run complete. No files were actually renamed.');
return;
}
// Interactive mode (default) - ask for each file individually
if (!options.force) {
console.log(`š Found ${filesToRename.length} files that can be renamed:\n`);
let successCount = 0;
let errorCount = 0;
let skippedCount = 0;
for (const file of filesToRename) {
const { shouldRename } = await inquirer_1.default.prompt([{
type: 'confirm',
name: 'shouldRename',
message: `Rename "${file.oldName}" to "${file.newName}"?`,
default: true
}]);
if (shouldRename) {
try {
(0, fs_1.renameSync)(file.oldPath, file.newPath);
console.log(`ā
${file.oldName} ā ${file.newName}`);
successCount++;
}
catch (error) {
console.log(`ā Failed to rename ${file.oldName}: ${error}`);
errorCount++;
}
}
else {
console.log(`āļø Skipped ${file.oldName}`);
skippedCount++;
}
console.log(''); // Empty line for readability
}
console.log(`š Completed: ${successCount} renamed, ${skippedCount} skipped, ${errorCount} failed`);
return;
}
// Force mode - rename all without asking
console.log(`š Renaming ${filesToRename.length} files...\n`);
let successCount = 0;
let errorCount = 0;
for (const file of filesToRename) {
try {
(0, fs_1.renameSync)(file.oldPath, file.newPath);
console.log(`ā
${file.oldName} ā ${file.newName}`);
successCount++;
}
catch (error) {
console.log(`ā Failed to rename ${file.oldName}: ${error}`);
errorCount++;
}
}
console.log(`\nš Completed: ${successCount} renamed, ${errorCount} failed`);
}
async function addExceptionCommand(name) {
const configManager = new config_manager_1.ConfigManager();
const config = configManager.loadConfig();
const normalized = name.trim().toLowerCase();
if (config.exceptions?.includes(normalized)) {
console.log(`ā¹ļø '${normalized}' is already in the exceptions list.`);
return;
}
config.exceptions = [...(config.exceptions ?? []), normalized];
configManager.saveConfig(config);
console.log(`ā
Added '${normalized}' to exceptions.`);
console.log(`š Current exceptions: ${config.exceptions.join(', ')}`);
}
//# sourceMappingURL=commands.js.map