d7d4dc71a0ea

setup wizard: scan for config and flake.nix, smart defaults — firstRunWizard now scans /etc/nixos (scanConfig: flake.nix -> flake, configuration.nix -> channel, else none), reports the detection, and pre-fills the config-path prompt (output.firstRunPromptDefault, [default] enter-to-confirm); flake-ness is re-detected at the chosen path via exec.dirHasFlake and written as 'flake = true' — never asked. Dropped the [1]/[2] manage-type question and the local prompt (defaults local=true); removed now-dead firstRunManageType. exec.pathExists made pub, configHasFlake refactored onto dirHasFlake. Verified both branches: plain dir -> no flake line; dir with flake.nix -> flake=true. build+tests green

time
2026-06-10 01:32 UTC · 4 months ago
machine
koh2
agent
claude-opus-4-8
session
flake + home manager support
parent
f41c183586ea
3 files src/commands.zig+31 −14src/exec.zig+9 −6src/output.zig+29 −11
src/commands.zig+31 −14
@@ -1467,28 +1467,45 @@14671467     output.hello(ctx.cfg.machines);14681468 }14691469 1470-// Runs when ~/.nina.conf is absent on first invocation (interactive TTY only).1471-// Prompts for machine name, config path, and local flag, then writes the file.1470+const ConfigKind = enum { flake, channel, none };1471+1472+// Scan the conventional /etc/nixos for a flake.nix (flake-based) or a1473+// configuration.nix (channel-based) to pre-fill the wizard's defaults.1474+fn scanConfig(io: std.Io) ConfigKind {1475+    if (exec.pathExists(io, "/etc/nixos/flake.nix")) return .flake;1476+    if (exec.pathExists(io, "/etc/nixos/configuration.nix")) return .channel;1477+    return .none;1478+}1479+1480+// Runs when the config is absent on first invocation (interactive TTY only).1481+// Scans for an existing nixos config, detects flake vs channel, and offers a1482+// smart default path — flake-ness is detected, never asked.14721483 pub fn firstRunWizard(gpa: std.mem.Allocator, io: std.Io, environ: *const std.process.Environ.Map) !void {14731484     output.firstRunBanner();14741485     output.firstRunNoConfig();14751486 14761487     var name_buf: [256]u8 = undefined;14771488     var path_buf: [256]u8 = undefined;1478-    var local_buf: [256]u8 = undefined;14791489 14801490     const machine_name = output.firstRunPrompt("machine name", "my-machine", &name_buf);14811491 1482-    // Flake-based systems don't have a traditional /etc/nixos/configuration.nix —1483-    // their config lives wherever flake.nix is. Ask, then prompt for the right1484-    // path and record `flake = true` so channel commands can warn appropriately.1485-    const use_flakes = output.firstRunManageType();1486-    const config_path = if (use_flakes)1487-        output.firstRunPrompt("flake directory", "~/nixos-config", &path_buf)1488-    else1489-        output.firstRunPrompt("config path", "/etc/nixos", &path_buf);1490-    const local_str = output.firstRunPrompt("local", "true", &local_buf);1492+    output.firstRunScanning();1493+    const kind = scanConfig(io);1494+    switch (kind) {1495+        .flake => output.firstRunDetected("/etc/nixos/flake.nix", true),1496+        .channel => output.firstRunDetected("/etc/nixos/configuration.nix", false),1497+        .none => output.firstRunNoneFound(),1498+    }14911499 1500+    // Detected systems default to /etc/nixos (press enter to confirm); when1501+    // nothing was found, leave the default empty so the user must type a path.1502+    const default_path: []const u8 = if (kind == .none) "" else "/etc/nixos";1503+    const config_path = output.firstRunPromptDefault("config path", default_path, kind != .none, &path_buf);1504+1505+    // Re-detect at the chosen path in case the user pointed elsewhere — flake.nix1506+    // there means a flake system, recorded so channel commands warn appropriately.1507+    const use_flakes = exec.dirHasFlake(gpa, io, config_path);1508+14921509     const conf_path = try config.configPath(gpa, environ);14931510     defer gpa.free(conf_path);14941511     if (std.fs.path.dirname(conf_path)) |dir| exec.ensureDir(gpa, io, dir);@@ -1498,8 +1515,8 @@14981515     var content_buf: [1024]u8 = undefined;14991516     const content = try std.fmt.bufPrint(15001517         &content_buf,1501-        "{s}[machine]\nname = {s}\nconfig = {s}\nlocal = {s}\n",1502-        .{ flake_line, machine_name, config_path, local_str },1518+        "{s}[machine]\nname = {s}\nconfig = {s}\nlocal = true\n",1519+        .{ flake_line, machine_name, config_path },15031520     );15041521 15051522     const file = std.Io.Dir.createFile(.cwd(), io, conf_path, .{}) catch {

view file

src/exec.zig+9 −6
@@ -175,13 +175,16 @@175175 // machine — the file lives on the local filesystem. Remote flake detection would176176 // need an ssh round-trip; a remote flake at the default /etc/nixos needs no flag177177 // anyway, so we don't bother.178+// True if `dir` contains a flake.nix (local filesystem check).179+pub fn dirHasFlake(gpa: std.mem.Allocator, io: std.Io, dir: []const u8) bool {180+    const flake_path = std.fs.path.join(gpa, &.{ dir, "flake.nix" }) catch return false;181+    defer gpa.free(flake_path);182+    return pathExists(io, flake_path);183+}184+178185 pub fn configHasFlake(gpa: std.mem.Allocator, io: std.Io, machine: *const types.Machine) bool {179186     if (!machine.local) return false;180-    const flake_path = std.fs.path.join(gpa, &.{ machine.config_path, "flake.nix" }) catch return false;181-    defer gpa.free(flake_path);182-    const f = std.Io.Dir.openFile(.cwd(), io, flake_path, .{}) catch return false;183-    f.close(io);184-    return true;187+    return dirHasFlake(gpa, io, machine.config_path);185188 }186189 187190 // nixos-rebuild auto-detects /etc/nixos/flake.nix, so the --flake flag is only@@ -1082,7 +1085,7 @@10821085     return std.fmt.allocPrint(gpa, "{s}/home.nix", .{dir}) catch null;10831086 }10841087 1085-fn pathExists(io: std.Io, path: []const u8) bool {1088+pub fn pathExists(io: std.Io, path: []const u8) bool {10861089     const f = std.Io.Dir.openFile(.cwd(), io, path, .{}) catch return false;10871090     f.close(io);10881091     return true;

view file

src/output.zig+29 −11
@@ -1085,15 +1085,30 @@10851085     return input;10861086 }10871087 1088-// Ask whether the system is flake-based or channel-based. Returns true for1089-// flakes. Defaults to channels (Enter or anything but '2'), echoing the choice.1090-pub fn firstRunManageType() bool {1091-    p("\n   how do you manage your system?\n", .{});1092-    p("   {s}[1]{s} nixos channels (classic)\n", .{ c(DIM), c(RESET) });1093-    p("   {s}[2]{s} nix flakes\n\n", .{ c(DIM), c(RESET) });1094-    p("   {s}> {s}", .{ c(DIM), c(RESET) });1088+pub fn firstRunScanning() void {1089+    p("\n   {s}:: scanning for config...{s}\n", .{ c(DIM), c(RESET) });1090+}1091+1092+// Report what the scan found at /etc/nixos, noting flake vs channel.1093+pub fn firstRunDetected(path: []const u8, is_flake: bool) void {1094+    const kind = if (is_flake) "flake-based system" else "channel-based system";1095+    p("   {s}-> found {s}{s}   {s}({s}){s}\n\n", .{ c(CYAN), path, c(RESET), c(DIM), kind, c(RESET) });1096+}1097+1098+pub fn firstRunNoneFound() void {1099+    p("   {s}-> nothing found at /etc/nixos{s}\n\n", .{ c(DIM), c(RESET) });1100+}1101+1102+// Prompt with a pre-filled default shown in brackets. Empty input accepts the1103+// default. When `detected` and a default is present, note that enter confirms.1104+pub fn firstRunPromptDefault(label: []const u8, default: []const u8, detected: bool, buf: []u8) []const u8 {1105+    if (default.len > 0) {1106+        p("   {s:<14} {s}[{s}]{s} > ", .{ label, c(DIM), default, c(RESET) });1107+        if (detected) p("{s}enter to confirm{s} ", .{ c(DIM), c(RESET) });1108+    } else {1109+        p("   {s:<14} {s}> {s}", .{ label, c(DIM), c(RESET) });1110+    }10951111     flush();1096-    var buf: [64]u8 = undefined;10971112     var i: usize = 0;10981113     while (i < buf.len - 1) {10991114         const n = std.posix.read(std.posix.STDIN_FILENO, buf[i .. i + 1]) catch break;@@ -1102,9 +1117,12 @@11021117         i += 1;11031118     }11041119     const input = std.mem.trim(u8, buf[0..i], " \t\r");1105-    const flakes = input.len > 0 and input[0] == '2';1106-    p("{s}{s}{s}\n", .{ c(DIM), if (flakes) "nix flakes" else "nixos channels", c(RESET) });1107-    return flakes;1120+    if (input.len == 0) {1121+        p("{s}{s}{s}\n", .{ c(DIM), default, c(RESET) });1122+        flush();1123+        return default;1124+    }1125+    return input;11081126 }11091127 11101128 // Shown before channel operations on a flake-managed system, where nix-channel

view file