#!/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 # Validate Nushell syntax --compress # Compress bundle with gzip --exclude-dev # Exclude development files --verbose # Enable verbose logging ] { 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 ] { 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 } } let result = (do { # 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)" } | complete) if $result.exit_code != 0 { log error $"Failed to bundle ($component.name): ($result.stderr)" { component: $component.name status: "failed" reason: $result.stderr target: $component.target } } else { { component: $component.name status: "success" source: $component.source target: $component.target size: (get_directory_size $component.target) } } } # Bundle a configuration file def bundle_config_file [ config: record bundle_config: 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 } } let result = (do { # 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)" } | complete) if $result.exit_code != 0 { log error $"Failed to bundle config ($config.name): ($result.stderr)" { component: $config.name status: "failed" reason: $result.stderr target: $config.target } } else { { component: $config.name status: "success" source: $config.source 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 = (glob ($source + "/**") | where {|file| ($file | path type) == "file" and ( not ($exclude_patterns | any {|pattern| $file =~ $pattern }) ) }) # Copy each file, preserving directory structure $files | each {|file| let relative_path = ($file | str replace $source "" | str trim --left --char "/") 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] { log info "Validating Nushell syntax..." let nu_files = (glob ($bundle_dir + "/**/*.nu") | where {|file| ($file | path type) == "file"}) let validation_results = ($nu_files | each {|file| let result = (do { nu --check $file } | complete) if $result.exit_code != 0 { log error $"Syntax error in ($file): ($result.stderr)" { file: $file error: $result.stderr status: "error" } } else { { file: $file status: "success" } } }) let validation_errors = ($validation_results | where status == "error") let validated_count = ($validation_results | where status == "success" | length) 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] { log info "Compressing bundle..." 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) let result = (do { cd $parent_dir tar -czf $archive_name $bundle_name } | complete) if $result.exit_code != 0 { log error $"Failed to compress bundle: ($result.stderr)" { status: "failed" reason: $result.stderr } } else { 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) let ratio_percent = ($compression_ratio | math round) let msg = $"Bundle compressed: ($original_size) -> ($compressed_size) ($ratio_percent)% of original" log info $msg { status: "success" archive_path: $archive_path original_size: $original_size compressed_size: $compressed_size compression_ratio: $compression_ratio } } } # Get directory size recursively def get_directory_size [dir: string] { if not ($dir | path exists) { return 0 } if ($dir | path type) == "file" { return (ls $dir | get 0.size) } let total_size = (glob ($dir + "/**") | where {|file| ($file | path type) == "file"} | 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: (glob ($bundle_dir + "/**") | where {|file| ($file | path type) == "file"} | length) nu_files: (glob ($bundle_dir + "/**/*.nu") | where {|file| ($file | path type) == "file"} | length) } } }