3912f93017a4

home init: nina home init generates HM config, --switch activates, warns if already exists (v3.3.0)

time
2026-06-13 14:45 UTC · 4 months ago
machine
koh2
agent
claude-opus-4-8
session
nina home init
parent
9a7b9486f3f3
5 files VERSION+1 −1src/commands.zig+35 −0src/exec.zig+72 −0src/main.zig+3 −0src/output.zig+27 −0
VERSION+1 −1
@@ -1 +1 @@1-3.2.61+3.3.0

view file

src/commands.zig+35 −0
@@ -1160,6 +1160,8 @@11601160 // --- Home Manager commands ---11611161 11621162 const home_help = [_][2][]const u8{1163+    .{ "init", "set up home manager for the first time" },1164+    .{ "init --switch", "set up and activate immediately" },11631165     .{ "apply", "apply home-manager configuration" },11641166     .{ "apply --dry", "preview changes without activating" },11651167     .{ "back", "roll back one generation" },@@ -1257,6 +1259,10 @@12571259         return;12581260     }12591261 1262+    // init runs before mode detection: its whole purpose is bootstrapping a host1263+    // that has no home-manager yet, so a .none mode must not short-circuit it.1264+    if (std.mem.eql(u8, ctx.sub, "init")) return homeInitCmd(ctx);1265+12601266     const mode = exec.detectHomeManager(ctx.gpa, ctx.io, ctx.environ);12611267     if (mode == .none) {12621268         output.homeNotInstalled();@@ -1418,6 +1424,35 @@14181424     const code = try exec.homeManagerRollback(ctx.io, &ctx.machine, ctx.gpa);14191425     if (code != 0) return error.BuildFailed;14201426     output.homeBackDone(prev);1427+}1428+1429+fn homeInitCmd(ctx: Ctx) !void {1430+    var switch_on_init = false;1431+    var dir: ?[]const u8 = null;1432+    for (ctx.args) |arg| {1433+        if (std.mem.eql(u8, arg, "--switch")) {1434+            switch_on_init = true;1435+        } else if (!std.mem.startsWith(u8, arg, "-")) {1436+            dir = arg;1437+        }1438+    }1439+1440+    // Guard only the default location — a custom dir is the user's call, and1441+    // home-manager creates it. Never clobber an existing ~/.config/home-manager.1442+    if (dir == null) {1443+        if (exec.homeNixPath(ctx.gpa, ctx.environ)) |path| {1444+            defer ctx.gpa.free(path);1445+            if (exec.pathExists(ctx.io, path)) {1446+                output.homeInitExists(path);1447+                return;1448+            }1449+        }1450+    }1451+1452+    output.homeInitStart(switch_on_init, dir);1453+    const code = try exec.homeManagerInit(ctx.io, &ctx.machine, ctx.gpa, switch_on_init, dir);1454+    if (code != 0) return error.BuildFailed;1455+    output.homeInitDone(switch_on_init);14211456 }14221457 14231458 fn homeApply(ctx: Ctx, mode: exec.HomeManagerMode) !void {

view file

src/exec.zig+72 −0
@@ -1540,6 +1540,78 @@15401540     return std.fmt.allocPrint(gpa, "{s}#{s}", .{ dir, user }) catch null;15411541 }15421542 1543+// Map a nixos-version string to the matching home-manager branch: an unstable1544+// channel ("…pre…"/"unstable") → master; a stable release like1545+// "25.05.20250115.abcdef (Warbler)" → release-25.05. Anything unparseable1546+// falls back to master. Caller frees.1547+fn homeBranchFromVersion(gpa: std.mem.Allocator, version_raw: []const u8) ![]const u8 {1548+    const v = std.mem.trim(u8, version_raw, " \t\r\n");1549+    if (std.mem.indexOf(u8, v, "pre") != null or1550+        std.mem.indexOf(u8, v, "unstable") != null)1551+        return gpa.dupe(u8, "master");1552+1553+    const head = v[0 .. std.mem.indexOfScalar(u8, v, ' ') orelse v.len];1554+    var it = std.mem.splitScalar(u8, head, '.');1555+    const major = it.next() orelse "";1556+    const minor = it.next() orelse "";1557+    if (allDigits(major) and allDigits(minor))1558+        return std.fmt.allocPrint(gpa, "release-{s}.{s}", .{ major, minor });1559+1560+    return gpa.dupe(u8, "master");1561+}1562+1563+fn allDigits(s: []const u8) bool {1564+    if (s.len == 0) return false;1565+    for (s) |ch| if (!std.ascii.isDigit(ch)) return false;1566+    return true;1567+}1568+1569+test "homeBranchFromVersion maps channels to HM branches" {1570+    const a = std.testing.allocator;1571+    const cases = [_]struct { in: []const u8, want: []const u8 }{1572+        .{ .in = "25.05.20250115.abcdef (Warbler)", .want = "release-25.05" },1573+        .{ .in = "24.11.20241201.deadbee (Vicuna)", .want = "release-24.11" },1574+        .{ .in = "25.11pre-git", .want = "master" },1575+        .{ .in = "25.05pre789012.abcdef (Warbler)", .want = "master" },1576+        .{ .in = "nixos-unstable", .want = "master" },1577+        .{ .in = "garbage", .want = "master" },1578+    };1579+    for (cases) |c| {1580+        const got = try homeBranchFromVersion(a, c.in);1581+        defer a.free(got);1582+        try std.testing.expectEqualStrings(c.want, got);1583+    }1584+}1585+1586+// The home-manager branch matching the running nixpkgs. Falls back to master1587+// when nixos-version is unavailable (non-NixOS host). Caller frees.1588+fn detectHomeManagerBranch(gpa: std.mem.Allocator, io: std.Io, machine: *const types.Machine) ![]const u8 {1589+    const r = capture(gpa, io, machine, &.{"nixos-version"}) catch1590+        return gpa.dupe(u8, "master");1591+    defer gpa.free(r.stdout);1592+    defer gpa.free(r.stderr);1593+    if (r.exit_code != 0) return gpa.dupe(u8, "master");1594+    return homeBranchFromVersion(gpa, r.stdout);1595+}1596+1597+// `nix run home-manager/<branch> -- init [--switch] [dir]` — generates1598+// flake.nix + home.nix (in dir, or ~/.config/home-manager by default),1599+// activating immediately when switch_on_init is set.1600+pub fn homeManagerInit(io: std.Io, machine: *const types.Machine, gpa: std.mem.Allocator, switch_on_init: bool, dir: ?[]const u8) !u8 {1601+    const branch = try detectHomeManagerBranch(gpa, io, machine);1602+    defer gpa.free(branch);1603+    const flake_ref = try std.fmt.allocPrint(gpa, "home-manager/{s}", .{branch});1604+    defer gpa.free(flake_ref);1605+1606+    var argv: std.ArrayList([]const u8) = .empty;1607+    defer argv.deinit(gpa);1608+    try argv.appendSlice(gpa, &.{ "nix", "run", flake_ref, "--", "init" });1609+    if (switch_on_init) try argv.append(gpa, "--switch");1610+    if (dir) |d| try argv.append(gpa, d);1611+1612+    return stream(io, machine, gpa, argv.items);1613+}1614+15431615 pub fn homeManagerSwitch(io: std.Io, machine: *const types.Machine, gpa: std.mem.Allocator) !u8 {15441616     output.flush();15451617     const r = try capture(gpa, io, machine, &.{ "home-manager", "switch" });

view file

src/main.zig+3 −0
@@ -291,6 +291,9 @@291291         } else if (std.mem.eql(u8, a, "-f")) {292292             // -f flag for service logs follow293293             try rest.append(arena, "-f");294+        } else if (std.mem.eql(u8, a, "--switch")) {295+            // --switch flag for `home init` (activate after init)296+            try rest.append(arena, "--switch");294297         } else if (a.len > 0 and a[0] != '-') {295298             try rest.append(arena, a);296299         }

view file

src/output.zig+27 −0
@@ -1389,6 +1389,33 @@13891389     p("{s}home packages  {s}\n\n", .{ hdr(), machine });13901390 }13911391 1392+pub fn homeInitStart(switch_on_init: bool, dir: ?[]const u8) void {1393+    const target = dir orelse "~/.config/home-manager";1394+    if (switch_on_init) {1395+        p("{s}{s}setting up home manager  {s}{s}{s}\n", .{ KAO_COZY, hdr(), c(PINK), target, c(RESET) });1396+        p("   {s}-> will activate after init{s}\n", .{ c(DIM), c(RESET) });1397+    } else {1398+        p("{s}initialising home manager  {s}{s}{s}\n", .{ hdr(), c(PINK), target, c(RESET) });1399+        p("   {s}-> edit home.nix then run {s}nina home apply{s}\n", .{ c(DIM), c(BOLD), c(RESET) });1400+    }1401+}1402+1403+pub fn homeInitDone(switch_on_init: bool) void {1404+    if (switch_on_init) {1405+        p("{s}{s}home manager ready\n", .{ KAO_GENTLE, hdr() });1406+    } else {1407+        p("{s}done  edit home.nix and run {s}nina home apply{s} when ready\n", .{ hdr(), c(BOLD), c(RESET) });1408+    }1409+}1410+1411+// `home init` against a config that already exists — never overwrite; point at1412+// apply instead.1413+pub fn homeInitExists(path: []const u8) void {1414+    p("{s}home manager config already exists\n", .{if (color_enabled) WARN_C else WARN_P});1415+    p("   {s}{s}{s}\n", .{ c(DIM), path, c(RESET) });1416+    p("   {s}-> run {s}nina home apply{s}{s} to activate it{s}\n", .{ c(CYAN), c(BOLD), c(RESET), c(CYAN), c(RESET) });1417+}1418+13921419 // --- First-run wizard ---13931420 13941421 pub fn firstRunBanner() void {

view file