7323b63a7566

setup: on NixOS offer to append 'nix.settings.experimental-features = [ "nix-command" "flakes" ];' into configuration.nix (insert before final brace, sudo cp, default-No prompt) instead of writing ~/.config/nix/nix.conf; show copy-paste manual fallback + nixos-rebuild instruction; detect already-declared-needs-rebuild case; add exec.appendFeaturesToNixosConfig/nixosConfigEnablesFeatures and output.confirmDefaultNo + setupNixos* messages

time
2026-06-05 16:34 UTC · 4 months ago
machine
koh2
agent
claude-opus-4-8
session
nina setup declarative on nixos
parent
29642d33c40e
3 files src/commands.zig+25 −1src/exec.zig+88 −0src/output.zig+43 −5
src/commands.zig+25 −1
@@ -1102,11 +1102,35 @@11021102         return;11031103     }11041104 1105+    // On NixOS the features belong in configuration.nix (the declarative,1106+    // permanent home), not ~/.config/nix/nix.conf. Offer to add the one line;1107+    // the user runs the rebuild. We don't pin the registry here because the1108+    // features aren't live until that rebuild lands.1109+    if (exec.isNixOS(io)) {1110+        const conf_path = "/etc/nixos/configuration.nix";1111+        if (exec.nixosConfigEnablesFeatures(gpa, io, conf_path)) {1112+            output.setupNixosNeedsRebuild(conf_path);1113+            return;1114+        }1115+        output.setupNixosOffer(exec.NIXOS_FEATURES_LINE, conf_path);1116+        if (output.confirmDefaultNo("add it now?")) {1117+            if (exec.appendFeaturesToNixosConfig(gpa, io, environ, conf_path)) |_| {1118+                output.setupNixosWrote(conf_path);1119+            } else |_| {1120+                output.setupNixosManual(exec.NIXOS_FEATURES_LINE, conf_path);1121+            }1122+        } else {1123+            output.setupNixosManual(exec.NIXOS_FEATURES_LINE, conf_path);1124+        }1125+        return;1126+    }1127+1128+    // Non-NixOS: a user-level nix.conf takes effect immediately, no sudo needed.11051129     output.setupEnabling();11061130     const path = try exec.enableNixFeatures(gpa, io, environ);11071131     defer gpa.free(path);11081132 1109-    output.setupDone(path, exec.isNixOS(io));1133+    output.setupDone(path);11101134     api.ensureRegistryPinned(gpa, io, environ);11111135 }11121136 

view file

src/exec.zig+88 −0
@@ -1064,3 +1064,91 @@10641064 10651065     return file_path;10661066 }1067+1068+// --- NixOS: declarative experimental-features ---1069+//1070+// On NixOS the permanent, declarative home for these flags is configuration.nix,1071+// not ~/.config/nix/nix.conf. nina offers to add this one line and then leaves1072+// the `nixos-rebuild switch` to the user.1073+1074+pub const NIXOS_FEATURES_LINE =1075+    "nix.settings.experimental-features = [ \"nix-command\" \"flakes\" ];";1076+1077+// Best-effort, non-sudo check (configuration.nix is 0644 by default). Returns1078+// false on any read error so the caller still offers to add the line.1079+pub fn nixosConfigEnablesFeatures(gpa: std.mem.Allocator, io: std.Io, path: []const u8) bool {1080+    const conf = std.Io.Dir.readFileAlloc(.cwd(), io, path, gpa, .unlimited) catch return false;1081+    defer gpa.free(conf);1082+    return std.mem.indexOf(u8, conf, "experimental-features") != null and1083+        std.mem.indexOf(u8, conf, "nix-command") != null and1084+        std.mem.indexOf(u8, conf, "flakes") != null;1085+}1086+1087+fn readMaybeSudo(gpa: std.mem.Allocator, io: std.Io, path: []const u8) ![]u8 {1088+    if (std.Io.Dir.readFileAlloc(.cwd(), io, path, gpa, .unlimited) catch null) |data| return data;1089+    return captureLocal(gpa, io, &.{ "sudo", "cat", path });1090+}1091+1092+// Insert NIXOS_FEATURES_LINE just before the final `}` of configuration.nix1093+// (i.e. inside the top-level attribute set) and write it back via a single1094+// `sudo cp`, which prompts for the password on the terminal. Returns an error1095+// for the caller to fall back to printed manual instructions.1096+pub fn appendFeaturesToNixosConfig(1097+    gpa: std.mem.Allocator,1098+    io: std.Io,1099+    environ: *const std.process.Environ.Map,1100+    path: []const u8,1101+) !void {1102+    const conf = readMaybeSudo(gpa, io, path) catch {1103+        errors.error_info.setMessage("could not read {s}", .{path});1104+        return error.ConfigNotFound;1105+    };1106+    defer gpa.free(conf);1107+1108+    if (std.mem.indexOf(u8, conf, "experimental-features") != null) return; // already declared1109+1110+    const brace = std.mem.lastIndexOfScalar(u8, conf, '}') orelse {1111+        errors.error_info.setMessage("no closing brace in {s} — add the line by hand", .{path});1112+        return error.MalformedConfig;1113+    };1114+1115+    const home = environ.get("HOME") orelse {1116+        errors.error_info.setMessage("HOME not set", .{});1117+        return error.ConfigNotFound;1118+    };1119+    const tmp_path = try std.fmt.allocPrint(gpa, "{s}/.nina-configuration.nix", .{home});1120+    defer gpa.free(tmp_path);1121+1122+    var buf: std.ArrayList(u8) = .empty;1123+    defer buf.deinit(gpa);1124+    try buf.appendSlice(gpa, conf[0..brace]);1125+    if (brace == 0 or conf[brace - 1] != '\n') try buf.append(gpa, '\n');1126+    try buf.appendSlice(gpa, "  # added by nina setup — flakes + the new nix CLI\n");1127+    try buf.appendSlice(gpa, "  ");1128+    try buf.appendSlice(gpa, NIXOS_FEATURES_LINE);1129+    try buf.append(gpa, '\n');1130+    try buf.appendSlice(gpa, conf[brace..]);1131+1132+    const tmp = std.Io.Dir.createFile(.cwd(), io, tmp_path, .{}) catch {1133+        errors.error_info.setMessage("could not write {s}", .{tmp_path});1134+        return error.FileNotFound;1135+    };1136+    tmp.writePositionalAll(io, buf.items, 0) catch {1137+        tmp.close(io);1138+        std.Io.Dir.deleteFile(.cwd(), io, tmp_path) catch {};1139+        errors.error_info.setMessage("could not write {s}", .{tmp_path});1140+        return error.FileNotFound;1141+    };1142+    tmp.close(io);1143+1144+    const code = streamLocal(io, &.{ "sudo", "cp", tmp_path, path }) catch {1145+        std.Io.Dir.deleteFile(.cwd(), io, tmp_path) catch {};1146+        errors.error_info.setMessage("could not write {s}", .{path});1147+        return error.ProcessFailed;1148+    };1149+    std.Io.Dir.deleteFile(.cwd(), io, tmp_path) catch {};1150+    if (code != 0) {1151+        errors.error_info.setMessage("sudo cp into {s} failed", .{path});1152+        return error.ProcessFailed;1153+    }1154+}

view file

src/output.zig+43 −5
@@ -161,6 +161,24 @@161161     return input[0] != 'n' and input[0] != 'N';162162 }163163 164+// Like confirm, but defaults to NO on an empty answer — for prompts that touch165+// system files, where a stray Enter should never trigger the action.166+pub fn confirmDefaultNo(question: []const u8) bool {167+    p("   {s} [y/N]  ", .{question});168+    flush();169+    var buf: [64]u8 = undefined;170+    var i: usize = 0;171+    while (i < buf.len) {172+        const n = std.posix.read(std.posix.STDIN_FILENO, buf[i .. i + 1]) catch break;173+        if (n == 0) break;174+        if (buf[i] == '\n') break;175+        i += 1;176+    }177+    const input = std.mem.trim(u8, buf[0..i], " \t");178+    if (input.len == 0) return false;179+    return input[0] == 'y' or input[0] == 'Y';180+}181+164182 // --- Apply / rebuild ---165183 166184 pub fn applyStart(machine: []const u8) void {@@ -858,13 +876,33 @@858876     p("{s}enabling experimental features\n", .{hdr()});859877 }860878 861-pub fn setupDone(path: []const u8, is_nixos: bool) void {879+pub fn setupDone(path: []const u8) void {862880     p("{s}wrote {s}\n", .{ step(), path });863881     p("{s}done  open a new shell to pick up the change\n", .{hdr()});864-    if (is_nixos) {865-        printSubstep("to persist across nixos-rebuild, add to configuration.nix:", .{});866-        printSubstep("  nix.settings.experimental-features = [ \"nix-command\" \"flakes\" ];", .{});867-    }882+}883+884+// --- NixOS setup (declarative configuration.nix) ---885+886+pub fn setupNixosOffer(features_line: []const u8, conf_path: []const u8) void {887+    p("{s}on NixOS these go in your system config\n", .{hdr()});888+    p("   nina can add this line to {s}{s}{s}:\n\n", .{ c(DIM), conf_path, c(RESET) });889+    p("       {s}{s}{s}\n\n", .{ c(CYAN), features_line, c(RESET) });890+}891+892+pub fn setupNixosWrote(conf_path: []const u8) void {893+    p("{s}added the line to {s}\n", .{ step(), conf_path });894+    p("{s}now run {s}sudo nixos-rebuild switch{s} — then nina's ready\n", .{ hdr(), c(BOLD), c(RESET) });895+}896+897+pub fn setupNixosManual(features_line: []const u8, conf_path: []const u8) void {898+    p("{s}no problem — add this line inside the {s}{{ }}{s} in {s}:\n\n", .{ hdr(), c(DIM), c(RESET), conf_path });899+    p("       {s}{s}{s}\n\n", .{ c(CYAN), features_line, c(RESET) });900+    p("   then run {s}sudo nixos-rebuild switch{s} — then nina's ready\n", .{ c(BOLD), c(RESET) });901+}902+903+pub fn setupNixosNeedsRebuild(conf_path: []const u8) void {904+    p("{s}already declared in {s}\n", .{ hdr(), conf_path });905+    p("   just run {s}sudo nixos-rebuild switch{s} to activate it\n", .{ c(BOLD), c(RESET) });868906 }869907 870908 pub fn pinDone(input: []const u8, commit: []const u8) void {

view file