#!/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); }