348 lines
10 KiB
Plaintext
Raw Permalink Normal View History

2026-03-13 00:21:04 +00:00
#!/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 <start|stop|restart> daemon" + $reset)
print ($gray + " Monitor: ontoref services <status|health> [daemon|db]" + $reset)
print ($gray + " Events: strat nats <status|listen|emit>" + $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)"
}
}