/** * Migration: rTime TasksDoc → rTasks BoardDoc + WeavingDoc * * Reads all {space}:rtime:tasks docs and: * 1. Creates TaskItem entries in the rTasks board (reusing Task.id) * 2. Creates WeavingOverlay entries in the WeavingDoc * 3. Moves connections + execStates from TasksDoc → WeavingDoc * * Usage: npx tsx scripts/migrate-rtime-tasks.ts [space] * Default space: demo * * This script must be run on the server where Automerge data lives. * It operates on the SyncServer data files directly. */ import * as Automerge from '@automerge/automerge'; import type { SyncServer } from '../server/local-first/sync-server'; import { tasksDocId, weavingDocId, weavingSchema, } from '../modules/rtime/schemas'; import type { TasksDoc, WeavingDoc, Task, Connection, ExecState } from '../modules/rtime/schemas'; import { boardDocId, createTaskItem, boardSchema } from '../modules/rtasks/schemas'; import type { BoardDoc, TaskItem } from '../modules/rtasks/schemas'; const space = process.argv[2] || 'demo'; console.log(`[migrate] Starting rTime → rTasks migration for space: ${space}`); /** * Standalone migration logic. Call with a SyncServer instance. * This is exported so it can also be called from server startup if needed. */ export function migrateRTimeTasks(syncServer: SyncServer, spaceSlug: string): { tasksCreated: number; overlaysCreated: number; connectionsMoved: number; execStatesMoved: number; } { const result = { tasksCreated: 0, overlaysCreated: 0, connectionsMoved: 0, execStatesMoved: 0 }; // 1. Read legacy TasksDoc const oldDocId = tasksDocId(spaceSlug); const oldDoc = syncServer.getDoc(oldDocId); if (!oldDoc) { console.log(`[migrate] No TasksDoc found at ${oldDocId}, nothing to migrate.`); return result; } const oldTasks = Object.values(oldDoc.tasks || {}); const oldConnections = Object.values(oldDoc.connections || {}); const oldExecStates = Object.values(oldDoc.execStates || {}); if (oldTasks.length === 0 && oldConnections.length === 0) { console.log(`[migrate] TasksDoc is empty, nothing to migrate.`); return result; } console.log(`[migrate] Found ${oldTasks.length} tasks, ${oldConnections.length} connections, ${oldExecStates.length} exec states`); // 2. Ensure rTasks board exists const bDocId = boardDocId(spaceSlug, spaceSlug); let boardDoc = syncServer.getDoc(bDocId); if (!boardDoc) { boardDoc = Automerge.change(Automerge.init(), 'init board for migration', (d) => { const init = boardSchema.init(); Object.assign(d, init); d.meta.spaceSlug = spaceSlug; d.board.id = spaceSlug; d.board.slug = spaceSlug; d.board.name = `${spaceSlug} Board`; }); syncServer.setDoc(bDocId, boardDoc); console.log(`[migrate] Created rTasks board: ${bDocId}`); } // 3. Create TaskItem for each rTime Task (reuse ID) for (const task of oldTasks) { if (boardDoc.tasks[task.id]) { console.log(`[migrate] Task ${task.id} already exists in board, skipping.`); continue; } const taskItem = createTaskItem(task.id, spaceSlug, task.name, { description: task.description || '', }); syncServer.changeDoc(bDocId, `migrate task: ${task.name}`, (d) => { d.tasks[task.id] = taskItem as any; }); result.tasksCreated++; } // 4. Ensure WeavingDoc exists const wDocId = weavingDocId(spaceSlug); let wDoc = syncServer.getDoc(wDocId); if (!wDoc) { wDoc = Automerge.change(Automerge.init(), 'init weaving for migration', (d) => { const init = weavingSchema.init(); Object.assign(d, init); d.meta.spaceSlug = spaceSlug; d.boardSlug = spaceSlug; }); syncServer.setDoc(wDocId, wDoc); } // 5. Create WeavingOverlay for each task (with needs, notes, links) let canvasX = 300; for (const task of oldTasks) { if (wDoc.weavingOverlays[task.id]) { console.log(`[migrate] Overlay for ${task.id} already exists, skipping.`); continue; } syncServer.changeDoc(wDocId, `migrate overlay: ${task.name}`, (d) => { d.weavingOverlays[task.id] = { rtasksId: task.id, needs: task.needs || {}, canvasX, canvasY: 150, notes: task.notes || '', links: task.links || [], intentFrameId: task.intentFrameId, } as any; }); canvasX += 280; result.overlaysCreated++; } // 6. Move connections from TasksDoc → WeavingDoc for (const conn of oldConnections) { syncServer.changeDoc(wDocId, `migrate connection: ${conn.id}`, (d) => { if (!d.connections[conn.id]) { d.connections[conn.id] = { ...conn } as any; result.connectionsMoved++; } }); } // 7. Move execStates from TasksDoc → WeavingDoc for (const es of oldExecStates) { syncServer.changeDoc(wDocId, `migrate exec state: ${es.taskId}`, (d) => { if (!d.execStates[es.taskId]) { d.execStates[es.taskId] = { ...es } as any; result.execStatesMoved++; } }); } // Refresh doc after changes const finalW = syncServer.getDoc(wDocId)!; result.connectionsMoved = Object.keys(finalW.connections).length; result.execStatesMoved = Object.keys(finalW.execStates).length; console.log(`[migrate] Migration complete: Tasks created in rTasks: ${result.tasksCreated} Weaving overlays created: ${result.overlaysCreated} Connections moved: ${result.connectionsMoved} Exec states moved: ${result.execStatesMoved}`); return result; } // CLI entry point — only runs when executed directly if (process.argv[1]?.includes('migrate-rtime-tasks')) { console.log('[migrate] This script must be imported and called with a SyncServer instance.'); console.log('[migrate] Example: import { migrateRTimeTasks } from "./scripts/migrate-rtime-tasks";'); console.log('[migrate] migrateRTimeTasks(syncServer, "demo");'); }