Add migration script to backfill existing sessions into projects: - Scans session files from Claude Sessions directory - Extracts unique project names from metadata.project field - Creates projects with random icons and colors - Links sessions to their respective projects in database - Provides detailed progress reporting and summary Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
304 lines
8.7 KiB
JavaScript
Executable File
304 lines
8.7 KiB
JavaScript
Executable File
const fs = require('fs');
|
||
const path = require('path');
|
||
const Database = require('better-sqlite3');
|
||
|
||
/**
|
||
* Database Migration Script: Migrate existing sessions to projects
|
||
*
|
||
* This script:
|
||
* 1. Scans existing session files from Claude Sessions directory
|
||
* 2. Extracts unique project names from metadata.project field
|
||
* 3. Creates projects with random icons and colors
|
||
* 4. Updates sessions table with projectId references
|
||
*/
|
||
|
||
const VAULT_PATH = '/home/uroma/obsidian-vault';
|
||
const CLAUDE_SESSIONS_DIR = path.join(VAULT_PATH, 'Claude Sessions');
|
||
const DB_PATH = path.join(__dirname, '..', 'database.sqlite');
|
||
|
||
const ICONS = ['🚀', '📱', '🎨', '💻', '🔧', '📊', '🎯', '🔮', '⚡', '🌟'];
|
||
const COLORS = ['#4a9eff', '#ff6b6b', '#51cf66', '#ffd43b', '#cc5de8', '#ff922b', '#20c997', '#339af0'];
|
||
|
||
/**
|
||
* Get random item from array
|
||
*/
|
||
function getRandomItem(array) {
|
||
return array[Math.floor(Math.random() * array.length)];
|
||
}
|
||
|
||
/**
|
||
* Parse frontmatter from markdown content
|
||
*/
|
||
function parseFrontmatter(content) {
|
||
const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
|
||
if (!frontmatterMatch) {
|
||
return null;
|
||
}
|
||
|
||
const frontmatter = {};
|
||
frontmatterMatch[1].split('\n').forEach(line => {
|
||
const [key, ...valueParts] = line.split(':');
|
||
if (key && valueParts.length > 0) {
|
||
frontmatter[key.trim()] = valueParts.join(':').trim();
|
||
}
|
||
});
|
||
|
||
return frontmatter;
|
||
}
|
||
|
||
/**
|
||
* Scan session files and extract project information
|
||
*/
|
||
function scanSessionFiles() {
|
||
console.log('📂 Scanning session files...');
|
||
console.log(` Directory: ${CLAUDE_SESSIONS_DIR}`);
|
||
|
||
if (!fs.existsSync(CLAUDE_SESSIONS_DIR)) {
|
||
console.log('⚠️ Claude Sessions directory does not exist');
|
||
return [];
|
||
}
|
||
|
||
const files = fs.readdirSync(CLAUDE_SESSIONS_DIR);
|
||
const sessionFiles = files.filter(f => f.endsWith('.md') && f.includes('session-'));
|
||
|
||
console.log(` Found ${sessionFiles.length} session files`);
|
||
|
||
const sessions = [];
|
||
const projectMap = new Map(); // projectName -> { sessions: [], path: null }
|
||
|
||
sessionFiles.forEach(file => {
|
||
const filepath = path.join(CLAUDE_SESSIONS_DIR, file);
|
||
const content = fs.readFileSync(filepath, 'utf-8');
|
||
const frontmatter = parseFrontmatter(content);
|
||
|
||
if (!frontmatter) {
|
||
console.log(` ⚠️ Skipping ${file}: No frontmatter found`);
|
||
return;
|
||
}
|
||
|
||
const sessionId = frontmatter.session_id;
|
||
const projectName = frontmatter.project;
|
||
const workingDir = frontmatter.working_dir;
|
||
const createdAt = frontmatter.created_at;
|
||
|
||
if (!sessionId) {
|
||
console.log(` ⚠️ Skipping ${file}: No session_id in frontmatter`);
|
||
return;
|
||
}
|
||
|
||
// Add session to list
|
||
sessions.push({
|
||
id: sessionId,
|
||
projectName,
|
||
workingDir,
|
||
createdAt,
|
||
file
|
||
});
|
||
|
||
// Group by project
|
||
if (projectName) {
|
||
if (!projectMap.has(projectName)) {
|
||
projectMap.set(projectName, {
|
||
sessions: [],
|
||
path: workingDir,
|
||
earliestDate: createdAt
|
||
});
|
||
}
|
||
projectMap.get(projectName).sessions.push(sessionId);
|
||
|
||
// Update earliest date if this session is older
|
||
const currentDate = new Date(createdAt);
|
||
const existingDate = new Date(projectMap.get(projectName).earliestDate);
|
||
if (currentDate < existingDate) {
|
||
projectMap.get(projectName).earliestDate = createdAt;
|
||
}
|
||
}
|
||
});
|
||
|
||
console.log(` ✅ Processed ${sessions.length} sessions`);
|
||
console.log(` 📊 Found ${projectMap.size} unique projects`);
|
||
|
||
return { sessions, projectMap };
|
||
}
|
||
|
||
/**
|
||
* Create projects in database
|
||
*/
|
||
function createProjects(db, projectMap) {
|
||
console.log('\n🎨 Creating projects in database...');
|
||
|
||
const insertProject = db.prepare(`
|
||
INSERT INTO projects (name, description, icon, color, path, createdAt, lastActivity)
|
||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||
`);
|
||
|
||
const projects = {};
|
||
let createdCount = 0;
|
||
|
||
for (const [projectName, data] of projectMap.entries()) {
|
||
const icon = getRandomItem(ICONS);
|
||
const color = getRandomItem(COLORS);
|
||
const now = new Date().toISOString();
|
||
|
||
try {
|
||
const result = insertProject.run(
|
||
projectName,
|
||
`Sessions for ${projectName}`,
|
||
icon,
|
||
color,
|
||
data.path || VAULT_PATH,
|
||
data.earliestDate,
|
||
now
|
||
);
|
||
|
||
projects[projectName] = {
|
||
id: result.lastInsertRowid,
|
||
name: projectName,
|
||
icon,
|
||
color,
|
||
sessionCount: data.sessions.length
|
||
};
|
||
|
||
createdCount++;
|
||
console.log(` ✅ Created project: ${projectName} (${icon})`);
|
||
console.log(` - ID: ${result.lastInsertRowid}`);
|
||
console.log(` - Sessions: ${data.sessions.length}`);
|
||
console.log(` - Path: ${data.path || VAULT_PATH}`);
|
||
} catch (error) {
|
||
console.error(` ❌ Error creating project ${projectName}:`, error.message);
|
||
}
|
||
}
|
||
|
||
console.log(`\n ✅ Created ${createdCount} projects`);
|
||
|
||
return projects;
|
||
}
|
||
|
||
/**
|
||
* Update sessions with projectId
|
||
*/
|
||
function updateSessions(db, sessions, projects) {
|
||
console.log('\n🔗 Linking sessions to projects...');
|
||
|
||
const insertSession = db.prepare(`
|
||
INSERT OR REPLACE INTO sessions (id, projectId)
|
||
VALUES (?, ?)
|
||
`);
|
||
|
||
let updatedCount = 0;
|
||
let unlinkedCount = 0;
|
||
|
||
sessions.forEach(session => {
|
||
const projectName = session.projectName;
|
||
|
||
if (projectName && projects[projectName]) {
|
||
const projectId = projects[projectName].id;
|
||
|
||
try {
|
||
insertSession.run(session.id, projectId);
|
||
updatedCount++;
|
||
console.log(` ✅ Linked ${session.id} -> ${projectName} (ID: ${projectId})`);
|
||
} catch (error) {
|
||
console.error(` ❌ Error linking session ${session.id}:`, error.message);
|
||
}
|
||
} else {
|
||
// Session without a project - add to sessions table without projectId
|
||
try {
|
||
insertSession.run(session.id, null);
|
||
unlinkedCount++;
|
||
console.log(` ⚠️ Unlinked session: ${session.id} (no project)`);
|
||
} catch (error) {
|
||
console.error(` ❌ Error adding session ${session.id}:`, error.message);
|
||
}
|
||
}
|
||
});
|
||
|
||
console.log(`\n ✅ Linked ${updatedCount} sessions to projects`);
|
||
console.log(` ℹ️ ${unlinkedCount} sessions without projects`);
|
||
|
||
return { updatedCount, unlinkedCount };
|
||
}
|
||
|
||
/**
|
||
* Display migration summary
|
||
*/
|
||
function displaySummary(sessions, projects, stats) {
|
||
console.log('\n' + '='.repeat(60));
|
||
console.log('📊 MIGRATION SUMMARY');
|
||
console.log('='.repeat(60));
|
||
console.log(`\nSessions Processed: ${sessions.length}`);
|
||
console.log(`Projects Created: ${Object.keys(projects).length}`);
|
||
console.log(`Sessions Linked: ${stats.updatedCount}`);
|
||
console.log(`Sessions Unlinked: ${stats.unlinkedCount}`);
|
||
|
||
console.log('\n📁 Projects Created:');
|
||
Object.values(projects).forEach(project => {
|
||
console.log(` ${project.icon} ${project.name} (${project.color})`);
|
||
console.log(` - ID: ${project.id}`);
|
||
console.log(` - Sessions: ${project.sessionCount}`);
|
||
});
|
||
|
||
console.log('\n' + '='.repeat(60));
|
||
console.log('✅ Migration completed successfully!');
|
||
console.log('='.repeat(60) + '\n');
|
||
}
|
||
|
||
/**
|
||
* Main migration function
|
||
*/
|
||
function migrate() {
|
||
console.log('🚀 Starting database migration for projects...\n');
|
||
|
||
// Check if database exists
|
||
if (!fs.existsSync(DB_PATH)) {
|
||
console.error('❌ Database file not found:', DB_PATH);
|
||
console.error('Please run the application first to initialize the database.');
|
||
process.exit(1);
|
||
}
|
||
|
||
// Open database
|
||
console.log('📦 Opening database...');
|
||
const db = new Database(DB_PATH);
|
||
db.pragma('journal_mode = WAL');
|
||
|
||
try {
|
||
// Step 1: Scan session files
|
||
const { sessions, projectMap } = scanSessionFiles();
|
||
|
||
if (sessions.length === 0) {
|
||
console.log('\n⚠️ No sessions found to migrate');
|
||
db.close();
|
||
return;
|
||
}
|
||
|
||
// Step 2: Create projects
|
||
const projects = createProjects(db, projectMap);
|
||
|
||
if (Object.keys(projects).length === 0) {
|
||
console.log('\n⚠️ No projects found to create');
|
||
db.close();
|
||
return;
|
||
}
|
||
|
||
// Step 3: Update sessions
|
||
const stats = updateSessions(db, sessions, projects);
|
||
|
||
// Step 4: Display summary
|
||
displaySummary(sessions, projects, stats);
|
||
|
||
} catch (error) {
|
||
console.error('\n❌ Migration failed:', error);
|
||
throw error;
|
||
} finally {
|
||
db.close();
|
||
console.log('📦 Database connection closed\n');
|
||
}
|
||
}
|
||
|
||
// Run migration if executed directly
|
||
if (require.main === module) {
|
||
migrate();
|
||
}
|
||
|
||
module.exports = { migrate };
|