ObsiViewer/server/migrate-excalidraw.mjs

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);
}