trigo / trigo-web /tools /migrateTGN.ts
k-l-lambda's picture
updated
502af73
#!/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 -- <directory>
* npm run migrate:tgn -- <directory> --dry-run
* npm run migrate:tgn -- <directory> --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 <directory> [options]")
.command("$0 <directory>", "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 };