5096769f9bb3

fix goodbye to cleanly remove via nix profile remove; delete install.sh; drop ~/nina.nix system-managed path; update README install section to 3 paths; bump to v3.0.18

time
2026-06-07 19:21 UTC · 4 months ago
machine
koh2
agent
claude-sonnet-4-6
session
goodbye overhaul and install.sh removal
parent
b487cd10cf40
6 files README.md+13 −27VERSION+1 −1build.zig.zon+1 −1install.sh+0 −357src/commands.zig+18 −38src/output.zig+4 −11
README.md+13 −27
@@ -15,14 +15,15 @@1515 1616 ## install1717 18+**nix profile** (recommended — instant, per-user, no `sudo`):19+1820 ```sh19-curl -fsSL kepr.uk/nina/install.sh | sh21+nix profile add 'https://kepr.uk/nina/archive/HEAD.tar.gz#nina'2022 ```2123 22-On NixOS this installs nina into your **nix profile** — instant, per-user, no23-`sudo`, and it never touches `/etc/nixos`. Remove it any time with `nina goodbye`.24+Remove any time with `nina goodbye`.2425 25-or clone and build:26+**koh** (build from source via koh):2627 2728 ```sh2829 koh steal kepr.uk/nina@@ -30,33 +31,18 @@3031 sudo cp zig-out/bin/nina /usr/local/bin/nina3132 ```3233 33-**linux · nixos 24.05+** — requires `nix-command` and `flakes` experimental features.  34-Run `nina setup` on first use to enable them automatically.34+**source archive** (download and build manually):3535 36-### declarative install (power users)36+Download the source archive from [kepr.uk/nina/releases](https://kepr.uk/nina/releases), then:3737 38-Prefer to manage nina alongside the rest of your system? The repo ships a NixOS39-module, so you can install her the declarative way instead of the profile.40-41-Plain `configuration.nix` — add to your `imports`:42-43-```nix44-imports = [45-  "${builtins.fetchTarball "https://kepr.uk/nina/archive/HEAD.tar.gz"}/nixos-module.nix"46-];47-programs.nina.enable = true;38+```sh39+tar xf nina-*.tar.gz && cd nina-*/40+zig build -Doptimize=ReleaseFast41+sudo cp zig-out/bin/nina /usr/local/bin/nina4842 ```4943 50-Flake-based config — add nina as an input:51-52-```nix53-inputs.nina.url = "https://kepr.uk/nina/archive/HEAD.tar.gz";54-# in your nixosModules list:  nina.nixosModules.default55-# then set:                   programs.nina.enable = true;56-```57-58-Then `sudo nixos-rebuild switch`. Pick **one** method — the profile install and59-the declarative module both put `nina` on your PATH, so don't run both.44+**linux · nixos 24.05+** — requires `nix-command` and `flakes` experimental features.  45+Run `nina setup` on first use to enable them automatically.6046 6147 ---6248 

view file

VERSION+1 −1
@@ -1 +1 @@1-3.0.171+3.0.18

view file

build.zig.zon+1 −1
@@ -9,7 +9,7 @@99     .name = .nina,1010     // This is a [Semantic Version](https://semver.org/).1111     // In a future version of Zig it will be used for package deduplication.12-    .version = "3.0.15",12+    .version = "3.0.18",1313     // Together with name, this represents a globally unique package1414     // identifier. This field is generated by the Zig toolchain when the1515     // package is first created, and then *never changes*. This allows

view file

install.sh+0 −357
@@ -1,357 +0,0 @@1-#!/usr/bin/env sh2-# nina install script3-# Run:  curl -fsSL kepr.uk/nina/install.sh | sh4-# Or:   NINA_INSTALL_DIR=/usr/local/bin sh install.sh5-6-set -e7-8-FLAKE_URL="https://kepr.uk/nina/archive/HEAD.tar.gz"9-RELEASES_URL="https://kepr.uk/api/nina/releases/latest"10-INSTALL_DIR="${NINA_INSTALL_DIR:-$HOME/.local/bin}"11-MIN_ZIG_MINOR=1612-13-# Color, but only when stdout is a real terminal and NO_COLOR is unset. Under14-# `curl … | sh` stdin is the pipe yet stdout is still the tty, so -t 1 holds.15-# Palette mirrors nina's own: pink prefix, cyan for paths/commands.16-if [ -t 1 ] && [ -z "${NO_COLOR:-}" ]; then17-  PINK="$(printf '\033[38;2;255;183;197m')"18-  CYAN="$(printf '\033[36m')"19-  RED="$(printf '\033[31m')"20-  DIM="$(printf '\033[2m')"21-  RESET="$(printf '\033[0m')"22-else23-  PINK=""; CYAN=""; RED=""; DIM=""; RESET=""24-fi25-26-os="$(uname -s)"27-arch="$(uname -m)"28-case "${os}-${arch}" in29-  Linux-x86_64)               platform="linux-x86_64"  ;;30-  Linux-aarch64|Linux-arm64)  platform="linux-aarch64" ;;31-  Darwin-arm64)               platform="macos-aarch64" ;;32-  Darwin-x86_64)              platform="macos-x86_64"  ;;33-  *)34-    printf 'nina: unsupported platform %s-%s\n' "$os" "$arch" >&235-    exit 1 ;;36-esac37-38-have()  { command -v "$1" >/dev/null 2>&1; }39-say()   { printf '%snina:%s %s\n' "$PINK" "$RESET" "$1"; }40-die()   { printf '%snina:%s %serror:%s %s\n' "$PINK" "$RESET" "$RED" "$RESET" "$1" >&2; exit 1; }41-blank() { printf '\n'; }42-is_nixos() { [ -e /etc/NIXOS ]; }43-44-fetch_text() {45-  if have curl;   then curl -fsSL "$1"46-  elif have wget; then wget -qO- "$1"47-  else die "curl or wget required"; fi48-}49-50-fetch_file() {51-  if have curl;   then curl -fsSL "$1" -o "$2"52-  elif have wget; then wget -qO  "$2" "$1"53-  else die "curl or wget required"; fi54-}55-56-# ── banner ────────────────────────────────────────────────────────────────────57-58-banner() {59-  cat <<'BANNER'60-61-   _ __   _  _ __    __ _62-  | '_ \ (_)| '_ \  / _` |63-  | | | || || | | || (_| |64-  |_| |_||_||_| |_| \__,_|65-66-  NixOS Intuitive Navigation Assistant  ·  kepr.uk/nina67-68-BANNER69-}70-71-# ── fetch source archive ──────────────────────────────────────────────────────72-73-fetch_source() {74-  # This function's stdout IS the source path (captured via command75-  # substitution), so every diagnostic must go to stderr — otherwise the76-  # caller's `cd "$srcdir"` receives the progress text too and fails.77-  srcdir="$(mktemp -d /tmp/nina-src.XXXXXX)"78-  say "fetching source from Kepr..." >&279-  fetch_file "$FLAKE_URL" "${srcdir}/nina.tar.gz" \80-    || die "could not download source archive — check your connection"81-  tar -xzf "${srcdir}/nina.tar.gz" -C "$srcdir" --strip-components=1 \82-    || die "could not extract source archive"83-  printf '%s' "$srcdir"84-}85-86-# ── binary install from Kepr ─────────────────────────────────────────────────87-88-install_binary() {89-  say "fetching release metadata..."90-  meta="$(fetch_text "$RELEASES_URL" 2>/dev/null)" || return 191-  [ -n "$meta" ] || return 192-93-  if have python3; then94-    version="$(printf '%s' "$meta" | python3 -c "95-import sys, json96-d = json.load(sys.stdin)97-print(d.get('version') or '')98-" 2>/dev/null)"99-    dl_url="$(printf '%s' "$meta" | python3 -c "100-import sys, json101-d = json.load(sys.stdin)102-print((d.get('platforms') or {}).get('${platform}', {}).get('url') or '')103-" 2>/dev/null)"104-    cksum="$(printf '%s' "$meta" | python3 -c "105-import sys, json106-d = json.load(sys.stdin)107-p = (d.get('platforms') or {}).get('${platform}', {})108-raw = p.get('blake3') or p.get('checksum') or ''109-print(raw[len('blake3:'):] if raw.startswith('blake3:') else raw)110-" 2>/dev/null)"111-  elif have jq; then112-    version="$(printf '%s' "$meta" | jq -r '.version // empty')"113-    dl_url="$(printf '%s' "$meta" | jq -r ".platforms.\"${platform}\".url // empty")"114-    cksum_raw="$(printf '%s' "$meta" | jq -r ".platforms.\"${platform}\".blake3 // .platforms.\"${platform}\".checksum // empty")"115-    cksum="${cksum_raw#blake3:}"116-  else117-    version="$(printf '%s' "$meta" | grep -o '"version":"[^"]*"' | sed 's/"version":"//;s/"//')"118-    plat_block="$(printf '%s' "$meta" | grep -o "\"${platform}\":{[^}]*}" | head -1)"119-    dl_url="$(printf '%s' "$plat_block" | grep -o '"url":"[^"]*"' | sed 's/"url":"//;s/"//')"120-    cksum=""121-  fi122-123-  [ -n "$dl_url" ] || return 1124-  [ -n "$version" ] || version="latest"125-126-  say "downloading nina ${version} for ${platform}..."127-  tmpfile="$(mktemp /tmp/nina.XXXXXX)"128-  fetch_file "$dl_url" "$tmpfile" || { rm -f "$tmpfile"; return 1; }129-130-  if [ -n "$cksum" ] && have b3sum; then131-    actual="$(b3sum --no-names "$tmpfile" 2>/dev/null | awk '{print $1}')"132-    if [ "$actual" != "$cksum" ]; then133-      say "checksum mismatch — aborting"134-      rm -f "$tmpfile"135-      return 1136-    fi137-    say "checksum verified"138-  fi139-140-  chmod +x "$tmpfile"141-  mkdir -p "$INSTALL_DIR"142-  mv "$tmpfile" "${INSTALL_DIR}/nina"143-  say "installed nina ${version} → ${INSTALL_DIR}/nina"144-  return 0145-}146-147-# ── NixOS: nix profile add (primary) ─────────────────────────────────────────148-149-nixos_profile_install() {150-  have nix || return 1151-  say "installing nina to ~/.nix-profile/bin/nina via nix profile..."152-  blank153-  # Tarball flakes are fetched read-only into the store, so nix cannot persist154-  # a freshly-resolved flake.lock and aborts with "cannot write modified lock155-  # file". --no-write-lock-file resolves inputs in memory without writing.156-  # `add` is the current verb; `install` is a deprecated alias that prints a157-  # warning on modern nix. Mirrors exec.zig profileInstall.158-  if nix profile add --no-write-lock-file "${FLAKE_URL}#nina"; then159-    return 0160-  fi161-  blank162-  say "nix profile add failed. Common causes:"163-  say "  flakes not enabled — add to /etc/nix/nix.conf or ~/.config/nix/nix.conf:"164-  say "    experimental-features = nix-command flakes"165-  say "  once nina is installed: nina setup enables this automatically"166-  blank167-  return 1168-}169-170-# ── NixOS: nix-shell source build (fallback) ──────────────────────────────────171-172-nixos_shell_build() {173-  have nix-shell || return 1174-  say "fetching source and building nina using nix-shell..."175-  blank176-  srcdir="$(fetch_source)"177-178-  # Try the user's channel first; fall back to a pinned nixpkgs-unstable if179-  # zig_0_16 is not yet in the channel (common on NixOS 24.11 stable).180-  build_cmd="cd '${srcdir}' && zig build -Doptimize=ReleaseSafe"181-  if nix-shell -p zig_0_16 --run "$build_cmd" 2>/dev/null; then182-    :183-  else184-    say "zig_0_16 not in channel — fetching from nixpkgs-unstable..."185-    # channels.nixos.org redirects to the current unstable release tarball;186-    # the older releases.nixos.org/.../nixos-unstable/ path 404s.187-    nix-shell \188-      -I nixpkgs=https://channels.nixos.org/nixos-unstable/nixexprs.tar.xz \189-      -p zig_0_16 \190-      --run "$build_cmd" \191-      || die "build failed — see output above"192-  fi193-194-  mkdir -p "$INSTALL_DIR"195-  cp "${srcdir}/zig-out/bin/nina" "${INSTALL_DIR}/nina"196-  chmod +x "${INSTALL_DIR}/nina"197-  blank198-  say "installed from source → ${INSTALL_DIR}/nina"199-  say "note: per-user binary, not nix-managed (nina goodbye removes it cleanly)"200-}201-202-# ── Zig version check ─────────────────────────────────────────────────────────203-204-check_zig() {205-  if ! have zig; then206-    blank207-    say "Zig not found. Building nina requires Zig 0.${MIN_ZIG_MINOR}+."208-    blank209-    say "  Download: https://ziglang.org/download/"210-    if is_nixos; then211-      say "  NixOS:    nix-shell -p zig_0_16 --run 'sh install.sh'"212-    fi213-    blank214-    die "Zig 0.${MIN_ZIG_MINOR}+ required"215-  fi216-217-  zig_ver="$(zig version 2>/dev/null)"218-  major="$(printf '%s' "$zig_ver" | cut -d. -f1)"219-  minor="$(printf '%s' "$zig_ver" | cut -d. -f2)"220-  ok=0221-  [ "$major" -ge 1 ] 2>/dev/null && ok=1222-  [ "$major" -eq 0 ] 2>/dev/null && [ "$minor" -ge "$MIN_ZIG_MINOR" ] 2>/dev/null && ok=1223-224-  if [ "$ok" -ne 1 ]; then225-    blank226-    say "found zig ${zig_ver}, but nina requires Zig 0.${MIN_ZIG_MINOR}+."227-    if is_nixos; then228-      say "  run: nix-shell -p zig_0_16 --run 'sh install.sh'"229-    else230-      say "  update: https://ziglang.org/download/"231-    fi232-    blank233-    die "Zig 0.${MIN_ZIG_MINOR}+ required; found ${zig_ver}"234-  fi235-236-  say "zig ${zig_ver} found"237-}238-239-# ── generic source build (non-NixOS) ─────────────────────────────────────────240-241-build_from_source() {242-  say "building from source..."243-  check_zig244-245-  if [ -f "build.zig" ] && [ -f "src/main.zig" ]; then246-    srcdir="."247-  else248-    srcdir="$(fetch_source)"249-  fi250-251-  (cd "$srcdir" && zig build -Doptimize=ReleaseSafe) \252-    || die "build failed — see output above"253-254-  mkdir -p "$INSTALL_DIR"255-  cp "${srcdir}/zig-out/bin/nina" "${INSTALL_DIR}/nina"256-  chmod +x "${INSTALL_DIR}/nina"257-  say "installed from source → ${INSTALL_DIR}/nina"258-}259-260-# ── PATH reminder ─────────────────────────────────────────────────────────────261-262-check_path() {263-  case ":${PATH}:" in264-    *":${INSTALL_DIR}:"*) ;;265-    *)266-      blank267-      say "${INSTALL_DIR} is not in your PATH. Add it:"268-      say "  export PATH=\"\${PATH}:${INSTALL_DIR}\""269-      say "  echo 'export PATH=\"\${PATH}:${INSTALL_DIR}\"' >> ~/.bashrc"270-      if is_nixos; then271-        say "  NixOS: add to environment.sessionVariables in configuration.nix"272-      fi273-      ;;274-  esac275-}276-277-# ── enable nix experimental features ──────────────────────────────────────────278-# nina needs nix-command + flakes to search and install. If they're off, offer to279-# turn them on via the freshly-installed `nina setup`. Read from /dev/tty so the280-# prompt works even under `curl … | sh`, where stdin is the script itself.281-282-maybe_setup_features() {283-  have nix || return 0284-  # `nix eval` itself requires nix-command, so this doubles as the feature probe.285-  nix eval --expr '1' >/dev/null 2>&1 && return 0286-287-  NINA="${INSTALL_DIR}/nina"288-  [ -x "$NINA" ] || NINA="nina"289-290-  blank291-  say "nina needs nix's experimental features (nix-command and flakes) to"292-  say "search and install packages."293-294-  if [ -r /dev/tty ]; then295-    printf 'nina: enable them now? [Y/n] ' > /dev/tty296-    IFS= read -r ans < /dev/tty 2>/dev/null || ans="y"297-    case "$ans" in298-      [Nn]*) say "skipped — enable later with:  nina setup"; return 0 ;;299-    esac300-    "$NINA" setup || say "automatic setup failed — run:  nina setup"301-  else302-    say "run  nina setup  to enable them"303-  fi304-}305-306-# ── closing message ──────────────────────────────────────────────────────────307-308-farewell() {309-  blank310-  printf '  %sthank you!!%s get started by running %snina hello%s\n' "$PINK" "$RESET" "$CYAN" "$RESET"311-  printf '  run %snina update%s occasionally to keep me in tip-top shape\n' "$CYAN" "$RESET"312-  blank313-  printf '  %snot for you?%s %snina goodbye%s %sremoves her cleanly%s\n' "$DIM" "$RESET" "$CYAN" "$RESET" "$DIM" "$RESET"314-  blank315-}316-317-# ── main ──────────────────────────────────────────────────────────────────────318-319-banner320-321-if is_nixos; then322-  say "NixOS detected"323-  if nixos_profile_install; then324-    maybe_setup_features325-    farewell326-    exit 0327-  fi328-  # nina's binaries link no libc, so the Linux builds are fully static and run329-  # on NixOS — try the prebuilt before falling back to a from-source build.330-  say "trying prebuilt binary from Kepr..."331-  if install_binary; then332-    check_path333-    maybe_setup_features334-    farewell335-    exit 0336-  fi337-  say "no prebuilt binary available — building from source"338-  nixos_shell_build339-  check_path340-  maybe_setup_features341-  farewell342-  exit 0343-fi344-345-# macOS / generic Linux: download binary, fall back to source build346-if install_binary; then347-  check_path348-  maybe_setup_features349-  farewell350-  exit 0351-fi352-353-say "no pre-built binary for ${platform} — falling back to source build"354-build_from_source355-check_path356-maybe_setup_features357-farewell

view file at the parent

src/commands.zig+18 −38
@@ -293,7 +293,7 @@293293             if (nina_element) |n| ctx.gpa.free(n);294294         }295295         if (nina_element == null) {296-            output.updateSelfNixSystemManaged();296+            output.updateSelfNotInProfile();297297             return;298298         }299299         const element = nina_element.?;@@ -347,9 +347,9 @@347347     output.updateSelfDone(version.VERSION, remote_ver);348348 }349349 350-// nina goodbye — clean uninstall. Handles the three install methods, and the351-// case where more than one is present at once (the installer can add nina to352-// the nix profile AND wire ~/nina.nix into the system config).350+// nina goodbye — clean uninstall. Three install paths:351+// - nix profile (binary in nix store with a profile element) → nix profile remove352+// - koh build or source build (plain binary on PATH) → delete the file353353 pub fn goodbye(ctx: Ctx) !void {354354     output.goodbyeStart();355355 @@ -359,7 +359,7 @@359359         return error.Internal;360360     };361361 362-    // A binary outside the nix store is a plain install — just delete it.362+    // A binary outside the nix store is a koh build or source build — delete it.363363     if (!std.mem.startsWith(u8, install_path, "/nix/store/")) {364364         output.goodbyeFile(install_path);365365         if (ctx.cfg.confirm and !output.confirm("")) return;@@ -371,48 +371,28 @@371371         return;372372     }373373 374-    // In the nix store: a `nix profile` install, a NixOS system package375-    // (programs.nina.enable via ~/nina.nix), or both — they're independent and376-    // both resolve into the store. Remove the profile element if present, and377-    // separately flag the system module. We never auto-remove the system one:378-    // editing /etc/nixos needs sudo, and deleting ~/nina.nix while it's still379-    // imported breaks the next rebuild.374+    // Binary is in the nix store — find and remove the profile element.380375     const nina_element = exec.profileNinaElement(ctx.gpa, ctx.io, &ctx.machine);381376     defer {382377         if (nina_element) |n| ctx.gpa.free(n);383378     }384-    const system_managed = ninaNixExists(ctx);385379 386-    if (nina_element) |element| {387-        output.goodbyeNix();388-        if (ctx.cfg.confirm and !output.confirm("")) return;389-        const ec = exec.streamLocal(ctx.io, &.{ "nix", "profile", "remove", element }) catch {390-            output.goodbyeFailed("nix profile remove failed");391-            return;392-        };393-        if (ec != 0) {394-            output.goodbyeFailed("nix profile remove failed");395-            return;396-        }380+    if (nina_element == null) {381+        errors.error_info.setMessage("nina not found in nix profile — was it installed via 'nix profile add'?", .{});382+        return error.Internal;397383     }398384 399-    // System module present, or nothing in the profile and not a plain binary400-    // (so it must be system-managed) — instruct rather than touch /etc/nixos.401-    if (system_managed or nina_element == null) {402-        output.goodbyeNixSystemManaged();385+    output.goodbyeNix();386+    if (ctx.cfg.confirm and !output.confirm("")) return;387+    const ec = exec.streamLocal(ctx.io, &.{ "nix", "profile", "remove", nina_element.? }) catch {388+        output.goodbyeFailed("nix profile remove failed");389+        return;390+    };391+    if (ec != 0) {392+        output.goodbyeFailed("nix profile remove failed");393+        return;403394     }404-405395     output.goodbyeDone();406-}407-408-// Read-only check for the installer's ~/nina.nix system-config snippet.409-fn ninaNixExists(ctx: Ctx) bool {410-    const home = ctx.environ.get("HOME") orelse return false;411-    const path = std.fmt.allocPrint(ctx.gpa, "{s}/nina.nix", .{home}) catch return false;412-    defer ctx.gpa.free(path);413-    const bytes = std.Io.Dir.readFileAlloc(.cwd(), ctx.io, path, ctx.gpa, .unlimited) catch return false;414-    ctx.gpa.free(bytes);415-    return true;416396 }417397 418398 fn channelUpdateCmd(ctx: Ctx) !void {

view file

src/output.zig+4 −11
@@ -773,10 +773,9 @@773773     p("{s}try manually: nix profile upgrade '.*nina.*'\n", .{step()});774774 }775775 776-pub fn updateSelfNixSystemManaged() void {777-    p("{s}nina appears to be managed by your NixOS system configuration\n", .{hdr()});778-    p("{s}to update: sudo nixos-rebuild switch\n", .{step()});779-    p("{s}to reinstall as a profile package: curl -fsSL kepr.uk/nina/install.sh | sh\n", .{step()});776+pub fn updateSelfNotInProfile() void {777+    p("{s}nina is in the nix store but not found as a profile element\n", .{hdr()});778+    p("{s}reinstall via 'nix profile add' to enable self-update\n", .{step()});780779 }781780 782781 // --- Goodbye / uninstall ---@@ -794,14 +793,8 @@794793     p("{s}command: nix profile remove nina\n", .{step()});795794 }796795 797-pub fn goodbyeNixSystemManaged() void {798-    p("{s}nina is managed by your NixOS system configuration\n", .{hdr()});799-    p("{s}remove the nina import from /etc/nixos/configuration.nix first\n", .{step()});800-    p("{s}then delete ~/nina.nix and run sudo nixos-rebuild switch\n", .{step()});801-}802-803796 pub fn goodbyeDone() void {804-    p("{s}goodbye! reinstall anytime: curl -fsSL kepr.uk/nina/install.sh | sh\n", .{hdr()});797+    p("{s}goodbye! see kepr.uk/nina to reinstall\n", .{hdr()});805798 }806799 807800 pub fn goodbyeFailed(msg: []const u8) void {

view file