#!/usr/bin/env tsx /** * TGN Migration Tool * * Converts old TGN files with lowercase "pass" and "resign" to uppercase "Pass" and "Resign" * to match the updated TGN specification. * * Usage: * npm run migrate:tgn -- * npm run migrate:tgn -- --dry-run * npm run migrate:tgn -- --backup */ import * as fs from "fs"; import * as path from "path"; import yargs from "yargs"; import { hideBin } from "yargs/helpers"; interface MigrationOptions { directory: string; dryRun: boolean; backup: boolean; recursive: boolean; } interface MigrationStats { filesScanned: number; filesModified: number; passReplacements: number; resignReplacements: number; errors: string[]; } /** * Check if a file is a TGN file */ function isTGNFile(filePath: string): boolean { return path.extname(filePath).toLowerCase() === ".tgn"; } /** * Find all TGN files in a directory */ function findTGNFiles(dir: string, recursive: boolean): string[] { const files: string[] = []; try { const entries = fs.readdirSync(dir, { withFileTypes: true }); for (const entry of entries) { const fullPath = path.join(dir, entry.name); if (entry.isDirectory() && recursive) { files.push(...findTGNFiles(fullPath, recursive)); } else if (entry.isFile() && isTGNFile(fullPath)) { files.push(fullPath); } } } catch (error) { console.error(`Error reading directory ${dir}:`, error); } return files; } /** * Migrate TGN content from lowercase to uppercase */ function migrateTGNContent(content: string): { newContent: string; passCount: number; resignCount: number } { let passCount = 0; let resignCount = 0; // Replace "pass" with "Pass" (as a standalone word in move notation) // Use word boundary to avoid replacing "pass" in other contexts const newContent = content .replace(/\bpass\b/g, () => { passCount++; return "Pass"; }) .replace(/\bresign\b/g, () => { resignCount++; return "Resign"; }); return { newContent, passCount, resignCount }; } /** * Create backup of a file */ function backupFile(filePath: string): string { const backupPath = filePath + ".backup"; fs.copyFileSync(filePath, backupPath); return backupPath; } /** * Migrate a single TGN file */ function migrateFile( filePath: string, options: MigrationOptions, stats: MigrationStats ): void { stats.filesScanned++; try { // Read file content const content = fs.readFileSync(filePath, "utf-8"); // Migrate content const { newContent, passCount, resignCount } = migrateTGNContent(content); // Check if file needs migration if (passCount === 0 && resignCount === 0) { return; // File already up to date } // Update statistics stats.passReplacements += passCount; stats.resignReplacements += resignCount; stats.filesModified++; // Log changes console.log(`\nšŸ“„ ${path.basename(filePath)}`); if (passCount > 0) { console.log(` āœ“ Replaced ${passCount} "pass" → "Pass"`); } if (resignCount > 0) { console.log(` āœ“ Replaced ${resignCount} "resign" → "Resign"`); } // Skip write in dry-run mode if (options.dryRun) { console.log(" (dry-run: no changes written)"); return; } // Create backup if requested if (options.backup) { const backupPath = backupFile(filePath); console.log(` šŸ“¦ Backup created: ${path.basename(backupPath)}`); } // Write migrated content fs.writeFileSync(filePath, newContent, "utf-8"); console.log(` āœ… File updated`); } catch (error) { const errorMsg = `Error processing ${filePath}: ${error}`; stats.errors.push(errorMsg); console.error(` āŒ ${errorMsg}`); } } /** * Main migration function */ function migrateTGNFiles(options: MigrationOptions): MigrationStats { const stats: MigrationStats = { filesScanned: 0, filesModified: 0, passReplacements: 0, resignReplacements: 0, errors: [] }; console.log("šŸ”„ TGN Migration Tool"); console.log("=" .repeat(50)); console.log(`Directory: ${options.directory}`); console.log(`Recursive: ${options.recursive}`); console.log(`Dry run: ${options.dryRun}`); console.log(`Backup: ${options.backup}`); console.log("=" .repeat(50)); // Check if directory exists if (!fs.existsSync(options.directory)) { console.error(`āŒ Directory not found: ${options.directory}`); process.exit(1); } // Find all TGN files const files = findTGNFiles(options.directory, options.recursive); if (files.length === 0) { console.log("\nāš ļø No TGN files found."); return stats; } console.log(`\nšŸ“ Found ${files.length} TGN file(s)\n`); // Migrate each file for (const file of files) { migrateFile(file, options, stats); } // Print summary console.log("\n" + "=".repeat(50)); console.log("šŸ“Š Migration Summary"); console.log("=".repeat(50)); console.log(`Files scanned: ${stats.filesScanned}`); console.log(`Files modified: ${stats.filesModified}`); console.log(`Total "pass" → "Pass": ${stats.passReplacements}`); console.log(`Total "resign" → "Resign": ${stats.resignReplacements}`); if (stats.errors.length > 0) { console.log(`\nāŒ Errors: ${stats.errors.length}`); stats.errors.forEach((error) => console.error(` - ${error}`)); } if (options.dryRun) { console.log("\nāš ļø Dry run mode - no files were modified"); } else if (stats.filesModified > 0) { console.log("\nāœ… Migration completed successfully!"); } else { console.log("\n✨ All files are already up to date!"); } return stats; } /** * CLI Entry Point */ async function main() { const argv = await yargs(hideBin(process.argv)) .usage("Usage: $0 [options]") .command("$0 ", "Migrate TGN files to uppercase Pass/Resign format", (yargs) => { return yargs.positional("directory", { describe: "Directory containing TGN files", type: "string", demandOption: true }); }) .option("dry-run", { alias: "n", type: "boolean", default: false, description: "Preview changes without modifying files" }) .option("backup", { alias: "b", type: "boolean", default: false, description: "Create .backup files before modifying" }) .option("recursive", { alias: "r", type: "boolean", default: false, description: "Process subdirectories recursively" }) .example([ ["$0 output", "Migrate all TGN files in output directory"], ["$0 output --dry-run", "Preview changes without modifying files"], ["$0 output --backup", "Create backups before modifying"], ["$0 output --recursive", "Process all subdirectories"] ]) .help("h") .alias("h", "help") .strict() .parse(); const options: MigrationOptions = { directory: argv.directory as string, dryRun: argv["dry-run"] as boolean, backup: argv.backup as boolean, recursive: argv.recursive as boolean }; migrateTGNFiles(options); } // Run the CLI when executed directly if (process.argv[1] === new URL(import.meta.url).pathname) { main().catch((error) => { console.error("Fatal error:", error); process.exit(1); }); } export { migrateTGNFiles, migrateTGNContent, MigrationOptions, MigrationStats };