#!/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/(?.+?)(/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/(?.+?)(/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/(?.+)') 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/(?[^/]+/[^/]+)') 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 }