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
↓ nina-4f2faa73.tar.gzatlas at this save
files changed5 files
install.sh+8 −5src/commands.zig+73 −6src/exec.zig+21 −0src/main.zig+1 −0src/output.zig+51 −2install.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 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 {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;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 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 ---