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
↓ nina-5096769f.tar.gzatlas at this save
files changed6 files
README.md+13 −27VERSION+1 −1build.zig.zon+1 −1install.sh+0 −357src/commands.zig+18 −38src/output.zig+4 −11README.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 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 allowsinstall.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-farewellsrc/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 {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 {