ontoref-code/install/install.nu
2026-07-10 01:44:59 +01:00

481 lines
20 KiB
Text
Executable file
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env nu
# install/install.nu — ontoref-daemon installer
#
# Installed layout:
#
# ~/.local/bin/
# ontoref ← global CLI wrapper (install/ontoref-global)
# ontoref-daemon ← bootstrapper: nickel export | ontoref-daemon.bin (ADR-004)
# ontoref-daemon.bin ← compiled binary (not called directly)
#
# Platform data/config:
# macOS:
# data ~/Library/Application Support/ontoref/ templates, public, nulib
# config ~/.config/ontoref/
# Linux:
# data ~/.local/share/ontoref/
# config ~/.config/ontoref/
#
# Dev mode (Cargo.toml present + nu in PATH): also installs ncl-bootstrap Nu helper
# Service mode: bootstrapper is the sole entrypoint — .bin never called directly
def install-if-changed [src: string, dest: string, label: string] {
let needs_update = if ($dest | path exists) {
(open --raw $src | hash sha256) != (open --raw $dest | hash sha256)
} else {
true
}
if $needs_update {
cp $src $dest
chmod +x $dest
print $"✓ ($label) ($dest)"
} else {
print $"— ($label) unchanged"
}
}
def main [] {
# Derive repo_root from script location — robust regardless of CWD.
# install.nu lives at <repo>/install/install.nu → parent of parent = repo root.
let repo_root = ($env.CURRENT_FILE | path expand | path dirname | path dirname)
let is_dev = (($"($repo_root)/Cargo.toml" | path exists) and ((which nu | length) > 0))
let platform = (sys host | get name)
let is_mac = ((^uname) == "Darwin")
let data_dir = if $is_mac {
$"($env.HOME)/Library/Application Support/ontoref"
} else {
$"($env.HOME)/.local/share/ontoref"
}
let config_dir = $"($env.HOME)/.config/ontoref"
let bin_dir = $"($env.HOME)/.local/bin"
mkdir $bin_dir
# ── 1. Binary → ontoref-daemon.bin ────────────────────────────────────────
let target_dir = (cd $repo_root; ^cargo metadata --format-version 1 --no-deps | from json | get target_directory)
let bin_src = $"($target_dir)/release/ontoref-daemon"
let bin_dest = $"($bin_dir)/ontoref-daemon.bin"
if not ($bin_src | path exists) {
error make { msg: $"binary not found: ($bin_src)\n run: cargo build --release -p ontoref-daemon" }
}
# Stop any running instance — macOS kills the new process if the old one holds the file
do { ^pkill -x ontoref-daemon.bin } | ignore
do { ^pkill -x ontoref-daemon } | ignore
cp $bin_src $bin_dest
chmod +x $bin_dest
#if $is_mac {
# do { ^xattr -d com.apple.quarantine $bin_dest } | ignore
#}
print $"✓ binary ($bin_dest)"
# ── 2. Bootstrapper → ontoref-daemon ──────────────────────────────────────
# The bootstrapper IS the public entrypoint. Users call ontoref-daemon, never .bin directly.
let boot_src = $"($repo_root)/install/ontoref-daemon-boot"
let boot_dest = $"($bin_dir)/ontoref-daemon"
install-if-changed $boot_src $boot_dest "bootstrapper"
# ── 3. Global CLI wrapper → ontoref ───────────────────────────────────────
# 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 workspace_version = (open $"($repo_root)/Cargo.toml" | get workspace.package.version)
let cli_baked = (
open --raw $cli_src
| str replace 'ONTOREF_ROOT="${ONTOREF_ROOT:-ontoref}"' $'ONTOREF_ROOT="${ONTOREF_ROOT:-($data_dir)}"'
| str replace --regex '# Release: \S+' $"# Release: ($workspace_version)"
)
let needs_update = if ($cli_dest | path exists) {
($cli_baked | hash sha256) != (open --raw $cli_dest | hash sha256)
} else {
true
}
if $needs_update {
$cli_baked | save --force $cli_dest
chmod +x $cli_dest
print $"✓ cli ($cli_dest)"
} else {
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).
# Repo-side source is `.ontoref/reflection/` (ADR-032 layout); falls back to
# the legacy root-level `reflection/` for pre-0023 repos. The installed
# `$data_dir/reflection/` keeps the protocol-resource layout so consumer
# tooling can reference `$ONTOREF_ROOT/reflection/...` without needing
# `.ontoref/` in shared installation paths.
let reflection_src = (
if ($"($repo_root)/.ontoref/reflection" | path exists) {
$"($repo_root)/.ontoref/reflection"
} else if ($"($repo_root)/../.ontoref/reflection" | path exists) {
# Constellation layout: spine lives at the parent of the code/ repo.
$"($repo_root)/../.ontoref/reflection" | path expand
} else {
$"($repo_root)/reflection"
}
)
let reflection_dest = $"($data_dir)/reflection"
if not ($reflection_src | path exists) {
error make { msg: $"reflection/ not found in repo: ($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)"
# ── 3b-bis. Witness projection (governed-delivery, migration 0041) ────────
# Single source: scripts/witness.nu, versioned with the install pipeline.
# Projected into the shipped reflection tree so consumer projects reach it
# at $ONTOREF_ROOT/reflection/bin/witness.nu — never vendored by copy into
# consumer repos (governed-delivery §7.4).
let witness_src = $"($repo_root)/scripts/witness.nu"
if ($witness_src | path exists) {
cp $witness_src $"($reflection_dest)/bin/witness.nu"
print $"✓ witness projected → ($reflection_dest)/bin/witness.nu"
}
# ── 3c. Domain extensions (domains/) ─────────────────────────────────────
# Domain commands are dispatched by the bash wrapper at runtime from $ONTOREF_ROOT/domains/.
let domains_src = $"($repo_root)/domains"
let domains_dest = $"($data_dir)/domains"
if ($domains_src | path exists) {
mkdir $domains_dest
mut dom_updated = 0
mut dom_skipped = 0
for src_file in (glob $"($domains_src)/**/*" | where { |f| ($f | path type) == "file" }) {
let rel = ($src_file | str replace $"($domains_src)/" "")
let dest_file = $"($domains_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
$dom_updated = $dom_updated + 1
} else {
$dom_skipped = $dom_skipped + 1
}
}
print $"✓ domains ($domains_dest)/ updated=($dom_updated) unchanged=($dom_skipped)"
# Build aliases.txt and install short_alias bin wrappers for each domain.
mut alias_lines = []
for domain_dir in (ls $domains_src | where type == "dir" | get name) {
let domain_ncl = $"($domain_dir)/domain.ncl"
if not ($domain_ncl | path exists) { continue }
let domain_data = (do { ^nickel export $domain_ncl } | complete)
if $domain_data.exit_code != 0 { continue }
let d = ($domain_data.stdout | from json)
let alias = ($d.short_alias? | default "")
let domain_id = ($d.id? | default "")
if ($alias | is-empty) { continue }
# aliases.txt entry — only when alias differs from domain id (e.g. prov → provisioning)
# skipped when alias == domain_id (e.g. personal → personal): ore already handles it natively
if $alias != $domain_id {
$alias_lines = ($alias_lines | append $"($alias)=($domain_id)")
}
# standalone bin wrapper — always created when short_alias is set,
# even when alias == domain_id, so `personal state` works without `ore`
let alias_dest = $"($bin_dir)/($alias)"
let alias_body = $"#!/bin/bash\nexec ontoref ($domain_id) \"$@\"\n"
let needs_update = if ($alias_dest | path exists) {
($alias_body | hash sha256) != (open --raw $alias_dest | hash sha256)
} else { true }
if $needs_update {
$alias_body | save --force $alias_dest
chmod +x $alias_dest
print $"✓ alias ($alias_dest) → ontoref ($domain_id)"
} else {
print $"— alias ($alias) unchanged"
}
}
# Write consolidated aliases.txt to the installed domains dir
let aliases_dest = $"($domains_dest)/aliases.txt"
if ($alias_lines | is-not-empty) {
let aliases_body = ($alias_lines | str join "\n")
let needs_update = if ($aliases_dest | path exists) {
($aliases_body | hash sha256) != (open --raw $aliases_dest | hash sha256)
} else { true }
if $needs_update {
$aliases_body | save --force $aliases_dest
print $"✓ domain-aliases ($aliases_dest)"
} else {
print $"— domain-aliases unchanged"
}
}
} else {
print $" (ansi yellow)warn(ansi reset) domains/ not found at ($domains_src)"
}
# ── 3d. CLI templates (project.ncl, ontoref-config.ncl, ontology/ stubs) ──
# `ontoref setup` reads from $ONTOREF_ROOT/templates/ — copy the repo-level
# templates/ tree so the installed CLI works without the source repo present.
let cli_templates_src = $"($repo_root)/templates"
let cli_templates_dest = $"($data_dir)/templates"
if ($cli_templates_src | path exists) {
mkdir $cli_templates_dest
mut tmpl_updated = 0
mut tmpl_skipped = 0
for src_file in (glob $"($cli_templates_src)/**/*" | where { |f| ($f | path type) == "file" }) {
let rel = ($src_file | str replace $"($cli_templates_src)/" "")
let dest_file = $"($cli_templates_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
$tmpl_updated = $tmpl_updated + 1
} else {
$tmpl_skipped = $tmpl_skipped + 1
}
}
print $"✓ cli-templates ($cli_templates_dest)/ updated=($tmpl_updated) unchanged=($tmpl_skipped)"
} else {
print $" (ansi yellow)warn(ansi reset) templates/ not found at ($cli_templates_src)"
}
# ── 3e. Ontology defaults + schemas (data dir) ───────────────────────────
# Consumer projects import "ontology/defaults/state.ncl", "defaults/manifest.ncl", etc.
# These must be resolvable from $ONTOREF_ROOT (the data dir).
# Structure: $data_dir/ontology/{defaults,schemas}/
# The bash wrapper includes $ONTOREF_ROOT in NICKEL_IMPORT_PATH, so:
# import "ontology/defaults/state.ncl" → $data_dir/ontology/defaults/state.ncl ✓
# import "defaults/state.ncl" → $data_dir/ontology/defaults/state.ncl ✓ (via $ONTOREF_ROOT/ontology in nickel-import-path)
let ontology_src = $"($repo_root)/ontology"
let ontology_dest = $"($data_dir)/ontology"
if ($ontology_src | path exists) {
mkdir $ontology_dest
mut ont_updated = 0
mut ont_skipped = 0
for src_file in (glob $"($ontology_src)/**/*.ncl" | where { |f| ($f | path type) == "file" }) {
let rel = ($src_file | str replace $"($ontology_src)/" "")
let dest_file = $"($ontology_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; $ont_updated = $ont_updated + 1 } else { $ont_skipped = $ont_skipped + 1 }
}
print $"✓ ontology ($ontology_dest)/ updated=($ont_updated) unchanged=($ont_skipped)"
}
# ── 3f. ADR protocol schema (data dir) ────────────────────────────────────
# Consumer projects' ADRs do `import "adr-defaults.ncl"` (→ adr-schema.ncl →
# adr-constraints.ncl). Bundle just the protocol SCHEMA (not the ontoref
# project's own adr-NNN files) into $data_dir/adrs so it resolves from
# $ONTOREF_ROOT/adrs (on the wrapper's nickel-import-path). Spine is at
# repo_root/.ontoref/adrs (ADR-032) or ../.ontoref/adrs (constellation).
let adrs_src = (
if ($"($repo_root)/.ontoref/adrs" | path exists) {
$"($repo_root)/.ontoref/adrs"
} else if ($"($repo_root)/../.ontoref/adrs" | path exists) {
$"($repo_root)/../.ontoref/adrs" | path expand
} else {
$"($repo_root)/adrs"
}
)
let adrs_dest = $"($data_dir)/adrs"
if ($adrs_src | path exists) {
mkdir $adrs_dest
for f in ["adr-defaults.ncl" "adr-schema.ncl" "adr-constraints.ncl"] {
let s = $"($adrs_src)/($f)"
if ($s | path exists) { cp $s $"($adrs_dest)/($f)" }
}
print $"✓ adr schema ($adrs_dest)/"
}
# ── 4. UI assets (data dir) ────────────────────────────────────────────────
let templates_src = $"($repo_root)/crates/ontoref-daemon/templates"
let public_src = $"($repo_root)/crates/ontoref-daemon/public"
if not ($templates_src | path exists) {
error make { msg: $"templates not found: ($templates_src)" }
}
mkdir $data_dir
let asset_dirs = [$templates_src $public_src]
mut updated = 0
mut skipped = 0
for asset_dir in $asset_dirs {
let dir_name = ($asset_dir | path basename)
let dest_base = $"($data_dir)/($dir_name)"
mkdir $dest_base
for src_file in (glob $"($asset_dir)/**/*" | where { |f| ($f | path type) == "file" }) {
let rel = ($src_file | str replace $"($asset_dir)/" "")
let dest_file = $"($dest_base)/($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
$updated = $updated + 1
} else {
$skipped = $skipped + 1
}
}
}
print $"✓ assets ($data_dir)/ updated=($updated) unchanged=($skipped)"
# ── 5. Config skeleton + global NATS topology ─────────────────────────────
let streams_default = $"($repo_root)/install/resources/streams.json"
let streams_dest = $"($config_dir)/streams.json"
if ($streams_default | path exists) {
mkdir $config_dir
if not ($streams_dest | path exists) {
cp $streams_default $streams_dest
print $"✓ nats topology ($streams_dest)"
} else {
print $"— nats topology unchanged"
}
}
let config_default = $"($repo_root)/install/resources/config.ncl"
let config_dest = $"($config_dir)/config.ncl"
let config_example = $"($config_dir)/config.ncl.example"
if ($config_default | path exists) {
mkdir $config_dir
cp $config_default $config_example
if ($config_dest | path exists) {
print $" config already exists — not overwritten"
print $" example kept: ($config_example)"
} else {
cp $config_default $config_dest
print $"✓ config ($config_dest)"
print $" edit with: ontoref config-edit"
}
}
# projects.ncl — local projects; populated by `ontoref project-add`
let projects_default = $"($repo_root)/install/resources/projects.ncl"
let projects_dest = $"($config_dir)/projects.ncl"
if ($projects_default | path exists) and not ($projects_dest | path exists) {
cp $projects_default $projects_dest
print $"✓ projects ($projects_dest)"
}
# remote-projects.ncl — remote/push-only projects; populated by `ontoref project-add-remote`
let remote_default = $"($repo_root)/install/resources/remote-projects.ncl"
let remote_dest = $"($config_dir)/remote-projects.ncl"
if ($remote_default | path exists) and not ($remote_dest | path exists) {
cp $remote_default $remote_dest
print $"✓ remote-projects ($remote_dest)"
}
# schemas/ — project contract schemas imported by per-project .ontoref/project.ncl
let schemas_src = $"($repo_root)/install/resources/schemas"
let schemas_dest = $"($config_dir)/schemas"
if ($schemas_src | path exists) {
mkdir $schemas_dest
for f in (ls $schemas_src | get name) {
let dest_f = $"($schemas_dest)/(($f | path basename))"
if not ($dest_f | path exists) {
cp $f $dest_f
print $"✓ schema ($dest_f)"
}
}
}
# ── 6. Install scripts (gen-projects.nu, etc.) + hooks ────────────────────
# The bootstrapper looks for *.nu at $data_dir/install/.
# `ontoref hooks-install` looks for install/hooks/{post-commit,post-merge}.
let install_dest = $"($data_dir)/install"
mkdir $install_dest
for f in (glob $"($repo_root)/install/*.nu") {
let dest_f = $"($install_dest)/(($f | path basename))"
install-if-changed $f $dest_f $"install/(($f | path basename))"
}
let hooks_src = $"($repo_root)/install/hooks"
let hooks_dest = $"($install_dest)/hooks"
if ($hooks_src | path exists) {
mkdir $hooks_dest
for f in (glob $"($hooks_src)/*" | where { |p| ($p | path type) == "file" }) {
let dest_f = $"($hooks_dest)/(($f | path basename))"
install-if-changed $f $dest_f $"install/hooks/(($f | path basename))"
}
}
# ── 7. Dev extras: ncl-bootstrap Nu helper ────────────────────────────────
if $is_dev {
let nulib_dest = $"($data_dir)/nulib"
mkdir $nulib_dest
let bootstrap_src = (
if ($"($repo_root)/.ontoref/reflection/nulib/bootstrap.nu" | path exists) {
$"($repo_root)/.ontoref/reflection/nulib/bootstrap.nu"
} else if ($"($repo_root)/../.ontoref/reflection/nulib/bootstrap.nu" | path exists) {
$"($repo_root)/../.ontoref/reflection/nulib/bootstrap.nu" | path expand
} else {
$"($repo_root)/reflection/nulib/bootstrap.nu"
}
)
cp $bootstrap_src $"($nulib_dest)/bootstrap.nu"
print $"✓ ncl-bootstrap ($nulib_dest)/bootstrap.nu"
}
# ── Summary ────────────────────────────────────────────────────────────────
let mode_tag = if $is_dev { "dev" } else { "service" }
print $"\ninstalled mode=($mode_tag) platform=($platform)"
print $" bin ($bin_dir)/ontoref, ontoref-daemon, ontoref-daemon.bin"
print $" data ($data_dir)/ \(reflection/, domains/, templates/, ...\)"
print $" config ($config_dir)/"
print ""
print " next: nu install/config-setup.nu"
}