# Workspace Migration Framework # Handles workspace migrations between versions with backups and rollback use std log use platform/user/config.nu [get-workspace-path validate-workspace-exists] use domain/workspace/version.nu [ add-migration-record get-system-version get-workspace-metadata-path init-workspace-metadata load-workspace-metadata ] export def get-migration-strategies [] { { "unknown_to_2.0.5": { name: "Initialize metadata" description: "Add metadata tracking to existing workspace" from: "unknown" to: "2.0.5" breaking: false auto_migrate: true } "2.0.0_to_2.0.5": { name: "Workspace switching update" description: "Add workspace switching support and metadata" from: "2.0.0" to: "2.0.5" breaking: false auto_migrate: true } "2.0.5_to_3.0.0": { name: "Major version update" description: "Migrate to v3.0.0 with breaking changes" from: "2.0.5" to: "3.0.0" breaking: true auto_migrate: false } } } export def find-migration-path [ from_version: string to_version: string ] { let strategies = (get-migration-strategies) mut path = [] if $from_version == "unknown" { $path = ($path | append { strategy: "unknown_to_2.0.5" details: $strategies.unknown_to_2.0.5 }) return $path } let strategy_key = $"($from_version)_to_($to_version)" if ($strategy_key in $strategies) { $path = ($path | append { strategy: $strategy_key details: ($strategies | get $strategy_key) }) return $path } return $path } export def create-workspace-backup [ workspace_path: string backup_reason: string ] { let workspace_name = ($workspace_path | path basename) let timestamp = (date now | format date "%Y%m%d_%H%M%S") let backup_name = $"($workspace_name)_backup_($timestamp)" let backup_dir = ($workspace_path | path dirname | path join ".workspace_backups") if not ($backup_dir | path exists) { mkdir $backup_dir } let backup_path = ($backup_dir | path join $backup_name) print $"(ansi cyan)Creating backup...(ansi reset)" let backup_result = (do { cp -r $workspace_path $backup_path let backup_metadata = { original_path: $workspace_path backup_path: $backup_path timestamp: $timestamp reason: $backup_reason created: (date now | format date "%Y-%m-%dT%H:%M:%SZ") } $backup_metadata | to yaml | save -f ($backup_path | path join ".backup_info.yaml") {success: true, backup_path: $backup_path, metadata: $backup_metadata} } | complete) if $backup_result.exit_code == 0 { print $"(ansi green)✓(ansi reset) Backup created: [$backup_path]" $backup_result.stdout } else { print $"(ansi red)✗(ansi reset) Backup failed: ($backup_result.stderr)" { success: false, error: $backup_result.stderr } } } export def migrate-unknown-to-2_0_5 [ workspace_path: string workspace_name: string ] { print $"(ansi cyan)Migrating workspace to version 2.0.5...(ansi reset)" let result = (do { let metadata = (init-workspace-metadata $workspace_path $workspace_name) add-migration-record $workspace_path "unknown" "2.0.5" "metadata_initialization" true "Initial metadata creation" { success: true, message: "Workspace migrated to version 2.0.5", metadata: $metadata } } | complete) if $result.exit_code == 0 { $result.stdout } else { { success: false, error: $result.stderr, message: "Migration failed" } } } export def migrate-2_0_0-to-2_0_5 [ workspace_path: string workspace_name: string ] { print $"(ansi cyan)Migrating workspace from 2.0.0 to 2.0.5...(ansi reset)" let result = (do { let metadata_path = (get-workspace-metadata-path $workspace_path) if ($metadata_path | path exists) { let metadata = (load-workspace-metadata $workspace_path) add-migration-record $workspace_path "2.0.0" "2.0.5" "version_update" true "Updated to workspace switching support" } else { let metadata = (init-workspace-metadata $workspace_path $workspace_name) add-migration-record $workspace_path "2.0.0" "2.0.5" "metadata_creation" true "Added metadata tracking" } { success: true, message: "Workspace migrated to version 2.0.5" } } | complete) if $result.exit_code == 0 { $result.stdout } else { { success: false, error: $result.stderr, message: "Migration failed" } } } export def execute-migration [ workspace_path: string workspace_name: string strategy: record ] { print "" print $"(ansi green_bold)Migration Strategy:(ansi reset) ($strategy.details.name)" print $"(ansi cyan)Description:(ansi reset) ($strategy.details.description)" print $"(ansi cyan)From:(ansi reset) ($strategy.details.from) → (ansi cyan)To:(ansi reset) ($strategy.details.to)" print "" match $strategy.strategy { "unknown_to_2.0.5" => { migrate-unknown-to-2_0_5 $workspace_path $workspace_name } "2.0.0_to_2.0.5" => { migrate-2_0_0-to-2_0_5 $workspace_path $workspace_name } _ => { return { success: false, error: $"Unknown migration strategy: ($strategy.strategy)" } } } } export def migrate-workspace [ workspace_name: string --skip-backup (-s) --force (-f) --target-version: string ] { print "" print $"(ansi green_bold)Workspace Migration(ansi reset)" print "" if not (validate-workspace-exists $workspace_name) { return { success: false, error: $"Workspace '($workspace_name)' not found in registry" } } let workspace_path = (get-workspace-path $workspace_name) if not ($workspace_path | path exists) { return { success: false, error: $"Workspace directory not found: ($workspace_path)" } } print $"(ansi cyan)Workspace:(ansi reset) ($workspace_name)" print $"(ansi cyan)Path:(ansi reset) ($workspace_path)" print "" let metadata = (load-workspace-metadata $workspace_path) let current_version = $metadata.version.provisioning let system_version = (get-system-version) let target = ($target_version | default $system_version) print $"(ansi cyan)Current version:(ansi reset) ($current_version)" print $"(ansi cyan)Target version:(ansi reset) ($target)" print "" let migration_path = (find-migration-path $current_version $target) if ($migration_path | is-empty) { if $current_version == $target { return { success: true, message: "Workspace is already at target version", no_migration_needed: true } } else { return { success: false, error: $"No migration path found from ($current_version) to ($target)" } } } let has_breaking = ($migration_path | any { |m| $m.details.breaking }) if $has_breaking { print $"(ansi yellow_bold)⚠ Warning: This migration includes breaking changes(ansi reset)" print "" } if not $force { print $"(ansi yellow)This will migrate the workspace from ($current_version) to ($target)(ansi reset)" if not $skip_backup { print $"(ansi cyan)A backup will be created before migration.(ansi reset)" } print "" let response = (input "Continue with migration? (y/N): ") if $response != "y" and $response != "Y" { print $"(ansi yellow)Migration cancelled(ansi reset)" return { success: false, cancelled: true } } print "" } mut backup_result = {success: false} if not $skip_backup { $backup_result = (create-workspace-backup $workspace_path "pre_migration") if not $backup_result.success { print $"(ansi red)✗(ansi reset) Cannot proceed without backup" return { success: false, error: "Backup creation failed", backup_error: $backup_result.error } } print "" } mut migration_results = [] for migration in $migration_path { let result = (execute-migration $workspace_path $workspace_name $migration) $migration_results = ($migration_results | append $result) if not $result.success { print "" print $"(ansi red_bold)✗ Migration failed(ansi reset)" print $"(ansi red)Error:(ansi reset) ($result.error)" print "" if ($backup_result != null) { print $"(ansi cyan)Backup available at:(ansi reset) ($backup_result.backup_path)" } print "" return { success: false, error: $result.error, backup: $backup_result, partial_results: $migration_results } } print $"(ansi green)✓(ansi reset) ($migration.details.name) completed" } print "" print $"(ansi green_bold)✓ Migration completed successfully(ansi reset)" print "" return { success: true, message: $"Workspace migrated from ($current_version) to ($target)", backup: $backup_result, migrations: $migration_results } } export def list-workspace-backups [ workspace_name?: string ] { let workspace_path = if ($workspace_name | is-not-empty) { get-workspace-path $workspace_name } else { null } let backup_base_dir = if ($workspace_path | is-not-empty) { $workspace_path | path dirname | path join ".workspace_backups" } else { return [] } if not ($backup_base_dir | path exists) { return [] } ls $backup_base_dir | where ($it.type == "dir") | each { |backup| let info_file = ($backup.name | path join ".backup_info.yaml") if ($info_file | path exists) { let info = (open $info_file) { name: ($backup.name | path basename), path: $backup.name, created: $info.created, reason: $info.reason, size: $backup.size } } else { { name: ($backup.name | path basename), path: $backup.name, created: "unknown", reason: "unknown", size: $backup.size } } } } export def restore-workspace-from-backup [ backup_path: string --force (-f) ] { if not ($backup_path | path exists) { return { success: false, error: $"Backup not found: ($backup_path)" } } let info_file = ($backup_path | path join ".backup_info.yaml") if not ($info_file | path exists) { return { success: false, error: "Backup metadata not found - cannot restore safely" } } let info = (open $info_file) let original_path = $info.original_path print "" print $"(ansi green_bold)Restore Workspace from Backup(ansi reset)" print "" print $"(ansi cyan)Backup:(ansi reset) ($backup_path)" print $"(ansi cyan)Original path:(ansi reset) ($original_path)" print $"(ansi cyan)Created:(ansi reset) ($info.created)" print $"(ansi cyan)Reason:(ansi reset) ($info.reason)" print "" if not $force { print $"(ansi yellow)⚠ This will replace the current workspace at:(ansi reset)" print $" ($original_path)" print "" let response = (input "Continue with restore? (y/N): ") if $response != "y" and $response != "Y" { print $"(ansi yellow)Restore cancelled(ansi reset)" return { success: false, cancelled: true } } print "" } let result = (do { if ($original_path | path exists) { rm -rf $original_path } cp -r $backup_path $original_path let restored_info = ($original_path | path join ".backup_info.yaml") if ($restored_info | path exists) { rm $restored_info } { success: true, restored_path: $original_path } } | complete) if $result.exit_code == 0 { print $"(ansi green)✓(ansi reset) Workspace restored from backup" print "" $result.stdout } else { print $"(ansi red)✗(ansi reset) Restore failed: ($result.stderr)" { success: false, error: $result.stderr } } }