348 lines
10 KiB
Plaintext
348 lines
10 KiB
Plaintext
|
|
#!/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)"
|
||
|
|
}
|
||
|
|
}
|