#!/usr/bin/env nu # services.nu - Config-driven service lifecycle management with dependency resolution. # # Services configured in .ontoref/config.ncl: # services.services[] - array of {id, enabled, depends_on[], config} # services.startup_order - explicit startup order (optional) # services.shutdown_order - explicit shutdown order (optional) # # Usage: # ontoref services # Show all services status # ontoref services start [id] # Start service (and dependencies) # ontoref services stop [id] # Stop service # ontoref services restart [id] # Restart service # ontoref services status [id] # Show service status # ontoref services health [id] # Health check service use ../modules/store.nu * # -- Configuration ------------------------------------------------------------ def get-project-config []: nothing -> record { let root = ($env.ONTOREF_PROJECT_ROOT? | default $env.ONTOREF_ROOT) let config_path = $"($root)/.ontoref/config.ncl" if not ($config_path | path exists) { return { services: { services: [] }, daemon: {} } } let result = (do { ^nickel export $config_path } | complete) if $result.exit_code == 0 { $result.stdout | from json } else { { services: { services: [] }, daemon: {} } } } def get-internal-services []: nothing -> list { # Only daemon is managed by onref. DB is external. [ { id: "daemon", enabled: false, depends_on: [], managed: true } ] } def get-services-list []: nothing -> list { let config = (get-project-config) let all = ($config.services.services? | default []) # Filter to only managed services (daemon) $all | where { |s| $s.id == "daemon" } } def get-service [id: string]: nothing -> record { let services = (get-services-list) let service = ($services | where id == $id | first) if ($service | is-empty) { error make { msg: $"Service not found: ($id)" } } $service } def get-startup-order []: nothing -> list { let config = (get-project-config) let explicit = ($config.services.startup_order? | default null) if ($explicit | is-not-empty) { $explicit } else { # Default order: daemon first, then db ["daemon", "db"] } } def get-shutdown-order []: nothing -> list { let config = (get-project-config) let explicit = ($config.services.shutdown_order? | default null) if ($explicit | is-not-empty) { $explicit } else { # Reverse order: db first, then daemon ["db", "daemon"] } } def daemon-pid-file []: nothing -> string { $"($env.HOME)/.ontoref/daemon.pid" } def daemon-running? []: nothing -> bool { let pid_file = (daemon-pid-file) if not ($pid_file | path exists) { return false } let pid = (open $pid_file | str trim) let result = (do { ^kill -0 $pid } | complete) $result.exit_code == 0 } # -- Status ------------------------------------------------------------------- def "services" [action?: string, id?: string]: nothing -> nothing { match ($action | default "") { "" => { services overview }, "start" => { services start $id }, "stop" => { services stop $id }, "restart" => { services restart $id }, "status" => { services status $id }, "health" => { services health $id }, _ => { print $"Unknown action: ($action). Use: start, stop, restart, status, health" } } } export def "main" [action?: string, id?: string]: nothing -> nothing { services $action $id } export def "services overview" []: nothing -> nothing { let config = (get-project-config) let services = (get-services-list) # Color codes (defined once) let cyan = (ansi cyan) let blue = (ansi blue) let green = (ansi green) let red = (ansi red) let gray = (ansi dark_gray) let yellow = (ansi yellow) let reset = (ansi reset) print "" print ($cyan + " ontoref services (managed by ontoref)" + $reset) print ($gray + " ----------------------------------------" + $reset) print "" # Show managed services (daemon) for service in $services { let status_text = if (daemon-running?) { "running" } else { "stopped" } let status_icon = if ($status_text == "running") { "✓" } else { "✗" } if $service.enabled { let port = ($config.daemon.port? | default 7891) let status_color = if ($status_text == "running") { $green } else { $red } let line = $blue + " " + $service.id + $reset + " [" + $status_color + $status_icon + $reset + "] " + $status_text + " - port " + $yellow + ($port | into string) + $reset print $line } else { let line = $blue + " " + $service.id + $reset + " [" + $gray + "-" + $reset + "] " + $gray + "disabled" + $reset print $line } } print "" print ($cyan + " External services (monitored only)" + $reset) print ($gray + " ----------------------------------------" + $reset) print "" # Show external services: DB and NATS let db_config = ($config.services.services? | default [] | where id == "db" | first) if ($db_config | is-not-empty) { if ($db_config.enabled | default false) { let db_url = ($config.db.url? | default "") if ($db_url | is-not-empty) { let line = $blue + " db" + $reset + " " + $gray + "[status check only]" + $reset + " " + $yellow + $db_url + $reset print $line } else { let line = $blue + " db" + $reset + " " + $gray + "[disabled]" + $reset + " no URL configured" print $line } } else { let line = $blue + " db" + $reset + " [" + $gray + "-" + $reset + "] " + $gray + "disabled" + $reset print $line } } # NATS status let nats_config = ($config.nats_events? | default { enabled: false, url: "" }) if ($nats_config.enabled | default false) { let nats_url = ($nats_config.url? | default "nats://localhost:4222") let line = $blue + " nats" + $reset + " " + $gray + "[event system]" + $reset + " " + $yellow + $nats_url + $reset print $line } else { let line = $blue + " nats" + $reset + " [" + $gray + "-" + $reset + "] " + $gray + "disabled" + $reset print $line } print "" print ($gray + " Manage: ontoref services daemon" + $reset) print ($gray + " Monitor: ontoref services [daemon|db]" + $reset) print ($gray + " Events: strat nats " + $reset) print "" } # -- Lifecycle Management ----------------------------------------------------- export def "services start" [id?: string]: nothing -> nothing { let requested = ($id | default "daemon") match $requested { "daemon" => { if (daemon-running?) { print " ✓ daemon already running" } else { daemon-start } }, "db" => { print " [ext] database is external - manage separately" }, _ => { print $" [warn] unknown service: ($requested)" } } } export def "services stop" [id?: string]: nothing -> nothing { let requested = ($id | default "daemon") match $requested { "daemon" => { daemon-stop }, "db" => { print " [ext] database must be stopped externally" }, _ => { print $" [warn] unknown service: ($requested)" } } } export def "services restart" [id?: string]: nothing -> nothing { let requested = ($id | default "daemon") match $requested { "daemon" => { print " Restarting daemon..." daemon-stop sleep 500ms daemon-start }, "db" => { print " [ext] database must be restarted externally" }, _ => { print $" [warn] unknown service: ($requested)" } } } export def "services status" [id?: string]: nothing -> nothing { if ($id | is-not-empty) { match $id { "daemon" => { print "" if (daemon-running?) { let pid_file = (daemon-pid-file) let pid = (open $pid_file | str trim) print $" daemon: running (PID $pid)" } else { print " daemon: stopped" } print "" }, "db" => { print " database: check externally" }, _ => { print $" unknown service: ($id)" } } } else { services overview } } export def "services health" [id?: string]: nothing -> nothing { if ($id | is-not-empty) { match $id { "daemon" => { daemon-health }, "db" => { db-health }, _ => { print $" unknown service: ($id)" } } } else { let services = (get-services-list | get id) for svc_id in $services { match $svc_id { "daemon" => { daemon-health }, "db" => { db-health }, _ => {} } } } } # -- Built-in Service Handlers ------------------------------------------------ def daemon-start []: nothing -> nothing { let root = ($env.ONTOREF_PROJECT_ROOT? | default $env.ONTOREF_ROOT) let pid_file = (daemon-pid-file) print " Starting daemon..." mkdir ($pid_file | path dirname) do { \^ontoref-daemon --project-root $root --pid-file $pid_file } & sleep 500ms if (daemon-running?) { print " ✓ daemon started" } else { print " ✗ failed to start daemon" } } def daemon-stop []: nothing -> nothing { if not (daemon-running?) { print " ✓ daemon not running" return } let pid_file = (daemon-pid-file) let pid = (open $pid_file | str trim) print " Stopping daemon..." do { ^kill $pid } | complete sleep 500ms if not (daemon-running?) { print " ✓ daemon stopped" rm -f $pid_file } else { do { ^kill -9 $pid } | complete rm -f $pid_file } } def daemon-health []: nothing -> nothing { if not (daemon-running?) { print " ✗ daemon not running" return } let url = ($env.ONTOREF_DAEMON_URL? | default "http://127.0.0.1:7891") let result = (do { ^curl -sf $"($url)/health" } | complete) if $result.exit_code != 0 { print " ✗ daemon health check failed" return } let health = ($result.stdout | from json) print $" ✓ daemon healthy (uptime: ($health.uptime_secs)s, cache: ($health.cache_entries) entries)" } def db-health []: nothing -> nothing { let config = (get-project-config) let db_config = ($config.services.services? | default [] | where id == "db" | first) if ($db_config.config.url? | default "" | is-empty) { print " ⚠ database URL not configured" return } let url = $db_config.config.url let result = (do { ^curl -sf $url } | complete) if $result.exit_code == 0 { print $" ✓ database healthy ($url)" } else { print $" ✗ database check failed ($url)" } }