#!/usr/bin/env nu # Agnostic Version Management Core # No hardcoded tools or specific implementations # use ../utils/error.nu * # use ../utils/format.nu * # Generic version record schema export def version-schema [] { { id: "" # Unique identifier type: "" # Component type (tool/provider/taskserv/cluster) version: "" # Current version fixed: false # Version pinning source: {} # Source configuration detector: {} # Detection configuration updater: {} # Update configuration metadata: {} # Any additional data } } # Generic version operations interface export def version-operations [] { { detect: { |config| "" } # Detect installed version fetch: { |config| "" } # Fetch available versions compare: { |v1, v2| 0 } # Compare versions update: { |config, version| {} } # Update to version } } # Version comparison (works with semantic and non-semantic versions) export def compare-versions [ v1: string v2: string --strategy: string = "semantic" # semantic, string, numeric, custom ] { if $v1 == $v2 { return 0 } if ($v1 | is-empty) { return (-1) } if ($v2 | is-empty) { return 1 } match $strategy { "semantic" => { # Try semantic versioning - safely parse version parts let parts1 = ($v1 | split row "." | each { |p| let trimmed = ($p | str trim) if ($trimmed | is-empty) { 0 } else { # Attempt to convert to int, with error handling if ($trimmed | parse -r "^[0-9]+$" | is-empty) { 0 } else { $trimmed | into int } } }) let parts2 = ($v2 | split row "." | each { |p| let trimmed = ($p | str trim) if ($trimmed | is-empty) { 0 } else { # Attempt to convert to int, with error handling if ($trimmed | parse -r "^[0-9]+$" | is-empty) { 0 } else { $trimmed | into int } } }) let max_len = ([$parts1 $parts2] | each { |it| $it | length } | math max) for i in 0..<$max_len { let p1 = if ($i < ($parts1 | length)) { $parts1 | get $i } else { 0 } let p2 = if ($i < ($parts2 | length)) { $parts2 | get $i } else { 0 } if $p1 < $p2 { return (-1) } if $p1 > $p2 { return 1 } } 0 } "string" => { # Simple string comparison if $v1 < $v2 { (-1) } else if $v1 > $v2 { 1 } else { 0 } } "numeric" => { # Numeric comparison (for build numbers) let n1 = ($v1 | into float | default 0) let n2 = ($v2 | into float | default 0) if $n1 < $n2 { (-1) } else if $n1 > $n2 { 1 } else { 0 } } _ => 0 } } # Execute command and extract version export def detect-version [ config: record # Detection configuration ] { if ($config | is-empty) { return "" } let method = ($config | get method? | default "command") match $method { "command" => { let cmd = ($config | get command? | default "") if ($cmd | is-empty) { return "" } let result = (^sh -c $cmd err> /dev/null | complete) if $result.exit_code == 0 { let output = $result.stdout # Apply extraction pattern if provided if ($config | get pattern? | default null | is-not-empty) { let parsed = ($output | parse -r $config.pattern) if ($parsed | length) > 0 { let row = ($parsed | get 0) let capture_name = ($config | get capture? | default "capture0") if ($capture_name in ($row | columns)) { $row | get $capture_name } else { "" } } else { "" } } else { $output | str trim } } else { "" } } "file" => { let path = ($config | get path? | default "") if not ($path | path exists) { return "" } let content = (open $path) if ($config | get field? | default null | is-not-empty) { $content | get $config.field? | default "" } else { $content | str trim } } "api" => { let url = ($config | get url? | default "") if ($url | is-empty) { return "" } let result = (http get $url --headers [User-Agent "provisionin-version-checker"] | complete) if $result.exit_code == 0 and ($result.stdout | length) > 0 { let response = ($result.stdout | from json) if ($config | get field? | default null | is-not-empty) { $response | get $config.field? | default "" } else { $response | to text | str trim } } else { "" } } "script" => { # Execute custom script let script = ($config | get script? | default "") if ($script | is-empty) { return "" } (nu -c $script | str trim | default "") } _ => "" } } # Fetch available versions from source export def fetch-versions [ config: record # Source configuration --limit: int = 10 ] { if ($config | is-empty) { return [] } let type = ($config | get type? | default "") match $type { "github" => { let repo = ($config | get repo? | default "") if ($repo | is-empty) { return [] } # Try releases first, then tags let endpoints = [ $"https://api.github.com/repos/($repo)/releases" $"https://api.github.com/repos/($repo)/tags" ] for endpoint in $endpoints { let result = (http get $endpoint --headers [User-Agent "provisionin-version-checker"] | complete) if $result.exit_code == 0 and ($result.stdout | length) > 0 { let response = ($result.stdout | from json | default []) if ($response | length) > 0 { return ($response | first $limit | each { |item| let version = ($item | get tag_name? | default "") $version | str replace -r '^v' '' }) } } } [] } "docker" => { let image = ($config | get image? | default "") if ($image | is-empty) { return [] } # Parse namespace/repo let parts = ($image | split row "/") let namespace = if ($parts | length) > 1 { $parts | get 0 } else { "library" } let repo = ($parts | last) let url = $"https://hub.docker.com/v2/namespaces/($namespace)/repositories/($repo)/tags" let result = (http get $url --headers [User-Agent "provisionin-version-checker"] | complete) if $result.exit_code == 0 and ($result.stdout | length) > 0 { let response = ($result.stdout | from json) if ($response | get results? | default null | is-not-empty) { $response | get results? | default null | first $limit | each { |tag| $tag.name } | where { |v| $v !~ "latest|dev|nightly|edge|alpha|beta|rc" } } else { [] } } else { [] } } "url" => { let url = ($config | get url? | default "") if ($url | is-empty) { return [] } let result = (http get $url --headers [User-Agent "provisionin-version-checker"] | complete) if $result.exit_code == 0 and ($result.stdout | length) > 0 { let response = ($result.stdout | from json) let field = ($config | get field? | default "") if ($field | is-not-empty) { if ($field in ($response | columns)) { $response | get $field } else { [] } } else { [$response | to text | str trim] } } else { [] } } "script" => { let script = ($config | get script? | default "") if ($script | is-empty) { return [] } (nu -c $script | lines | default []) } _ => [] } } # Generic version check export def check-version [ component: record --fetch-latest = false --respect-fixed = true ] { # Detect installed version let installed = if (($component | get detector? | default null) != null) { (detect-version $component.detector) } else { "" } # Get configured version let configured = ($component | get version? | default "") # Check if fixed let is_fixed = ($component | get fixed? | default false) # Fetch latest if requested let latest = if $fetch_latest and (not $is_fixed or not $respect_fixed) { if (($component | get source? | default null) != null) { let versions = (fetch-versions $component.source --limit=1) if ($versions | length) > 0 { $versions | get 0 } else { $configured } } else { $configured } } else { $configured } # Compare versions let comparison_strategy = ($component | get comparison? | default "semantic") let status = if $is_fixed and $respect_fixed { "fixed" } else if ($installed | is-empty) { "not_installed" } else if ($installed | is-not-empty) and ($latest != $installed) and ((compare-versions $installed $latest --strategy=$comparison_strategy) < 0) { "update_available" } else if (compare-versions $installed $configured --strategy=$comparison_strategy) < 0 { "behind_config" } else if (compare-versions $installed $configured --strategy=$comparison_strategy) > 0 { "ahead_config" } else { "up_to_date" } { id: $component.id type: $component.type installed: $installed configured: $configured latest: $latest fixed: $is_fixed status: $status } }