#!/usr/bin/env nu # Nu Version Manager - Nushell implementation # Usage: nu update_nu_versions.nu [list|ls|update|help] # Function to get version from nushell Cargo.toml files def get_nushell_version [cargo_file: path] { if ($cargo_file | path exists) { try { open --raw $cargo_file | lines | where $it =~ '^version = ' | first | parse 'version = "{version}"' | get version.0 } catch { null } } else { null } } # Function to find version for a nu-* dependency in nushell crates def get_target_version_for_dependency [dep_name: string, nushell_dir: string] { let target_path = ($nushell_dir | path join $"crates/($dep_name)/Cargo.toml") if ($target_path | path exists) { get_nushell_version $target_path } else { null } } # Function to extract the correct dependency name from a path def get_correct_dep_name_from_path [line: string] { try { let path_match = ($line | parse --regex 'path = "([^"]*)"' | get capture0.0) # Extract the crate name from the path (last component after /crates/) $path_match | str replace --regex '.*/crates/' '' | str replace --regex '/.*' '' } catch { null } } # Function to fix dependency name conflicts def fix_dependency_name_conflicts [content: list] { mut updated_content = $content mut changes = [] # Find lines with nu-* dependencies that have paths let conflicts = ( $content | enumerate | where ($it.item | str contains "nu-") and ($it.item | str contains "path =") | each { |line| let declared_name = try { $line.item | parse --regex '^[[:space:]]*(nu-[a-zA-Z0-9_-]+)[[:space:]]*=' | get capture0.0 } catch { null } let correct_name = get_correct_dep_name_from_path $line.item if ($declared_name | is-not-empty) and ($correct_name | is-not-empty) and $declared_name != $correct_name { { index: $line.index, declared_name: $declared_name, correct_name: $correct_name, line: $line.item } } else { null } } | where $it != null ) for $conflict in $conflicts { let new_line = ($conflict.line | str replace $conflict.declared_name $conflict.correct_name) $updated_content = ($updated_content | update $conflict.index $new_line) $changes = ($changes | append $"⚠ Fixed dependency name conflict: ($conflict.declared_name) -> ($conflict.correct_name)") } { content: $updated_content, changes: $changes } } # Auto-detect versions from nushell source let NUSHELL_DIR = if ("nushell" | path exists) { if ("nushell" | path type) == "symlink" { # Follow symlink to get actual directory "nushell" | path expand } else { "nushell" } } else { error make { msg: "Nushell directory not found" } } # Get target versions from nushell source let NU_PLUGIN_VERSION = get_nushell_version ($NUSHELL_DIR | path join "crates/nu-plugin/Cargo.toml") let NU_PROTOCOL_VERSION = get_nushell_version ($NUSHELL_DIR | path join "crates/nu-protocol/Cargo.toml") let NU_PLUGIN_TEST_VERSION = get_nushell_version ($NUSHELL_DIR | path join "crates/nu-plugin-test-support/Cargo.toml") # Use nu-plugin version as the primary version (they should all be the same) let NEW_VERSION = $NU_PLUGIN_VERSION if ($NEW_VERSION | is-empty) { error make { msg: "Could not determine target version from nushell source. Make sure the nushell directory exists and contains the expected Cargo.toml files" } } # Function to get all unique versions currently used in plugin Cargo.toml files for nu-* dependencies def get_current_versions [] { let plugin_dirs = get_plugin_dirs $plugin_dirs | each { |dir| let cargo_file = ($dir | path join "Cargo.toml") if ($cargo_file | path exists) { try { open --raw $cargo_file | lines | where $it =~ 'nu-[a-zA-Z0-9_-]+.*version = ' | each { |line| try { $line | parse --regex 'version = "([^"]*)"' | get capture0.0 } catch { null } } | where $it != null } catch { [] } } else { [] } } | flatten | uniq | where $it != $NEW_VERSION } # Function to show usage information def show_usage [] { print $"(ansi blue)Nu Version Manager(ansi reset)" print "" print "Usage: nu update_nu_versions.nu [command]" print "" print "Commands:" print $" (ansi green)list, ls(ansi reset) Show current versions in all Cargo.toml files" print $" (ansi green)update(ansi reset) Update versions to match nushell source ((ansi green)($NEW_VERSION)(ansi reset))" print $" (ansi green)help, -h(ansi reset) Show this help message" print "" print "If no command is specified, 'update' is assumed." print "" } # Function to extract version from a line def extract_version [line: string] { let version_match = ($line | parse --regex 'version = "([^"]*)"') if ($version_match | length) > 0 { $version_match.0.capture0 } else { null } } # Function to check if line has path dependency def has_path_dependency [line: string] { $line | str contains "path =" } # Function to get plugin directories def get_plugin_dirs [] { glob "nu_plugin_*" | where ($it | path type) == "dir" | sort } # Function to parse cargo.toml for version information def parse_cargo_toml [cargo_file: path] { if not ($cargo_file | path exists) { return { package_version: null nu_plugin: null nu_protocol: null nu_json: null nu_plugin_test_support: null error: "Cargo.toml not found" } } let content = open --raw $cargo_file | lines # Get package version let package_version = try { $content | where $it =~ '^version = ' | first | extract_version $in } catch { null } # Get nu-plugin version let nu_plugin_line = try { $content | where ($it =~ 'nu-plugin' and not ($it =~ 'nu-plugin-test-support')) | first } catch { null } let nu_plugin_info = if $nu_plugin_line != null { let version = extract_version $nu_plugin_line let has_path = has_path_dependency $nu_plugin_line { version: $version has_path: $has_path status: (if $version != null { if $has_path { "version_with_path" } else { "version_only" } } else { if $has_path { "path_only" } else { "not_found" } }) } } else { { version: null, has_path: false, status: "not_found" } } # Get nu-protocol version let nu_protocol_line = try { $content | where $it =~ 'nu-protocol' | first } catch { null } let nu_protocol_info = if $nu_protocol_line != null { let version = extract_version $nu_protocol_line let has_path = has_path_dependency $nu_protocol_line { version: $version has_path: $has_path status: (if $version != null { if $has_path { "version_with_path" } else { "version_only" } } else { if $has_path { "path_only" } else { "not_found" } }) } } else { { version: null, has_path: false, status: "not_found" } } # Get nu-json version let nu_json_line = try { $content | where $it =~ 'nu-json' | first } catch { null } let nu_json_info = if $nu_json_line != null { let version = extract_version $nu_json_line let has_path = has_path_dependency $nu_json_line { version: $version has_path: $has_path status: (if $version != null { if $has_path { "version_with_path" } else { "version_only" } } else { if $has_path { "path_only" } else { "not_found" } }) } } else { { version: null, has_path: false, status: "not_found" } } # Get nu-plugin-test-support version let nu_test_line = try { $content | where $it =~ 'nu-plugin-test-support' | first } catch { null } let nu_test_info = if $nu_test_line != null { let version = extract_version $nu_test_line let has_path = has_path_dependency $nu_test_line { version: $version has_path: $has_path status: (if $version != null { if $has_path { "version_with_path" } else { "version_only" } } else { if $has_path { "path_only" } else { "not_found" } }) } } else { { version: null, has_path: false, status: "not_found" } } { package_version: $package_version nu_plugin: $nu_plugin_info nu_protocol: $nu_protocol_info nu_json: $nu_json_info nu_plugin_test_support: $nu_test_info error: null } } # Function to format dependency info for display def format_dependency [dep_info: record] { match $dep_info.status { "version_with_path" => ($dep_info.version + " " + (ansi yellow) + "(path)" + (ansi reset)) "version_only" => $dep_info.version "path_only" => ((ansi yellow) + "path dependency only" + (ansi reset)) "not_found" => ((ansi red) + "not found" + (ansi reset)) } } # Function to list current versions def list_versions [] { print $"(ansi blue)Nu Version Listing(ansi reset)" print "Current versions in all nu_plugin_* directories:" print "" let plugin_dirs = get_plugin_dirs if ($plugin_dirs | length) == 0 { print $"(ansi yellow)No nu_plugin_* directories found(ansi reset)" return } for $dir in $plugin_dirs { let plugin_name = ($dir | path basename) let cargo_file = ($dir | path join "Cargo.toml") print $"(ansi cyan) ($plugin_name)(ansi reset):" let cargo_info = parse_cargo_toml $cargo_file if $cargo_info.error != null { print $" (ansi red)Error: ($cargo_info.error)(ansi reset)" print "" continue } # Package version let pkg_version = if $cargo_info.package_version != null { $cargo_info.package_version } else { $"(ansi yellow)not specified(ansi reset)" } print $" (ansi purple)Package(ansi reset): ($pkg_version)" # nu-plugin print $" (ansi purple)nu-plugin(ansi reset): (format_dependency $cargo_info.nu_plugin)" # nu-protocol print $" (ansi purple)nu-protocol(ansi reset): (format_dependency $cargo_info.nu_protocol)" # nu-json (only if found) if $cargo_info.nu_json.status != "not_found" { print $" (ansi purple)nu-json(ansi reset): (format_dependency $cargo_info.nu_json)" } # nu-plugin-test-support (only if found) if $cargo_info.nu_plugin_test_support.status != "not_found" { print $" (ansi purple)nu-plugin-test-support(ansi reset): (format_dependency $cargo_info.nu_plugin_test_support)" } print "" } } # Function to update a line with version replacement def update_version_line [line: string, old_version: string, new_version: string] { $line | str replace $'version = "($old_version)"' $'version = "($new_version)"' } # Function to update a single cargo.toml file def update_cargo_toml [cargo_file: path, nushell_dir: string] { if not ($cargo_file | path exists) { return { success: false, message: "Cargo.toml not found", changes: [] } } let content = open --raw $cargo_file | lines let backup_file = $"($cargo_file).backup" # Create backup $content | str join (char newline) | save -f --raw $backup_file # First, fix any dependency name conflicts let conflict_fix = fix_dependency_name_conflicts $content mut updated_content = $conflict_fix.content mut changes = $conflict_fix.changes # Find all nu-* dependencies in the file (use updated content after conflict fixes) let nu_dependencies = ( $updated_content | enumerate | where $it.item =~ '^[[:space:]]*nu-[a-zA-Z0-9_-]+[[:space:]]*=' | each { |line| let dep_name = ($line.item | parse --regex '^[[:space:]]*(nu-[a-zA-Z0-9_-]+)[[:space:]]*=' | get capture0.0) { index: $line.index, dep_name: $dep_name, line: $line.item } } | group-by dep_name | transpose dep_name lines | each { |group| { dep_name: $group.dep_name, lines: $group.lines } } ) for $dep_group in $nu_dependencies { let dep_name = $dep_group.dep_name let target_version = get_target_version_for_dependency $dep_name $nushell_dir if ($target_version | is-empty) { $changes = ($changes | append $"⚠ ($dep_name): No corresponding crate found in nushell source - skipping") continue } # Process each line for this dependency for $line_info in $dep_group.lines { let current_line = $line_info.line let line_index = $line_info.index # Check if it already has a path dependency if ($current_line | str contains "path =") { # Update version but keep path if ($current_line | str contains "version =") { let old_version = try { $current_line | parse --regex 'version = "([^"]*)"' | get capture0.0 } catch { null } if ($old_version | is-not-empty) and $old_version != $target_version { let new_line = ($current_line | str replace $'version = "($old_version)"' $'version = "($target_version)"') $updated_content = ($updated_content | update $line_index $new_line) $changes = ($changes | append $"✓ Updated ($dep_name) version from ($old_version) to ($target_version) path preserved") } else if ($old_version | is-not-empty) and $old_version == $target_version { $changes = ($changes | append $"→ ($dep_name): Already at target version ($target_version) with path") } } else { # Add version to existing path dependency let new_line = ($current_line | str replace "{" $'{ version = "($target_version)",') $updated_content = ($updated_content | update $line_index $new_line) $changes = ($changes | append $"✓ Added version ($target_version) to ($dep_name) path dependency") } } else { # No path dependency - add both version and path let current_version = try { $current_line | parse --regex 'version = "([^"]*)"' | get capture0.0 } catch { null } if ($current_version | is-not-empty) { # Replace version-only dependency with version + path let relative_path = $"../nushell/crates/($dep_name)" let new_line = ($current_line | str replace $'version = "($current_version)"' $'version = "($target_version)", path = "($relative_path)"') $updated_content = ($updated_content | update $line_index $new_line) $changes = ($changes | append $"✓ Updated ($dep_name) from ($current_version) to ($target_version) and added path dependency") } else { $changes = ($changes | append $"⚠ ($dep_name): Could not parse current version - skipping") } } } } # Save updated content if changes were made let actual_changes = ($changes | where not ($it | str starts-with "→") and not ($it | str starts-with "⚠")) if ($actual_changes | length) > 0 { $updated_content | str join (char newline) | save -f --raw $cargo_file { success: true message: $"Backup created: ($backup_file)" changes: $changes } } else { # Remove backup if no changes rm $backup_file { success: true message: "No version updates needed" changes: $changes } } } # Function to run the update process def run_update [] { print $"(ansi blue)Nu Version Updater(ansi reset)" print $"Target version from nushell source: (ansi green)($NEW_VERSION)(ansi reset)" # Show detected current versions let current_versions = get_current_versions if ($current_versions | length) > 0 { print $"Current versions found in plugins: (ansi red)($current_versions | str join ', ')(ansi reset)" } else { print $"(ansi yellow)All plugins are already at target version(ansi reset)" } print "" let plugin_dirs = get_plugin_dirs if ($plugin_dirs | length) == 0 { print $"(ansi yellow)No nu_plugin_* directories found(ansi reset)" return } print $"(ansi blue)Found plugin directories:(ansi reset)" for $dir in $plugin_dirs { print $" - ($dir)" } print "" # Process each plugin directory for $dir in $plugin_dirs { print $"(ansi blue)Processing ($dir)...(ansi reset)" let cargo_file = ($dir | path join "Cargo.toml") let result = update_cargo_toml $cargo_file $NUSHELL_DIR for $change in $result.changes { print $" (ansi green)✓(ansi reset) ($change)" } if ($result.changes | length) > 0 { print $" (ansi blue)($result.message)(ansi reset)" } else { print $" (ansi yellow)($result.message)(ansi reset)" } print "" } print $"(ansi green)Version update process completed!(ansi reset)" print "" print ((ansi yellow) + "Note:" + (ansi reset) + " Backup files (.backup) have been created for modified files.") print ("You can restore them with: " + (ansi blue) + "mv file.backup file" + (ansi reset)) print "" print $"(ansi yellow)Don't forget to:(ansi reset)" print "1. Test the plugins after the update" print "2. Update any documentation if needed" print "3. Commit the changes to version control" } # Main function def main [command?: string] { # Ensure we're in the right directory (assume script is in the plugin directory) # cd (pwd) let cmd = $command | default "update" match $cmd { "list" | "ls" => { list_versions } "update" => { run_update } "help" | "-h" | "--help" => { show_usage } _ => { print $"(ansi red)Error: Unknown command '($cmd)'(ansi reset)" print "" show_usage } } } # Export functions for use export def "nu-version list" [] { list_versions } export def "nu-version ls" [] { list_versions } export def "nu-version update" [] { run_update } export def "nu-version help" [] { show_usage } # Main entry point function export def "nu-version" [command?: string] { main ($command | default "help") }