432 lines
15 KiB
Text
432 lines
15 KiB
Text
|
|
#!/usr/bin/env nu
|
||
|
|
# Dynamic configuration loader for version management
|
||
|
|
# Discovers and loads version configurations from the filesystem
|
||
|
|
|
||
|
|
# version/core star-import was dead — dropped (ADR-025 Phase 3 Layer 2).
|
||
|
|
use tools/nickel/process.nu [ncl-eval ncl-eval-soft]
|
||
|
|
|
||
|
|
# Discover version configurations
|
||
|
|
export def discover-configurations [
|
||
|
|
--base-path: string = ""
|
||
|
|
--types: list = [] # Filter by types
|
||
|
|
] {
|
||
|
|
let base = if ($base_path | is-empty) {
|
||
|
|
($env.PROVISIONING? | default $env.PWD)
|
||
|
|
} else { $base_path }
|
||
|
|
mut configurations = []
|
||
|
|
|
||
|
|
# Load from known version files directly - try Nickel first, then YAML
|
||
|
|
let version_files_nickel = [
|
||
|
|
($base | path join "core" | path join "versions.ncl")
|
||
|
|
]
|
||
|
|
|
||
|
|
for file in $version_files_nickel {
|
||
|
|
if ($file | path exists) {
|
||
|
|
let configs = (load-nickel-version-file $file)
|
||
|
|
if ($configs | is-not-empty) {
|
||
|
|
$configurations = ($configurations | append $configs)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
# Fallback to YAML for backward compatibility
|
||
|
|
let version_files_yaml = [
|
||
|
|
($base | path join "versions.yaml")
|
||
|
|
($base | path join "core" | path join "versions.yaml")
|
||
|
|
]
|
||
|
|
|
||
|
|
for file in $version_files_yaml {
|
||
|
|
if ($file | path exists) {
|
||
|
|
let configs = (load-configuration-file $file)
|
||
|
|
if ($configs | is-not-empty) {
|
||
|
|
$configurations = ($configurations | append $configs)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
# Also check providers directory
|
||
|
|
let providers_path = ($base | path join "catalog" | path join "providers")
|
||
|
|
let providers_path_fallback = ($base | path join "providers")
|
||
|
|
|
||
|
|
# Try catalog/providers first, then fall back to providers
|
||
|
|
let active_providers_path = if ($providers_path | path exists) {
|
||
|
|
$providers_path
|
||
|
|
} else if ($providers_path_fallback | path exists) {
|
||
|
|
$providers_path_fallback
|
||
|
|
} else {
|
||
|
|
""
|
||
|
|
}
|
||
|
|
|
||
|
|
if ($active_providers_path | is-not-empty) {
|
||
|
|
for provider_item in (ls $active_providers_path) {
|
||
|
|
let provider_dir = ($active_providers_path | path join $provider_item.name)
|
||
|
|
|
||
|
|
# Try Nickel version file first (single source of truth)
|
||
|
|
let nickel_version_file = ($provider_dir | path join "nickel" | path join "version.ncl")
|
||
|
|
if ($nickel_version_file | path exists) {
|
||
|
|
let configs = (load-nickel-version-file $nickel_version_file)
|
||
|
|
if ($configs | is-not-empty) {
|
||
|
|
$configurations = ($configurations | append $configs)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
# Fallback to YAML for backward compatibility
|
||
|
|
let version_file = ($provider_dir | path join "versions.yaml")
|
||
|
|
if ($version_file | path exists) {
|
||
|
|
let configs = (load-configuration-file $version_file)
|
||
|
|
if ($configs | is-not-empty) {
|
||
|
|
$configurations = ($configurations | append $configs)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
# Filter by types if specified
|
||
|
|
if ($types | length) > 0 {
|
||
|
|
$configurations | where type in $types
|
||
|
|
} else {
|
||
|
|
$configurations
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
# Load configuration from file
|
||
|
|
export def load-configuration-file [
|
||
|
|
file_path: string
|
||
|
|
] {
|
||
|
|
if not ($file_path | path exists) { return [] }
|
||
|
|
|
||
|
|
let ext = ($file_path | path parse | get extension)
|
||
|
|
let parent_dir = ($file_path | path dirname)
|
||
|
|
let context = (extract-context $parent_dir)
|
||
|
|
|
||
|
|
mut configs = []
|
||
|
|
|
||
|
|
match $ext {
|
||
|
|
"yaml" | "yml" => {
|
||
|
|
let data = (open $file_path)
|
||
|
|
if ($data | describe | str contains "record") {
|
||
|
|
# Convert record entries to configurations
|
||
|
|
# Only process items that look like tool/component configs (have version, source, tags, detector, or check_cmd)
|
||
|
|
for item in ($data | transpose key value) {
|
||
|
|
let value_desc = ($item.value | describe)
|
||
|
|
# Check if this is a valid configuration (record with tool-like fields)
|
||
|
|
# Must have version AND at least one of: source, tags, detector, check_cmd
|
||
|
|
let is_config = if ($value_desc | str contains "record") {
|
||
|
|
let has_version = (($item.value | get version? | default null) != null)
|
||
|
|
let has_source_or_tags = (
|
||
|
|
(($item.value | get source? | default null) != null) or
|
||
|
|
(($item.value | get tags? | default null) != null) or
|
||
|
|
(($item.value | get detector? | default null) != null) or
|
||
|
|
(($item.value | get check_cmd? | default null) != null)
|
||
|
|
)
|
||
|
|
$has_version and $has_source_or_tags
|
||
|
|
} else {
|
||
|
|
false
|
||
|
|
}
|
||
|
|
|
||
|
|
# Only append if it's a valid configuration, skip metadata-only entries
|
||
|
|
if $is_config {
|
||
|
|
let config = (create-configuration $item.key $item.value $context $file_path)
|
||
|
|
if ($config | is-not-empty) {
|
||
|
|
$configs = ($configs | append $config)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
} else if ($data | describe | str contains "list") {
|
||
|
|
# Already a list of configurations
|
||
|
|
$configs = $data
|
||
|
|
}
|
||
|
|
}
|
||
|
|
"k" => {
|
||
|
|
# Parse Nickel files for version information
|
||
|
|
let content = (open $file_path)
|
||
|
|
let version_data = (extract-nickel-versions $content)
|
||
|
|
for item in $version_data {
|
||
|
|
let config = (create-configuration $item.name $item $context $file_path)
|
||
|
|
$configs = ($configs | append $config)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
"toml" => {
|
||
|
|
let data = (open $file_path)
|
||
|
|
for section in ($data | transpose key value) {
|
||
|
|
if (($section.value | get version? | default null) != null) {
|
||
|
|
let config = (create-configuration $section.key $section.value $context $file_path)
|
||
|
|
$configs = ($configs | append $config)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
"json" => {
|
||
|
|
let data = (open $file_path)
|
||
|
|
if (($data | get components? | default null) != null) {
|
||
|
|
$configs = $data.components
|
||
|
|
} else {
|
||
|
|
# Treat as single configuration
|
||
|
|
$configs = [$data]
|
||
|
|
}
|
||
|
|
}
|
||
|
|
_ => []
|
||
|
|
}
|
||
|
|
|
||
|
|
$configs
|
||
|
|
}
|
||
|
|
|
||
|
|
# Load Nickel version file by compiling it to JSON
|
||
|
|
export def load-nickel-version-file [
|
||
|
|
file_path: string
|
||
|
|
] {
|
||
|
|
if not ($file_path | path exists) { return [] }
|
||
|
|
|
||
|
|
# Determine parent context - could be provider or core
|
||
|
|
# provider: catalog/providers/{name}/nickel/version.ncl -> catalog/providers/{name}
|
||
|
|
# core: core/versions.ncl -> core (no nickel dir)
|
||
|
|
let parent_dir = if ($file_path | str contains "/nickel/version.ncl") {
|
||
|
|
$file_path | path dirname | path dirname # nickel/version.ncl -> provider_dir
|
||
|
|
} else {
|
||
|
|
$file_path | path dirname # versions.ncl -> core
|
||
|
|
}
|
||
|
|
let context = (extract-context $parent_dir)
|
||
|
|
|
||
|
|
mut configs = []
|
||
|
|
|
||
|
|
# Compile Nickel to JSON (null on failure — return empty configs)
|
||
|
|
let json_data = (ncl-eval-soft $file_path [] null)
|
||
|
|
if ($json_data | is-empty) { return $configs }
|
||
|
|
|
||
|
|
# Handle different Nickel output formats:
|
||
|
|
# 1. Provider files: Single object with {name, version, dependencies}
|
||
|
|
# 2. Core files: Object {core_versions: [{}]} or plain array [{}]
|
||
|
|
let is_array = ($json_data | describe | str contains "^list")
|
||
|
|
let items = (
|
||
|
|
if ($json_data | get core_versions? | default null) != null {
|
||
|
|
# Wrapped in core_versions key (core files)
|
||
|
|
$json_data.core_versions
|
||
|
|
} else if $is_array {
|
||
|
|
# It's an array (plain array format)
|
||
|
|
$json_data
|
||
|
|
} else if ($json_data | get name? | default null) != null {
|
||
|
|
# It's a single object (provider nickel/version.ncl)
|
||
|
|
[$json_data]
|
||
|
|
} else {
|
||
|
|
[]
|
||
|
|
}
|
||
|
|
)
|
||
|
|
|
||
|
|
# Process each tool in items
|
||
|
|
for item in $items {
|
||
|
|
let tool_name = ($item | get name?)
|
||
|
|
if ($tool_name | is-empty) { continue }
|
||
|
|
|
||
|
|
let version_obj = ($item | get version? | default {})
|
||
|
|
let current_version = ($version_obj | get current? | default "")
|
||
|
|
let source = ($version_obj | get source? | default "")
|
||
|
|
let tags = ($version_obj | get tags? | default "")
|
||
|
|
let site = ($version_obj | get site? | default "")
|
||
|
|
let detector_obj = ($item | get detector? | default {})
|
||
|
|
|
||
|
|
# Transform to our configuration format
|
||
|
|
let source_config = (if ($source | is-not-empty) {
|
||
|
|
if ($source | str contains "github") {
|
||
|
|
let repo_parse = ($source | parse -r 'github\.com/(?<repo>.+?)(/releases)?$')
|
||
|
|
let repo = (if ($repo_parse | is-empty) { "" } else { $repo_parse | get 0.repo | default "" })
|
||
|
|
{ type: "github", repo: $repo }
|
||
|
|
} else {
|
||
|
|
{ type: "url", url: $source }
|
||
|
|
}
|
||
|
|
} else if ($tags | is-not-empty) {
|
||
|
|
if ($tags | str contains "github") {
|
||
|
|
let repo_parse = ($tags | parse -r 'github\.com/(?<repo>.+?)(/tags)?$')
|
||
|
|
let repo = (if ($repo_parse | is-empty) { "" } else { $repo_parse | get 0.repo | default "" })
|
||
|
|
{ type: "github", repo: $repo }
|
||
|
|
} else {
|
||
|
|
{ type: "url", url: $tags }
|
||
|
|
}
|
||
|
|
} else {
|
||
|
|
{}
|
||
|
|
})
|
||
|
|
|
||
|
|
let config = {
|
||
|
|
id: $tool_name
|
||
|
|
type: ($context.type? | default "unknown")
|
||
|
|
category: ($context.category | default "")
|
||
|
|
version: $current_version
|
||
|
|
fixed: false
|
||
|
|
source: $source_config
|
||
|
|
detector: $detector_obj
|
||
|
|
comparison: "semantic"
|
||
|
|
metadata: {
|
||
|
|
source_file: $file_path
|
||
|
|
site: $site
|
||
|
|
description: ""
|
||
|
|
install_cmd: ""
|
||
|
|
lib: ""
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
if ($config | is-not-empty) {
|
||
|
|
$configs = ($configs | append $config)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
$configs
|
||
|
|
}
|
||
|
|
|
||
|
|
# Extract context from path
|
||
|
|
export def extract-context [
|
||
|
|
dir_path: string
|
||
|
|
] {
|
||
|
|
let parts = ($dir_path | split row "/")
|
||
|
|
|
||
|
|
# Determine type based on path structure
|
||
|
|
let type = if ($parts | any { |p| $p == "providers" }) {
|
||
|
|
"provider"
|
||
|
|
} else if ($parts | any { |p| $p == "taskservs" }) {
|
||
|
|
"taskserv"
|
||
|
|
} else if ($parts | any { |p| $p == "clusters" }) {
|
||
|
|
"cluster"
|
||
|
|
} else if ($parts | any { |p| $p == "tools" }) {
|
||
|
|
"tool"
|
||
|
|
} else {
|
||
|
|
"generic"
|
||
|
|
}
|
||
|
|
|
||
|
|
# Extract category/subcategory
|
||
|
|
let category = if $type == "provider" {
|
||
|
|
$parts | skip while { |p| $p != "providers" } | skip 1 | first
|
||
|
|
} else if $type == "taskserv" {
|
||
|
|
$parts | skip while { |p| $p != "taskservs" } | skip 1 | first
|
||
|
|
} else {
|
||
|
|
""
|
||
|
|
}
|
||
|
|
|
||
|
|
{
|
||
|
|
type: $type
|
||
|
|
category: $category
|
||
|
|
path: $dir_path
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
# Create configuration object
|
||
|
|
export def create-configuration [
|
||
|
|
id: string
|
||
|
|
data: record
|
||
|
|
context: record
|
||
|
|
source_file: string
|
||
|
|
] {
|
||
|
|
# Build detector configuration
|
||
|
|
let detector = if (($data | get check_cmd? | default null) != null) {
|
||
|
|
{
|
||
|
|
method: "command"
|
||
|
|
command: $data.check_cmd
|
||
|
|
pattern: ($data | get parse_pattern? | default "")
|
||
|
|
capture: ($data | get capture_group? | default "version")
|
||
|
|
}
|
||
|
|
} else if (($data | get detector? | default null) != null) {
|
||
|
|
$data.detector
|
||
|
|
} else {
|
||
|
|
{}
|
||
|
|
}
|
||
|
|
|
||
|
|
# Build source configuration
|
||
|
|
let source = if (($data | get source? | default null) != null) {
|
||
|
|
if ($data.source | str contains "github.com") {
|
||
|
|
let parsed = ($data.source | parse -r 'github\.com/(?<repo>.+)')
|
||
|
|
let repo_str = if ($parsed | is-empty) { "" } else { $parsed | get 0 | get repo? | default "" }
|
||
|
|
let repo_final = ($repo_str | str replace -r '/(releases|tags).*$' '')
|
||
|
|
{
|
||
|
|
type: "github"
|
||
|
|
repo: $repo_final
|
||
|
|
}
|
||
|
|
} else if ($data.source | str starts-with "docker") {
|
||
|
|
{
|
||
|
|
type: "docker"
|
||
|
|
image: ($data.source | str replace "docker://" "")
|
||
|
|
}
|
||
|
|
} else if ($data.source | str starts-with "http") {
|
||
|
|
{
|
||
|
|
type: "url"
|
||
|
|
url: $data.source
|
||
|
|
field: ($data | get version_field? | default "")
|
||
|
|
}
|
||
|
|
} else {
|
||
|
|
{ type: "custom", config: $data.source }
|
||
|
|
}
|
||
|
|
} else if (($data | get tags? | default null) != null) {
|
||
|
|
# Infer from tags URL
|
||
|
|
if ($data.tags | str contains "github") {
|
||
|
|
let parsed = ($data.tags | parse -r 'github\.com/(?<repo>[^/]+/[^/]+)')
|
||
|
|
let repo_str = if ($parsed | is-empty) { "" } else { $parsed | get 0 | get repo? | default "" }
|
||
|
|
{
|
||
|
|
type: "github"
|
||
|
|
repo: $repo_str
|
||
|
|
}
|
||
|
|
} else {
|
||
|
|
{ type: "url", url: $data.tags }
|
||
|
|
}
|
||
|
|
} else {
|
||
|
|
{}
|
||
|
|
}
|
||
|
|
|
||
|
|
# Build complete configuration
|
||
|
|
{
|
||
|
|
id: $id
|
||
|
|
type: $context.type
|
||
|
|
category: ($context.category | default "")
|
||
|
|
version: ($data | get version? | default "")
|
||
|
|
fixed: ($data | get fixed? | default false)
|
||
|
|
source: $source
|
||
|
|
detector: $detector
|
||
|
|
comparison: ($data | get comparison? | default "semantic")
|
||
|
|
metadata: {
|
||
|
|
source_file: $source_file
|
||
|
|
site: ($data | get site? | default "")
|
||
|
|
description: ($data | get description? | default "")
|
||
|
|
install_cmd: ($data | get install_cmd? | default "")
|
||
|
|
lib: ($data | get lib? | default "")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
# Extract version info from Nickel content
|
||
|
|
export def extract-nickel-versions [
|
||
|
|
content: string
|
||
|
|
] {
|
||
|
|
mut versions = []
|
||
|
|
|
||
|
|
# Look for schema definitions with version fields
|
||
|
|
let lines = ($content | lines)
|
||
|
|
mut current_schema = ""
|
||
|
|
mut current_data = {}
|
||
|
|
|
||
|
|
for line in $lines {
|
||
|
|
if ($line | str contains "schema ") {
|
||
|
|
# New schema found
|
||
|
|
if ($current_schema | is-not-empty) and (($current_data | get version? | default null) != null) {
|
||
|
|
$versions = ($versions | append {
|
||
|
|
name: $current_schema
|
||
|
|
...$current_data
|
||
|
|
})
|
||
|
|
}
|
||
|
|
let parsed_schema = ($line | parse -r 'schema\s+(\w+)')
|
||
|
|
$current_schema = if ($parsed_schema | is-empty) { "" } else { ($parsed_schema | get 0 | get capture0? | default "") }
|
||
|
|
$current_data = {}
|
||
|
|
} else if ($line | str contains "version:") or ($line | str contains "version =") {
|
||
|
|
# Extract version
|
||
|
|
let parsed_version = ($line | parse -r 'version[:\s=]+"?([^"]+)"?')
|
||
|
|
let version = if ($parsed_version | is-empty) { "" } else { ($parsed_version | get 0 | get capture0? | default "") }
|
||
|
|
if ($version | is-not-empty) {
|
||
|
|
$current_data.version = $version
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
# Add last schema if valid
|
||
|
|
if ($current_schema | is-not-empty) and (($current_data | get version? | default null) != null) {
|
||
|
|
$versions = ($versions | append {
|
||
|
|
name: $current_schema
|
||
|
|
...$current_data
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
$versions
|
||
|
|
}
|