// commands.zig — all command handlers // Orchestrates exec/api/output. Zero direct stdout writes. const std = @import("std"); const types = @import("types.zig"); const errors = @import("errors.zig"); const config = @import("config.zig"); const output = @import("output.zig"); const exec = @import("exec.zig"); const api = @import("api.zig"); const search_tui = @import("search.zig"); pub const Ctx = struct { gpa: std.mem.Allocator, io: std.Io, environ: *const std.process.Environ.Map, machine: types.Machine, cfg: types.NinaConfig, args: []const []const u8, // positional args after subcommand sub: []const u8, // subcommand (e.g. "list" in "service list") dry: bool, check: bool, no_apply: bool, all: bool, last: u32, json: bool, }; pub fn apply(ctx: Ctx) !void { if (ctx.dry) { output.applyDry(ctx.machine.name); const code = try exec.nixosRebuildDryActivate(ctx.io, &ctx.machine, ctx.gpa); if (code != 0) return error.BuildFailed; output.applyDryDone(); return; } if (ctx.check) { return check(ctx); } output.applyStart(ctx.machine.name); const start = std.Io.Clock.awake.now(ctx.io); const code = exec.nixosRebuildSwitch(ctx.io, &ctx.machine, ctx.gpa) catch |e| { output.applyFailed(""); return e; }; if (code != 0) { output.applyFailed(""); return error.BuildFailed; } const elapsed_ns = @max(0, start.untilNow(ctx.io, .awake).toNanoseconds()); const elapsed: u64 = @intCast(@divFloor(elapsed_ns, 1_000_000)); const gen = exec.getCurrentGeneration(ctx.gpa, ctx.io, &ctx.machine) catch 0; output.applyDone(gen, elapsed); logAction(ctx, "apply", gen); } pub fn check(ctx: Ctx) !void { output.checkStart(ctx.machine.name); const code = try exec.nixosRebuildBuild(ctx.io, &ctx.machine, ctx.gpa); if (code != 0) { output.checkFailed(""); return error.BuildFailed; } output.checkOk(); } pub fn back(ctx: Ctx) !void { const gens = try exec.getGenerations(ctx.gpa, ctx.io, &ctx.machine); defer ctx.gpa.free(gens); var current: u32 = 0; for (gens) |g| if (g.current) { current = g.number; }; const prev = if (current > 1) current - 1 else current; output.backStart(ctx.machine.name, current, prev); if (ctx.cfg.confirm and !output.confirm("")) return; const code = try exec.rollback(ctx.io, &ctx.machine, ctx.gpa); if (code != 0) return error.BuildFailed; output.generationDone(prev); } pub fn go(ctx: Ctx) !void { if (ctx.args.len == 0) return error.Internal; const target = std.fmt.parseInt(u32, ctx.args[0], 10) catch return error.Internal; const current = try exec.getCurrentGeneration(ctx.gpa, ctx.io, &ctx.machine); output.goStart(ctx.machine.name, current, target); if (ctx.cfg.confirm and !output.confirm("")) return; const code = try exec.switchGeneration(ctx.io, &ctx.machine, ctx.gpa, target); if (code != 0) return error.ProcessFailed; output.generationDone(target); } pub fn history(ctx: Ctx) !void { const gens = try exec.getGenerations(ctx.gpa, ctx.io, &ctx.machine); defer ctx.gpa.free(gens); output.historyHeader(ctx.machine.name); for (gens) |g| output.historyRow(g); } pub fn genCmd(ctx: Ctx) !void { if (std.mem.eql(u8, ctx.sub, "list")) return history(ctx); if (std.mem.eql(u8, ctx.sub, "current")) { const n = try exec.getCurrentGeneration(ctx.gpa, ctx.io, &ctx.machine); output.genCurrent(n); return; } if (std.mem.eql(u8, ctx.sub, "delete")) { if (ctx.args.len == 0) return error.Internal; if (std.mem.eql(u8, ctx.args[0], "old")) { if (ctx.cfg.confirm and !output.confirm("")) return; _ = try exec.deleteOldGenerations(ctx.io, &ctx.machine, ctx.gpa); } else { const n = std.fmt.parseInt(u32, ctx.args[0], 10) catch return error.Internal; if (ctx.cfg.confirm and !output.confirm("")) return; _ = try exec.deleteGeneration(ctx.io, &ctx.machine, ctx.gpa, n); } output.done(); return; } return error.Internal; } pub fn clean(ctx: Ctx) !void { const gens = try exec.getGenerations(ctx.gpa, ctx.io, &ctx.machine); defer ctx.gpa.free(gens); output.cleanStart(ctx.machine.name, ctx.cfg.generations, @intCast(gens.len)); if (ctx.cfg.confirm and !output.confirm("removing — cannot be undone")) return; const all = ctx.all or std.mem.eql(u8, ctx.sub, "all"); _ = try exec.collectGarbage(ctx.io, &ctx.machine, ctx.gpa, all, ctx.cfg.generations); output.cleanDone("?"); } pub fn statusCmd(ctx: Ctx) !void { output.statusHeader(); const gen = exec.getCurrentGeneration(ctx.gpa, ctx.io, &ctx.machine) catch 0; output.statusRow(ctx.machine.name, gen, "", "", true); } pub fn diff(ctx: Ctx) !void { const gens = try exec.getGenerations(ctx.gpa, ctx.io, &ctx.machine); defer ctx.gpa.free(gens); var current: u32 = 0; var prev: u32 = 0; for (gens) |g| { if (g.current) current = g.number; if (!g.current and g.number > prev and g.number < current) prev = g.number; } if (ctx.args.len >= 2) { const a = std.fmt.parseInt(u32, ctx.args[0], 10) catch current - 1; const b = std.fmt.parseInt(u32, ctx.args[1], 10) catch current; output.diffHeader(a, b, ctx.machine.name); if (try exec.diffClosures(ctx.io, &ctx.machine, ctx.gpa, a, b) != 0) return error.ProcessFailed; } else { output.diffHeader(if (prev > 0) prev else current - 1, current, ctx.machine.name); if (try exec.diffClosures(ctx.io, &ctx.machine, ctx.gpa, if (prev > 0) prev else current - 1, current) != 0) return error.ProcessFailed; } } pub fn doctor(ctx: Ctx) !void { output.doctorHeader(ctx.machine.name); const checks = try exec.runDoctorChecks(ctx.gpa, ctx.io, &ctx.machine); defer ctx.gpa.free(checks); var warnings: u32 = 0; var failures: u32 = 0; for (checks) |ch| { output.doctorRow(ch); if (ch.status == .warn) warnings += 1; if (ch.status == .fail) failures += 1; } output.doctorSummary(warnings, failures); } pub fn update(ctx: Ctx) !void { output.updateStart(ctx.machine.name); _ = try exec.channelUpdate(ctx.io, &ctx.machine, ctx.gpa); output.updateDone(); } pub fn upgrade(ctx: Ctx) !void { output.upgradeStart(ctx.machine.name); output.printSubstep("updating channels", .{}); _ = exec.channelUpdate(ctx.io, &ctx.machine, ctx.gpa) catch {}; output.printSubstep("rebuilding", .{}); const start = std.Io.Clock.awake.now(ctx.io); const code = try exec.nixosRebuildSwitch(ctx.io, &ctx.machine, ctx.gpa); if (code != 0) { output.applyFailed(""); return error.BuildFailed; } const elapsed_ns = @max(0, start.untilNow(ctx.io, .awake).toNanoseconds()); const elapsed: u64 = @intCast(@divFloor(elapsed_ns, 1_000_000)); const gen = exec.getCurrentGeneration(ctx.gpa, ctx.io, &ctx.machine) catch 0; output.applyDone(gen, elapsed); } pub fn log(ctx: Ctx) !void { output.logHeader(ctx.machine.name); const content = exec.readNinaLog(ctx.gpa, ctx.io, ctx.environ, ctx.last) catch { // No log yet — that's fine return; }; defer ctx.gpa.free(content); var lines = std.mem.splitScalar(u8, content, '\n'); while (lines.next()) |line| { if (line.len == 0) continue; var parts = std.mem.tokenizeScalar(u8, line, '\t'); const date = parts.next() orelse continue; const time = parts.next() orelse ""; const action = parts.next() orelse ""; const detail = parts.next() orelse ""; output.logRow(date, time, action, detail); } } pub fn edit(ctx: Ctx) !void { const hardware = ctx.args.len > 0 and std.mem.eql(u8, ctx.args[0], "hardware"); const filename = if (hardware) "hardware-configuration.nix" else "configuration.nix"; const path = try std.fmt.allocPrint(ctx.gpa, "{s}/{s}", .{ ctx.machine.config_path, filename }); defer ctx.gpa.free(path); output.editOpen(path, ctx.cfg.editor); _ = try exec.openEditor(ctx.io, ctx.gpa, ctx.cfg.editor, path, null); } pub fn fmt(ctx: Ctx) !void { const path = try std.fmt.allocPrint(ctx.gpa, "{s}/configuration.nix", .{ctx.machine.config_path}); defer ctx.gpa.free(path); if (ctx.check) { const code = try exec.nixpkgsFmt(ctx.io, &ctx.machine, ctx.gpa, path, true); if (code != 0) output.fmtNeedsFormat() else output.fmtDone(); return; } output.fmtStart(path); _ = try exec.nixpkgsFmt(ctx.io, &ctx.machine, ctx.gpa, path, false); output.fmtDone(); } pub fn repl(ctx: Ctx) !void { output.replStart(); _ = try exec.nixRepl(ctx.io, &ctx.machine, ctx.gpa); } pub fn develop(ctx: Ctx) !void { if (std.mem.eql(u8, ctx.sub, "show")) { _ = try exec.nixBuild(ctx.io, &ctx.machine, ctx.gpa, null); return; } const run_cmd: ?[]const u8 = if (ctx.args.len > 0 and std.mem.eql(u8, ctx.sub, "run")) ctx.args[0] else null; output.developStart(&.{}); _ = try exec.nixDevelop(ctx.io, &ctx.machine, ctx.gpa, run_cmd); output.developDone(); } pub fn info(ctx: Ctx) !void { output.infoHeader(ctx.machine.name); const nixos_ver = exec.systemInfo(ctx.gpa, ctx.io, &ctx.machine) catch try ctx.gpa.dupe(u8, "unknown"); defer ctx.gpa.free(nixos_ver); output.infoRow("nixos", std.mem.trim(u8, nixos_ver, " \t\n\r")); const kernel = exec.kernelInfo(ctx.gpa, ctx.io, &ctx.machine) catch try ctx.gpa.dupe(u8, "unknown"); defer ctx.gpa.free(kernel); output.infoRow("kernel", std.mem.trim(u8, kernel, " \t\n\r")); const uptime = exec.uptimeInfo(ctx.gpa, ctx.io, &ctx.machine) catch try ctx.gpa.dupe(u8, "unknown"); defer ctx.gpa.free(uptime); output.infoRow("uptime", std.mem.trim(u8, uptime, " \t\n\r")); } pub fn boot(ctx: Ctx) !void { output.bootHeader(ctx.machine.name); const entries = exec.bootEntries(ctx.gpa, ctx.io, &ctx.machine) catch &.{}; defer ctx.gpa.free(entries); for (entries, 0..) |entry, i| output.bootEntry(entry, i); } pub fn searchCmd(ctx: Ctx) !void { const query = if (ctx.args.len > 0) ctx.args[0] else ""; const result = try search_tui.run(ctx.gpa, ctx.io, query, .packages); if (result) |r| { installFromResult(ctx, r) catch |e| return e; } } pub fn optionCmd(ctx: Ctx) !void { const query = if (ctx.args.len > 0) ctx.args[0] else ""; _ = try search_tui.run(ctx.gpa, ctx.io, query, .options); } pub fn install(ctx: Ctx) !void { const pkg_name = if (ctx.args.len > 0) ctx.args[0] else return error.PackageNotFound; // Search for the package to get the attr const pkgs = api.searchPackages(ctx.gpa, ctx.io, pkg_name) catch { errors.error_info.setMessage("'{s}' not found in nixpkgs", .{pkg_name}); errors.error_info.setSuggestion("nina search {s}", .{pkg_name}); return error.PackageNotFound; }; defer ctx.gpa.free(pkgs); if (pkgs.len == 0) { errors.error_info.setMessage("'{s}' not found in nixpkgs", .{pkg_name}); errors.error_info.setSuggestion("nina search {s}", .{pkg_name}); return error.PackageNotFound; } // Prefer an exact name match over the first search result — `install ripgrep` // must pick "ripgrep", not the alphabetically-first match like "repgrep". var found = pkgs[0]; for (pkgs) |p| { if (std.mem.eql(u8, p.pname, pkg_name)) { found = p; break; } } output.installSelectPrompt(found.pname, found.version); var choice_buf: [8]u8 = undefined; var choice_len: usize = 0; while (choice_len < choice_buf.len) { const n = std.posix.read(std.posix.STDIN_FILENO, choice_buf[choice_len .. choice_len + 1]) catch break; if (n == 0) break; if (choice_buf[choice_len] == '\n') break; choice_len += 1; } const choice = std.mem.trim(u8, choice_buf[0..choice_len], " \t"); if (choice.len == 0 or choice[0] == 'i' or choice[0] == 'I') { try profileInstallPkg(ctx, found); } else if (choice[0] == 's' or choice[0] == 'S') { try systemInstallPkg(ctx, found); } else if (choice[0] == 't' or choice[0] == 'T') { try tryPkg(ctx, found); } } fn installFromResult(ctx: Ctx, result: types.SearchResult) !void { switch (result.action) { .profile => try profileInstallPkg(ctx, result.package), .system => try systemInstallPkg(ctx, result.package), } } fn profileInstallPkg(ctx: Ctx, npkg: types.NixPackage) !void { output.installStart(npkg.pname, .profile); output.printSubstep("nix profile install nixpkgs#{s}", .{npkg.attr}); output.printTeach(try std.fmt.allocPrint(ctx.gpa, "nix profile install nixpkgs#{s}", .{npkg.attr})); const code = try exec.profileInstall(ctx.io, &ctx.machine, ctx.gpa, npkg.attr); if (code != 0) return error.ProcessFailed; output.installDone(npkg.pname); } fn systemInstallPkg(ctx: Ctx, npkg: types.NixPackage) !void { const config_path = try std.fmt.allocPrint(ctx.gpa, "{s}/configuration.nix", .{ctx.machine.config_path}); defer ctx.gpa.free(config_path); const line = api.findSystemPackagesLine(ctx.io, config_path) orelse 1; output.installEditorPrompt(npkg.pname, config_path, line, ctx.cfg.editor); _ = try exec.openEditor(ctx.io, ctx.gpa, ctx.cfg.editor, config_path, line); if (ctx.cfg.confirm) { if (output.confirm("apply now? ")) { try apply(ctx); } } } fn tryPkg(ctx: Ctx, npkg: types.NixPackage) !void { output.tryStart(npkg.pname); output.printSubstep("nix shell nixpkgs#{s}", .{npkg.attr}); _ = try exec.nixShell(ctx.io, &ctx.machine, ctx.gpa, npkg.attr); output.tryDone(); } pub fn remove(ctx: Ctx) !void { const pkg_name = if (ctx.args.len > 0) ctx.args[0] else return error.PackageNotFound; output.installStart(pkg_name, .profile); // reuse installStart for remove UI const code = try exec.profileRemove(ctx.io, &ctx.machine, ctx.gpa, pkg_name); if (code != 0) return error.ProcessFailed; output.done(); } pub fn list(ctx: Ctx) !void { output.listHeader(); const profile_out = exec.profileList(ctx.gpa, ctx.io, &ctx.machine) catch null; if (profile_out) |out| { defer ctx.gpa.free(out); output.listSection("profile"); var lines = std.mem.splitScalar(u8, out, '\n'); while (lines.next()) |line| { const t = std.mem.trim(u8, line, " \t\r"); if (t.len == 0) continue; output.listPkg(t, ""); } } } pub fn tryCmd(ctx: Ctx) !void { const pkg_name_arg = if (ctx.args.len > 0) ctx.args[0] else return error.PackageNotFound; const shell_pkg = types.NixPackage{ .attr = pkg_name_arg, .pname = pkg_name_arg, .version = "", .description = "" }; try tryPkg(ctx, shell_pkg); } // --- Service commands --- pub fn service(ctx: Ctx) !void { if (std.mem.eql(u8, ctx.sub, "list") or ctx.sub.len == 0) { output.serviceListHeader(ctx.machine.name); const svcs = exec.serviceList(ctx.gpa, ctx.io, &ctx.machine) catch &.{}; defer ctx.gpa.free(svcs); for (svcs) |s| output.serviceRow(s); return; } if (std.mem.eql(u8, ctx.sub, "logs")) { const name = if (ctx.args.len > 0) ctx.args[0] else return error.Internal; const follow = ctx.args.len > 1 and std.mem.eql(u8, ctx.args[1], "-f"); output.serviceLogsHeader(name, ctx.last, ctx.machine.name); _ = try exec.serviceLogs(ctx.io, &ctx.machine, ctx.gpa, name, ctx.last, follow); return; } if (std.mem.eql(u8, ctx.sub, "status")) { const name = if (ctx.args.len > 0) ctx.args[0] else return error.Internal; output.serviceVerb("status", name, ctx.machine.name); _ = try exec.serviceCtl(ctx.io, &ctx.machine, ctx.gpa, "status", name); return; } // start/stop/restart/enable/disable const name = if (ctx.args.len > 0) ctx.args[0] else return error.Internal; if (ctx.cfg.confirm and (std.mem.eql(u8, ctx.sub, "stop") or std.mem.eql(u8, ctx.sub, "disable"))) { if (!output.confirm("")) return; } output.serviceVerb(ctx.sub, name, ctx.machine.name); _ = try exec.serviceCtl(ctx.io, &ctx.machine, ctx.gpa, ctx.sub, name); output.serviceDone(); } // --- Flake commands --- pub fn flake(ctx: Ctx) !void { if (std.mem.eql(u8, ctx.sub, "update")) { output.flakeUpdateStart(); const input: ?[]const u8 = if (ctx.args.len > 0) ctx.args[0] else null; _ = try exec.flakeUpdate(ctx.io, &ctx.machine, ctx.gpa, input); output.flakeLockWritten(); return; } if (std.mem.eql(u8, ctx.sub, "check")) { output.flakeCheckStart(); const code = try exec.flakeCheck(ctx.io, &ctx.machine, ctx.gpa); if (code == 0) output.flakeValid() else output.checkFailed(""); return; } if (std.mem.eql(u8, ctx.sub, "show")) { const json_out = exec.flakeShow(ctx.gpa, ctx.io, &ctx.machine) catch null; if (json_out) |j| { defer ctx.gpa.free(j); output.flakeShowHeader("."); // Parse and display the flake outputs const parsed = std.json.parseFromSlice(std.json.Value, ctx.gpa, j, .{}) catch return; defer parsed.deinit(); if (parsed.value == .object) { var it = parsed.value.object.iterator(); while (it.next()) |entry| { if (entry.value_ptr.* == .object) { var inner = entry.value_ptr.object.iterator(); while (inner.next()) |e2| { const desc = if (e2.value_ptr.* == .object) if (e2.value_ptr.object.get("description")) |d| if (d == .string) d.string else "" else "" else ""; output.flakeShowEntry(entry.key_ptr.*, e2.key_ptr.*, desc); } } } } } return; } if (std.mem.eql(u8, ctx.sub, "init")) { _ = try exec.flakeInit(ctx.io, &ctx.machine, ctx.gpa); return; } if (std.mem.eql(u8, ctx.sub, "lock")) { _ = try exec.flakeLock(ctx.io, &ctx.machine, ctx.gpa); return; } if (std.mem.eql(u8, ctx.sub, "clone")) { const url = if (ctx.args.len > 0) ctx.args[0] else return error.Internal; _ = try exec.flakeClone(ctx.io, &ctx.machine, ctx.gpa, url); return; } } // --- Store commands --- pub fn store(ctx: Ctx) !void { if (std.mem.eql(u8, ctx.sub, "gc")) { _ = try exec.storeGc(ctx.io, &ctx.machine, ctx.gpa); return; } if (std.mem.eql(u8, ctx.sub, "verify")) { output.storeVerifyStart(); _ = try exec.storeVerify(ctx.io, &ctx.machine, ctx.gpa); return; } if (std.mem.eql(u8, ctx.sub, "repair")) { if (ctx.cfg.confirm and !output.confirm("")) return; output.storeRepairStart(); _ = try exec.storeRepair(ctx.io, &ctx.machine, ctx.gpa); return; } if (std.mem.eql(u8, ctx.sub, "info")) { const si = try exec.storeInfo(ctx.gpa, ctx.io, &ctx.machine); output.storeInfo(si); return; } if (std.mem.eql(u8, ctx.sub, "path")) { const attr = if (ctx.args.len > 0) ctx.args[0] else return error.Internal; const path = try exec.storePath(ctx.gpa, ctx.io, &ctx.machine, attr); defer ctx.gpa.free(path); output.storePath(path); return; } // default: info const si = try exec.storeInfo(ctx.gpa, ctx.io, &ctx.machine); output.storeInfo(si); } // --- Channel commands --- pub fn channel(ctx: Ctx) !void { if (std.mem.eql(u8, ctx.sub, "list") or ctx.sub.len == 0) { output.channelListHeader(ctx.machine.name); const chans = try exec.channelList(ctx.gpa, ctx.io, &ctx.machine); defer ctx.gpa.free(chans); for (chans) |ch| output.channelRow(ch); return; } if (std.mem.eql(u8, ctx.sub, "add")) { const url = if (ctx.args.len > 0) ctx.args[0] else return error.Internal; const name = if (ctx.args.len > 1) ctx.args[1] else ""; _ = try exec.channelAdd(ctx.io, &ctx.machine, ctx.gpa, url, name); output.channelDone(); return; } if (std.mem.eql(u8, ctx.sub, "remove")) { const name = if (ctx.args.len > 0) ctx.args[0] else return error.Internal; _ = try exec.channelRemove(ctx.io, &ctx.machine, ctx.gpa, name); output.channelDone(); return; } if (std.mem.eql(u8, ctx.sub, "update")) { return update(ctx); } } // --- Profile commands --- pub fn profile(ctx: Ctx) !void { if (std.mem.eql(u8, ctx.sub, "list") or ctx.sub.len == 0) { output.profileListHeader(); const out = exec.profileList(ctx.gpa, ctx.io, &ctx.machine) catch null; if (out) |o| { defer ctx.gpa.free(o); var lines = std.mem.splitScalar(u8, o, '\n'); while (lines.next()) |line| { const t = std.mem.trim(u8, line, " \t\r"); if (t.len == 0) continue; output.profileRow(t, "", ""); } } return; } if (std.mem.eql(u8, ctx.sub, "install")) { const attr = if (ctx.args.len > 0) ctx.args[0] else return error.PackageNotFound; _ = try exec.profileInstall(ctx.io, &ctx.machine, ctx.gpa, attr); output.done(); return; } if (std.mem.eql(u8, ctx.sub, "remove")) { const attr = if (ctx.args.len > 0) ctx.args[0] else return error.PackageNotFound; _ = try exec.profileRemove(ctx.io, &ctx.machine, ctx.gpa, attr); output.done(); return; } if (std.mem.eql(u8, ctx.sub, "upgrade")) { _ = try exec.profileUpgrade(ctx.io, &ctx.machine, ctx.gpa); output.done(); return; } } // --- Pkg commands --- pub fn pkg(ctx: Ctx) !void { const name = if (ctx.args.len > 0) ctx.args[0] else return error.PackageNotFound; if (std.mem.eql(u8, ctx.sub, "why")) { _ = try exec.pkgWhy(ctx.io, &ctx.machine, ctx.gpa, name); return; } if (std.mem.eql(u8, ctx.sub, "deps")) { _ = try exec.pkgDeps(ctx.io, &ctx.machine, ctx.gpa, name); return; } if (std.mem.eql(u8, ctx.sub, "size")) { const s = exec.pkgSize(ctx.gpa, ctx.io, &ctx.machine, name) catch null; if (s) |sz| { defer ctx.gpa.free(sz); output.pkgResult("size", sz); } return; } if (std.mem.eql(u8, ctx.sub, "path")) { const p = try exec.storePath(ctx.gpa, ctx.io, &ctx.machine, name); defer ctx.gpa.free(p); output.pkgResult("path", p); return; } if (std.mem.eql(u8, ctx.sub, "closure")) { _ = try exec.pkgClosure(ctx.io, &ctx.machine, ctx.gpa, name); return; } } // --- Pin / Unpin --- pub fn pin(ctx: Ctx) !void { const input = if (ctx.args.len > 0) ctx.args[0] else return error.Internal; const commit = if (ctx.args.len > 1) ctx.args[1] else return error.Internal; exec.editFlakeLock(ctx.gpa, input, commit) catch return error.ProcessFailed; output.pinDone(input, commit); } pub fn unpin(ctx: Ctx) !void { const input = if (ctx.args.len > 0) ctx.args[0] else return error.Internal; _ = try exec.flakeUpdate(ctx.io, &ctx.machine, ctx.gpa, input); output.unpinDone(input); } // --- Hash / Fetch --- pub fn hash(ctx: Ctx) !void { const path = if (ctx.args.len > 0) ctx.args[0] else return error.Internal; const h = try exec.nixHash(ctx.gpa, ctx.io, &ctx.machine, path); defer ctx.gpa.free(h); output.hashResult(std.mem.trim(u8, h, " \t\n\r")); } pub fn fetch(ctx: Ctx) !void { const url = if (ctx.args.len > 0) ctx.args[0] else return error.Internal; const h = try exec.nixFetch(ctx.gpa, ctx.io, &ctx.machine, url); defer ctx.gpa.free(h); output.fetchResult(std.mem.trim(u8, h, " \t\n\r"), url); } // --- Build / Run --- pub fn build(ctx: Ctx) !void { const attr: ?[]const u8 = if (ctx.args.len > 0) ctx.args[0] else null; output.buildStart(attr orelse "default"); const code = try exec.nixBuild(ctx.io, &ctx.machine, ctx.gpa, attr); if (code != 0) return error.BuildFailed; output.buildDone(); } pub fn runCmd(ctx: Ctx) !void { const pkg_arg = if (ctx.args.len > 0) ctx.args[0] else return error.PackageNotFound; output.runStart(pkg_arg); _ = try exec.nixRun(ctx.io, &ctx.machine, ctx.gpa, pkg_arg); } // --- Hello --- pub fn hello(ctx: Ctx) !void { output.hello(ctx.cfg.machines); } pub fn setup(gpa: std.mem.Allocator, io: std.Io, environ: *const std.process.Environ.Map) !void { output.setupCheck(); const status = exec.checkNixFeatures(gpa, io, environ); output.setupFeatures(status.nix_command, status.flakes); if (status.allEnabled()) { output.setupAllSet(); return; } output.setupEnabling(); const path = try exec.enableNixFeatures(gpa, io, environ); defer gpa.free(path); output.setupDone(path, exec.isNixOS(io)); } // --- Log helper --- fn logAction(ctx: Ctx, action: []const u8, gen: u32) void { var buf: [128]u8 = undefined; const entry = std.fmt.bufPrint(&buf, "{s}\t{s}\t{s}\tgen {d}\n", .{ "2026", "00:00", action, gen, }) catch return; exec.appendNinaLog(ctx.io, ctx.environ, entry); }