309 lines
11 KiB
Text
309 lines
11 KiB
Text
|
|
#!/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
|
||
|
|
}
|
||
|
|
}
|