#!/usr/bin/env nu # Verify Installation Script # Comprehensive installation verification for Nushell full distribution # Tests nu binary, all plugins, configuration, and generates detailed report use lib/common_lib.nu [ log_info, log_error, log_success, log_warn, log_debug, get_current_platform, check_binary ] def main [ --platform (-p): string = "" # Target platform for verification --install-dir (-i): string = "" # Installation directory to check --config-dir (-c): string = "" # Configuration directory to check --plugins (-P): list = [] # Specific plugins to verify --all-plugins (-a) # Verify all found plugins --report (-r): string = "" # Generate report to file --json # Output report in JSON format --quick (-q) # Quick verification (basic checks only) --verbose (-v) # Verbose output with detailed information --fix-permissions # Attempt to fix permission issues ] { log_info "๐Ÿ” Nushell Full Distribution Installation Verification" log_info "=================================================" if $verbose != null { log_debug "Verbose mode enabled" } # Get verification configuration let config = get_verification_config $platform $install_dir $config_dir log_info "" log_info "๐Ÿ“ Verification Configuration:" log_info $" Platform: ($config.platform)" log_info $" Binary directory: ($config.bin_dir)" log_info $" Config directory: ($config.config_dir)" if $quick != null { log_info " Mode: Quick verification" } else { log_info " Mode: Comprehensive verification" } # Run verification checks let verification_results = run_verification_suite $config $plugins $all_plugins $quick $verbose $fix_permissions # Generate and display report let report = generate_verification_report $verification_results $config display_verification_report $report $verbose $json # Save report to file if requested if ($report | str length) > 0 { save_verification_report $report $json $report } # Exit with appropriate code let overall_status = $report.summary.overall_status if $overall_status == "passed" { log_success "๐ŸŽ‰ All verification checks passed!" exit 0 } else if $overall_status == "warning" { log_warn "โš ๏ธ Verification completed with warnings" exit 0 } else { log_error "โŒ Verification failed" exit 1 } } # Get verification configuration def get_verification_config [platform: string, install_dir: string, config_dir: string] -> record { let current_platform = if ($platform | str length) > 0 { $platform } else { get_current_platform } let home_dir = ($env.HOME | path expand) # Determine binary directory let bin_dir = if ($install_dir | str length) > 0 { ($install_dir | path expand) } else if (which nu | length) > 0 { (which nu | get 0.path | path dirname) } else { $"($home_dir)/.local/bin" } # Determine configuration directory let config_dir = if ($config_dir | str length) > 0 { ($config_dir | path expand) } else if ($env.XDG_CONFIG_HOME? | is-not-empty) { $"($env.XDG_CONFIG_HOME)/nushell" } else { $"($home_dir)/.config/nushell" } { platform: $current_platform, bin_dir: $bin_dir, config_dir: $config_dir, binary_extension: (if ($current_platform | str starts-with "windows") { ".exe" } else { "" }), home_dir: $home_dir } } # Run complete verification suite def run_verification_suite [ config: record, plugins: list, all_plugins: bool, quick: bool, verbose: bool, fix_permissions: bool ] -> record { let results = {} # Verify nushell binary log_info "๐Ÿš€ Verifying Nushell binary..." let nushell_result = verify_nushell_binary $config $verbose $fix_permissions $results = ($results | insert "nushell" $nushell_result) # Verify plugins log_info "๐Ÿ”Œ Verifying plugins..." let plugin_results = verify_plugins $config $plugins $all_plugins $verbose $fix_permissions $results = ($results | insert "plugins" $plugin_results) # Verify configuration log_info "โš™๏ธ Verifying configuration..." let config_results = verify_configuration $config $verbose $results = ($results | insert "configuration" $config_results) # Verify PATH integration log_info "๐Ÿ›ฃ๏ธ Verifying PATH integration..." let path_results = verify_path_integration $config $verbose $results = ($results | insert "path_integration" $path_results) # Extended checks for comprehensive verification if not $quick { # Verify plugin registration log_info "๐Ÿ“ Verifying plugin registration..." let registration_results = verify_plugin_registration $config $verbose $results = ($results | insert "plugin_registration" $registration_results) # Test basic functionality log_info "๐Ÿงช Testing basic functionality..." let functionality_results = test_basic_functionality $config $verbose $results = ($results | insert "functionality" $functionality_results) # System integration checks log_info "๐Ÿ”— Verifying system integration..." let system_results = verify_system_integration $config $verbose $results = ($results | insert "system_integration" $system_results) } $results } # Verify nushell binary def verify_nushell_binary [config: record, verbose: bool, fix_permissions: bool] -> record { let nu_path = $"($config.bin_dir)/nu($config.binary_extension)" let result = { component: "nushell_binary", checks: [] } # Check if binary exists let exists_check = if ($nu_path | path exists) { {name: "binary_exists", status: "passed", message: $"Binary found at ($nu_path)"} } else { {name: "binary_exists", status: "failed", message: $"Binary not found at ($nu_path)"} } $result.checks = ($result.checks | append $exists_check) if $exists_check.status == "passed" { # Check if binary is executable let executable_check = if (check_binary $nu_path) { {name: "binary_executable", status: "passed", message: "Binary is executable"} } else { let check_result = if $fix_permissions { try { chmod +x $nu_path if (check_binary $nu_path) { {name: "binary_executable", status: "passed", message: "Binary permissions fixed and is now executable"} } else { {name: "binary_executable", status: "failed", message: "Binary not executable (permission fix failed)"} } } catch { {name: "binary_executable", status: "failed", message: "Binary not executable (cannot fix permissions)"} } } else { {name: "binary_executable", status: "failed", message: "Binary not executable (use --fix-permissions to attempt fix)"} } $check_result } $result.checks = ($result.checks | append $executable_check) # Test version command if $executable_check.status == "passed" { let version_check = try { let version_output = (run-external $nu_path "--version") {name: "version_command", status: "passed", message: $"Version: ($version_output)", details: $version_output} } catch {|err| {name: "version_command", status: "failed", message: $"Version command failed: ($err.msg)"} } $result.checks = ($result.checks | append $version_check) # Test basic command let basic_test = try { let test_output = (run-external $nu_path "-c" "echo 'test'") if $test_output == "test" { {name: "basic_command", status: "passed", message: "Basic command execution works"} } else { {name: "basic_command", status: "warning", message: $"Unexpected output: ($test_output)"} } } catch {|err| {name: "basic_command", status: "failed", message: $"Basic command failed: ($err.msg)"} } $result.checks = ($result.checks | append $basic_test) } } $result } # Verify plugins def verify_plugins [config: record, specific_plugins: list, all_plugins: bool, verbose: bool, fix_permissions: bool] -> record { let plugin_list = if $all_plugins { # Find all plugin binaries in bin directory if ($config.bin_dir | path exists) { ls $config.bin_dir | where name =~ $"nu_plugin_.*($config.binary_extension)$" | get name | each {|path| ($path | path basename | str replace $config.binary_extension "")} } else { [] } } else if ($specific_plugins | length) > 0 { $specific_plugins } else { # Default plugins to check [ "nu_plugin_clipboard", "nu_plugin_hashes", "nu_plugin_desktop_notifications", "nu_plugin_highlight" ] } let results = { component: "plugins", plugin_count: ($plugin_list | length), plugins: {} } if ($plugin_list | length) == 0 { $results = ($results | insert "message" "No plugins to verify") return $results } for plugin in $plugin_list { let plugin_path = $"($config.bin_dir)/($plugin)($config.binary_extension)" let plugin_result = { name: $plugin, path: $plugin_path, checks: [] } # Check if plugin binary exists let exists_check = if ($plugin_path | path exists) { {name: "binary_exists", status: "passed", message: $"Plugin binary found at ($plugin_path)"} } else { {name: "binary_exists", status: "failed", message: $"Plugin binary not found at ($plugin_path)"} } $plugin_result.checks = ($plugin_result.checks | append $exists_check) if $exists_check.status == "passed" { # Check if plugin is executable let executable_check = if (check_binary $plugin_path) { {name: "binary_executable", status: "passed", message: "Plugin binary is executable"} } else { let check_result = if $fix_permissions { try { chmod +x $plugin_path if (check_binary $plugin_path) { {name: "binary_executable", status: "passed", message: "Plugin permissions fixed"} } else { {name: "binary_executable", status: "failed", message: "Plugin not executable (fix failed)"} } } catch { {name: "binary_executable", status: "failed", message: "Plugin not executable (cannot fix)"} } } else { {name: "binary_executable", status: "failed", message: "Plugin not executable"} } $check_result } $plugin_result.checks = ($plugin_result.checks | append $executable_check) # Test plugin help command (if executable) if $executable_check.status == "passed" { let help_check = try { let help_output = (run-external $plugin_path "--help") {name: "help_command", status: "passed", message: "Plugin help command works"} } catch {|err| {name: "help_command", status: "warning", message: $"Plugin help command issue: ($err.msg)"} } $plugin_result.checks = ($plugin_result.checks | append $help_check) } } $results.plugins = ($results.plugins | insert $plugin $plugin_result) } $results } # Verify configuration files def verify_configuration [config: record, verbose: bool] -> record { let config_files = [ {name: "config.nu", required: true, description: "Main nushell configuration"}, {name: "env.nu", required: true, description: "Environment configuration"}, {name: "distribution_config.toml", required: false, description: "Distribution metadata"} ] let results = { component: "configuration", config_dir: $config.config_dir, files: {} } for file_info in $config_files { let file_path = $"($config.config_dir)/($file_info.name)" let file_result = { name: $file_info.name, path: $file_path, required: $file_info.required, description: $file_info.description, checks: [] } # Check if file exists let exists_check = if ($file_path | path exists) { {name: "file_exists", status: "passed", message: $"Configuration file found: ($file_info.name)"} } else { let status = if $file_info.required { "failed" } else { "warning" } {name: "file_exists", status: $status, message: $"Configuration file not found: ($file_info.name)"} } $file_result.checks = ($file_result.checks | append $exists_check) if $exists_check.status == "passed" { # Check file readability let readable_check = try { let content = (open $file_path --raw) let size = ($content | str length) {name: "file_readable", status: "passed", message: $"File readable (($size) characters)"} } catch {|err| {name: "file_readable", status: "failed", message: $"File not readable: ($err.msg)"} } $file_result.checks = ($file_result.checks | append $readable_check) # Syntax check for .nu files if ($file_info.name | str ends-with ".nu") and $readable_check.status == "passed" { let syntax_check = try { let nu_path = $"($config.bin_dir)/nu($config.binary_extension)" if ($nu_path | path exists) { # Test parsing the configuration file run-external $nu_path "-c" $"source ($file_path); echo 'syntax ok'" {name: "syntax_check", status: "passed", message: "Configuration syntax is valid"} } else { {name: "syntax_check", status: "warning", message: "Cannot verify syntax (nu binary not available)"} } } catch {|err| {name: "syntax_check", status: "failed", message: $"Syntax error in configuration: ($err.msg)"} } $file_result.checks = ($file_result.checks | append $syntax_check) } } $results.files = ($results.files | insert $file_info.name $file_result) } $results } # Verify PATH integration def verify_path_integration [config: record, verbose: bool] -> record { let results = { component: "path_integration", bin_dir: $config.bin_dir, checks: [] } # Check if bin directory is in PATH let path_entries = ($env.PATH | split row (if (sys host | get name) == "Windows" { ";" } else { ":" })) let in_path_check = if ($config.bin_dir in $path_entries) { {name: "bin_dir_in_path", status: "passed", message: $"Binary directory is in PATH: ($config.bin_dir)"} } else { {name: "bin_dir_in_path", status: "warning", message: $"Binary directory not in PATH: ($config.bin_dir)"} } $results.checks = ($results.checks | append $in_path_check) # Check if nu command is available globally let global_nu_check = if (which nu | length) > 0 { let nu_location = (which nu | get 0.path) {name: "nu_globally_available", status: "passed", message: $"nu command available globally at: ($nu_location)"} } else { {name: "nu_globally_available", status: "failed", message: "nu command not available globally"} } $results.checks = ($results.checks | append $global_nu_check) $results } # Verify plugin registration def verify_plugin_registration [config: record, verbose: bool] -> record { let results = { component: "plugin_registration", checks: [] } let nu_path = $"($config.bin_dir)/nu($config.binary_extension)" if not ($nu_path | path exists) { $results.checks = ($results.checks | append { name: "nu_binary_available", status: "failed", message: "Cannot verify plugin registration - nu binary not available" }) return $results } # Get list of registered plugins let registered_plugins_check = try { let plugin_list = (run-external $nu_path "-c" "plugin list | get name") { name: "list_registered_plugins", status: "passed", message: $"Found (($plugin_list | length)) registered plugins", details: $plugin_list } } catch {|err| { name: "list_registered_plugins", status: "warning", message: $"Could not list registered plugins: ($err.msg)" } } $results.checks = ($results.checks | append $registered_plugins_check) # Test plugin functionality if registration check passed if $registered_plugins_check.status == "passed" and ($registered_plugins_check.details? | is-not-empty) { let plugin_test_check = try { # Test a simple plugin command if clipboard plugin is available if "nu_plugin_clipboard" in $registered_plugins_check.details { run-external $nu_path "-c" "clipboard --help" {name: "plugin_functionality", status: "passed", message: "Plugin commands are functional"} } else { {name: "plugin_functionality", status: "warning", message: "No testable plugins found"} } } catch {|err| {name: "plugin_functionality", status: "warning", message: $"Plugin functionality test failed: ($err.msg)"} } $results.checks = ($results.checks | append $plugin_test_check) } $results } # Test basic functionality def test_basic_functionality [config: record, verbose: bool] -> record { let results = { component: "basic_functionality", checks: [] } let nu_path = $"($config.bin_dir)/nu($config.binary_extension)" if not ($nu_path | path exists) { $results.checks = ($results.checks | append { name: "nu_binary_available", status: "failed", message: "Cannot test functionality - nu binary not available" }) return $results } # Test basic arithmetic let arithmetic_test = try { let result = (run-external $nu_path "-c" "2 + 2") if ($result | str trim) == "4" { {name: "arithmetic", status: "passed", message: "Basic arithmetic works"} } else { {name: "arithmetic", status: "warning", message: $"Unexpected arithmetic result: ($result)"} } } catch {|err| {name: "arithmetic", status: "failed", message: $"Arithmetic test failed: ($err.msg)"} } $results.checks = ($results.checks | append $arithmetic_test) # Test piping let pipe_test = try { let result = (run-external $nu_path "-c" "echo 'hello world' | str upcase") if ($result | str trim) == "HELLO WORLD" { {name: "piping", status: "passed", message: "Command piping works"} } else { {name: "piping", status: "warning", message: $"Unexpected piping result: ($result)"} } } catch {|err| {name: "piping", status: "failed", message: $"Piping test failed: ($err.msg)"} } $results.checks = ($results.checks | append $pipe_test) # Test table operations let table_test = try { let result = (run-external $nu_path "-c" "[{name: 'test', value: 42}] | get 0.value") if ($result | str trim) == "42" { {name: "tables", status: "passed", message: "Table operations work"} } else { {name: "tables", status: "warning", message: $"Unexpected table result: ($result)"} } } catch {|err| {name: "tables", status: "failed", message: $"Table test failed: ($err.msg)"} } $results.checks = ($results.checks | append $table_test) $results } # Verify system integration def verify_system_integration [config: record, verbose: bool] -> record { let results = { component: "system_integration", checks: [] } # Check shell profile integration let profile_files = [ "~/.bashrc", "~/.zshrc", "~/.profile", "~/.bash_profile" ] let profile_check = { name: "shell_profile_integration", status: "warning", message: "Shell profile integration not detected", details: [] } for profile_file in $profile_files { let expanded_path = ($profile_file | path expand) if ($expanded_path | path exists) { try { let content = (open $expanded_path --raw) if ($content | str contains $config.bin_dir) { $profile_check.status = "passed" $profile_check.message = $"Shell profile integration found in ($profile_file)" break } } catch { # Ignore errors reading profile files } } } $results.checks = ($results.checks | append $profile_check) # Check terminal integration (can nu be started as a shell) let terminal_test = try { let nu_path = $"($config.bin_dir)/nu($config.binary_extension)" if ($nu_path | path exists) { # Test if nu can be started (quick test) run-external $nu_path "-c" "exit" {name: "terminal_integration", status: "passed", message: "Nu can be started as shell"} } else { {name: "terminal_integration", status: "failed", message: "Nu binary not available for terminal test"} } } catch {|err| {name: "terminal_integration", status: "warning", message: $"Terminal integration test issue: ($err.msg)"} } $results.checks = ($results.checks | append $terminal_test) $results } # Generate verification report def generate_verification_report [results: record, config: record] -> record { let summary = calculate_summary $results { metadata: { verification_time: (date now | format date "%Y-%m-%d %H:%M:%S UTC"), platform: $config.platform, bin_directory: $config.bin_dir, config_directory: $config.config_dir, nushell_version: (try { (nu --version) } catch { "unknown" }) }, summary: $summary, detailed_results: $results } } # Calculate summary statistics def calculate_summary [results: record] -> record { mut total_checks = 0 mut passed_checks = 0 mut warning_checks = 0 mut failed_checks = 0 let components = [] for component in ($results | items) { let component_name = $component.key let component_data = $component.value let component_summary = if "checks" in ($component_data | columns) { # Direct checks in component let checks = $component_data.checks $total_checks = $total_checks + ($checks | length) let component_passed = ($checks | where status == "passed" | length) let component_warning = ($checks | where status == "warning" | length) let component_failed = ($checks | where status == "failed" | length) $passed_checks = $passed_checks + $component_passed $warning_checks = $warning_checks + $component_warning $failed_checks = $failed_checks + $component_failed { name: $component_name, total: ($checks | length), passed: $component_passed, warning: $component_warning, failed: $component_failed } } else if "plugins" in ($component_data | columns) { # Plugin component with nested structure mut plugin_total = 0 mut plugin_passed = 0 mut plugin_warning = 0 mut plugin_failed = 0 for plugin in ($component_data.plugins | items) { let plugin_checks = $plugin.value.checks $plugin_total = $plugin_total + ($plugin_checks | length) $plugin_passed = $plugin_passed + ($plugin_checks | where status == "passed" | length) $plugin_warning = $plugin_warning + ($plugin_checks | where status == "warning" | length) $plugin_failed = $plugin_failed + ($plugin_checks | where status == "failed" | length) } $total_checks = $total_checks + $plugin_total $passed_checks = $passed_checks + $plugin_passed $warning_checks = $warning_checks + $plugin_warning $failed_checks = $failed_checks + $plugin_failed { name: $component_name, total: $plugin_total, passed: $plugin_passed, warning: $plugin_warning, failed: $plugin_failed } } else if "files" in ($component_data | columns) { # Configuration component with files mut config_total = 0 mut config_passed = 0 mut config_warning = 0 mut config_failed = 0 for file in ($component_data.files | items) { let file_checks = $file.value.checks $config_total = $config_total + ($file_checks | length) $config_passed = $config_passed + ($file_checks | where status == "passed" | length) $config_warning = $config_warning + ($file_checks | where status == "warning" | length) $config_failed = $config_failed + ($file_checks | where status == "failed" | length) } $total_checks = $total_checks + $config_total $passed_checks = $passed_checks + $config_passed $warning_checks = $warning_checks + $config_warning $failed_checks = $failed_checks + $config_failed { name: $component_name, total: $config_total, passed: $config_passed, warning: $config_warning, failed: $config_failed } } else { { name: $component_name, total: 0, passed: 0, warning: 0, failed: 0 } } $components = ($components | append $component_summary) } let overall_status = if $failed_checks > 0 { "failed" } else if $warning_checks > 0 { "warning" } else { "passed" } { overall_status: $overall_status, total_checks: $total_checks, passed_checks: $passed_checks, warning_checks: $warning_checks, failed_checks: $failed_checks, components: $components } } # Display verification report def display_verification_report [report: record, verbose: bool, json_format: bool] { if $json_format { $report | to json } else { log_info "" log_info "๐Ÿ“Š Verification Summary" log_info "======================" let summary = $report.summary let status_icon = match $summary.overall_status { "passed" => "โœ…", "warning" => "โš ๏ธ", "failed" => "โŒ" } log_info $"Overall Status: ($status_icon) ($summary.overall_status | str upcase)" log_info $"Total Checks: ($summary.total_checks)" log_info $"โœ… Passed: ($summary.passed_checks)" log_info $"โš ๏ธ Warnings: ($summary.warning_checks)" log_info $"โŒ Failed: ($summary.failed_checks)" log_info "" log_info "๐Ÿ“‹ Component Summary:" for component in $summary.components { let comp_status = if $component.failed > 0 { "โŒ" } else if $component.warning > 0 { "โš ๏ธ" } else { "โœ…" } log_info $" ($comp_status) ($component.name): ($component.passed)/($component.total) passed" } if $verbose { log_info "" log_info "๐Ÿ” Detailed Results:" display_detailed_results $report.detailed_results } } } # Display detailed results def display_detailed_results [results: record] { for component in ($results | items) { log_info $"" log_info $"๐Ÿ“‚ ($component.key | str upcase)" log_info $" {'=' | str repeat (($component.key | str length) + 2)}" let data = $component.value if "checks" in ($data | columns) { display_checks $data.checks " " } else if "plugins" in ($data | columns) { for plugin in ($data.plugins | items) { log_info $" ๐Ÿ”Œ ($plugin.key):" display_checks $plugin.value.checks " " } } else if "files" in ($data | columns) { for file in ($data.files | items) { log_info $" ๐Ÿ“„ ($file.key):" display_checks $file.value.checks " " } } } } # Display individual checks def display_checks [checks: list, indent: string] { for check in $checks { let status_icon = match $check.status { "passed" => "โœ…", "warning" => "โš ๏ธ", "failed" => "โŒ" } log_info $"($indent)($status_icon) ($check.name): ($check.message)" } } # Save verification report to file def save_verification_report [report: record, json_format: bool, output_path: string] { let content = if $json_format { ($report | to json) } else { generate_text_report $report } $content | save -f $output_path log_success $"โœ… Verification report saved: ($output_path)" } # Generate text format report def generate_text_report [report: record] -> string { let lines = [ "Nushell Full Distribution - Installation Verification Report", "=" | str repeat 58, "", $"Generated: ($report.metadata.verification_time)", $"Platform: ($report.metadata.platform)", $"Binary Directory: ($report.metadata.bin_directory)", $"Config Directory: ($report.metadata.config_directory)", $"Nushell Version: ($report.metadata.nushell_version)", "", "SUMMARY", "-------", $"Overall Status: ($report.summary.overall_status | str upcase)", $"Total Checks: ($report.summary.total_checks)", $"Passed: ($report.summary.passed_checks)", $"Warnings: ($report.summary.warning_checks)", $"Failed: ($report.summary.failed_checks)", "" ] # Add component details let component_lines = $report.summary.components | each {|comp| [ $"($comp.name): ($comp.passed)/($comp.total) passed" ] } | flatten $lines | append $component_lines | str join "\n" }