diff --git a/package.json b/package.json index e787db4f..c6f138c1 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,7 @@ "scripts": { "start": "node server.js", "dev": "node server.js", + "migrate:projects": "node scripts/migrate-to-projects.js", "test": "echo \"Error: no test specified\" && exit 1" }, "keywords": [], diff --git a/scripts/migrate-to-projects.js b/scripts/migrate-to-projects.js new file mode 100755 index 00000000..f8c1ed4c --- /dev/null +++ b/scripts/migrate-to-projects.js @@ -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 };