313 lines
15 KiB
Text
313 lines
15 KiB
Text
|
|
#!/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)" }
|
||
|
|
}
|