# List all servers in active workspace # This file is sourced by bash after lib_minimal.nu is loaded # Not meant to be run standalone # Usage: Called from bash with optional $INFRA_FILTER environment variable # PWD-based workspace detection: if we're inside a workspace root that has # config/provisioning.ncl, use it — takes precedence over the active workspace. let pwd_config_file = ($env.PWD | path join "config" "provisioning.ncl") use ../lib_provisioning/utils/nickel_processor.nu [ncl-eval-soft] let pwd_ws_config = if ($pwd_config_file | path exists) { ncl-eval-soft $pwd_config_file [] {} } else { {} } let pwd_ws_name = ($pwd_ws_config | get --optional workspace | default "") let pwd_current_infra = ($pwd_ws_config | get --optional current_infra | default "") # Convention fallback: if config/provisioning.ncl has no current_infra but # infra//settings.ncl exists, that's the default infra. let pwd_convention_infra = if ($pwd_current_infra | is-empty) { let candidate = ($env.PWD | path join "infra" ($env.PWD | path basename) | path join "settings.ncl") if ($candidate | path exists) { $env.PWD | path basename } else { "" } } else { "" } let pwd_infra = if ($pwd_current_infra | is-not-empty) { $pwd_current_infra } else { $pwd_convention_infra } # Resolve workspace: PWD-inferred takes precedence over session active workspace let ws_path = if ($pwd_ws_name | is-not-empty) { # We are inside the workspace root — PWD is the workspace path $env.PWD } else { # Fall back to active workspace from user_config.yaml let ws_result = (workspace-active) let active_ws = if (is-ok $ws_result) { $ws_result.ok } else { "" } if ($active_ws | is-empty) { print 'No active workspace. Run: provisioning workspace activate ' exit 1 } let user_config_path = ( $env.HOME | path join 'Library' | path join 'Application Support' | path join 'provisioning' | path join 'user_config.yaml' ) if not ($user_config_path | path exists) { print 'Config not found' exit 1 } let config = (open $user_config_path) let workspaces = ($config | get --optional workspaces | default []) let ws = ($workspaces | where { $in.name == $active_ws } | first) if ($ws | is-empty) { print $"Workspace '($active_ws)' not found in user config" exit 1 } $ws.path } let infra_path = ($ws_path | path join 'infra') if not ($infra_path | path exists) { print 'No infrastructures found' exit 0 } # Resolve filter: explicit INFRA_FILTER > PWD current_infra > convention > workspace default_infra let filter_raw = ($env.INFRA_FILTER? | default "") let filter = if ($filter_raw | is-not-empty) { $filter_raw | path basename } else if ($pwd_infra | is-not-empty) { $pwd_infra } else { # Last resort: registered workspace default_infra from user config let ws_result2 = (workspace-active) let active_ws2 = if (is-ok $ws_result2) { $ws_result2.ok } else { "" } if ($active_ws2 | is-not-empty) { let uc = ( $env.HOME | path join 'Library' | path join 'Application Support' | path join 'provisioning' | path join 'user_config.yaml' ) if ($uc | path exists) { let wlist = (open $uc | get --optional workspaces | default []) let wentry = ($wlist | where { $in.name == $active_ws2 } | first) $wentry | get --optional default_infra | default "" } else { "" } } else { "" } } # List server definitions from infrastructure (filtered if --infra specified) let servers = ( ls $infra_path | where type == 'dir' | each {|infra| let infra_name = ($infra.name | path basename) # Skip if filter is specified and doesn't match if (($filter | is-not-empty) and ($infra_name != $filter)) { [] } else { # servers.ncl can live directly in infra dir or under defs/ let infra_dir = ($infra_path | path join $infra_name) let servers_file_direct = ($infra_dir | path join 'servers.ncl') let servers_file_defs = ($infra_dir | path join 'defs' | path join 'servers.ncl') let servers_file = if ($servers_file_direct | path exists) { $servers_file_direct } else { $servers_file_defs } if ($servers_file | path exists) { # Parse servers.ncl: correlate hostname / server_type / private_ip per block. # Strategy: scan lines in order; a new server block begins at each `make_server {` # (or at the first hostname = "..." after the previous block closes). # We accumulate fields until the next block starts. let lines = (open $servers_file --raw | split row "\n") let extract_quoted = {|line| let parts = ($line | split row '"') if ($parts | length) >= 2 { $parts | get 1 } else { "" } } # Build one record per server by scanning lines top-to-bottom. # Reset on each `make_server {` boundary. let parsed = ( $lines | reduce --fold {blocks: [], cur: {hostname: "", server_type: "", private_ip: ""}} {|line, acc| let trimmed = ($line | str trim) if ($trimmed =~ 'make_server\s*\{') { # flush previous if it had a hostname let blocks = if ($acc.cur.hostname | is-not-empty) { $acc.blocks | append $acc.cur } else { $acc.blocks } {blocks: $blocks, cur: {hostname: "", server_type: "", private_ip: ""}} } else if ($trimmed =~ '^hostname\s*=\s*"') { {blocks: $acc.blocks, cur: ($acc.cur | upsert hostname (do $extract_quoted $trimmed))} } else if ($trimmed =~ '^server_type\s*=\s*"') { {blocks: $acc.blocks, cur: ($acc.cur | upsert server_type (do $extract_quoted $trimmed))} } else if ($trimmed =~ '^private_ip\s*=\s*"') { {blocks: $acc.blocks, cur: ($acc.cur | upsert private_ip (do $extract_quoted $trimmed))} } else { $acc } } ) # flush last block let all_blocks = if ($parsed.cur.hostname | is-not-empty) { $parsed.blocks | append $parsed.cur } else { $parsed.blocks } $all_blocks | where {|b| $b.hostname | is-not-empty } | each {|b| { name: $b.hostname infrastructure: $infra_name server_type: $b.server_type private_ip: $b.private_ip path: $servers_file } } } else { [] } } } | flatten ) # Read persisted server state (written by server_create workflow post-sync) # Key: server name → { provider_id, public_ip, location, status, floating_ip, floating_ip_address } let cached_state_path = ($ws_path | path join "infra" | path join $filter | path join ".servers-state.json") let cached_state = if ($filter | is-not-empty) and ($cached_state_path | path exists) { open $cached_state_path } else { {} } # Bootstrap state: FIP name → actual IP (fallback when server not in cached_state) let bs_state_path = ($ws_path | path join ".provisioning-state.json") let bs_fips = if ($bs_state_path | path exists) { open $bs_state_path | get -o bootstrap.floating_ips | default {} } else { {} } # Query live status from hcloud for real-time status updates let hcloud_res = (do { ^hcloud server list -o json } | complete) let live_servers_all = if $hcloud_res.exit_code == 0 and ($hcloud_res.stdout | str trim | is-not-empty) { let parsed = ($hcloud_res.stdout | from json) if (($parsed | describe) | str starts-with "list") { $parsed } else { [] } } else { [] } let live_servers = if ($filter | is-not-empty) { $live_servers_all | where {|l| ($servers | any {|s| $s.name == $l.name }) } } else { $live_servers_all } def status_icon [s: string] { match $s { "running" => "🟢" "off" => "🔴" "starting" => "🟡" "stopping" => "🟡" "rebuilding" => "🔵" "migrating" => "🔵" _ => "⚪" } } if ($servers | length) == 0 { print '📦 Available Servers: (none configured)' } else { print '' let rows = ($servers | each {|srv| let live = ($live_servers | where {|l| $l.name == $srv.name} | first | default null) let cached = ($cached_state | get -o $srv.name | default null) # Status: hcloud live > cached state > unknown let status = if $live != null { $live.status } else if $cached != null { $cached.status } else { "—" } # Public IP: hcloud live > cached state let pub_ip = if $live != null { $live.public_net?.ipv4?.ip? | default "" } else if $cached != null { $cached.public_ip? | default "" } else { "" } # Private IP: hcloud live (actual) > NCL (desired) let priv_ip = if $live != null { $live.private_net? | default [] | first | default null | get --optional ip | default "" } else { $srv.private_ip? | default "" } # Server type: hcloud live > NCL (type is config, not runtime state) let srv_type = if $live != null { $live.server_type?.name? | default ($srv.server_type? | default "") } else { $srv.server_type? | default "" } # Location: hcloud live > cached state let location = if $live != null { $live.datacenter?.location?.name? | default "" } else if $cached != null { $cached.location? | default "" } else { "" } # Floating IP: cached state (has name+ip) > bootstrap state lookup by FIP name let fip_display = if $cached != null and ($cached.floating_ip? | default "" | is-not-empty) { let fip_ip = ($cached.floating_ip_address? | default "") if ($fip_ip | is-not-empty) { $"($cached.floating_ip) ($fip_ip)" } else { $cached.floating_ip } } else { # Fallback: resolve FIP IP from bootstrap state using the FIP name in NCL let fip_name = ($srv | get -o floating_ip | default "") if ($fip_name | is-not-empty) { let fip_key = ($fip_name | str replace --all "librecloud-fip-" "" | str replace --all "-" "_") let fip_rec = ($bs_fips | get -o $fip_key | default null) if $fip_rec != null { $"($fip_name) ($fip_rec.ip? | default "")" } else { $fip_name } } else { "" } } # Delete protection: hcloud live > cached state let protected = if $live != null { $live.protection?.delete? | default false } else if $cached != null { $cached.protection_delete? | default false } else { false } let lock_icon = if $protected { "🔒" } else { "" } { hostname: $srv.name type: $srv_type location: $location status: $status public_ip: $pub_ip private_ip: $priv_ip floating_ip: $fip_display protected: $lock_icon provider: "hetzner" } } ) print ($rows | table -i false) }