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:
@@ -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
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