#!/usr/bin/env nu # Workspace Migration Tool # Migrates existing workspaces to new KCL package and module loader structure use ../core/nulib/lib_provisioning/config/accessor.nu * # Main migration command def main [workspace_path: string] { print $"Migrating workspace: ($workspace_path)" # Validate workspace exists if not ($workspace_path | path exists) { error make { msg: $"Workspace not found: ($workspace_path)" } } # Analyze current workspace let analysis = analyze-workspace $workspace_path print $"📊 Workspace Analysis:" print $" Type: ($analysis.type)" print $" KCL files: ($analysis.kcl_files | length)" print $" Has kcl.mod: ($analysis.has_kcl_mod)" print $" Extension references: ($analysis.extension_refs | length)" # Perform migration steps let migration_result = migrate-workspace $workspace_path $analysis print $"✅ Migration completed" print $"📁 New structure created" print $"📄 Configuration files updated" print $"🔄 Import paths converted" if not ($migration_result.warnings | is-empty) { print "" print "⚠️ Warnings:" for $warning in $migration_result.warnings { print $" - ($warning)" } } print "" print "Next steps:" print " 1. Review updated kcl.mod file" print " 2. Load required modules: module-loader load taskservs . [modules...]" print " 3. Test configuration: kcl run servers.k" print " 4. Deploy: provisioning server create --infra . --check" } # Analyze existing workspace structure def analyze-workspace [workspace_path: string]: nothing -> record { let workspace_abs = ($workspace_path | path expand) # Find KCL files let kcl_files = try { glob ($workspace_abs | path join "**/*.k") | where { |path| $path | path exists } } catch { [] } # Check for kcl.mod let kcl_mod_path = ($workspace_abs | path join "kcl.mod") let has_kcl_mod = ($kcl_mod_path | path exists) # Find extension references in KCL files let extension_refs = ($kcl_files | each { |file| let content = try { open $file } catch { "" } # Look for relative imports pointing to extensions let imports = ($content | lines | where { |line| ($line | str contains "import") and ($line | str contains "../") }) $imports | each { |import| { file: $file, import: $import } } } | flatten) # Detect workspace type let workspace_type = if ($extension_refs | where { |ref| $ref.import | str contains "taskservs" } | length) > 0 { "legacy-with-taskservs" } else if ($kcl_files | length) > 0 { "kcl-workspace" } else { "basic" } { path: $workspace_abs, type: $workspace_type, kcl_files: $kcl_files, has_kcl_mod: $has_kcl_mod, extension_refs: $extension_refs, needs_migration: ((not $has_kcl_mod) or (($extension_refs | length) > 0)) } } # Perform workspace migration def migrate-workspace [workspace_path: string, analysis: record]: nothing -> record { let workspace_abs = ($workspace_path | path expand) let warnings = [] # Step 1: Create new directory structure print "🔧 Creating new directory structure..." mkdir ($workspace_abs | path join ".taskservs") mkdir ($workspace_abs | path join ".providers") mkdir ($workspace_abs | path join ".clusters") mkdir ($workspace_abs | path join ".manifest") # Ensure other standard directories exist mkdir ($workspace_abs | path join "data") mkdir ($workspace_abs | path join "tmp") mkdir ($workspace_abs | path join "resources") mkdir ($workspace_abs | path join "clusters") # Step 2: Create or update kcl.mod print "🔧 Creating/updating kcl.mod..." let kcl_mod_content = if $analysis.has_kcl_mod { # Update existing kcl.mod let existing = (open ($workspace_abs | path join "kcl.mod")) update-kcl-mod $existing } else { # Create new kcl.mod create-new-kcl-mod ($workspace_path | path basename) } $kcl_mod_content | save -f ($workspace_abs | path join "kcl.mod") # Step 3: Convert import paths in KCL files print "🔧 Converting import paths..." let conversion_results = ($analysis.kcl_files | each { |file| convert-imports-in-file $file }) # Step 4: Create empty manifest files print "🔧 Creating manifest files..." let empty_manifest = { last_updated: (date now | format date "%Y-%m-%d %H:%M:%S"), workspace: $workspace_abs } ($empty_manifest | merge { loaded_taskservs: [] }) | to yaml | save -f ($workspace_abs | path join ".manifest" | path join "taskservs.yaml") ($empty_manifest | merge { loaded_providers: [] }) | to yaml | save -f ($workspace_abs | path join ".manifest" | path join "providers.yaml") ($empty_manifest | merge { loaded_clusters: [] }) | to yaml | save -f ($workspace_abs | path join ".manifest" | path join "clusters.yaml") # Step 5: Create .gitignore if it doesn't exist let gitignore_path = ($workspace_abs | path join ".gitignore") if not ($gitignore_path | path exists) { print "🔧 Creating .gitignore..." let gitignore_content = "# Workspace runtime data .manifest/ data/ tmp/ *.log # Module directories (managed by module-loader) .taskservs/ .providers/ .clusters/ # Generated files taskservs.k providers.k clusters.k # Secrets and sensitive data *.age *.enc *.key " $gitignore_content | save $gitignore_path } # Step 6: Create migration summary let migration_info = { migrated_at: (date now | format date "%Y-%m-%d %H:%M:%S"), original_type: $analysis.type, files_converted: ($conversion_results | length), extension_refs_converted: ($analysis.extension_refs | length) } $migration_info | to yaml | save ($workspace_abs | path join ".migration-info.yaml") { success: true, workspace: $workspace_abs, files_converted: ($conversion_results | length), warnings: $warnings } } # Update existing kcl.mod to add provisioning dependency def update-kcl-mod [existing_content: string]: nothing -> string { let lines = ($existing_content | lines) # Check if provisioning dependency already exists let has_provisioning_dep = ($lines | any { |line| $line | str contains "provisioning" }) if $has_provisioning_dep { return $existing_content } # Find [dependencies] section or add it let deps_line_idx = ($lines | enumerate | where { |item| $item.item | str contains "[dependencies]" } | get index | first) if ($deps_line_idx | is-empty) { # Add dependencies section $existing_content + "\n\n[dependencies]\nprovisioning = { path = \"~/.kcl/packages/provisioning\", version = \"0.0.1\" }\n" } else { # Insert after dependencies line let before = ($lines | first ($deps_line_idx + 1)) let after = ($lines | skip ($deps_line_idx + 1)) ($before | append ["provisioning = { path = \"~/.kcl/packages/provisioning\", version = \"0.0.1\" }"] | append $after | str join "\n") } } # Create new kcl.mod file def create-new-kcl-mod [workspace_name: string]: nothing -> string { $"[package] name = \"($workspace_name)\" edition = \"v0.11.3\" version = \"0.0.1\" [dependencies] provisioning = { path = \"~/.kcl/packages/provisioning\", version = \"0.0.1\" } " } # Convert import paths in a KCL file def convert-imports-in-file [file_path: string]: nothing -> record { let content = try { open $file_path } catch { return { file: $file_path, status: "error", message: "Could not read file" } } # Define import conversion patterns let conversions = [ # Convert relative imports to package imports { pattern: "import ../../../kcl/", replacement: "import provisioning." } { pattern: "import ../../kcl/", replacement: "import provisioning." } { pattern: "import ../kcl/", replacement: "import provisioning." } # Convert extension imports (these will need to be loaded via module-loader) { pattern: "import ../../../extensions/taskservs/", replacement: "# import .taskservs." } { pattern: "import ../../extensions/taskservs/", replacement: "# import .taskservs." } { pattern: "import ../extensions/taskservs/", replacement: "# import .taskservs." } { pattern: "import ../../../extensions/providers/", replacement: "# import .providers." } { pattern: "import ../../extensions/providers/", replacement: "# import .providers." } { pattern: "import ../extensions/providers/", replacement: "# import .providers." } { pattern: "import ../../../extensions/clusters/", replacement: "# import .clusters." } { pattern: "import ../../extensions/clusters/", replacement: "# import .clusters." } { pattern: "import ../extensions/clusters/", replacement: "# import .clusters." } ] # Apply conversions let updated_content = ($conversions | reduce -f $content { |acc, conv| $acc | str replace -a $conv.pattern $conv.replacement }) # Check if any changes were made if $content != $updated_content { # Backup original file let backup_path = ($file_path + ".bak") $content | save $backup_path # Save updated content $updated_content | save -f $file_path { file: $file_path, status: "converted", backup: $backup_path, changes: true } } else { { file: $file_path, status: "no-changes", changes: false } } } # Dry run migration (analyze what would be changed) export def "main dry-run" [workspace_path: string]: nothing -> record { print $"Analyzing workspace for migration: ($workspace_path)" let analysis = analyze-workspace $workspace_path print $"📊 Migration Analysis Report:" print $" Workspace: ($analysis.path)" print $" Type: ($analysis.type)" print $" Needs migration: ($analysis.needs_migration)" print "" print $" KCL files found: ($analysis.kcl_files | length)" for $file in $analysis.kcl_files { print $" - ($file | str replace ($analysis.path + '/') '')" } print "" print $" Extension references: ($analysis.extension_refs | length)" for $ref in $analysis.extension_refs { let file_rel = ($ref.file | str replace ($analysis.path + '/') '') print $" - ($file_rel): ($ref.import | str trim)" } if $analysis.needs_migration { print "" print "🔧 Migration steps that would be performed:" print " 1. Create .taskservs/, .providers/, .clusters/, .manifest/ directories" print " 2. Create/update kcl.mod with provisioning dependency" print " 3. Convert import paths from relative to package-based" print " 4. Comment out extension imports (load via module-loader)" print " 5. Create manifest files for module tracking" print " 6. Create .gitignore if missing" } else { print "" print "✅ Workspace appears to already be migrated or needs no migration" } $analysis } # Rollback migration (restore from backups) export def "main rollback" [workspace_path: string]: nothing -> nothing { print $"Rolling back migration for: ($workspace_path)" let workspace_abs = ($workspace_path | path expand) # Find backup files let backup_files = try { glob ($workspace_abs | path join "**/*.bak") } catch { [] } if ($backup_files | is-empty) { print "⚠️ No backup files found. Cannot rollback." return } print $"Found ($backup_files | length) backup files" # Restore each backup for $backup in $backup_files { let original = ($backup | str replace ".bak" "") print $"Restoring: ($original | str replace ($workspace_abs + '/') '')" # Copy backup back to original cp $backup $original # Remove backup rm $backup } # Remove migration artifacts let migration_artifacts = [ ".taskservs" ".providers" ".clusters" ".manifest" ".migration-info.yaml" ] for $artifact in $migration_artifacts { let artifact_path = ($workspace_abs | path join $artifact) if ($artifact_path | path exists) { rm -rf $artifact_path } } print "✅ Migration rolled back successfully" print " Original files restored from backups" print " Migration artifacts removed" } # Show migration status export def "main status" [workspace_path: string]: nothing -> record { let workspace_abs = ($workspace_path | path expand) let has_new_structure = [".taskservs", ".providers", ".clusters", ".manifest"] | all { |dir| ($workspace_abs | path join $dir) | path exists } let migration_info_path = ($workspace_abs | path join ".migration-info.yaml") let migration_info = if ($migration_info_path | path exists) { open $migration_info_path } else { null } let backup_files = try { glob ($workspace_abs | path join "**/*.bak") | length } catch { 0 } { workspace: $workspace_abs, has_new_structure: $has_new_structure, migration_info: $migration_info, backup_files: $backup_files, is_migrated: ($has_new_structure and ($migration_info != null)) } }