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
↓ nina-3912f930.tar.gzatlas at this save
files changed5 files
VERSION+1 −1src/commands.zig+35 −0src/exec.zig+72 −0src/main.zig+3 −0src/output.zig+27 −0src/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 {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" });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 }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 {