307 lines
11 KiB
Plaintext
307 lines
11 KiB
Plaintext
|
|
#!/usr/bin/env nu
|
||
|
|
# store.nu — Nushell client for ontoref-daemon HTTP API.
|
||
|
|
#
|
||
|
|
# Provides cached nickel export via daemon (with subprocess fallback),
|
||
|
|
# plus query/sync/nodes/dimensions when daemon has DB enabled.
|
||
|
|
#
|
||
|
|
# All HTTP calls use ^curl (external command) because Nushell's internal
|
||
|
|
# http get/post cannot be captured with `| complete` on connection errors.
|
||
|
|
#
|
||
|
|
# Usage:
|
||
|
|
# use ../modules/store.nu *
|
||
|
|
#
|
||
|
|
# daemon-export ".ontology/core.ncl" # cached export
|
||
|
|
# daemon-export ".ontology/core.ncl" --import-path $ip # with NICKEL_IMPORT_PATH
|
||
|
|
# store nodes --level Axiom # query ontology nodes
|
||
|
|
# store dimensions # query dimensions
|
||
|
|
# daemon-health # check daemon status
|
||
|
|
|
||
|
|
# ── Utilities (self-contained to avoid circular imports with shared.nu) ─────────
|
||
|
|
|
||
|
|
def project-root []: nothing -> string {
|
||
|
|
let pr = ($env.ONTOREF_PROJECT_ROOT? | default "")
|
||
|
|
if ($pr | is-not-empty) and ($pr != $env.ONTOREF_ROOT) { $pr } else { $env.ONTOREF_ROOT }
|
||
|
|
}
|
||
|
|
|
||
|
|
def nickel-import-path [root: string]: nothing -> string {
|
||
|
|
let entries = [
|
||
|
|
$"($root)/.ontology"
|
||
|
|
$"($root)/adrs"
|
||
|
|
$"($root)/.ontoref/ontology/schemas"
|
||
|
|
$"($root)/.ontoref/adrs"
|
||
|
|
$"($root)/.onref"
|
||
|
|
$root
|
||
|
|
$"($env.ONTOREF_ROOT)/ontology"
|
||
|
|
$"($env.ONTOREF_ROOT)/ontology/schemas"
|
||
|
|
$"($env.ONTOREF_ROOT)/adrs"
|
||
|
|
$env.ONTOREF_ROOT
|
||
|
|
]
|
||
|
|
let valid = ($entries | where { |p| $p | path exists } | uniq)
|
||
|
|
let existing = ($env.NICKEL_IMPORT_PATH? | default "")
|
||
|
|
if ($existing | is-not-empty) {
|
||
|
|
($valid | append $existing) | str join ":"
|
||
|
|
} else {
|
||
|
|
$valid | str join ":"
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
# ── Configuration ────────────────────────────────────────────────────────────────
|
||
|
|
|
||
|
|
def daemon-url []: nothing -> string {
|
||
|
|
$env.ONTOREF_DAEMON_URL? | default "http://127.0.0.1:7891"
|
||
|
|
}
|
||
|
|
|
||
|
|
# Load project config to check if DB is enabled
|
||
|
|
def project-config-db-status []: nothing -> record<enabled: bool, url: string, namespace: string> {
|
||
|
|
let root = (project-root)
|
||
|
|
let config_path = $"($root)/.ontoref/config.ncl"
|
||
|
|
if not ($config_path | path exists) {
|
||
|
|
return { enabled: false, url: "", namespace: "" }
|
||
|
|
}
|
||
|
|
|
||
|
|
# Export config and extract db section
|
||
|
|
let result = (do { ^nickel export $config_path } | complete)
|
||
|
|
if $result.exit_code != 0 {
|
||
|
|
return { enabled: false, url: "", namespace: "" }
|
||
|
|
}
|
||
|
|
|
||
|
|
# Parse JSON safely
|
||
|
|
let parse_result = (do { $result.stdout | from json } | complete)
|
||
|
|
if $parse_result.exit_code != 0 {
|
||
|
|
return { enabled: false, url: "", namespace: "" }
|
||
|
|
}
|
||
|
|
|
||
|
|
let config = $parse_result.stdout
|
||
|
|
let db = ($config.db? | default {})
|
||
|
|
{
|
||
|
|
enabled: ($db.enabled? | default false),
|
||
|
|
url: ($db.url? | default ""),
|
||
|
|
namespace: ($db.namespace? | default "ontoref"),
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
# ── HTTP helpers (external ^curl) ────────────────────────────────────────────────
|
||
|
|
|
||
|
|
def http-get [url: string]: nothing -> record {
|
||
|
|
do { ^curl -sf $url } | complete
|
||
|
|
}
|
||
|
|
|
||
|
|
def http-post-json [url: string, body: string]: nothing -> record {
|
||
|
|
do { ^curl -sf -X POST -H "Content-Type: application/json" -d $body $url } | complete
|
||
|
|
}
|
||
|
|
|
||
|
|
# ── Availability check ───────────────────────────────────────────────────────────
|
||
|
|
|
||
|
|
# Check if daemon is reachable. Caches "true" in env var for the session.
|
||
|
|
# A "false" result is NOT cached — allows recovery when daemon starts mid-session.
|
||
|
|
export def --env daemon-available []: nothing -> bool {
|
||
|
|
let cached = ($env.ONTOREF_DAEMON_AVAILABLE? | default "")
|
||
|
|
if $cached == "true" { return true }
|
||
|
|
|
||
|
|
let url = $"(daemon-url)/health"
|
||
|
|
let result = (http-get $url)
|
||
|
|
if $result.exit_code == 0 {
|
||
|
|
$env.ONTOREF_DAEMON_AVAILABLE = "true"
|
||
|
|
true
|
||
|
|
} else {
|
||
|
|
false
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
# ── Health ───────────────────────────────────────────────────────────────────────
|
||
|
|
|
||
|
|
# Show daemon health status with optional DB info from config.
|
||
|
|
# Returns record with { status, uptime_secs, cache_*, db_enabled?, db_config? }
|
||
|
|
# or null if daemon unreachable or response is not valid JSON.
|
||
|
|
export def daemon-health []: nothing -> any {
|
||
|
|
let url = $"(daemon-url)/health"
|
||
|
|
let result = (http-get $url)
|
||
|
|
if $result.exit_code != 0 {
|
||
|
|
null
|
||
|
|
} else {
|
||
|
|
let body = ($result.stdout | str trim)
|
||
|
|
if not ($body | str starts-with "{") {
|
||
|
|
null
|
||
|
|
} else {
|
||
|
|
let parse_result = (do { $body | from json } | complete)
|
||
|
|
if $parse_result.exit_code != 0 {
|
||
|
|
null
|
||
|
|
} else {
|
||
|
|
let health = $parse_result.stdout
|
||
|
|
let db_config = (project-config-db-status)
|
||
|
|
$health | insert db_config $db_config
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
# ── NCL Export (core function) ───────────────────────────────────────────────────
|
||
|
|
|
||
|
|
# Export a Nickel file to JSON via daemon (cached) with subprocess fallback.
|
||
|
|
#
|
||
|
|
# When daemon is available: POST /nickel/export → cached result.
|
||
|
|
# When daemon is unreachable: falls back to ^nickel export subprocess.
|
||
|
|
# System works identically either way — just slower without daemon.
|
||
|
|
export def --env daemon-export [
|
||
|
|
file: string,
|
||
|
|
--import-path: string = "",
|
||
|
|
]: nothing -> any {
|
||
|
|
let ip = if ($import_path | is-not-empty) {
|
||
|
|
$import_path
|
||
|
|
} else {
|
||
|
|
nickel-import-path (project-root)
|
||
|
|
}
|
||
|
|
if (daemon-available) {
|
||
|
|
let result = (daemon-export-http $file $ip)
|
||
|
|
if $result != null { return $result }
|
||
|
|
# HTTP call failed despite health check — clear cache to re-probe next call
|
||
|
|
$env.ONTOREF_DAEMON_AVAILABLE = ""
|
||
|
|
}
|
||
|
|
daemon-export-subprocess $file $ip
|
||
|
|
}
|
||
|
|
|
||
|
|
# Safe version: returns null on failure instead of throwing.
|
||
|
|
# Use for call sites that handle missing data gracefully (return [] or {}).
|
||
|
|
export def --env daemon-export-safe [
|
||
|
|
file: string,
|
||
|
|
--import-path: string = "",
|
||
|
|
]: nothing -> any {
|
||
|
|
if not ($file | path exists) { return null }
|
||
|
|
let ip = if ($import_path | is-not-empty) {
|
||
|
|
$import_path
|
||
|
|
} else {
|
||
|
|
nickel-import-path (project-root)
|
||
|
|
}
|
||
|
|
if (daemon-available) {
|
||
|
|
let result = (daemon-export-http $file $ip)
|
||
|
|
if $result != null { return $result }
|
||
|
|
$env.ONTOREF_DAEMON_AVAILABLE = ""
|
||
|
|
}
|
||
|
|
let result = do { with-env { NICKEL_IMPORT_PATH: $ip } { ^nickel export $file } } | complete
|
||
|
|
if $result.exit_code != 0 { return null }
|
||
|
|
$result.stdout | from json
|
||
|
|
}
|
||
|
|
|
||
|
|
# ── Daemon HTTP calls ────────────────────────────────────────────────────────────
|
||
|
|
|
||
|
|
def daemon-export-http [file: string, import_path: string]: nothing -> any {
|
||
|
|
let url = $"(daemon-url)/nickel/export"
|
||
|
|
let body = if ($import_path | is-not-empty) {
|
||
|
|
{ path: $file, import_path: $import_path } | to json
|
||
|
|
} else {
|
||
|
|
{ path: $file } | to json
|
||
|
|
}
|
||
|
|
let result = (http-post-json $url $body)
|
||
|
|
if $result.exit_code != 0 { return null }
|
||
|
|
let response = ($result.stdout | from json)
|
||
|
|
$response.data? | default null
|
||
|
|
}
|
||
|
|
|
||
|
|
# ── Subprocess fallback ──────────────────────────────────────────────────────────
|
||
|
|
|
||
|
|
def daemon-export-subprocess [file: string, import_path: string]: nothing -> any {
|
||
|
|
let ip = if ($import_path | is-not-empty) {
|
||
|
|
$import_path
|
||
|
|
} else {
|
||
|
|
let root = (project-root)
|
||
|
|
nickel-import-path $root
|
||
|
|
}
|
||
|
|
let result = do { with-env { NICKEL_IMPORT_PATH: $ip } { ^nickel export $file } } | complete
|
||
|
|
if $result.exit_code != 0 {
|
||
|
|
error make { msg: $"nickel export failed for ($file): ($result.stderr)" }
|
||
|
|
}
|
||
|
|
$result.stdout | from json
|
||
|
|
}
|
||
|
|
|
||
|
|
# ── Store query commands ─────────────────────────────────────────────────────────
|
||
|
|
|
||
|
|
# Query ontology nodes. Returns empty list on failure.
|
||
|
|
export def --env "store nodes" [
|
||
|
|
--level: string = "",
|
||
|
|
]: nothing -> list {
|
||
|
|
let root = (project-root)
|
||
|
|
let core_file = $"($root)/.ontology/core.ncl"
|
||
|
|
if not ($core_file | path exists) { return [] }
|
||
|
|
let data = (daemon-export-safe $core_file)
|
||
|
|
if $data == null { return [] }
|
||
|
|
let nodes = ($data.nodes? | default [])
|
||
|
|
if ($level | is-not-empty) {
|
||
|
|
$nodes | where { |n| ($n.level? | default "") == $level }
|
||
|
|
} else {
|
||
|
|
$nodes
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
# Query ontology dimensions. Returns empty list on failure.
|
||
|
|
export def --env "store dimensions" []: nothing -> list {
|
||
|
|
let root = (project-root)
|
||
|
|
let state_file = $"($root)/.ontology/state.ncl"
|
||
|
|
if not ($state_file | path exists) { return [] }
|
||
|
|
let data = (daemon-export-safe $state_file)
|
||
|
|
if $data == null { return [] }
|
||
|
|
$data.dimensions? | default []
|
||
|
|
}
|
||
|
|
|
||
|
|
# Query membranes. Returns empty list on failure.
|
||
|
|
export def --env "store membranes" [
|
||
|
|
--all = false,
|
||
|
|
]: nothing -> list {
|
||
|
|
let root = (project-root)
|
||
|
|
let gate_file = $"($root)/.ontology/gate.ncl"
|
||
|
|
if not ($gate_file | path exists) { return [] }
|
||
|
|
let data = (daemon-export-safe $gate_file)
|
||
|
|
if $data == null { return [] }
|
||
|
|
let membranes = ($data.membranes? | default [])
|
||
|
|
if $all {
|
||
|
|
$membranes
|
||
|
|
} else {
|
||
|
|
$membranes | where { |m| ($m.active? | default false) == true }
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
# ── Cache management ─────────────────────────────────────────────────────────────
|
||
|
|
|
||
|
|
# Show daemon cache statistics.
|
||
|
|
export def --env "store cache-stats" []: nothing -> any {
|
||
|
|
if not (daemon-available) {
|
||
|
|
print " daemon not available"
|
||
|
|
return null
|
||
|
|
}
|
||
|
|
let url = $"(daemon-url)/cache/stats"
|
||
|
|
let result = (http-get $url)
|
||
|
|
if $result.exit_code == 0 {
|
||
|
|
$result.stdout | from json
|
||
|
|
} else {
|
||
|
|
null
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
# Invalidate daemon cache (all entries or by prefix/file).
|
||
|
|
export def --env "store cache-invalidate" [
|
||
|
|
--prefix: string = "",
|
||
|
|
--file: string = "",
|
||
|
|
--all = false,
|
||
|
|
]: nothing -> any {
|
||
|
|
if not (daemon-available) {
|
||
|
|
print " daemon not available"
|
||
|
|
return null
|
||
|
|
}
|
||
|
|
let url = $"(daemon-url)/cache/invalidate"
|
||
|
|
if (not $all) and ($prefix | is-empty) and ($file | is-empty) {
|
||
|
|
error make { msg: "cache-invalidate requires --all, --prefix, or --file" }
|
||
|
|
}
|
||
|
|
let body = if $all {
|
||
|
|
{ all: true } | to json
|
||
|
|
} else if ($prefix | is-not-empty) {
|
||
|
|
{ prefix: $prefix } | to json
|
||
|
|
} else {
|
||
|
|
{ file: $file } | to json
|
||
|
|
}
|
||
|
|
let result = (http-post-json $url $body)
|
||
|
|
if $result.exit_code == 0 {
|
||
|
|
$result.stdout | from json
|
||
|
|
} else {
|
||
|
|
null
|
||
|
|
}
|
||
|
|
}
|