# Platform Service Manager - Service management for deployment # Handles loading deployment configuration and service management use tools/nickel/process.nu [ncl-eval ncl-eval-soft] use platform/user/config.nu [get-active-workspace-details get-platform-config-dir] # Normalize service name: strip "provisioning-" or "provisioning_" prefix if present # Returns the normalized name (e.g., "provisioning_daemon" → "daemon") export def normalize-service-name [service_name: string] { if ($service_name | str starts-with "provisioning-") { $service_name | str replace "provisioning-" "" } else if ($service_name | str starts-with "provisioning_") { $service_name | str replace "provisioning_" "" } else { $service_name } } # Load deployment mode configuration from Nickel export def load-deployment-mode [] { # Prefer explicit env override; otherwise the OS-aware platform dir. let possible_paths = [ ($env.PROVISIONING_USER_PLATFORM? | default null), (get-platform-config-dir), ] let config_file = ( $possible_paths | where { |p| $p != null } | each { |p| $p | path expand | path join "deployment-mode.ncl" } | where { |p| $p | path exists } | get 0? ) if ($config_file == null) { error make {msg: "Deployment mode file not found in any of the expected locations"} } let import_path = ($env.PROVISIONING? | default "") let import_paths = if ($import_path | is-not-empty) { [$import_path] } else { [] } ncl-eval $config_file $import_paths } # Load individual service configuration export def load-service-config [service_name: string] { # Prefer explicit env override; otherwise the OS-aware platform dir. let possible_bases = [ ($env.PROVISIONING_USER_PLATFORM? | default null), (get-platform-config-dir), ] # Try to find the config file let config_file = ( $possible_bases | where { |b| $b != null } | each { |base| # Try both the underscore and dash versions let base_expanded = ($base | path expand) let path1 = ($base_expanded | path join "config" ($"($service_name).ncl")) let path2 = ($base_expanded | path join "config" ($"($service_name | str replace "_" "-").ncl")) if ($path1 | path exists) { $path1 } else if ($path2 | path exists) { $path2 } else { null } } | where { |p| $p != null } | get 0? ) if ($config_file == null) { return null } ncl-eval-soft $config_file [] null } # Get the port for a service from its per-service NCL config file. export def get-service-port [service_name: string] { let config = (load-service-config $service_name) if ($config == null) { return "?" } let service_key = $service_name | str replace "-" "_" if ($config | get --optional $service_key) != null { let service_config = ($config | get $service_key) if ($service_config | get --optional "server") != null { if ($service_config.server | get --optional "port") != null { return ($service_config.server.port | into string) } } if ($service_config | get --optional "build") != null { if ($service_config.build | get --optional "port") != null { return ($service_config.build.port | into string) } } if ($service_config | get --optional "http") != null { if ($service_config.http | get --optional "port") != null { return ($service_config.http.port | into string) } } if ($service_config | get --optional "port") != null { return ($service_config.port | into string) } } "?" } # Start required services based on deployment configuration export def start-required-services [] { let deployment = (load-deployment-mode) # Get enabled services from deployment config let services = $deployment.services let all_service_names = ($services | columns) # Filter to enabled services let enabled_services = ( $all_service_names | where {|name| let config = ($services | get $name) ($config | get --optional "enabled" | default false) } ) if ($enabled_services | length) == 0 { print "⚠ No services enabled in deployment-mode.ncl" return } # Get current running processes once let running_processes = (^ps aux) # Start each enabled service for service_name in $enabled_services { let port = (get-service-port $service_name) let normalized_name = (normalize-service-name $service_name) let binary_name = $"provisioning-($normalized_name | str replace "_" "-")" let is_running = ($running_processes | str contains $binary_name) # Check if binary exists let home = ($env.HOME? | default "~" | path expand) let binary_path = ($home | path join ".local/bin" $binary_name) if not ($binary_path | path exists) { print $"✗ ($service_name) binary not found" continue } # If already running, just report it if $is_running { let status_msg = $"((ansi green))started((ansi reset))" print $"✓ ($service_name) on port ($port) — ($status_msg)" continue } # Start in background print $"→ Starting ($service_name)..." let log_dir = ($home | path join ".provisioning/logs") (do { mkdir ($log_dir) } | ignore) let log_file = ($log_dir | path join $"($service_name).log") # Set environment variables for service let platform_path = (get-platform-config-dir) let provisioning_path = ($home | path join ".local/bin/provisioning") # Build start command (only orchestrator accepts --provisioning-path) let start_cmd = if ($normalized_name == "orchestrator") { $"nohup env PROVISIONING_USER_PLATFORM=\"($platform_path)\" PROVISIONING_CONFIG_DIR=\"($platform_path)\" ($binary_path) --provisioning-path \"($provisioning_path)\" >>\"($log_file)\" 2>&1 &" } else { $"nohup env PROVISIONING_USER_PLATFORM=\"($platform_path)\" PROVISIONING_CONFIG_DIR=\"($platform_path)\" ($binary_path) >>\"($log_file)\" 2>&1 &" } (^sh -c $start_cmd | ignore) sleep 2sec let started_msg = $"((ansi green))started((ansi reset))" print $"✓ ($service_name) on port ($port) — ($started_msg)" } # ncl-sync: Nickel config cache daemon — always started, independent of deployment mode. ncl-sync-start print "" } # Start specific services by name # Usage: start-services ["orchestrator", "vault_service"] or ["orchestrator,vault_service"] export def start-services [service_list: list] { # Parse service names (handle comma-separated strings) let services = ( $service_list | each { |item| $item | split row "," | each { |s| $s | str trim } } | flatten | where { |s| ($s | is-not-empty) } ) if ($services | length) == 0 { print "⚠ No services specified" return } # Start each service for service_name in $services { let normalized_name = (normalize-service-name $service_name) let port = (get-service-port $normalized_name) let binary_name = $"provisioning-($normalized_name | str replace "_" "-")" let is_running = ((^pgrep -f $"[/]($binary_name)$" | complete).exit_code == 0) # Check if binary exists let home = ($env.HOME? | default "~" | path expand) let binary_path = ($home | path join ".local/bin" $binary_name) if not ($binary_path | path exists) { print $"✗ ($service_name) binary not found" continue } # If already running, just report it if $is_running { let status_msg = $"((ansi green))started((ansi reset))" print $"✓ ($service_name) on port ($port) — ($status_msg)" continue } # Start in background print $"→ Starting ($service_name)..." let log_dir = ($home | path join ".provisioning/logs") (do { mkdir ($log_dir) } | ignore) let log_file = ($log_dir | path join $"($service_name).log") # Set environment variables for service let platform_path = (get-platform-config-dir) let provisioning_path = ($home | path join ".local/bin/provisioning") # Build start command (only orchestrator accepts --provisioning-path) let start_cmd = if ($normalized_name == "orchestrator") { $"nohup env PROVISIONING_USER_PLATFORM=\"($platform_path)\" PROVISIONING_CONFIG_DIR=\"($platform_path)\" ($binary_path) --provisioning-path \"($provisioning_path)\" >>\"($log_file)\" 2>&1 &" } else { $"nohup env PROVISIONING_USER_PLATFORM=\"($platform_path)\" PROVISIONING_CONFIG_DIR=\"($platform_path)\" ($binary_path) >>\"($log_file)\" 2>&1 &" } (^sh -c $start_cmd | ignore) sleep 2sec let started_msg = $"((ansi green))started((ansi reset))" print $"✓ ($service_name) on port ($port) — ($started_msg)" } ncl-sync-start print "" } # Stop specific services by name # Usage: stop-services ["orchestrator", "vault_service"] or ["orchestrator,vault_service"] export def stop-services [service_list: list] { # Parse service names (handle comma-separated strings) let services = ( $service_list | each { |item| $item | split row "," | each { |s| $s | str trim } } | flatten | where { |s| ($s | is-not-empty) } ) if ($services | length) == 0 { print "⚠ No services specified" return } # Stop each service for service_name in $services { # Normalize service name: strip "provisioning-" or "provisioning_" prefix if present let normalized_name = ( if ($service_name | str starts-with "provisioning-") { $service_name | str replace "provisioning-" "" } else if ($service_name | str starts-with "provisioning_") { $service_name | str replace "provisioning_" "" } else { $service_name } ) let port = (get-service-port $normalized_name) let binary_name = $"provisioning-($normalized_name | str replace "_" "-")" let is_running = ((^pgrep -f $"[/]($binary_name)$" | complete).exit_code == 0) if $is_running { (^sh -c $"pkill -f '($binary_name)' 2>/dev/null || true") | ignore sleep 500ms let stopped_msg = $"((ansi red))stopped((ansi reset))" print $"✓ ($service_name) on port ($port) — ($stopped_msg)" } else { let stopped_msg = $"((ansi red))already stopped((ansi reset))" print $"✓ ($service_name) on port ($port) — ($stopped_msg)" } } print "" } # Check if a port is listening (health check for external services) export def is-port-listening [port: number] { let uname_result = (do { ^uname -s } | complete) let os_type = (if $uname_result.exit_code == 0 { $uname_result.stdout | str trim } else { "Linux" }) if $os_type == "Darwin" { # macOS: use lsof to check listening ports # Pattern matches both "*:PORT" and "127.0.0.1:PORT" formats let check = (do { ^lsof -i -P -n } | complete) if $check.exit_code == 0 { let port_str = $"($port)" $check.stdout | str contains $"($port_str)" | if $in { true } else { false } } else { false } } else { # Linux: use netstat to check listening ports let check = (do { ^netstat -tuln } | complete) if $check.exit_code == 0 { $check.stdout | str contains $":$port" } else { false } } } # Get external services from user configuration file export def get-external-services [] { # Prefer explicit env override; otherwise the OS-aware platform dir. let possible_bases = [ ($env.PROVISIONING_USER_PLATFORM? | default null), (get-platform-config-dir), ] # Try to find the external services config file let external_services_file = ( $possible_bases | where { |b| $b != null } | each { |base| ($base | path expand | path join "config/external-services.ncl") } | where { |p| $p | path exists } | get 0? ) if ($external_services_file == null) { return [] } ncl-eval-soft $external_services_file [] [] | default [] } # Start nats-server as a child process for solo mode. # Requires nats-server to be in PATH. # Returns a record with {pid, port, jetstream_dir} on success. export def nats_start [config: record]: nothing -> record { let port = ($config.port? | default 4222) let data_dir = ($env.HOME | path join ".local/share/provisioning/nats") let js_dir = ($config.jetstream_store_dir? | default $data_dir) let mk_result = (do { ^mkdir -p $js_dir } | complete) if ($mk_result.exit_code != 0) { error make {msg: $"Failed to create NATS data dir ($js_dir): ($mk_result.stderr)"} } # Spawn nats-server in background — nohup keeps it alive after this shell exits let cmd = $"nohup nats-server -js -sd ($js_dir) -p ($port) >/dev/null 2>&1 &" let start_result = (do { ^sh -c $cmd } | complete) if ($start_result.exit_code != 0) { error make {msg: $"nats-server failed to start: ($start_result.stderr)"} } # Poll for readiness — up to 10 seconds in 500ms increments let ready = ( 1..20 | each { |_i| sleep 500ms (nats_health {port: $port}) } | where { |r| $r } | length | $in > 0 ) if (not $ready) { error make {msg: "nats-server did not become ready within 10 seconds"} } let pid_result = (do { ^pgrep -f $"nats-server" } | complete) let pid = (if ($pid_result.exit_code == 0) { $pid_result.stdout | lines | get 0? | default "0" | into int } else { 0 }) {pid: $pid, port: $port, jetstream_dir: $js_dir} } # Stop the nats-server process. export def nats_stop [config: record]: nothing -> nothing { let port = ($config.port? | default 4222) let kill_result = (do { ^pkill -f "nats-server" } | complete) if ($kill_result.exit_code != 0) { print $"Warning: nats-server on port ($port) was not running or could not be stopped" } } # Check if nats-server is accepting TCP connections on the configured port. # Returns true if healthy, false otherwise. export def nats_health [config: record]: nothing -> bool { let port = ($config.port? | default 4222) let check = (do { ^nc -z -w 1 127.0.0.1 $port } | complete) $check.exit_code == 0 } # ============================================================================ # ncl-sync daemon management # ============================================================================ def ncl-sync-cache-dir []: nothing -> string { if ($env.NCL_CACHE_DIR? | is-not-empty) { return $env.NCL_CACHE_DIR } # Walk up from PWD to find workspace root let pwd = $env.PWD let ws_pwd = if ($pwd | path join "infra" | path exists) or ($pwd | path join "config" "provisioning.ncl" | path exists) or ($pwd | path join ".ontology" | path exists) { $pwd } else { "" } if ($ws_pwd | is-not-empty) { return ($ws_pwd | path join ".ncl-cache") } # Fallback to active workspace from user_config let details = (get-active-workspace-details) if ($details | is-not-empty) and ($details | get -o path | is-not-empty) { return ($details.path | path join ".ncl-cache") } let home = ($env.HOME? | default "~" | path expand) $home | path join ".cache" "provisioning" "config-cache" } def ncl-sync-bin []: nothing -> string { let home = ($env.HOME? | default "~" | path expand) $home | path join ".local" "bin" "provisioning-ncl-sync" } def ncl-sync-running []: nothing -> bool { (do { ^ps aux } | complete).stdout | str contains "provisioning-ncl-sync" } # Check that a path has workspace markers (infra/, config/provisioning.ncl, or .ontology/). def is-workspace-dir [path: string]: nothing -> bool { if ($path | is-empty) or (not ($path | path exists)) { return false } let has_infra = ($path | path join "infra" | path exists) let has_config = ($path | path join "config" "provisioning.ncl" | path exists) let has_onto = ($path | path join ".ontology" | path exists) $has_infra or $has_config or $has_onto } # Walk up from `path` until a workspace root is found or we reach filesystem root. def find-workspace-up [path: string]: nothing -> string { if ($path | is-empty) or $path == "/" { return "" } if (is-workspace-dir $path) { return $path } let parent = ($path | path dirname) if $parent == $path { return "" } find-workspace-up $parent } # Start the ncl-sync daemon if not already running. # Workspace resolution priority: # 1. $NCL_CACHE_DIR's parent (explicit override) # 2. Walk up from PWD until a workspace root is found # 3. get-active-workspace-details from user_config.yaml (if it's a valid workspace path) # 4. skip (avoid watching HOME or random dirs) export def ncl-sync-start []: nothing -> nothing { if (ncl-sync-running) { return } let bin = (ncl-sync-bin) if not ($bin | path exists) { return } let from_pwd = (find-workspace-up $env.PWD) let details = (get-active-workspace-details) let from_config = if ($details | is-not-empty) and ($details | get -o path | is-not-empty) { $details.path } else { "" } let ws_path = if ($from_pwd | is-not-empty) { $from_pwd } else if (is-workspace-dir $from_config) { $from_config } else { "" } if ($ws_path | is-empty) { print "→ ncl-sync: no workspace detected in PWD tree or user_config — skipping" return } let home = ($env.HOME? | default "~" | path expand) let log_dir = ($home | path join ".provisioning" "logs") (do { mkdir $log_dir } | ignore) let log_file = ($log_dir | path join "ncl-sync.log") let cmd = $"nohup ($bin) daemon --workspace \"($ws_path)\" >>\"($log_file)\" 2>&1 &" (^sh -c $cmd | ignore) sleep 500ms print $"→ ncl-sync started \(workspace: ($ws_path)\)" } # Stop all ncl-sync daemon instances. export def ncl-sync-stop []: nothing -> nothing { (do { ^pkill -f "provisioning-ncl-sync" } | complete | ignore) } # Status record for ncl-sync daemon. export def ncl-sync-status []: nothing -> record { let running = (ncl-sync-running) { service: "ncl-sync", running: $running, } }