123456set -e78FLAKE_URL="https://kepr.uk/nina/archive/HEAD.tar.gz"9RELEASES_URL="https://kepr.uk/api/nina/releases/latest"10INSTALL_DIR="${NINA_INSTALL_DIR:-$HOME/.local/bin}"11MIN_ZIG_MINOR=161213141516if [ -t 1 ] && [ -z "${NO_COLOR:-}" ]; then17 PINK="$(printf '\033[38;2;255;183;197m')"18 CYAN="$(printf '\033[36m')"19 GREEN="$(printf '\033[32m')"20 RED="$(printf '\033[31m')"21 DIM="$(printf '\033[2m')"22 BOLD="$(printf '\033[1m')"23 RESET="$(printf '\033[0m')"24else25 PINK=""; CYAN=""; GREEN=""; RED=""; DIM=""; BOLD=""; RESET=""26fi2728os="$(uname -s)"29arch="$(uname -m)"30case "${os}-${arch}" in31 Linux-x86_64) platform="linux-x86_64" ;;32 Linux-aarch64|Linux-arm64) platform="linux-aarch64" ;;33 Darwin-arm64) platform="macos-aarch64" ;;34 Darwin-x86_64) platform="macos-x86_64" ;;35 *)36 printf 'nina: unsupported platform %s-%s\n' "$os" "$arch" >&237 exit 1 ;;38esac3940have() { command -v "$1" >/dev/null 2>&1; }41say() { printf '%snina:%s %s\n' "$PINK" "$RESET" "$1"; }42die() { printf '%snina:%s %serror:%s %s\n' "$PINK" "$RESET" "$RED" "$RESET" "$1" >&2; exit 1; }43blank() { printf '\n'; }44is_nixos() { [ -e /etc/NIXOS ]; }45464748out() { printf ' %s\n' "$1"; }49hr() { printf ' %s────────────────────────────────────────────────%s\n' "$DIM" "$RESET"; }5051fetch_text() {52 if have curl; then curl -fsSL "$1"53 elif have wget; then wget -qO- "$1"54 else die "curl or wget required"; fi55}5657fetch_file() {58 if have curl; then curl -fsSL "$1" -o "$2"59 elif have wget; then wget -qO "$2" "$1"60 else die "curl or wget required"; fi61}62636465banner() {66 cat <<'BANNER'6768 _ __ _ _ __ __ _69 | '_ \ (_)| '_ \ / _` |70 | | | || || | | || (_| |71 |_| |_||_||_| |_| \__,_|7273 NixOS Intuitive Navigation Assistant · kepr.uk/nina7475BANNER76}77787980fetch_source() {81 82 83 84 srcdir="$(mktemp -d /tmp/nina-src.XXXXXX)"85 say "fetching source from Kepr..." >&286 fetch_file "$FLAKE_URL" "${srcdir}/nina.tar.gz" \87 || die "could not download source archive — check your connection"88 tar -xzf "${srcdir}/nina.tar.gz" -C "$srcdir" --strip-components=1 \89 || die "could not extract source archive"90 printf '%s' "$srcdir"91}92939495install_binary() {96 say "fetching release metadata..."97 meta="$(fetch_text "$RELEASES_URL" 2>/dev/null)" || return 198 [ -n "$meta" ] || return 199100 if have python3; then101 version="$(printf '%s' "$meta" | python3 -c "102import sys, json103d = json.load(sys.stdin)104print(d.get('version') or '')105" 2>/dev/null)"106 dl_url="$(printf '%s' "$meta" | python3 -c "107import sys, json108d = json.load(sys.stdin)109print((d.get('platforms') or {}).get('${platform}', {}).get('url') or '')110" 2>/dev/null)"111 cksum="$(printf '%s' "$meta" | python3 -c "112import sys, json113d = json.load(sys.stdin)114p = (d.get('platforms') or {}).get('${platform}', {})115raw = p.get('blake3') or p.get('checksum') or ''116print(raw[len('blake3:'):] if raw.startswith('blake3:') else raw)117" 2>/dev/null)"118 elif have jq; then119 version="$(printf '%s' "$meta" | jq -r '.version // empty')"120 dl_url="$(printf '%s' "$meta" | jq -r ".platforms.\"${platform}\".url // empty")"121 cksum_raw="$(printf '%s' "$meta" | jq -r ".platforms.\"${platform}\".blake3 // .platforms.\"${platform}\".checksum // empty")"122 cksum="${cksum_raw#blake3:}"123 else124 version="$(printf '%s' "$meta" | grep -o '"version":"[^"]*"' | sed 's/"version":"//;s/"//')"125 plat_block="$(printf '%s' "$meta" | grep -o "\"${platform}\":{[^}]*}" | head -1)"126 dl_url="$(printf '%s' "$plat_block" | grep -o '"url":"[^"]*"' | sed 's/"url":"//;s/"//')"127 cksum=""128 fi129130 [ -n "$dl_url" ] || return 1131 [ -n "$version" ] || version="latest"132133 say "downloading nina ${version} for ${platform}..."134 tmpfile="$(mktemp /tmp/nina.XXXXXX)"135 fetch_file "$dl_url" "$tmpfile" || { rm -f "$tmpfile"; return 1; }136137 if [ -n "$cksum" ] && have b3sum; then138 actual="$(b3sum --no-names "$tmpfile" 2>/dev/null | awk '{print $1}')"139 if [ "$actual" != "$cksum" ]; then140 say "checksum mismatch — aborting"141 rm -f "$tmpfile"142 return 1143 fi144 say "checksum verified"145 fi146147 chmod +x "$tmpfile"148 mkdir -p "$INSTALL_DIR"149 mv "$tmpfile" "${INSTALL_DIR}/nina"150 say "installed nina ${version} → ${INSTALL_DIR}/nina"151 return 0152}153154155156nixos_profile_install() {157 have nix || return 1158 say "installing nina to ~/.nix-profile/bin/nina via nix profile..."159 blank160 161 162 163 164 165 if nix profile add --no-write-lock-file "${FLAKE_URL}#nina"; then166 return 0167 fi168 blank169 say "nix profile add failed. Common causes:"170 say " flakes not enabled — add to /etc/nix/nix.conf or ~/.config/nix/nix.conf:"171 say " experimental-features = nix-command flakes"172 say " once nina is installed: nina setup enables this automatically"173 blank174 return 1175}176177178179nixos_shell_build() {180 have nix-shell || return 1181 say "fetching source and building nina using nix-shell..."182 blank183 srcdir="$(fetch_source)"184185 186 187 build_cmd="cd '${srcdir}' && zig build -Doptimize=ReleaseSafe"188 if nix-shell -p zig_0_16 --run "$build_cmd" 2>/dev/null; then189 :190 else191 say "zig_0_16 not in channel — fetching from nixpkgs-unstable..."192 193 194 nix-shell \195 -I nixpkgs=https://channels.nixos.org/nixos-unstable/nixexprs.tar.xz \196 -p zig_0_16 \197 --run "$build_cmd" \198 || die "build failed — see output above"199 fi200201 mkdir -p "$INSTALL_DIR"202 cp "${srcdir}/zig-out/bin/nina" "${INSTALL_DIR}/nina"203 chmod +x "${INSTALL_DIR}/nina"204 blank205 say "installed from source → ${INSTALL_DIR}/nina"206 say "note: binary built outside nix — adopt ~/nina.nix into configuration.nix"207 say " to make nina fully nix-managed (nina goodbye handles removal either way)"208}209210211212213214215216217offer_config_import() {218 config="$1"219 nina_nix="$2"220221 [ -f "$config" ] && [ -r "$config" ] || return 1222223 224 if grep -qF "$nina_nix" "$config" 2>/dev/null; then225 out "already imported in ${CYAN}${config}${RESET}"226 out "apply with ${BOLD}sudo nixos-rebuild switch${RESET}"227 blank228 return 0229 fi230231 232 233 grep -qE '^[[:space:]]*imports[[:space:]]*=[[:space:]]*\[' "$config" || return 1234235 236 [ -r /dev/tty ] || return 1237 printf ' add it to %s%s%s now? %s[y/N]%s ' "$CYAN" "$config" "$RESET" "$DIM" "$RESET" > /dev/tty238 IFS= read -r ans < /dev/tty 2>/dev/null || ans=""239 case "$ans" in240 [Yy]*) ;;241 *) return 1 ;;242 esac243244 ts="$(date +%Y%m%d%H%M%S 2>/dev/null || echo backup)"245 backup="${config}.nina-bak.${ts}"246 sudo cp "$config" "$backup" 2>/dev/null \247 || { blank; out "could not back up ${config} — add it manually instead."; blank; return 1; }248249 250 tmp="$(mktemp /tmp/nina-conf.XXXXXX)"251 awk -v p="$nina_nix" '252 !done && /imports[[:space:]]*=[[:space:]]*\[/ { sub(/\[/, "[ " p); done=1 }253 { print }254 ' "$config" > "$tmp" 2>/dev/null255256 if ! sudo cp "$tmp" "$config" 2>/dev/null; then257 rm -f "$tmp"258 blank259 out "could not write ${config}."260 out "restore the backup with: sudo cp ${backup} ${config}"261 blank262 return 1263 fi264 rm -f "$tmp"265266 blank267 out "${GREEN}✓${RESET} added the import to ${CYAN}${config}${RESET}"268 out " ${DIM}backup kept at ${backup}${RESET}"269 out " apply with ${BOLD}sudo nixos-rebuild switch${RESET}"270 blank271 return 0272}273274generate_nixos_module() {275 276 cat > "$HOME/nina.nix" <<'NIXEOF'277# ~/nina.nix — generated by the nina installer278#279# Declares nina as a NixOS system package.280#281# Add to /etc/nixos/configuration.nix:282# imports = [ /home/YOUR_USER/nina.nix ];283# Then: sudo nixos-rebuild switch284#285# To update: nina update (or re-run the installer)286# To remove: nina goodbye (or delete this file, then nixos-rebuild switch)287#288# For flake-based configurations, add nina as a flake input instead:289# inputs.nina.url = "https://kepr.uk/nina/archive/HEAD.tar.gz";290# # in your nixosModules list: nina.nixosModules.default291# # then set: programs.nina.enable = true;292{ ... }:293{294 imports = [295 "${builtins.fetchTarball "https://kepr.uk/nina/archive/HEAD.tar.gz"}/nixos-module.nix"296 ];297 programs.nina.enable = true;298}299NIXEOF300301 nina_nix="$HOME/nina.nix"302303 blank304 hr305 out "${BOLD}system-wide install${RESET} ${DIM}(optional)${RESET}"306 hr307 blank308 out "nina is installed for your user. To manage her alongside the"309 out "rest of your system, a module was written to:"310 blank311 out " ${CYAN}${nina_nix}${RESET}"312 blank313314 if offer_config_import "/etc/nixos/configuration.nix" "$nina_nix"; then315 return 0316 fi317318 blank319 out "to do it yourself, add this to the ${CYAN}imports${RESET} list in"320 out "${CYAN}/etc/nixos/configuration.nix${RESET}:"321 blank322 out " ${CYAN}${nina_nix}${RESET}"323 blank324 out "then run ${BOLD}sudo nixos-rebuild switch${RESET}"325 blank326}327328329330check_zig() {331 if ! have zig; then332 blank333 say "Zig not found. Building nina requires Zig 0.${MIN_ZIG_MINOR}+."334 blank335 say " Download: https://ziglang.org/download/"336 if is_nixos; then337 say " NixOS: nix-shell -p zig_0_16 --run 'sh install.sh'"338 fi339 blank340 die "Zig 0.${MIN_ZIG_MINOR}+ required"341 fi342343 zig_ver="$(zig version 2>/dev/null)"344 major="$(printf '%s' "$zig_ver" | cut -d. -f1)"345 minor="$(printf '%s' "$zig_ver" | cut -d. -f2)"346 ok=0347 [ "$major" -ge 1 ] 2>/dev/null && ok=1348 [ "$major" -eq 0 ] 2>/dev/null && [ "$minor" -ge "$MIN_ZIG_MINOR" ] 2>/dev/null && ok=1349350 if [ "$ok" -ne 1 ]; then351 blank352 say "found zig ${zig_ver}, but nina requires Zig 0.${MIN_ZIG_MINOR}+."353 if is_nixos; then354 say " run: nix-shell -p zig_0_16 --run 'sh install.sh'"355 else356 say " update: https://ziglang.org/download/"357 fi358 blank359 die "Zig 0.${MIN_ZIG_MINOR}+ required; found ${zig_ver}"360 fi361362 say "zig ${zig_ver} found"363}364365366367build_from_source() {368 say "building from source..."369 check_zig370371 if [ -f "build.zig" ] && [ -f "src/main.zig" ]; then372 srcdir="."373 else374 srcdir="$(fetch_source)"375 fi376377 (cd "$srcdir" && zig build -Doptimize=ReleaseSafe) \378 || die "build failed — see output above"379380 mkdir -p "$INSTALL_DIR"381 cp "${srcdir}/zig-out/bin/nina" "${INSTALL_DIR}/nina"382 chmod +x "${INSTALL_DIR}/nina"383 say "installed from source → ${INSTALL_DIR}/nina"384}385386387388check_path() {389 case ":${PATH}:" in390 *":${INSTALL_DIR}:"*) ;;391 *)392 blank393 say "${INSTALL_DIR} is not in your PATH. Add it:"394 say " export PATH=\"\${PATH}:${INSTALL_DIR}\""395 say " echo 'export PATH=\"\${PATH}:${INSTALL_DIR}\"' >> ~/.bashrc"396 if is_nixos; then397 say " NixOS: add to environment.sessionVariables in configuration.nix"398 fi399 ;;400 esac401}402403404405406407408maybe_setup_features() {409 have nix || return 0410 411 nix eval --expr '1' >/dev/null 2>&1 && return 0412413 NINA="${INSTALL_DIR}/nina"414 [ -x "$NINA" ] || NINA="nina"415416 blank417 say "nina needs nix's experimental features (nix-command and flakes) to"418 say "search and install packages."419420 if [ -r /dev/tty ]; then421 printf 'nina: enable them now? [Y/n] ' > /dev/tty422 IFS= read -r ans < /dev/tty 2>/dev/null || ans="y"423 case "$ans" in424 [Nn]*) say "skipped — enable later with: nina setup"; return 0 ;;425 esac426 "$NINA" setup || say "automatic setup failed — run: nina setup"427 else428 say "run nina setup to enable them"429 fi430}431432433434farewell() {435 blank436 printf ' %sthank you!!%s get started by running %snina hello%s\n' "$PINK" "$RESET" "$CYAN" "$RESET"437 printf ' run %snina update%s occasionally to keep me in tip-top shape\n' "$CYAN" "$RESET"438 blank439 printf ' %snot for you?%s %snina goodbye%s %sremoves her cleanly%s\n' "$DIM" "$RESET" "$CYAN" "$RESET" "$DIM" "$RESET"440 blank441}442443444445banner446447if is_nixos; then448 say "NixOS detected"449 if nixos_profile_install; then450 generate_nixos_module451 maybe_setup_features452 farewell453 exit 0454 fi455 456 457 say "trying prebuilt binary from Kepr..."458 if install_binary; then459 check_path460 maybe_setup_features461 farewell462 exit 0463 fi464 say "no prebuilt binary available — building from source"465 nixos_shell_build466 generate_nixos_module467 check_path468 maybe_setup_features469 farewell470 exit 0471fi472473474if install_binary; then475 check_path476 maybe_setup_features477 farewell478 exit 0479fi480481say "no pre-built binary for ${platform} — falling back to source build"482build_from_source483check_path484maybe_setup_features485farewell