| name | obsidian-migration-deep-dive |
| description | Execute major Obsidian plugin rewrites and migration strategies.
Use when migrating to or from Obsidian, performing major plugin rewrites,
or re-platforming existing note systems to Obsidian.
Trigger with phrases like "migrate to obsidian", "obsidian migration",
"convert notes to obsidian", "obsidian replatform".
|
| allowed-tools | Read, Write, Edit, Bash(npm:*), Bash(node:*) |
| version | 1.0.0 |
| license | MIT |
| author | Jeremy Longshore <jeremy@intentsolutions.io> |
Obsidian Migration Deep Dive
Overview
Comprehensive guide for migrating to Obsidian from other note-taking apps, or performing major plugin architecture rewrites.
Prerequisites
- Source data access
- Understanding of Obsidian vault structure
- Node.js for scripted migrations
- Backup of source data
Migration Types
| Type | Complexity | Duration | Risk |
|---|
| Single app import | Low | Hours | Low |
| Multi-source merge | Medium | Days | Medium |
| Plugin major rewrite | Medium | Weeks | Medium |
| Enterprise migration | High | Months | High |
Instructions
Step 1: Pre-Migration Assessment
interface MigrationAssessment {
sourceSystem: string;
noteCount: number;
attachmentCount: number;
totalSize: number;
linkCount: number;
tagCount: number;
uniqueTags: string[];
folderStructure: string[];
issues: MigrationIssue[];
}
interface MigrationIssue {
type: 'encoding' | 'format' | 'link' | 'attachment' | 'metadata';
severity: 'warning' | 'error';
description: string;
affectedFiles: string[];
}
async function assessMigration(sourcePath: string): Promise<MigrationAssessment> {
const assessment: MigrationAssessment = {
sourceSystem: 'unknown',
noteCount: 0,
attachmentCount: 0,
: ,
: ,
: ,
: [],
: [],
: [],
};
assessment;
}
(): {
;
}
Step 2: Format Converters
import * as fs from 'fs';
import * as path from 'path';
import { parseStringPromise } from 'xml2js';
interface EvernoteNote {
title: string;
content: string;
created: string;
updated: string;
tags: string[];
attachments: EvernoteAttachment[];
}
interface EvernoteAttachment {
filename: string;
mime: string;
data: string;
}
export async function convertEvernoteExport(
enexPath: string,
outputPath: string
): Promise<{ notes: number; attachments: number }> {
const content = fs.readFileSync(enexPath, 'utf-8');
const parsed = await (content);
notes = parsed[]?. || [];
noteCount = ;
attachmentCount = ;
( note notes) {
converted = (note);
fileName = (converted.) + ;
filePath = path.(outputPath, fileName);
markdown = (converted.);
frontmatter = ;
fs.(filePath, frontmatter + markdown);
noteCount++;
( attachment converted.) {
attachmentPath = path.(outputPath, , attachment.);
data = .(attachment., );
fs.(attachmentPath, data);
attachmentCount++;
}
}
{ : noteCount, : attachmentCount };
}
(): {
{
: note.?.[] || ,
: note.?.[] || ,
: (note.?.[]),
: (note.?.[]),
: note. || [],
: (note. || []),
};
}
(): <{ : ; : }> {
noteCount = ;
databaseCount = ;
{ : noteCount, : databaseCount };
}
(): <{ : ; : }> {
content = fs.(roamJsonPath, );
roamData = .(content);
pageCount = ;
blockCount = ;
( page roamData) {
markdown = (page);
fileName = (page.) + ;
fs.(path.(outputPath, fileName), markdown);
pageCount++;
blockCount += (page);
}
{ : pageCount, : blockCount };
}
(): {
: [] = [, ];
(page.) {
( block page.) {
lines.(...(block, ));
}
}
lines.();
}
(): [] {
: [] = [];
indent = .(depth);
content = (block. || );
lines.();
(block.) {
( child block.) {
lines.(...(child, depth + ));
}
}
lines;
}
(): {
text
.(, )
.(, )
.(, )
.(, );
}
Step 3: Link Migration
import * as fs from 'fs';
import * as path from 'path';
import * as glob from 'glob';
interface LinkMapping {
original: string;
converted: string;
type: 'internal' | 'external' | 'attachment';
}
export class LinkMigrator {
private linkMappings: Map<string, LinkMapping> = new Map();
private orphanedLinks: string[] = [];
async buildLinkIndex(vaultPath: string): Promise<void> {
const files = glob.sync('**/*.md', { cwd: vaultPath });
for (const file of files) {
const baseName = path.basename(file, '.md');
this..(baseName.(), {
: baseName,
: baseName,
: ,
});
}
}
(: ): <{
: ;
: [];
}> {
files = glob.(, { : vaultPath });
updatedCount = ;
( file files) {
filePath = path.(vaultPath, file);
content = fs.(filePath, );
modified = ;
linkRegex = ;
match;
((match = linkRegex.(content)) !== ) {
originalLink = match[];
alias = match[] || ;
resolvedLink = .(originalLink);
(resolvedLink !== originalLink) {
newLink = ;
content = content.(match[], newLink);
modified = ;
}
}
(modified) {
fs.(filePath, content);
updatedCount++;
}
}
{
: updatedCount,
: .,
};
}
(: ): {
mapping = ..(link.());
(mapping) {
mapping.;
}
baseName = path.(link);
baseMapping = ..(baseName.());
(baseMapping) {
baseMapping.;
}
(!..(link)) {
..(link);
}
link;
}
(: ): <> {
report = ;
fs.(
path.(vaultPath, , ),
report
);
}
}
Step 4: Batch Migration Script
import * as fs from 'fs';
import * as path from 'path';
import { convertEvernoteExport } from './converters/evernote';
import { convertNotionExport } from './converters/notion';
import { convertRoamExport } from './converters/roam';
import { LinkMigrator } from './migrate-links';
interface MigrationConfig {
source: {
type: 'evernote' | 'notion' | 'roam' | 'markdown';
path: string;
};
target: {
vaultPath: string;
createBackup: boolean;
};
options: {
preserveFolderStructure: boolean;
convertTags: boolean;
migrateAttachments: boolean;
fixLinks: boolean;
dryRun: boolean;
};
}
async function runMigration(: ): <> {
.();
.();
.();
(config.. && !config..) {
backupPath = ;
fs.(config.., backupPath, { : });
.();
}
migrationFolder = path.(config.., );
(!config..) {
fs.(migrationFolder, { : });
}
: { : ; [: ]: };
(config..) {
:
result = (
config..,
config..
);
;
:
result = (
config..,
config..
);
;
:
result = (
config..,
config..
);
;
:
();
}
.();
(config..) {
linkMigrator = ();
linkMigrator.(config..);
linkResult = linkMigrator.(config..);
.();
.();
(linkResult.. > ) {
linkMigrator.(config..);
}
}
report = (config, result);
(!config..) {
fs.(
path.(migrationFolder, ),
report
);
}
.();
}
(): {
;
}
: = {
: {
: ,
: ,
},
: {
: ,
: ,
},
: {
: ,
: ,
: ,
: ,
: ,
},
};
(config).(.);
Step 5: Plugin Architecture Migration
interface PluginMigrationPlan {
currentVersion: string;
targetVersion: string;
phases: MigrationPhase[];
rollbackPlan: string;
}
interface MigrationPhase {
name: string;
description: string;
changes: string[];
breakingChanges: string[];
migrationSteps: string[];
}
const migrationPlan: PluginMigrationPlan = {
currentVersion: '1.x',
targetVersion: '2.0',
phases: [
{
name: 'Phase 1: Settings Migration',
description: 'Migrate settings to new format',
changes: [
'New settings schema',
'Split monolithic settings into categories',
],
breakingChanges: [
'Old settings format deprecated',
],
migrationSteps: [
'Load old settings on upgrade',
'Transform to new format',
'Save new settings',
'Backup old settings',
],
},
{
name: 'Phase 2: API Changes',
: ,
: [
,
,
],
: [
,
,
],
: [
,
,
,
],
},
],
: ,
};
Output
- Pre-migration assessment
- Format converters for common apps
- Link migration and fixing
- Batch migration scripts
- Migration reports
Error Handling
| Issue | Cause | Solution |
|---|
| Encoding errors | Non-UTF8 content | Detect and convert encoding |
| Broken links | Renamed/deleted files | Generate orphaned links report |
| Missing attachments | Export incomplete | Re-export with attachments |
| Duplicate files | Same name different folders | Add path prefix |
Examples
Command Line Usage
npm install xml2js glob
npx ts-node scripts/migrate.ts
MIGRATION_DRY_RUN=true npx ts-node scripts/migrate.ts
Post-Migration Checklist
## Post-Migration Checklist
- [ ] Open vault in Obsidian
- [ ] Check random sample of notes (10-20)
- [ ] Verify links resolve correctly
- [ ] Check attachments display
- [ ] Verify tags imported
- [ ] Test search functionality
- [ ] Check folder structure
- [ ] Review orphaned links report
- [ ] Delete _migration folder
- [ ] Update any external integrations
Resources
Flagship+ Skills
Migration complete! You now have comprehensive Obsidian plugin development skills.