// --- Assumed/Mocked Models, Errors, and Storage --- // This section provides the necessary types and functions for the migration // logic to compile and run, simulating the missing Rust modules. enum TaskStatus { ACTIVE = 'ACTIVE', COMPLETED = 'COMPLETED', ARCHIVED = 'ARCHIVED', } interface Task { id: string; title: string; description: string; status: TaskStatus; parentProject: string; embeddingVector?: number[]; // Optional, generated by an external service } interface TodoziEmbeddingConfig { model: string; dimensions: number; } // A mock embedding service class TodoziEmbeddingService { private config: TodoziEmbeddingConfig; constructor(config: TodoziEmbeddingConfig) { this.config = config; } static async new(config: TodoziEmbeddingConfig): Promise { // In a real app, this might initialize an AI model client return new TodoziEmbeddingService(config); } async initialize(): Promise { // Simulate async initialization console.log(" ๐Ÿค– Embedding service initialized."); } prepareTaskContent(task: Task): string { return `${task.title}\n${task.description}`; } async generate_embedding(content: string): Promise { // Simulate API call to an embedding service console.log(` ๐Ÿ”ข Generating embedding for content: "${content.substring(0, 30)}..."`); return Array.from({ length: this.config.dimensions }, () => Math.random()); } } class TodoziError extends Error { constructor(message: string, public readonly context?: string) { super(message); this.name = 'TodoziError'; } static storage(message: string): TodoziError { return new TodoziError(message, 'STORAGE'); } } interface ProjectMigrationStats { projectName: string; initialTasks: number; migratedTasks: number; finalTasks: number; } interface MigrationReport { tasksFound: number; tasksMigrated: number; projectsMigrated: number; projectStats: ProjectMigrationStats[]; errors: string[]; } // A mock storage layer using in-memory maps and Node.js fs for persistence import { readFile, writeFile, unlink } from 'fs/promises'; import { join } from 'path'; const STORAGE_DIR = './.todozi_storage'; // A local directory for mock files const TASKS_DIR = join(STORAGE_DIR, 'tasks'); const PROJECTS_DIR = join(STORAGE_DIR, 'projects'); interface TaskCollection { name: string; tasks: { [id: string]: Task }; } // In-memory cache to simulate a more complex storage system const projectContainersCache = new Map(); class ProjectTaskContainer { public projectName: string; private tasks: Map; constructor(projectName: string) { this.projectName = projectName; this.tasks = new Map(); } static new(projectName: string): ProjectTaskContainer { return new ProjectTaskContainer(projectName); } addTask(task: Task): void { this.tasks.set(task.id, task); } getTask(id: string): Task | undefined { return this.tasks.get(id); } getAllTasks(): Task[] { return Array.from(this.tasks.values()); } } // Mock storage functions async function ensureDir(path: string): Promise { // This is a placeholder. In a real app, you'd use `fs.mkdir` with recursive: true. // For this example, we'll assume the directory exists or is created manually. } async function loadTaskCollection(collectionName: string): Promise { const filePath = join(TASKS_DIR, `${collectionName}.json`); try { const data = await readFile(filePath, 'utf-8'); return JSON.parse(data) as TaskCollection; } catch (e) { throw TodoziError.storage(`Could not load '${collectionName}' collection.`); } } async function loadProjectTaskContainer(projectName: string): Promise { if (projectContainersCache.has(projectName)) { return projectContainersCache.get(projectName)!; } const filePath = join(PROJECTS_DIR, `${projectName}.json`); try { const data = await readFile(filePath, 'utf-8'); const containerData = JSON.parse(data); const container = new ProjectTaskContainer(projectName); containerData.tasks.forEach((task: Task) => container.addTask(task)); projectContainersCache.set(projectName, container); return container; } catch (e) { throw TodoziError.storage(`Could not load project container for '${projectName}'.`); } } async function saveProjectTaskContainer(container: ProjectTaskContainer): Promise { projectContainersCache.set(container.projectName, container); const filePath = join(PROJECTS_DIR, `${container.projectName}.json`); const data = JSON.stringify({ projectName: container.projectName, tasks: container.getAllTasks(), }, null, 2); try { await writeFile(filePath, data, 'utf-8'); } catch (e) { throw TodoziError.storage(`Failed to save project container '${container.projectName}'.`); } } async function listProjectTaskContainers(): Promise { // In a real scenario, this would list files in PROJECTS_DIR. // For this mock, we return the items from our in-memory cache. return Array.from(projectContainersCache.values()); } function getStorageDir(): string { return STORAGE_DIR; // Placeholder } // --- End of Mocked Section --- // --- Translated Rust Code --- class TaskMigrator { public dryRun: boolean; public verbose: boolean; public forceOverwrite: boolean; constructor() { this.dryRun = false; this.verbose = false; this.forceOverwrite = false; } public static new(): TaskMigrator { return new TaskMigrator(); } public dryRun(dryRun: boolean): this { this.dryRun = dryRun; return this; } public verbose(verbose: boolean): this { this.verbose = verbose; return this; } public forceOverwrite(forceOverwrite: boolean): this { this.forceOverwrite = forceOverwrite; return this; } public async migrate(): Promise { const report: MigrationReport = { tasksFound: 0, tasksMigrated: 0, projectsMigrated: 0, projectStats: [], errors: [], }; if (this.verbose) { console.log("๐Ÿš€ Starting task migration to project-based system..."); if (this.dryRun) { console.log("๐Ÿ” DRY RUN MODE - No actual changes will be made"); } } const allTasks = this.loadLegacyTasks(report); if (allTasks.length === 0) { if (this.verbose) { console.log("โœ… No legacy tasks found - migration complete"); } return report; } const projectGroups = this.groupTasksByProject(allTasks); if (this.verbose) { console.log(`๐Ÿ“Š Found ${projectGroups.size} unique projects`); for (const [projectName, tasks] of projectGroups.entries()) { console.log(` โ€ข ${projectName}: ${tasks.length} tasks`); } } for (const [projectName, tasks] of projectGroups.entries()) { try { const projectReport = await this.migrateProjectTasks(projectName, tasks); report.projectStats.push(projectReport); report.projectsMigrated += 1; } catch (e) { const error = e as Error; report.errors.push(error.message); if (this.verbose) { console.error(` โŒ Failed to migrate project '${projectName}': ${error.message}`); } } } report.tasksMigrated = report.projectStats.reduce((sum, stat) => sum + stat.migratedTasks, 0); if (this.verbose) { this.printSummary(report); } return report; } private loadLegacyTasks(report: MigrationReport): Task[] { const collections = ["active", "completed", "archived"]; const allTasks: Task[] = []; for (const collectionName of collections) { try { const collection = loadTaskCollection(collectionName); for (const task of Object.values(collection.tasks)) { allTasks.push(task); report.tasksFound += 1; } if (this.verbose) { console.log( `๐Ÿ“‚ Loaded ${Object.keys(collection.tasks).length} tasks from '${collectionName}' collection` ); } } catch (_) { if (this.verbose) { console.log(`โš ๏ธ Could not load '${collectionName}' collection (may not exist)`); } } } return allTasks; } private groupTasksByProject(tasks: Task[]): Map { const projectGroups = new Map(); for (const task of tasks) { const project = task.parentProject.isEmpty() ? "general" : task.parentProject; if (!projectGroups.has(project)) { projectGroups.set(project, []); } projectGroups.get(project)!.push(task); } return projectGroups; } private async migrateProjectTasks( projectName: string, tasks: Task[], ): Promise { const stats: ProjectMigrationStats = { projectName: projectName, initialTasks: 0, migratedTasks: 0, finalTasks: 0, }; let container: ProjectTaskContainer; try { container = await loadProjectTaskContainer(projectName); stats.initialTasks = container.getAllTasks().length; if (!this.forceOverwrite && stats.initialTasks > 0) { if (this.verbose) { console.log( `โš ๏ธ Project '${projectName}' already exists with ${stats.initialTasks} tasks (use --force to overwrite)` ); } stats.finalTasks = stats.initialTasks; return stats; } } catch (_) { if (this.verbose) { console.log(`๐Ÿ“ Creating new project container for '${projectName}'`); } container = ProjectTaskContainer.new(projectName); } const initialCount = container.getAllTasks().length; for (const task of tasks) { if (container.getTask(task.id)) { if (this.verbose) { console.log(` โญ๏ธ Skipping duplicate task: ${task.id}`); } continue; } try { const embService = await TodoziEmbeddingService.new( { model: 'default-model', dimensions: 128 } ); await embService.initialize(); const embeddingContent = embService.prepareTaskContent(task); const vector = await embService.generate_embedding(embeddingContent); task.embeddingVector = vector; if (this.verbose) { console.log(` ๐Ÿง  Generated embedding for task: ${task.id}`); } } catch (e) { const error = e as Error; if (this.verbose) { console.log( ` โŒ Embedding generation failed for task ${task.id}: ${error.message}` ); } } container.addTask(task); stats.migratedTasks += 1; const allTasks = container.getAllTasks(); if (this.verbose) { console.log( ` โœ… Migrated task: ${allTasks[allTasks.length - 1].id} (status: ${allTasks[allTasks.length - 1].status})` ); } } stats.finalTasks = container.getAllTasks().length; if (!this.dryRun) { try { await saveProjectTaskContainer(container); if (this.verbose) { console.log(`๐Ÿ’พ Saved project container: ${projectName}`); } } catch (e) { const error = e as Error; const errorMsg = `Failed to save project container '${projectName}': ${error.message}`; if (this.verbose) { console.log(`โŒ ${errorMsg}`); } throw TodoziError.storage(errorMsg); } } else { if (this.verbose) { console.log( `๐Ÿ” DRY RUN: Would save project container: ${projectName} (${stats.finalTasks} tasks)` ); } } return stats; } private printSummary(report: MigrationReport): void { console.log("\n" + "=".repeat(60)); console.log("๐Ÿ“Š MIGRATION SUMMARY"); console.log("=".repeat(60)); console.log(`Total legacy tasks found: ${report.tasksFound}`); console.log(`Tasks migrated: ${report.tasksMigrated}`); console.log(`Projects processed: ${report.projectsMigrated}`); if (report.projectStats.length > 0) { console.log("\n๐Ÿ“‹ Project Details:"); for (const stat of report.projectStats) { console.log( ` โ€ข ${stat.projectName}: ${stat.initialTasks} โ†’ ${stat.finalTasks} tasks (${stat.migratedTasks} migrated)` ); } } if (report.errors.length > 0) { console.log("\nโš ๏ธ Errors encountered:"); for (const error of report.errors) { console.log(` โ€ข ${error}`); } } if (report.tasksMigrated === report.tasksFound && report.errors.length === 0) { console.log("\nโœ… Migration completed successfully!"); } else { console.log("\nโš ๏ธ Migration completed with warnings"); } console.log("=".repeat(60)); } public validateMigration(): boolean { if (this.verbose) { console.log("๐Ÿ” Validating migration integrity..."); } const legacyTasks = ["active", "completed", "archived"] .map(collection => { try { return Object.keys(loadTaskCollection(collection).tasks).length; } catch { return 0; } }) .reduce((sum, count) => sum + count, 0); const projectTasks = Array.from(projectContainersCache.values()) .reduce((sum, container) => sum + container.getAllTasks().length, 0); if (this.verbose) { console.log(`Legacy system tasks: ${legacyTasks}`); console.log(`Project system tasks: ${projectTasks}`); } const isValid = legacyTasks === 0 || (legacyTasks > 0 && projectTasks >= legacyTasks); if (isValid) { if (this.verbose) { console.log("โœ… Migration validation passed"); } } else { if (this.verbose) { console.log("โŒ Migration validation failed"); } } return isValid; } public async cleanupLegacy(): Promise { if (this.verbose) { console.log("๐Ÿงน Cleaning up legacy collections..."); } const collections = ["active", "completed", "archived"]; let cleanedCount = 0; for (const collectionName of collections) { try { const collection = await loadTaskCollection(collectionName); if (Object.keys(collection.tasks).length === 0) { const storageDir = getStorageDir(); const collectionPath = join(storageDir, "tasks", `${collectionName}.json`); if (this.dryRun) { if (this.verbose) { console.log(` ๐Ÿ” DRY RUN: Would remove empty collection '${collectionName}'`); } } else { try { await unlink(collectionPath); if (this.verbose) { console.log(` ๐Ÿ—‘๏ธ Removed empty collection '${collectionName}'`); } cleanedCount += 1; } catch (e) { const error = e as Error; if (this.verbose) { console.log(` โš ๏ธ Could not remove '${collectionName}': ${error.message}`); } } } } else { if (this.verbose) { console.log( ` โš ๏ธ Collection '${collectionName}' still has ${Object.keys(collection.tasks).length} tasks - not removing` ); } } } catch (_) { if (this.verbose) { console.log(` โ„น๏ธ Collection '${collectionName}' does not exist`); } } } if (this.verbose) { if (cleanedCount > 0) { console.log(`โœ… Cleaned up ${cleanedCount} empty legacy collections`); } else { console.log(`โ„น๏ธ No empty legacy collections to clean up`); } } } } class MigrationCli { private migrator: TaskMigrator; constructor() { this.migrator = TaskMigrator.new(); } public withDryRun(dryRun: boolean): this { this.migrator = this.migrator.dryRun(dryRun); return this; } public withVerbose(verbose: boolean): this { this.migrator = this.migrator.verbose(verbose); return this; } public withForce(force: boolean): this { this.migrator = this.migrator.forceOverwrite(force); return this; } public async run(): Promise { const report = await this.migrator.migrate(); if (!this.migrator.dryRun) { const isValid = this.migrator.validateMigration(); if (isValid && report.errors.length === 0) { await this.migrator.cleanupLegacy(); } } } } // --- Main execution block to demonstrate usage --- async function main() { // Helper to setup dummy data for the migration to process async function setupDummyData() { console.log("Setting up dummy data for migration demo..."); const tasksPath = join(TASKS_DIR, 'active.json'); const dummyTasks: TaskCollection = { name: 'active', tasks: { 'task-1': { id: 'task-1', title: 'Buy groceries', description: 'Milk, bread, eggs', status: TaskStatus.ACTIVE, parentProject: 'home' }, 'task-2': { id: 'task-2', title: 'Write proposal', description: 'Q4 marketing proposal', status: TaskStatus.ACTIVE, parentProject: '' }, // Goes to 'general' 'task-3': { id: 'task-3', title: 'Fix bug', description: 'Fix authentication issue', status: TaskStatus.ACTIVE, parentProject: 'work-project-alpha' }, } }; await writeFile(tasksPath, JSON.stringify(dummyTasks, null, 2)); console.log("Dummy data created."); } // Ensure a clean state for the demo // In a real script, you might use a library like 'rimraf' to remove directories console.log("Ensuring clean state for demo..."); try { await unlink(join(TASKS_DIR, 'active.json')); } catch {} try { await unlink(join(PROJECTS_DIR, 'home.json')); } catch {} try { await unlink(join(PROJECTS_DIR, 'general.json')); } catch {} try { await unlink(join(PROJECTS_DIR, 'work-project-alpha.json')); } catch {} await setupDummyData(); console.log("\n--- Running Migration ---"); const cli = new MigrationCli() .withVerbose(true) .withDryRun(false); // Set to true to see a dry run await cli.run(); console.log("\n--- Running Validation ---"); const migrator = new TaskMigrator().verbose(true); migrator.validate_migration(); console.log("\n--- Running Cleanup ---"); await migrator.cleanup_legacy(); } // Extension for string check (Rust's is_empty) if (!String.prototype.isEmpty) { String.prototype.isEmpty = function() { return this.length === 0; }; } // Uncomment the line below to run the demonstration // main().catch(console.error);