#!/usr/bin/env nu # Component management module — list, show, status for extensions/components. # # Two perspectives per component: # extension — what exists in extensions/components/{name}/ (metadata, modes, contract) # workspace — how it's instantiated in infra/{ws}/components/{name}.ncl # # Ontology data (FSM state, edges) is read via ontoref when available (defensive). use ../lib_provisioning/utils/nickel_processor.nu [ncl-eval-soft, default-ncl-paths] # Resolve the extensions/components/ base path. def _comp-ext-base []: 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 p = ($prov | path join "extensions" | path join "components") if ($p | path exists) { return $p } } "" } # Resolve the workspace root for a given workspace name. # Checks PROVISIONING_KLOUD_PATH env, then walks known workspace directories. def _ws-root [workspace: string]: nothing -> string { if ($workspace | is-empty) { return "" } let from_env = ($env.PROVISIONING_KLOUD_PATH? | default "") if ($from_env | is-not-empty) and ($from_env | path basename) == $workspace { return $from_env } let prov = ($env.PROVISIONING? | default "") if ($prov | is-not-empty) { let ws_root = ($prov | path dirname | path join "workspaces" | path join $workspace) if ($ws_root | path exists) { return $ws_root } } "" } # Export a Nickel file to a record. Returns null on failure. # Uses default-ncl-paths to match the daemon's cache key derivation. def _ncl-export [file_path: string]: nothing -> any { let ws_root = ($file_path | path dirname | path dirname | path dirname) ncl-eval-soft $file_path (default-ncl-paths $ws_root) null } # Read FSM dimension for a component from state.ncl via ontoref or raw NCL export. def _read-fsm-state [name: string, ws_root: string]: nothing -> record { let dim_id = $"($name)-status" # Try ontoref first (richer output) let onto_result = (do { ^ontoref describe state $dim_id --fmt json --workspace $ws_root } | complete) if $onto_result.exit_code == 0 { let parsed = (do { $onto_result.stdout | from json } | complete) if $parsed.exit_code == 0 { return $parsed.stdout } } # Fallback: export state.ncl and filter let state_path = ($ws_root | path join ".ontology" | path join "state.ncl") if not ($state_path | path exists) { return {} } let prov = ($env.PROVISIONING? | default "") let state_data = (ncl-eval-soft $state_path (default-ncl-paths $ws_root) {}) if ($state_data | is-empty) { return {} } let dims = ($state_data | get -o dimensions | default []) $dims | where {|d| ($d | get -o id | default "") == $dim_id } | get 0? | default {} } # Read ontology node and edges for a component from core.ncl. def _read-onto-node [name: string, ws_root: string]: nothing -> record { let core_path = ($ws_root | path join ".ontology" | path join "core.ncl") if not ($core_path | path exists) { return { node: null, edges_from: [], edges_to: [] } } let prov = ($env.PROVISIONING? | default "") let data = (ncl-eval-soft $core_path (default-ncl-paths $ws_root) null) if $data == null { return { node: null, edges_from: [], edges_to: [] } } let nodes = ($data | get -o nodes | default []) let edges = ($data | get -o edges | default []) let node = ($nodes | where {|n| ($n | get -o id | default "") == $name } | get 0? | default null) let edges_from = ($edges | where {|e| ($e | get -o from | default "") == $name }) let edges_to = ($edges | where {|e| ($e | get -o to | default "") == $name }) { node: $node, edges_from: $edges_from, edges_to: $edges_to } } # List all components from extensions/components/ with optional mode filter and workspace state. export def component-list [mode: string, workspace: string]: nothing -> nothing { let base = (_comp-ext-base) if ($base | is-empty) or not ($base | path exists) { print "❌ extensions/components/ not found. Set PROVISIONING env var." return } let ws_root = (_ws-root $workspace) let show_state = ($ws_root | is-not-empty) mut rows = [] for item in (ls $base | where type == "dir") { let name = ($item.name | path basename) let meta_p = ($item.name | path join "metadata.ncl") let meta = if ($meta_p | path exists) { _ncl-export $meta_p } else { null } let modes = if $meta != null { $meta | get -o modes | default ["taskserv"] } else { ["taskserv"] } let version = if $meta != null { $meta | get -o version | default "" } else { "" } let desc = if $meta != null { $meta | get -o description | default "" } else { "" } # Mode filter if ($mode | is-not-empty) and ($mode not-in $modes) { continue } let state = if $show_state { let dim = (_read-fsm-state $name $ws_root) if ($dim | is-empty) { "—" } else { let cur = ($dim | get -o current_state | default "—") let des = ($dim | get -o desired_state | default "") if ($des | is-not-empty) and $cur != $des { $"($cur) → ($des)" } else { $cur } } } else { "—" } $rows = ($rows | append { name: $name mode: ($modes | str join "·") state: $state version: $version }) } if ($rows | is-empty) { print "No components found." return } let header = if $show_state { $"Components [workspace: ($workspace)]" } else { "Components [extension catalog]" } print $header print "────────────────────────────────────────────────────────────" $rows | table } # Show full details for a named component. export def component-show [name: string, workspace: string, ext_only: bool]: nothing -> nothing { let base = (_comp-ext-base) let ext_dir = ($base | path join $name) if not ($ext_dir | path exists) { print $"❌ Component '($name)' not found in extensions/components/" return } let meta_p = ($ext_dir | path join "metadata.ncl") let meta = if ($meta_p | path exists) { _ncl-export $meta_p } else { null } # Extension section let modes = if $meta != null { $meta | get -o modes | default ["taskserv"] } else { ["taskserv"] } let version = if $meta != null { $meta | get -o version | default "" } else { "" } let desc = if $meta != null { $meta | get -o description | default "" } else { "" } let tags = if $meta != null { $meta | get -o tags | default [] | str join " · " } else { "" } # Defaults (requires/provides/operations from nickel/defaults.ncl) let defaults_p = ($ext_dir | path join "nickel" | path join "defaults.ncl") let defaults = if ($defaults_p | path exists) { _ncl-export $defaults_p } else { null } let def_rec = if $defaults != null { $defaults | get -o $name | default {} } else { {} } let requires = ($def_rec | get -o requires | default {}) let provides = ($def_rec | get -o provides | default {}) let operations = ($def_rec | get -o operations | default {}) print $"┌─ ($name | str upcase) ─────────────────────────────────" print $"│ ($desc)" print $"├────────────────────────────────────────────────────────" let modes_str = ($modes | str join " · ") print $"│ VERSION ($version)" print $"│ MODES ($modes_str)" if ($tags | is-not-empty) { print $"│ TAGS ($tags)" } # REQUIRES let req_storage = ($requires | get -o storage | default null) let req_ports = ($requires | get -o ports | default []) let req_creds = ($requires | get -o credentials | default []) if $req_storage != null or ($req_ports | is-not-empty) or ($req_creds | is-not-empty) { print "├─── REQUIRES ───────────────────────────────────────────" if $req_storage != null { let persist_label = if ($req_storage.persistent? | default false) { "persistent" } else { "ephemeral" } let stor_size = ($req_storage.size? | default "?") print $"│ storage ($stor_size) ($persist_label)" } for p in $req_ports { let pport = ($p.port? | default 0 | into string) let pproto = ($p.protocol? | default "TCP") let pexpose = ($p.exposure? | default "internal") print $"│ port ($pport)/($pproto) \(($pexpose)\)" } if ($req_creds | is-not-empty) { let creds_str = ($req_creds | str join " · ") print $"│ creds ($creds_str)" } } # PROVIDES let prov_svc = ($provides | get -o service | default "") let prov_port = ($provides | get -o port | default null) let prov_dbs = ($provides | get -o databases | default []) if ($prov_svc | is-not-empty) or $prov_port != null or ($prov_dbs | is-not-empty) { print "├─── PROVIDES ───────────────────────────────────────────" if ($prov_svc | is-not-empty) and $prov_port != null { print $"│ service ($prov_svc):($prov_port)" } else if ($prov_svc | is-not-empty) { print $"│ service ($prov_svc)" } if ($prov_dbs | is-not-empty) { let dbs_str = ($prov_dbs | str join " · ") print $"│ databases ($dbs_str)" } } # OPERATIONS let ops_enabled = ($operations | transpose k v | where v == true | each {|r| $r.k }) if ($ops_enabled | is-not-empty) { let ops_str = ($ops_enabled | str join " · ") print "├─── OPERATIONS ─────────────────────────────────────────" print $"│ ($ops_str)" } if not $ext_only and ($workspace | is-not-empty) { let ws_root = (_ws-root $workspace) if ($ws_root | is-not-empty) { # Workspace instance let comp_p = ($ws_root | path join "infra" | path join $workspace | path join "components" | path join $"($name).ncl") let comp_data = if ($comp_p | path exists) { _ncl-export $comp_p } else { null } let inst = if $comp_data != null { $comp_data | get -o $name | default {} } else { {} } let inst_mode = ($inst | get -o mode | default "") let inst_ns = ($inst | get -o namespace | default "") let inst_tgt = ($inst | get -o target | default "") print "├─── WORKSPACE INSTANCE ─────────────────────────────────" if ($inst_mode | is-not-empty) { print $"│ mode ($inst_mode)" } if ($inst_ns | is-not-empty) { print $"│ namespace ($inst_ns)" } if ($inst_tgt | is-not-empty) { print $"│ target ($inst_tgt)" } # FSM state let dim = (_read-fsm-state $name $ws_root) if not ($dim | is-empty) { let cur = ($dim | get -o current_state | default "—") let des = ($dim | get -o desired_state | default "") let blk = ($dim | get -o transitions | default [] | get 0? | default {} | get -o blocker | default "") let blk_short = ($blk | str substring 0..80) print "├─── STATE ───────────────────────────────────────────" print $"│ current ($cur)" if ($des | is-not-empty) { print $"│ desired ($des)" } if ($blk | is-not-empty) { print $"│ blocker ($blk_short)" } } # Ontology let onto = (_read-onto-node $name $ws_root) if $onto.node != null { let node = $onto.node let node_lvl = ($node.level? | default "?") let node_pole = ($node.pole? | default "?") print "├─── ONTOLOGY ────────────────────────────────────────" print $"│ node ($name) \(($node_lvl) / ($node_pole)\)" let arts = ($node | get -o artifact_paths | default []) if ($arts | is-not-empty) { let arts_str = ($arts | str join " · ") print $"│ artifacts ($arts_str)" } let adrs = ($node | get -o adrs | default []) if ($adrs | is-not-empty) { let adrs_str = ($adrs | str join " · ") print $"│ adrs ($adrs_str)" } if ($onto.edges_from | is-not-empty) { let consumers = ($onto.edges_from | each {|e| let eto = ($e | get -o to | default "?") let ekind = ($e | get -o kind | default "") $"($eto) \(($ekind)\)" } | str join " · ") print $"│ used-by ($consumers)" } if ($onto.edges_to | is-not-empty) { let uses = ($onto.edges_to | each {|e| let efrom = ($e | get -o from | default "?") let ekind = ($e | get -o kind | default "") $"($efrom) \(($ekind)\)" } | str join " · ") print $"│ uses ($uses)" } } } } print "└────────────────────────────────────────────────────────" } # Show only FSM state for a component. export def component-status [name: string, workspace: string]: nothing -> nothing { if ($workspace | is-empty) { print "❌ --workspace required for status" return } let ws_root = (_ws-root $workspace) if ($ws_root | is-empty) { print $"❌ Workspace '($workspace)' not found" return } let dim = (_read-fsm-state $name $ws_root) if ($dim | is-empty) { print $"No FSM dimension found for '($name)-status' in ($workspace)" return } let cur = ($dim | get -o current_state | default "—") let des = ($dim | get -o desired_state | default "—") let blk = ($dim | get -o transitions | default [] | get 0? | default {} | get -o blocker | default "") let cat = ($dim | get -o transitions | default [] | get 0? | default {} | get -o catalyst | default "") print $"($name) — FSM state [($workspace)]" print $" current: ($cur)" print $" desired: ($des)" if ($blk | is-not-empty) { print $" blocker: ($blk)" } if ($cat | is-not-empty) { print $" catalyst: ($cat)" } }