#!/usr/bin/env nu # Taskserv/Component Discovery System # Discovers available components (flat structure) and legacy taskservs (grouped structure). # Post-migration: extensions/components/ is the primary source; extensions/taskservs/ is legacy. use ../lib_provisioning/config/accessor.nu config-get use ../lib_provisioning/utils/nickel_processor.nu [ncl-eval-soft] # Resolve the components base path using all available signals. def _components-path []: nothing -> string { let from_env = ($env.PROVISIONING_COMPONENTS_PATH? | default "") if ($from_env | is-not-empty) and ($from_env | path exists) { return $from_env } let prov = ($env.PROVISIONING? | default "") if ($prov | is-not-empty) { let derived = ($prov | path join "extensions" | path join "components") if ($derived | path exists) { return $derived } } config-get "paths.components" "" } # Discover all available taskservs/components. # Searches components/ (flat, primary) then taskservs/ (grouped, legacy). # Returns a unified list compatible with existing callers. export def discover-taskservs [] { mut results = [] # Primary: flat components/ directory (post-migration) let comp_path = (_components-path) if ($comp_path | is-not-empty) and ($comp_path | path exists) { let items = (do { ls $comp_path } | complete) if $items.exit_code == 0 { for item in ($items.stdout | where type == "dir") { let name = ($item.name | path basename) let nickel_dir = ($item.name | path join "nickel") if not ($nickel_dir | path exists) { continue } $results = ($results | append { name: $name type: "component" group: "" version: "" schema_path: $nickel_dir main_schema: "" dependencies: [] description: "" available: true last_updated: $item.modified }) } } } # Legacy: grouped taskservs/ directory (non-migrated workspaces) let ts_path_raw = (config-get "paths.taskservs" "") if ($ts_path_raw | is-not-empty) { let ts_path = ($ts_path_raw | path expand) if ($ts_path | path exists) and $ts_path != $comp_path { let items = (do { ls $ts_path } | complete) if $items.exit_code == 0 { for item in ($items.stdout | where type == "dir") { let item_name = ($item.name | path basename) let schema_dir = ($item.name | path join "nickel") let mod_path = ($schema_dir | path join "nickel.mod") # Group dir (has nickel/nickel.mod): scan applications inside if ($mod_path | path exists) { let subs = (do { ls $item.name } | complete) if $subs.exit_code == 0 { for sub in ($subs.stdout | where type == "dir" | where {|s| let n = ($s.name | path basename) $n != "nickel" and $n != "images" }) { let app_name = ($sub.name | path basename) # Skip if already found in components/ if ($results | any {|r| $r.name == $app_name}) { continue } $results = ($results | append { name: $app_name type: "taskserv" group: $item_name version: "" schema_path: $schema_dir main_schema: "" dependencies: [] description: "" available: true last_updated: $sub.modified }) } } } } } } } $results | sort-by name } # Extract metadata from a taskserv's Nickel module (updated with group info) def extract_taskserv_metadata [name: string, schema_path: string, group: string] { let mod_path = ($schema_path | path join "nickel.mod") # Try to parse TOML, skip if corrupted let toml_result = (do { open $mod_path | from toml } | complete) if $toml_result.exit_code != 0 { print $"⚠️ Skipping ($name): corrupted nickel.mod file" return null } let mod_content = $toml_result.stdout # Find Nickel schema files let schema_files = (glob ($schema_path | path join "*.ncl")) let main_schema = ($schema_files | where ($it | str contains $name) | first | default "") # Extract dependencies let dependencies = ($mod_content.dependencies? | default {} | columns) # Get description from schema file if available let description = if ($main_schema != "") { extract_schema_description $main_schema } else { "" } { name: $name type: "taskserv" group: $group version: $mod_content.package.version schema_path: $schema_path main_schema: $main_schema dependencies: $dependencies description: $description available: true last_updated: (ls $mod_path | get 0.modified) } } # Extract description from Nickel schema file def extract_schema_description [schema_file: string] { if not ($schema_file | path exists) { return "" } # Read first few lines to find description let content = (open $schema_file | lines | take 10) let description_lines = ($content | where ($it | str starts-with "# ") | take 3) if ($description_lines | is-empty) { return "" } $description_lines | str replace "^# " "" | str join " " | str trim } # Search taskservs by name or description export def search-taskservs [query: string] { discover-taskservs | where ($it.name | str contains $query) or ($it.description | str contains $query) } # Get specific taskserv info (updated to search both flat and grouped) export def get-taskserv-info [name: string] { let taskservs = (discover-taskservs) let found = ($taskservs | where name == $name | first) if ($found | is-empty) { error make { msg: $"Taskserv '($name)' not found" } } $found } # List taskservs by group export def list-taskservs-by-group [group: string] { discover-taskservs | where group == $group } # List all groups export def list-taskserv-groups [] { discover-taskservs | get group | uniq | sort } # List taskservs by category/tag (legacy support) export def list-taskservs-by-tag [tag: string] { discover-taskservs | where ($it.description | str contains $tag) or ($it.group | str contains $tag) } # Validate taskserv availability export def validate-taskservs [names: list] { let available = (discover-taskservs | get name) let missing = ($names | where ($it not-in $available)) let found = ($names | where ($it in $available)) { requested: $names found: $found missing: $missing valid: ($missing | is-empty) } } # Get the resolved directory for a taskserv or component by name. # Returns the directory containing nickel/, taskserv/, etc. # Prefers components/ (flat, post-migration) over taskservs/ (grouped, legacy). export def get-taskserv-path [name: string]: nothing -> string { let info = get-taskserv-info $name # Component (flat structure) — base is already the directory if $info.type == "component" { let comp_base = (_components-path) return ($comp_base | path join $name) } # Legacy grouped taskserv let base_path = ($env.PROVISIONING? | default "" | path join "extensions/taskservs") if $info.group == "" or $info.group == "root" { $"($base_path)/($name)" } else { $"($base_path)/($info.group)/($name)" } } # Resolve the components base path from config (flat layout, no group dirs) def components-base-path []: nothing -> string { let explicit = (do -i { config-get "paths.components" } | complete) if $explicit.exit_code == 0 { $explicit.stdout | str trim | path expand } else { let ts_path = (config-get "paths.taskservs" | path expand) $ts_path | path dirname | path join "components" } } # Discover all available components (flat structure: components/{name}/) export def discover-components []: nothing -> list { let base = (components-base-path) if not ($base | path exists) { error make { msg: $"Components path not found: ($base)" } } ls $base | where type == "dir" | each {|item| let name = ($item.name | path basename) let meta_p = ($item.name | path join "metadata.ncl") let ncl_p = ($item.name | path join "nickel") let modes = if ($meta_p | path exists) { ncl-eval-soft $meta_p [] [] | get -o modes | default ["taskserv"] } else { ["taskserv"] } let version = if ($meta_p | path exists) { ncl-eval-soft $meta_p [] "" | get -o version | default "" } else { "" } let description = if ($meta_p | path exists) { ncl-eval-soft $meta_p [] "" | get -o description | default "" } else { "" } { name: $name type: "component" modes: $modes version: $version description: $description path: $item.name available: ($ncl_p | path exists) } } | sort-by name } # Return the filesystem path for a named component export def get-component-path [name: string]: nothing -> string { $"(components-base-path)/($name)" } # Return the first mode declared in a component's metadata.ncl export def get-component-mode [name: string]: nothing -> string { let meta_p = (get-component-path $name | path join "metadata.ncl") if not ($meta_p | path exists) { error make { msg: $"metadata.ncl not found for component '($name)'" } } let parsed = (ncl-eval-soft $meta_p [] null) if ($parsed | is-empty) { error make { msg: $"Failed to parse metadata.ncl for component '($name)'" } } $parsed | get -o modes | default ["taskserv"] | first } # Search components by name or description substring export def search-components [query: string]: nothing -> list { discover-components | where ($it.name | str contains $query) or ($it.description | str contains $query) }