158 lines
		
	
	
		
			4.3 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			158 lines
		
	
	
		
			4.3 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| #!/usr/bin/env node
 | |
| 
 | |
| /**
 | |
|  * Migration script for converting old flat JSON Excalidraw files to Obsidian format
 | |
|  * Usage: node server/migrate-excalidraw.mjs [--dry-run] [--vault-path=./vault]
 | |
|  */
 | |
| 
 | |
| import fs from 'fs';
 | |
| import path from 'path';
 | |
| import { fileURLToPath } from 'url';
 | |
| import { parseFlatJson, toObsidianExcalidrawMd, isValidExcalidrawScene } from './excalidraw-obsidian.mjs';
 | |
| 
 | |
| const __filename = fileURLToPath(import.meta.url);
 | |
| const __dirname = path.dirname(__filename);
 | |
| 
 | |
| // Parse command line arguments
 | |
| const args = process.argv.slice(2);
 | |
| const dryRun = args.includes('--dry-run');
 | |
| const vaultPathArg = args.find(arg => arg.startsWith('--vault-path='));
 | |
| const vaultPath = vaultPathArg 
 | |
|   ? path.resolve(vaultPathArg.split('=')[1])
 | |
|   : path.resolve(__dirname, '..', 'vault');
 | |
| 
 | |
| console.log('🔄 Excalidraw Migration Tool');
 | |
| console.log('━'.repeat(50));
 | |
| console.log(`Vault path: ${vaultPath}`);
 | |
| console.log(`Mode: ${dryRun ? 'DRY RUN (no changes)' : 'LIVE (will modify files)'}`);
 | |
| console.log('━'.repeat(50));
 | |
| console.log();
 | |
| 
 | |
| if (!fs.existsSync(vaultPath)) {
 | |
|   console.error(`❌ Vault directory not found: ${vaultPath}`);
 | |
|   process.exit(1);
 | |
| }
 | |
| 
 | |
| let filesScanned = 0;
 | |
| let filesConverted = 0;
 | |
| let filesSkipped = 0;
 | |
| let filesErrored = 0;
 | |
| 
 | |
| /**
 | |
|  * Recursively scan directory for Excalidraw files
 | |
|  */
 | |
| function scanDirectory(dir) {
 | |
|   const entries = fs.readdirSync(dir, { withFileTypes: true });
 | |
|   
 | |
|   for (const entry of entries) {
 | |
|     const fullPath = path.join(dir, entry.name);
 | |
|     
 | |
|     if (entry.isDirectory()) {
 | |
|       // Skip hidden directories
 | |
|       if (entry.name.startsWith('.')) continue;
 | |
|       scanDirectory(fullPath);
 | |
|       continue;
 | |
|     }
 | |
|     
 | |
|     if (!entry.isFile()) continue;
 | |
|     
 | |
|     const lower = entry.name.toLowerCase();
 | |
|     
 | |
|     // Look for .excalidraw or .json files (but not .excalidraw.md)
 | |
|     if (lower.endsWith('.excalidraw.md')) {
 | |
|       // Already in Obsidian format, skip
 | |
|       filesSkipped++;
 | |
|       continue;
 | |
|     }
 | |
|     
 | |
|     if (!lower.endsWith('.excalidraw') && !lower.endsWith('.json')) {
 | |
|       continue;
 | |
|     }
 | |
|     
 | |
|     filesScanned++;
 | |
|     processFile(fullPath);
 | |
|   }
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Process a single file
 | |
|  */
 | |
| function processFile(filePath) {
 | |
|   const relativePath = path.relative(vaultPath, filePath);
 | |
|   
 | |
|   try {
 | |
|     const content = fs.readFileSync(filePath, 'utf-8');
 | |
|     
 | |
|     // Try to parse as flat JSON
 | |
|     const scene = parseFlatJson(content);
 | |
|     
 | |
|     if (!scene || !isValidExcalidrawScene(scene)) {
 | |
|       console.log(`⏭️  Skipped (not valid Excalidraw): ${relativePath}`);
 | |
|       filesSkipped++;
 | |
|       return;
 | |
|     }
 | |
|     
 | |
|     // Convert to Obsidian format
 | |
|     const obsidianMd = toObsidianExcalidrawMd(scene);
 | |
|     
 | |
|     // Determine new file path
 | |
|     const dir = path.dirname(filePath);
 | |
|     const baseName = path.basename(filePath, path.extname(filePath));
 | |
|     const newPath = path.join(dir, `${baseName}.excalidraw.md`);
 | |
|     
 | |
|     if (dryRun) {
 | |
|       console.log(`✅ Would convert: ${relativePath} → ${path.basename(newPath)}`);
 | |
|       filesConverted++;
 | |
|       return;
 | |
|     }
 | |
|     
 | |
|     // Create backup of original
 | |
|     const backupPath = filePath + '.bak';
 | |
|     fs.copyFileSync(filePath, backupPath);
 | |
|     
 | |
|     // Write new file
 | |
|     fs.writeFileSync(newPath, obsidianMd, 'utf-8');
 | |
|     
 | |
|     // Remove original if new file is different
 | |
|     if (newPath !== filePath) {
 | |
|       fs.unlinkSync(filePath);
 | |
|     }
 | |
|     
 | |
|     console.log(`✅ Converted: ${relativePath} → ${path.basename(newPath)}`);
 | |
|     filesConverted++;
 | |
|     
 | |
|   } catch (error) {
 | |
|     console.error(`❌ Error processing ${relativePath}:`, error.message);
 | |
|     filesErrored++;
 | |
|   }
 | |
| }
 | |
| 
 | |
| // Run migration
 | |
| try {
 | |
|   scanDirectory(vaultPath);
 | |
|   
 | |
|   console.log();
 | |
|   console.log('━'.repeat(50));
 | |
|   console.log('📊 Migration Summary');
 | |
|   console.log('━'.repeat(50));
 | |
|   console.log(`Files scanned:   ${filesScanned}`);
 | |
|   console.log(`Files converted: ${filesConverted}`);
 | |
|   console.log(`Files skipped:   ${filesSkipped}`);
 | |
|   console.log(`Files errored:   ${filesErrored}`);
 | |
|   console.log('━'.repeat(50));
 | |
|   
 | |
|   if (dryRun) {
 | |
|     console.log();
 | |
|     console.log('💡 This was a dry run. Run without --dry-run to apply changes.');
 | |
|   } else if (filesConverted > 0) {
 | |
|     console.log();
 | |
|     console.log('✅ Migration complete! Backup files (.bak) were created.');
 | |
|   }
 | |
|   
 | |
|   process.exit(filesErrored > 0 ? 1 : 0);
 | |
|   
 | |
| } catch (error) {
 | |
|   console.error('❌ Migration failed:', error);
 | |
|   process.exit(1);
 | |
| }
 |