feat: add database migration script for projects
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>
This commit is contained in:
303
scripts/migrate-to-projects.js
Executable file
303
scripts/migrate-to-projects.js
Executable file
@@ -0,0 +1,303 @@
|
||||
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 };
|
||||
Reference in New Issue
Block a user