# KCL Packaging Library # Functions for packaging and distributing KCL modules # Author: JesusPerezLorenzo # Date: 2025-09-29 use config/accessor.nu * use utils * # Package core provisioning KCL schemas export def "pack-core" [ --output: string = "" # Output directory (from config if not specified) --version: string = "" # Version override ] { _print "📦 Packaging core provisioning schemas..." # Get config let dist_config = (get-distribution-config) let kcl_config = (get-kcl-config) # Get pack path from config or use provided output let pack_path = if ($output | is-empty) { $dist_config.pack_path } else { $output } # Get core module path from config let base_path = ($env.PROVISIONING_CONFIG? | default ($env.PROVISIONING? | default "")) if ($base_path | is-empty) { error make {msg: "PROVISIONING_CONFIG or PROVISIONING environment variable must be set"} } let core_module = ($kcl_config.core_module | str replace --all "{{paths.base}}" $base_path) let core_path = $core_module # Get version from config or use provided let core_version = if ($version | is-empty) { $kcl_config.core_version } else { $version } # Ensure pack_path exists and get absolute path mkdir $pack_path let abs_pack_path = ($pack_path | path expand) # Change to the KCL module directory to run packaging from inside cd $core_path # Check if kcl mod pkg is supported let help_result = (^kcl mod --help | complete) let has_pkg = ($help_result.stdout | str contains "pkg") if not $has_pkg { _print $" ⚠️ KCL does not support 'kcl mod pkg'" _print $" 💡 Please upgrade to KCL 0.11.3+ for packaging support" error make {msg: "KCL packaging not supported in this version"} } # Run kcl mod pkg from inside the module directory with --target _print $" Running: kcl mod pkg --target ($abs_pack_path)" let result = (^kcl mod pkg --target $abs_pack_path | complete) if $result.exit_code != 0 { error make {msg: $"Failed to package core: ($result.stderr)"} } _print $" ✓ KCL packaging completed" # Find the generated package in the target directory (kcl creates .tar files) cd $abs_pack_path let package_files = (glob *.tar) if ($package_files | is-empty) { _print $" ⚠️ No .tar file created in ($abs_pack_path)" _print $" 💡 Check if kcl.mod is properly configured" error make {msg: "KCL packaging did not create output file"} } let package_file = ($package_files | first) _print $" ✓ Package: ($package_file)" # Generate metadata generate-package-metadata $pack_path "provisioning_core" $core_version $package_file return $package_file } # Package a provider module export def "pack-provider" [ provider: string, # Provider name --output: string = "" # Output directory --version: string = "" # Version override ] { _print $"📦 Packaging provider: ($provider)..." # Get config let dist_config = (get-distribution-config) let pack_path = if ($output | is-empty) { $dist_config.pack_path } else { $output } # Get provider path from config let config = (get-config) let providers_base = ($config | get paths.providers) let provider_path = ($providers_base | path join $provider "kcl") if not ($provider_path | path exists) { error make {msg: $"Provider not found: ($provider) at ($provider_path)"} } # Ensure pack_path exists and get absolute path mkdir $pack_path let abs_pack_path = ($pack_path | path expand) # Change to the provider KCL directory to run packaging from inside cd $provider_path # Run kcl mod pkg with target directory _print $" Running: kcl mod pkg --target ($abs_pack_path)" let result = (^kcl mod pkg --target $abs_pack_path | complete) if $result.exit_code != 0 { error make {msg: $"Failed to package provider: ($result.stderr)"} } _print $" ✓ Provider ($provider) packaged" # Find the generated package in the target directory cd $abs_pack_path let package_files = (glob *.tar) if ($package_files | is-empty) { error make {msg: "No package file found"} } let package_file = ($package_files | first) _print $" ✓ Package: ($package_file)" # Read version from kcl.mod if not provided let pkg_version = if ($version | is-empty) { let kcl_mod = ($provider_path | path join "kcl.mod") if ($kcl_mod | path exists) { parse-kcl-version $kcl_mod } else { "0.0.1" } } else { $version } # Generate metadata generate-package-metadata $pack_path $"($provider)_prov" $pkg_version $package_file return $package_file } # Package all discovered providers export def "pack-all-providers" [ --output: string = "" # Output directory ] { use kcl_module_loader.nu * let dist_config = (get-distribution-config) let pack_path = if ($output | is-empty) { $dist_config.pack_path } else { $output } _print "📦 Packaging all providers..." let providers = (discover-kcl-modules "providers") mut packaged = [] for provider in $providers { # Use error handling without complete since pack-provider is internal let pack_result = (do -i { pack-provider $provider.name --output $pack_path }) if ($pack_result | is-not-empty) and not ($pack_result | describe | str contains "error") { $packaged = ($packaged | append {name: $provider.name, path: $pack_result, status: "success"}) } else { _print $" ❌ Failed to package ($provider.name)" $packaged = ($packaged | append {name: $provider.name, path: "", status: "failed"}) } } _print "" _print $"✅ Packaged ($packaged | where status == success | length)/($packaged | length) providers" return $packaged } # Generate package metadata def "generate-package-metadata" [ pack_path: string, module_name: string, version: string, package_file: string ] { let dist_config = (get-distribution-config) let registry_path = $dist_config.registry_path mkdir $registry_path let metadata_file = ($registry_path | path join $"($module_name).json") let dist_metadata = ($dist_config | get metadata) let metadata = { name: $module_name version: $version package_file: $package_file created: (date now | format date "%Y-%m-%d %H:%M:%S") maintainer: $dist_metadata.maintainer repository: $dist_metadata.repository license: $dist_metadata.license homepage: $dist_metadata.homepage } $metadata | to json | save -f $metadata_file _print $" ✓ Metadata: ($metadata_file)" } # Parse version from kcl.mod def "parse-kcl-version" [ kcl_mod_path: string ]: nothing -> string { let content = (open $kcl_mod_path) let lines = ($content | lines) for line in $lines { if ($line | str starts-with "version") { let version = ($line | parse 'version = "{version}"' | get version.0? | default "0.0.1") return $version } } return "0.0.1" } # List packaged modules export def "list-packages" [ --format: string = "table" # Output format ] { let dist_config = (get-distribution-config) let pack_path = $dist_config.pack_path if not ($pack_path | path exists) { _print "No packages found" return [] } # Look for .tar files cd $pack_path let tar_files = (glob *.tar) let packages = $tar_files | each {|file| let stats = (ls $file | first) { name: ($file | path basename | str replace ".tar" "") path: $file size: $stats.size modified: $stats.modified } } match $format { "json" => ($packages | to json) "yaml" => ($packages | to yaml) _ => ($packages | table) } } # Clean old packages export def "clean-packages" [ --keep-latest: int = 3 # Number of latest versions to keep --dry-run # Show what would be deleted ] { let dist_config = (get-distribution-config) let pack_path = $dist_config.pack_path if not ($pack_path | path exists) { _print "No packages directory found" return } _print $"🗑️ Cleaning packages \(keeping latest ($keep_latest)\)..." cd $pack_path let tar_files = (glob *.tar) if ($tar_files | is-empty) { _print " No packages to clean" return } let packages = ($tar_files | each {|file| let stats = (ls $file | first) { name: $file modified: $stats.modified } } | sort-by modified --reverse) let to_delete = ($packages | skip $keep_latest) if ($to_delete | is-empty) { _print " Nothing to clean" return } # Get registry path for metadata cleanup let dist_config = (get-distribution-config) let registry_path = $dist_config.registry_path for pkg in $to_delete { let pkg_basename = ($pkg.name | path basename) # Extract package name without version (e.g., "aws_prov_0.0.1.tar" -> "aws_prov") let pkg_name = ($pkg_basename | str replace -r '_\d+\.\d+\.\d+\.tar$' '') let metadata_file = ($registry_path | path join $"($pkg_name).json") if $dry_run { _print $" [DRY RUN] Would delete package: ($pkg_basename)" if ($metadata_file | path exists) { _print $" [DRY RUN] Would delete metadata: ($metadata_file | path basename)" } } else { rm $pkg.name _print $" ✓ Deleted package: ($pkg_basename)" # Also delete corresponding metadata if it exists if ($metadata_file | path exists) { rm $metadata_file _print $" ✓ Deleted metadata: ($metadata_file | path basename)" } } } let deleted_count = ($to_delete | length) _print $"✅ Cleaned ($deleted_count) old packages" } # Remove specific package and its metadata export def "remove-package" [ package_name: string, # Package name (e.g., "aws_prov", "upcloud_prov", "provisioning_core") --force = false # Skip confirmation prompt ] { _print $"🗑️ Removing package: ($package_name)" let dist_config = (get-distribution-config) let pack_path = $dist_config.pack_path let registry_path = $dist_config.registry_path # Find all versions of this package cd $pack_path let package_pattern = $"($package_name)*.tar" let matching_packages = (glob $package_pattern) if ($matching_packages | is-empty) { _print $" ❌ No packages found matching: ($package_name)" return } # Show what will be deleted _print "" _print " Packages to remove:" for pkg in $matching_packages { _print $" • ($pkg | path basename)" } # Check for metadata let metadata_file = ($registry_path | path join $"($package_name).json") if ($metadata_file | path exists) { _print $" • ($metadata_file | path basename) (metadata)" } # Confirmation unless forced if not $force { _print "" let response = (input $"⚠️ Remove ($matching_packages | length) package\(s\) and metadata? \(y/N\): ") if ($response | str downcase) != "y" { _print "❌ Cancelled" return } } # Delete packages _print "" for pkg in $matching_packages { rm $pkg _print $" ✓ Deleted: ($pkg | path basename)" } # Delete metadata if ($metadata_file | path exists) { rm $metadata_file _print $" ✓ Deleted: ($metadata_file | path basename)" } _print "" _print $"✅ Removed package: ($package_name)" } # Clean ALL packages and metadata export def "clean-all-packages" [ --dry-run = false, # Show what would be deleted --force = false # Skip confirmation prompt ] { _print "🗑️ Cleaning ALL packages..." let dist_config = (get-distribution-config) let pack_path = $dist_config.pack_path let registry_path = $dist_config.registry_path if not ($pack_path | path exists) { _print " No packages directory found" return } # Get all packages cd $pack_path let all_packages = (glob *.tar) if ($all_packages | is-empty) { _print " No packages to clean" return } # Get all metadata files cd $registry_path let all_metadata = (glob *.json) # Show what will be deleted _print "" _print $" Found ($all_packages | length) package\(s\) and ($all_metadata | length) metadata file\(s\)" if not $dry_run and not $force { _print "" let response = (input $"⚠️ Delete ALL packages and metadata? \(y/N\): ") if ($response | str downcase) != "y" { _print "❌ Cancelled" return } } # Delete packages _print "" for pkg in $all_packages { if $dry_run { _print $" [DRY RUN] Would delete: ($pkg | path basename)" } else { cd $pack_path rm $pkg _print $" ✓ Deleted: ($pkg | path basename)" } } # Delete metadata for meta in $all_metadata { if $dry_run { _print $" [DRY RUN] Would delete: ($meta | path basename)" } else { cd $registry_path rm $meta _print $" ✓ Deleted: ($meta | path basename)" } } _print "" if $dry_run { _print $"[DRY RUN] Would clean ($all_packages | length) packages and ($all_metadata | length) metadata files" } else { _print $"✅ Cleaned ($all_packages | length) packages and ($all_metadata | length) metadata files" } }