4f2faa7345dd

smart update: nix profile upgrade when in nix store; goodbye command for clean uninstall

time
2026-06-04 16:40 UTC · 4 months ago
machine
koh2
agent
claude-sonnet-4-6
session
install script and kepr setup
parent
1e7fb76f4530
5 files install.sh+8 −5src/commands.zig+73 −6src/exec.zig+21 −0src/main.zig+1 −0src/output.zig+51 −2
install.sh+8 −5
@@ -132,9 +132,9 @@132132 133133 nixos_profile_install() {134134   have nix || return 1135-  say "installing via nix profile install..."135+  say "installing nina to ~/.nix-profile/bin/nina via nix profile..."136136   blank137-  if nix profile install "$FLAKE_URL"; then137+  if nix profile install "${FLAKE_URL}#nina"; then138138     return 0139139   fi140140   blank@@ -178,12 +178,13 @@178178 #   imports = [ /home/YOUR_USER/nina.nix ];179179 # Then: sudo nixos-rebuild switch180180 #181+# To update:  nina update  (or re-run the installer)182+# To remove:  nina goodbye  (or delete this file, then nixos-rebuild switch)183+#181184 # For flake-based configurations, add nina as a flake input instead:182185 #   inputs.nina.url = "https://kepr.uk/nina/archive/HEAD.tar.gz";183-#   # in your modules: nina.nixosModules.default186+#   # in your nixosModules list: nina.nixosModules.default184187 #   # then set: programs.nina.enable = true;185-#186-# To update nina: nina update187188 { ... }:188189 {189190   imports = [@@ -280,6 +281,8 @@280281   printf '\n'281282   printf '  thank you!! running nina update occasionally will keep me in tip top shape.\n'282283   printf '  get started by running nina hello\n'284+  printf '\n'285+  printf '  if nina turns out not to be for you, nina goodbye will remove her cleanly.\n'283286   printf '\n'284287 }285288 

view file

src/commands.zig+73 −6
@@ -223,6 +223,25 @@223223     output.updateSelfAvailable(version.VERSION, remote_ver);224224     if (ctx.check) return;225225 226+    var exe_buf: [std.Io.Dir.max_path_bytes]u8 = undefined;227+    const install_path = exec.selfExePath(ctx.io, &exe_buf) catch {228+        errors.error_info.setMessage("could not determine install path", .{});229+        return error.Internal;230+    };231+232+    // When running from the nix store, defer to nix profile upgrade so nix233+    // manages the transition (the store is read-only; binary replacement fails).234+    if (std.mem.startsWith(u8, install_path, "/nix/store/")) {235+        output.updateSelfNixManaged(remote_ver);236+        if (ctx.cfg.confirm and !output.confirm("")) return;237+        const ec = exec.streamLocal(ctx.io, &.{ "nix", "profile", "upgrade", ".*nina.*" }) catch {238+            output.updateSelfNixFailed();239+            return;240+        };241+        if (ec != 0) output.updateSelfNixSystemManaged();242+        return;243+    }244+226245     const plat_info = blk: {227246         const pv = root.object.get("platforms") orelse break :blk null;228247         if (pv != .object) break :blk null;@@ -247,12 +266,6 @@247266 248267     if (ctx.cfg.confirm and !output.confirm("")) return;249268 250-    var exe_buf: [std.Io.Dir.max_path_bytes]u8 = undefined;251-    const install_path = exec.selfExePath(ctx.io, &exe_buf) catch {252-        errors.error_info.setMessage("could not determine install path", .{});253-        return error.Internal;254-    };255-256269     output.updateSelfDownloading(remote_ver, version.PLATFORM);257270     const tmp_path = exec.downloadToTemp(ctx.gpa, ctx.io, dl_url) catch {258271         output.printWarning("download failed — check your connection and try again");@@ -267,6 +280,60 @@267280     };268281 269282     output.updateSelfDone(version.VERSION, remote_ver);283+}284+285+// nina goodbye — clean uninstall from nix profile or binary location.286+pub fn goodbye(ctx: Ctx) !void {287+    output.goodbyeStart();288+289+    var exe_buf: [std.Io.Dir.max_path_bytes]u8 = undefined;290+    const install_path = exec.selfExePath(ctx.io, &exe_buf) catch {291+        errors.error_info.setMessage("could not determine install path", .{});292+        return error.Internal;293+    };294+295+    if (std.mem.startsWith(u8, install_path, "/nix/store/")) {296+        output.goodbyeNix();297+        if (ctx.cfg.confirm and !output.confirm("")) return;298+        const ec = exec.streamLocal(ctx.io, &.{ "nix", "profile", "remove", ".*nina.*" }) catch {299+            output.goodbyeFailed("nix profile remove failed");300+            return;301+        };302+        if (ec != 0) {303+            output.goodbyeNixSystemManaged();304+            return;305+        }306+    } else {307+        output.goodbyeFile(install_path);308+        if (ctx.cfg.confirm and !output.confirm("")) return;309+        std.Io.Dir.deleteFileAbsolute(ctx.io, install_path) catch |e| {310+            errors.error_info.setMessage("could not remove {s}", .{install_path});311+            return e;312+        };313+    }314+315+    // Offer to remove ~/nina.nix if it exists (generated by the installer).316+    if (ctx.environ.get("HOME")) |home| {317+        const nina_nix: ?[]u8 = std.fmt.allocPrint(ctx.gpa, "{s}/nina.nix", .{home}) catch null;318+        if (nina_nix) |path| {319+            defer ctx.gpa.free(path);320+            const exists = blk: {321+                const bytes = std.Io.Dir.readFileAlloc(.cwd(), ctx.io, path, ctx.gpa, .unlimited) catch break :blk false;322+                ctx.gpa.free(bytes);323+                break :blk true;324+            };325+            if (exists) {326+                output.goodbyeNixFileFound(path);327+                if (!ctx.cfg.confirm or output.confirm("")) {328+                    std.Io.Dir.deleteFileAbsolute(ctx.io, path) catch {329+                        output.printWarning("could not remove ~/nina.nix — remove it manually to clean up");330+                    };331+                }332+            }333+        }334+    }335+336+    output.goodbyeDone();270337 }271338 272339 fn channelUpdateCmd(ctx: Ctx) !void {

view file

src/exec.zig+21 −0
@@ -62,6 +62,27 @@6262     };6363 }6464 65+// Stream a local process with inherited stdio (no SSH routing — for nix commands).66+pub fn streamLocal(io: std.Io, cmd: []const []const u8) !u8 {67+    var child = std.process.spawn(io, .{68+        .argv = cmd,69+        .stdin = .inherit,70+        .stdout = .inherit,71+        .stderr = .inherit,72+    }) catch |e| {73+        errors.error_info.setMessage("failed to spawn: {s}", .{cmd[0]});74+        return e;75+    };76+    const term = child.wait(io) catch {77+        errors.error_info.setMessage("process wait failed", .{});78+        return error.ProcessFailed;79+    };80+    return switch (term) {81+        .exited => |c| c,82+        else => 1,83+    };84+}85+6586 // Run a process and capture output as a single string (no SSH routing — for local nix commands).6687 pub fn captureLocal(gpa: std.mem.Allocator, io: std.Io, cmd: []const []const u8) ![]u8 {6788     const result = std.process.run(gpa, io, .{ .argv = cmd }) catch return error.ProcessFailed;

view file

src/main.zig+1 −0
@@ -174,6 +174,7 @@174174     if (std.mem.eql(u8, cmd, "fetch")) return commands.fetch(ctx);175175     if (std.mem.eql(u8, cmd, "build")) return commands.build(ctx);176176     if (std.mem.eql(u8, cmd, "run")) return commands.runCmd(ctx);177+    if (std.mem.eql(u8, cmd, "goodbye")) return commands.goodbye(ctx);177178     if (std.mem.eql(u8, cmd, "hello")) return commands.hello(ctx);178179     if (std.mem.eql(u8, cmd, "help")) { output.help(); return; }179180 

view file

src/output.zig+51 −2
@@ -573,8 +573,9 @@573573         .{ "pin", "pin flake inputs" },574574         .{ "hash", "compute nix hash" },575575         .{ "fetch", "fetch and hash a url" },576-        .{ "hello", "list machines" },577-        .{ "help", "this message" },576+        .{ "goodbye",       "uninstall nina" },577+        .{ "hello",        "list machines" },578+        .{ "help",         "this message" },578579     };579580     for (cmds) |cmd| {580581         p("   {s:<18}  {s}{s}{s}\n", .{ cmd[0], c(DIM), cmd[1], c(RESET) });@@ -661,6 +662,54 @@661662 pub fn updateSelfChecksumFailed() void {662663     p("{s}checksum mismatch — download may be corrupt\n", .{if (color_enabled) ERR_C else ERR_P});663664     p("{s}try again or download manually from kepr.uk/nina/releases\n", .{step()});665+}666+667+pub fn updateSelfNixManaged(new_ver: []const u8) void {668+    p("{s}upgrading to {s}{s}{s} via nix profile\n", .{ step(), c(BOLD), new_ver, c(RESET) });669+}670+671+pub fn updateSelfNixFailed() void {672+    p("{s}nix profile upgrade failed\n", .{if (color_enabled) ERR_C else ERR_P});673+    p("{s}try manually: nix profile upgrade '.*nina.*'\n", .{step()});674+}675+676+pub fn updateSelfNixSystemManaged() void {677+    p("{s}nina appears to be managed by your NixOS system configuration\n", .{hdr()});678+    p("{s}to update: sudo nixos-rebuild switch\n", .{step()});679+    p("{s}to reinstall as a profile package: curl -fsSL kepr.uk/nina/install.sh | sh\n", .{step()});680+}681+682+// --- Goodbye / uninstall ---683+684+pub fn goodbyeStart() void {685+    p("{s}uninstalling nina\n", .{hdr()});686+}687+688+pub fn goodbyeFile(path: []const u8) void {689+    p("{s}will remove {s}\n", .{ step(), path });690+}691+692+pub fn goodbyeNix() void {693+    p("{s}will remove nina from nix profile\n", .{step()});694+    p("{s}command: nix profile remove '.*nina.*'\n", .{step()});695+}696+697+pub fn goodbyeNixSystemManaged() void {698+    p("{s}nina is managed by your NixOS system configuration\n", .{hdr()});699+    p("{s}to remove: delete ~/nina.nix, then run sudo nixos-rebuild switch\n", .{step()});700+    p("{s}or remove the nina import from /etc/nixos/configuration.nix directly\n", .{step()});701+}702+703+pub fn goodbyeNixFileFound(path: []const u8) void {704+    p("{s}found {s} (NixOS config snippet from the installer)\n", .{ step(), path });705+}706+707+pub fn goodbyeDone() void {708+    p("{s}goodbye! reinstall anytime: curl -fsSL kepr.uk/nina/install.sh | sh\n", .{hdr()});709+}710+711+pub fn goodbyeFailed(msg: []const u8) void {712+    p("{s}{s}\n", .{ if (color_enabled) ERR_C else ERR_P, msg });664713 }665714 666715 // --- Setup ---

view file