#!/usr/bin/env nu # Core bundle tool - bundles Nushell core libraries and CLI for distribution # # Bundles: # - Nushell provisioning CLI wrapper # - Core Nushell libraries (lib_provisioning) # - Configuration system # - Template system # - Extensions and plugins use std log def main [ --output-dir: string = "dist/core" # Output directory for core bundle --config-dir: string = "dist/config" # Configuration directory --validate: bool = false # Validate Nushell syntax --compress: bool = false # Compress bundle with gzip --exclude-dev: bool = true # Exclude development files --verbose: bool = false # Enable verbose logging ] -> record { let repo_root = ($env.PWD | path dirname | path dirname | path dirname) let bundle_config = { output_dir: ($output_dir | path expand) config_dir: ($config_dir | path expand) validate: $validate compress: $compress exclude_dev: $exclude_dev verbose: $verbose } log info $"Starting core bundle creation with config: ($bundle_config)" # Ensure output directories exist mkdir ($bundle_config.output_dir) mkdir ($bundle_config.config_dir) # Define core components to bundle let core_components = [ { name: "provisioning-cli" source: ($repo_root | path join "provisioning" "core" "nulib" "provisioning") target: ($bundle_config.output_dir | path join "bin" "provisioning") type: "executable" }, { name: "core-libraries" source: ($repo_root | path join "provisioning" "core" "nulib" "lib_provisioning") target: ($bundle_config.output_dir | path join "lib" "lib_provisioning") type: "directory" }, { name: "workflows" source: ($repo_root | path join "provisioning" "core" "nulib" "workflows") target: ($bundle_config.output_dir | path join "lib" "workflows") type: "directory" }, { name: "servers" source: ($repo_root | path join "provisioning" "core" "nulib" "servers") target: ($bundle_config.output_dir | path join "lib" "servers") type: "directory" }, { name: "extensions" source: ($repo_root | path join "provisioning" "extensions") target: ($bundle_config.output_dir | path join "extensions") type: "directory" } ] # Define configuration files let config_files = [ { name: "default-config" source: ($repo_root | path join "provisioning" "config.defaults.toml") target: ($bundle_config.config_dir | path join "config.defaults.toml") }, { name: "config-examples" source: ($repo_root | path join "provisioning" "config") target: ($bundle_config.config_dir | path join "examples") } ] let results = [] # Bundle core components let component_results = $core_components | each {|component| bundle_component $component $bundle_config $repo_root } let results = ($results | append $component_results) # Bundle configuration files let config_results = $config_files | each {|config| bundle_config_file $config $bundle_config } let results = ($results | append $config_results) # Validate Nushell syntax if requested let validation_result = if $bundle_config.validate { validate_nushell_syntax ($bundle_config.output_dir) } else { { status: "skipped", validated_files: 0, errors: [] } } # Create bundle metadata create_bundle_metadata $bundle_config $repo_root $results # Compress if requested let compression_result = if $bundle_config.compress { compress_bundle ($bundle_config.output_dir) } else { { status: "skipped" } } let summary = { total_components: ($results | length) successful: ($results | where status == "success" | length) failed: ($results | where status == "failed" | length) validation: $validation_result compression: $compression_result bundle_config: $bundle_config results: $results } if $summary.failed > 0 { log error $"Core bundle creation completed with ($summary.failed) failures" exit 1 } else { log info $"Core bundle creation completed successfully" } return $summary } # Bundle a single core component def bundle_component [ component: record bundle_config: record repo_root: string ] -> record { log info $"Bundling ($component.name)..." if not ($component.source | path exists) { log warning $"Source path does not exist: ($component.source)" return { component: $component.name status: "skipped" reason: "source not found" target: $component.target } } try { # Ensure target directory exists let target_parent = ($component.target | path dirname) mkdir $target_parent if $component.type == "executable" { # Copy executable file and make it executable cp $component.source $component.target chmod +x $component.target } else if $component.type == "directory" { # Copy directory recursively, excluding development files if requested if $bundle_config.exclude_dev { copy_directory_filtered $component.source $component.target } else { cp -r $component.source $component.target } } log info $"Successfully bundled ($component.name) -> ($component.target)" { component: $component.name status: "success" source: $component.source target: $component.target size: (get_directory_size $component.target) } } catch {|err| log error $"Failed to bundle ($component.name): ($err.msg)" { component: $component.name status: "failed" reason: $err.msg target: $component.target } } } # Bundle a configuration file def bundle_config_file [ config: record bundle_config: record ] -> record { log info $"Bundling config ($config.name)..." if not ($config.source | path exists) { log warning $"Config source does not exist: ($config.source)" return { component: $config.name status: "skipped" reason: "source not found" target: $config.target } } try { # Ensure target directory exists let target_parent = ($config.target | path dirname) mkdir $target_parent # Copy config file or directory cp -r $config.source $config.target log info $"Successfully bundled config ($config.name) -> ($config.target)" { component: $config.name status: "success" source: $config.source target: $config.target } } catch {|err| log error $"Failed to bundle config ($config.name): ($err.msg)" { component: $config.name status: "failed" reason: $err.msg target: $config.target } } } # Copy directory with filtering for development files def copy_directory_filtered [source: string, target: string] { let exclude_patterns = [ "*.tmp" "*.bak" "*~" "*.swp" ".git*" "test_*" "*_test.nu" "dev_*" "debug_*" ] # Create target directory mkdir $target # Get all files recursively, excluding patterns let files = (find $source -type f | where {|file| let exclude = $exclude_patterns | any {|pattern| $file =~ $pattern } not $exclude }) # Copy each file, preserving directory structure $files | each {|file| let relative_path = ($file | str replace $source "" | str trim-left "/") let target_path = ($target | path join $relative_path) let target_dir = ($target_path | path dirname) mkdir $target_dir cp $file $target_path } } # Validate Nushell syntax in bundled files def validate_nushell_syntax [bundle_dir: string] -> record { log info "Validating Nushell syntax..." let nu_files = (find $bundle_dir -name "*.nu" -type f) let mut validation_errors = [] let mut validated_count = 0 for file in $nu_files { try { # Use nu --check to validate syntax nu --check $file $validated_count = $validated_count + 1 } catch {|err| $validation_errors = ($validation_errors | append { file: $file error: $err.msg }) log error $"Syntax error in ($file): ($err.msg)" } } if ($validation_errors | length) > 0 { log error $"Found ($validation_errors | length) syntax errors" { status: "failed" validated_files: $validated_count total_files: ($nu_files | length) errors: $validation_errors } } else { log info $"All ($validated_count) Nushell files passed syntax validation" { status: "success" validated_files: $validated_count total_files: ($nu_files | length) errors: [] } } } # Create bundle metadata def create_bundle_metadata [bundle_config: record, repo_root: string, results: list] { let metadata = { bundle_version: "2.1.0" created_at: (date now | format date "%Y-%m-%d %H:%M:%S") created_by: "provisioning-build-system" source_commit: (cd $repo_root; git rev-parse HEAD) source_branch: (cd $repo_root; git branch --show-current) bundle_config: $bundle_config components: $results total_size: (get_directory_size $bundle_config.output_dir) } let metadata_file = ($bundle_config.output_dir | path join "bundle-metadata.json") $metadata | to json | save $metadata_file log info $"Created bundle metadata: ($metadata_file)" } # Compress bundle directory def compress_bundle [bundle_dir: string] -> record { log info "Compressing bundle..." try { let bundle_name = ($bundle_dir | path basename) let parent_dir = ($bundle_dir | path dirname) let archive_name = $"($bundle_name).tar.gz" let archive_path = ($parent_dir | path join $archive_name) cd $parent_dir tar -czf $archive_name $bundle_name let original_size = (get_directory_size $bundle_dir) let compressed_size = (ls $archive_path | get 0.size) let compression_ratio = (($compressed_size | into float) / ($original_size | into float) * 100) log info $"Bundle compressed: ($original_size) -> ($compressed_size) (($compression_ratio | math round)% of original)" { status: "success" archive_path: $archive_path original_size: $original_size compressed_size: $compressed_size compression_ratio: $compression_ratio } } catch {|err| log error $"Failed to compress bundle: ($err.msg)" { status: "failed" reason: $err.msg } } } # Get directory size recursively def get_directory_size [dir: string] -> int { if not ($dir | path exists) { return 0 } if ($dir | path type) == "file" { return (ls $dir | get 0.size) } let total_size = (find $dir -type f | each {|file| ls $file | get 0.size } | math sum) return ($total_size | if $in == null { 0 } else { $in }) } # Show bundle info def "main info" [bundle_dir: string = "dist/core"] { let bundle_dir = ($bundle_dir | path expand) if not ($bundle_dir | path exists) { log error $"Bundle directory does not exist: ($bundle_dir)" exit 1 } let metadata_file = ($bundle_dir | path join "bundle-metadata.json") if ($metadata_file | path exists) { open $metadata_file } else { { directory: $bundle_dir size: (get_directory_size $bundle_dir) files: (find $bundle_dir -type f | length) nu_files: (find $bundle_dir -name "*.nu" -type f | length) } } }