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:
uroma
2026-01-19 17:12:31 +00:00
Unverified
parent 06843e5300
commit 91e4835e03
2 changed files with 304 additions and 0 deletions

View File

@@ -5,6 +5,7 @@
"scripts": { "scripts": {
"start": "node server.js", "start": "node server.js",
"dev": "node server.js", "dev": "node server.js",
"migrate:projects": "node scripts/migrate-to-projects.js",
"test": "echo \"Error: no test specified\" && exit 1" "test": "echo \"Error: no test specified\" && exit 1"
}, },
"keywords": [], "keywords": [],

303
scripts/migrate-to-projects.js Executable file
View 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 };