From 9c171ffea29483a73d065f8d1fd51d82ba8b8e8a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jesu=CC=81s=20Pe=CC=81rez?= Date: Sat, 14 Mar 2026 09:38:40 +0000 Subject: [PATCH] chore: fix ctrl-c interactive and nickel_import --- install/gen-projects.nu | 2 +- install/install.nu | 38 ++++- install/ontoref-global | 287 ++++++++++++++++++++++++++++++-- reflection/nulib/interactive.nu | 10 +- 4 files changed, 320 insertions(+), 17 deletions(-) diff --git a/install/gen-projects.nu b/install/gen-projects.nu index 0361dc9..ed62973 100644 --- a/install/gen-projects.nu +++ b/install/gen-projects.nu @@ -36,7 +36,7 @@ def main [ $content | lines | each { |l| $l | str trim } - | where { |l| $l | str starts-with "import " } + | where { |l| ($l | str contains "import \"") } | each { |l| $l | parse --regex 'import\s+"(?P[^"]+)"' | get path | first } diff --git a/install/install.nu b/install/install.nu index 6dd900f..40c4c1c 100755 --- a/install/install.nu +++ b/install/install.nu @@ -80,13 +80,13 @@ def main [] { install-if-changed $boot_src $boot_dest "bootstrapper" # ── 3. Global CLI wrapper → ontoref ─────────────────────────────────────── - # Bake the repo root into the installed script so ONTOREF_ROOT resolves correctly - # from any working directory, not just from within the source tree. + # Bake the data dir as ONTOREF_ROOT so the installed wrapper is self-contained + # and does not require the source repo to be present at runtime. let cli_src = $"($repo_root)/install/ontoref-global" let cli_dest = $"($bin_dir)/ontoref" let cli_baked = ( open --raw $cli_src - | str replace 'ONTOREF_ROOT="${ONTOREF_ROOT:-ontoref}"' $'ONTOREF_ROOT="${ONTOREF_ROOT:-($repo_root)}"' + | str replace 'ONTOREF_ROOT="${ONTOREF_ROOT:-ontoref}"' $'ONTOREF_ROOT="${ONTOREF_ROOT:-($data_dir)}"' ) let needs_update = if ($cli_dest | path exists) { @@ -103,6 +103,38 @@ def main [] { print $"— cli unchanged" } + # ── 3b. Reflection scripts (data dir) ───────────────────────────────────── + # The global CLI wrapper calls $data_dir/reflection/bin/ontoref.nu directly. + # Copy the entire reflection/ tree so the install is autonomous (no dev repo needed). + let reflection_src = $"($repo_root)/reflection" + let reflection_dest = $"($data_dir)/reflection" + + if not ($reflection_src | path exists) { + error make { msg: $"reflection/ not found: ($reflection_src)" } + } + + mkdir $reflection_dest + mut refl_updated = 0 + mut refl_skipped = 0 + for src_file in (glob $"($reflection_src)/**/*" | where { |f| ($f | path type) == "file" }) { + let rel = ($src_file | str replace $"($reflection_src)/" "") + let dest_file = $"($reflection_dest)/($rel)" + let dest_parent = ($dest_file | path dirname) + mkdir $dest_parent + let needs_update = if ($dest_file | path exists) { + (open --raw $src_file | hash sha256) != (open --raw $dest_file | hash sha256) + } else { + true + } + if $needs_update { + cp $src_file $dest_file + $refl_updated = $refl_updated + 1 + } else { + $refl_skipped = $refl_skipped + 1 + } + } + print $"✓ reflection ($reflection_dest)/ updated=($refl_updated) unchanged=($refl_skipped)" + # ── 4. UI assets (data dir) ──────────────────────────────────────────────── let templates_src = $"($repo_root)/crates/ontoref-daemon/templates" let public_src = $"($repo_root)/crates/ontoref-daemon/public" diff --git a/install/ontoref-global b/install/ontoref-global index 7847b0b..da046d7 100755 --- a/install/ontoref-global +++ b/install/ontoref-global @@ -1,28 +1,120 @@ #!/bin/bash # ontoref — global entry point for the ontoref protocol CLI. +# Release: 0.1.0 # # Discovers project root by walking up from CWD looking for .ontology/. -# No per-project wrapper needed. Works from any subdirectory. +# Fully self-contained: does NOT delegate to the dev repo. +# +# ONTOREF_ROOT is baked at install time to the data directory +# (macOS: ~/Library/Application Support/ontoref, Linux: ~/.local/share/ontoref) +# where reflection/ scripts are installed. # # Usage: # ontoref adr l # ontoref sync # ONTOREF_PROJECT_ROOT=/path/to/project ontoref adr l # explicit override -# -# Install: -# ln -sf /path/to/ontoref/install/ontoref-global ~/.local/bin/ontoref set -euo pipefail +_release() { grep "^# Release:" "$0" | sed "s/# Release: //g"; } + +# Baked at install time — replaced by install.nu with the actual data dir path. ONTOREF_ROOT="${ONTOREF_ROOT:-ontoref}" -if [[ ! -f "${ONTOREF_ROOT}/ontoref" ]]; then - echo "ontoref: entry point not found at ${ONTOREF_ROOT}/ontoref" >&2 - echo " Set ONTOREF_ROOT to the correct path." >&2 +readonly DISPATCHER="${ONTOREF_ROOT}/reflection/bin/ontoref.nu" + +# ── Nushell check ───────────────────────────────────────────────────────────── + +if ! command -v nu &>/dev/null; then + echo "" + echo " ontoref requires Nushell (>= 0.110.0)" + echo "" + echo " Install:" + echo " cargo install nu # via Rust" + echo " brew install nushell # macOS" + echo " winget install nushell # Windows" + echo "" + echo " Then re-run: ontoref $*" + echo "" exit 1 fi -# Walk up from CWD to find the nearest directory containing .ontology/ +# ── Version gate ────────────────────────────────────────────────────────────── + +NU_VERSION="$(nu --version 2>/dev/null | tr -d '[:space:]')" +NU_MAJOR="${NU_VERSION%%.*}" +NU_MAJOR="${NU_MAJOR%%[^0-9]*}" +NU_MINOR="${NU_VERSION#*.}"; NU_MINOR="${NU_MINOR%%.*}" +NU_MINOR="${NU_MINOR%%[^0-9]*}" + +if [[ "${NU_MAJOR}" -lt 0 ]] || { [[ "${NU_MAJOR}" -eq 0 ]] && [[ "${NU_MINOR}" -lt 110 ]]; }; then + echo "" + echo " Nushell ${NU_VERSION} found — 0.110.0+ required" + echo " Update: cargo install nu --force" + echo "" + exit 1 +fi + +# ── Guard: dispatcher must exist ────────────────────────────────────────────── + +if [[ ! -f "${DISPATCHER}" ]]; then + echo "ontoref: reflection scripts not found at ${ONTOREF_ROOT}/reflection/" >&2 + echo " Re-run the installer: nu install/install.nu" >&2 + exit 1 +fi + +# ── Version flag (early exit) ───────────────────────────────────────────────── + +for _arg in "$@"; do + case "${_arg}" in + -V|-v|--version|version) + echo "ontoref $(_release)" + exit 0 + ;; + esac +done + +# ── Actor detection ──────────────────────────────────────────────────────────── + +ACTOR_FROM_ARGS="" +ENV_ONLY=0 +REMAINING_ARGS=() +while [[ $# -gt 0 ]]; do + case "$1" in + --actor) + shift + ACTOR_FROM_ARGS="${1:-}" + shift + ;; + --actor=*) + ACTOR_FROM_ARGS="${1#--actor=}" + shift + ;; + --env-only) + ENV_ONLY=1 + shift + ;; + *) + REMAINING_ARGS+=("$1") + shift + ;; + esac +done + +if [[ -n "${ACTOR_FROM_ARGS}" ]]; then + export ONTOREF_ACTOR="${ACTOR_FROM_ARGS}" +elif [[ -n "${ONTOREF_ACTOR:-}" ]]; then + : # already set by caller +elif [[ -n "${CI:-}" ]] || [[ -n "${GITHUB_ACTIONS:-}" ]] || [[ -n "${WOODPECKER_BUILD_NUMBER:-}" ]]; then + export ONTOREF_ACTOR="ci" +elif [[ ! -t 0 ]]; then + export ONTOREF_ACTOR="agent" +else + export ONTOREF_ACTOR="developer" +fi + +# ── Project root discovery ───────────────────────────────────────────────────── + _find_project_root() { local dir dir="$(pwd)" @@ -44,10 +136,181 @@ fi export ONTOREF_ROOT export ONTOREF_PROJECT_ROOT -_paths="${ONTOREF_PROJECT_ROOT}:${ONTOREF_PROJECT_ROOT}/.ontology:${ONTOREF_PROJECT_ROOT}/adrs:${ONTOREF_ROOT}/adrs:${ONTOREF_ROOT}/ontology/schemas:${ONTOREF_ROOT}" -export NICKEL_IMPORT_PATH="${_paths}${NICKEL_IMPORT_PATH:+:${NICKEL_IMPORT_PATH}}" -unset _paths +readonly CONFIG_NCL="${ONTOREF_PROJECT_ROOT}/.ontoref/config.ncl" +readonly LOCKS_DIR="${ONTOREF_PROJECT_ROOT}/.ontoref/locks" + +# ── Load project env vars (NICKEL_IMPORT_PATH) ──────────────────────────────── + +load_ontoref_env() { + # Installed schemas dir provides ontoref-project.ncl and other protocol schemas. + local installed_schemas="${HOME}/.config/ontoref/schemas" + + local fallback_paths="${ONTOREF_PROJECT_ROOT}:${ONTOREF_PROJECT_ROOT}/.ontology:${ONTOREF_PROJECT_ROOT}/adrs:${installed_schemas}:${ONTOREF_ROOT}" + + if [[ ! -f "${CONFIG_NCL}" ]]; then + export NICKEL_IMPORT_PATH="${fallback_paths}" + return 0 + fi + + local raw_paths="" + if command -v nickel &>/dev/null; then + # nickel_import_paths from project config are relative to ONTOREF_PROJECT_ROOT. + # shellcheck disable=SC2016 + raw_paths="$(nickel export "${CONFIG_NCL}" 2>/dev/null | ONTOREF_ROOT="${ONTOREF_PROJECT_ROOT}" nu -c 'from json | get nickel_import_paths | each { |p| $env.ONTOREF_ROOT + "/" + $p } | str join ":"' 2>/dev/null)" || true + fi + + if [[ -n "${raw_paths}" ]]; then + export NICKEL_IMPORT_PATH="${raw_paths}:${installed_schemas}:${ONTOREF_ROOT}" + else + export NICKEL_IMPORT_PATH="${fallback_paths}" + fi +} + +if [[ -z "${NICKEL_IMPORT_PATH:-}" ]]; then + load_ontoref_env +fi + +# ── Advisory locking (mkdir-based, POSIX-atomic) ────────────────────────────── + +determine_lock() { + local cmd="${REMAINING_ARGS[0]:-}" + local sub="${REMAINING_ARGS[1]:-}" + case "${cmd}" in + config) + case "${sub}" in + apply|rollback) echo "manifest" ;; + *) echo "" ;; + esac + ;; + register) echo "changelog" ;; + backlog) + case "${sub}" in + done|cancel) echo "backlog" ;; + *) echo "" ;; + esac + ;; + *) echo "" ;; + esac +} + +is_stale_lock() { + local lockdir="$1" + local owner_file="${lockdir}/owner" + if [[ ! -f "${owner_file}" ]]; then return 0; fi + local owner_pid + owner_pid="$(cut -d: -f1 "${owner_file}" 2>/dev/null)" || return 0 + if [[ -z "${owner_pid}" ]]; then return 0; fi + if ! kill -0 "${owner_pid}" 2>/dev/null; then return 0; fi + return 1 +} + +ACQUIRED_LOCK="" + +acquire_lock() { + local resource="$1" + local timeout="${2:-30}" + local lockdir="${LOCKS_DIR}/${resource}.lock" + local elapsed=0 + + mkdir -p "${LOCKS_DIR}" + + local stale_retries=0 + while ! mkdir "${lockdir}" 2>/dev/null; do + local stale=0 + # shellcheck disable=SC2310 + is_stale_lock "${lockdir}" && stale=1 || true + if [[ "${stale}" -eq 1 ]]; then + stale_retries=$((stale_retries + 1)) + if [[ "${stale_retries}" -gt 5 ]]; then + echo " ontoref: stale lock on '${resource}' could not be cleared after 5 attempts" >&2 + return 1 + fi + rm -rf "${lockdir}" + sleep 0.1 + continue + fi + if [[ "${elapsed}" -ge "${timeout}" ]]; then + local owner="unknown" + [[ -f "${lockdir}/owner" ]] && owner="$(cat "${lockdir}/owner")" + echo " ontoref: lock timeout on '${resource}' after ${timeout}s (held by: ${owner})" >&2 + return 1 + fi + sleep 1 + elapsed=$(( elapsed + 1 )) + done + + local now + now="$(date -u +%Y%m%dT%H%M%SZ)" + echo "$$:${ONTOREF_ACTOR}:${now}" > "${lockdir}/owner" + ACQUIRED_LOCK="${lockdir}" +} + +release_lock() { + if [[ -n "${ACQUIRED_LOCK}" ]]; then + rm -rf "${ACQUIRED_LOCK}" 2>/dev/null || true + ACQUIRED_LOCK="" + fi +} + +# ── Export caller identity ───────────────────────────────────────────────────── export ONTOREF_CALLER="${ONTOREF_CALLER:-ontoref}" -exec "${ONTOREF_ROOT}/ontoref" "$@" +if [[ "${ENV_ONLY}" -eq 1 ]]; then + # shellcheck disable=SC2317 + return 0 2>/dev/null || exit 0 +fi + +# ── No-args usage ───────────────────────────────────────────────────────────── + +if [[ "${#REMAINING_ARGS[@]}" -eq 0 ]]; then + echo "" + echo " ontoref" + echo " Usage: ontoref [options]" + echo "" + echo " Use 'ontoref help' for available commands" + echo "" + exit 0 +fi + +# ── Rewrite help flags ──────────────────────────────────────────────────────── + +_has_help=0 +_non_help_args=() +for _a in "${REMAINING_ARGS[@]+"${REMAINING_ARGS[@]}"}"; do + case "${_a}" in + --help|-help|-h) _has_help=1 ;; + *) _non_help_args+=("${_a}") ;; + esac +done +if [[ "${_has_help}" -eq 1 ]]; then + if [[ "${#_non_help_args[@]}" -gt 0 ]]; then + REMAINING_ARGS=("help" "${_non_help_args[@]}") + else + REMAINING_ARGS=("help") + fi +fi + +# ── Fix trailing flags that require a value ──────────────────────────────────── + +if [[ "${#REMAINING_ARGS[@]}" -gt 0 ]]; then + _last="${REMAINING_ARGS[${#REMAINING_ARGS[@]}-1]}" + # shellcheck disable=SC2249 + case "${_last}" in + --fmt|--format|-fmt|-f|--actor|--context|--severity|--backend|--kind|--priority|--status) + REMAINING_ARGS+=("select") + ;; + esac +fi + +# ── Delegate to Nushell dispatcher ──────────────────────────────────────────── + +LOCK_RESOURCE="$(determine_lock)" + +if [[ -n "${LOCK_RESOURCE}" ]]; then + acquire_lock "${LOCK_RESOURCE}" 30 + trap 'release_lock' EXIT INT TERM + nu "${DISPATCHER}" "${REMAINING_ARGS[@]+"${REMAINING_ARGS[@]}"}" +else + nu "${DISPATCHER}" "${REMAINING_ARGS[@]+"${REMAINING_ARGS[@]}"}" +fi diff --git a/reflection/nulib/interactive.nu b/reflection/nulib/interactive.nu index ede50e9..ff98c52 100644 --- a/reflection/nulib/interactive.nu +++ b/reflection/nulib/interactive.nu @@ -101,7 +101,15 @@ export def run-interactive [group: string] { print $" (ansi dark_gray)($cmd_info.desc)(ansi reset)" mut collected_args: list = [] for arg in $cmd_info.args { - let val = (input $" (ansi cyan)($arg.prompt):(ansi reset) ") + let val = try { + input $" (ansi cyan)($arg.prompt):(ansi reset) " + } catch { |err| + if ($err.msg | str contains "interrupted") { + print $"\n (ansi yellow)cancelled(ansi reset)" + return + } + error make { msg: $err.msg } + } if ($val | is-empty) and (not $arg.optional) { print $" (ansi yellow)required(ansi reset)" return