commands.zig

25.3 KB · raw

1// commands.zig — all command handlers2// Orchestrates exec/api/output. Zero direct stdout writes.3const std = @import("std");4const types = @import("types.zig");5const errors = @import("errors.zig");6const config = @import("config.zig");7const output = @import("output.zig");8const exec = @import("exec.zig");9const api = @import("api.zig");10const search_tui = @import("search.zig");1112pub const Ctx = struct {13    gpa: std.mem.Allocator,14    io: std.Io,15    environ: *const std.process.Environ.Map,16    machine: types.Machine,17    cfg: types.NinaConfig,18    args: []const []const u8,   // positional args after subcommand19    sub: []const u8,            // subcommand (e.g. "list" in "service list")20    dry: bool,21    check: bool,22    no_apply: bool,23    all: bool,24    last: u32,25    json: bool,26};2728pub fn apply(ctx: Ctx) !void {29    if (ctx.dry) {30        output.applyDry(ctx.machine.name);31        const code = try exec.nixosRebuildDryActivate(ctx.io, &ctx.machine, ctx.gpa);32        if (code != 0) return error.BuildFailed;33        output.applyDryDone();34        return;35    }36    if (ctx.check) {37        return check(ctx);38    }3940    output.applyStart(ctx.machine.name);41    const start = std.Io.Clock.awake.now(ctx.io);4243    const code = exec.nixosRebuildSwitch(ctx.io, &ctx.machine, ctx.gpa) catch |e| {44        output.applyFailed("");45        return e;46    };4748    if (code != 0) {49        output.applyFailed("");50        return error.BuildFailed;51    }5253    const elapsed_ns = @max(0, start.untilNow(ctx.io, .awake).toNanoseconds());54    const elapsed: u64 = @intCast(@divFloor(elapsed_ns, 1_000_000));55    const gen = exec.getCurrentGeneration(ctx.gpa, ctx.io, &ctx.machine) catch 0;56    output.applyDone(gen, elapsed);57    logAction(ctx, "apply", gen);58}5960pub fn check(ctx: Ctx) !void {61    output.checkStart(ctx.machine.name);62    const code = try exec.nixosRebuildBuild(ctx.io, &ctx.machine, ctx.gpa);63    if (code != 0) {64        output.checkFailed("");65        return error.BuildFailed;66    }67    output.checkOk();68}6970pub fn back(ctx: Ctx) !void {71    const gens = try exec.getGenerations(ctx.gpa, ctx.io, &ctx.machine);72    defer ctx.gpa.free(gens);7374    var current: u32 = 0;75    for (gens) |g| if (g.current) { current = g.number; };76    const prev = if (current > 1) current - 1 else current;7778    output.backStart(ctx.machine.name, current, prev);79    if (ctx.cfg.confirm and !output.confirm("")) return;8081    const code = try exec.rollback(ctx.io, &ctx.machine, ctx.gpa);82    if (code != 0) return error.BuildFailed;83    output.generationDone(prev);84}8586pub fn go(ctx: Ctx) !void {87    if (ctx.args.len == 0) return error.Internal;88    const target = std.fmt.parseInt(u32, ctx.args[0], 10) catch return error.Internal;89    const current = try exec.getCurrentGeneration(ctx.gpa, ctx.io, &ctx.machine);9091    output.goStart(ctx.machine.name, current, target);92    if (ctx.cfg.confirm and !output.confirm("")) return;9394    const code = try exec.switchGeneration(ctx.io, &ctx.machine, ctx.gpa, target);95    if (code != 0) return error.ProcessFailed;96    output.generationDone(target);97}9899pub fn history(ctx: Ctx) !void {100    const gens = try exec.getGenerations(ctx.gpa, ctx.io, &ctx.machine);101    defer ctx.gpa.free(gens);102    output.historyHeader(ctx.machine.name);103    for (gens) |g| output.historyRow(g);104}105106pub fn genCmd(ctx: Ctx) !void {107    if (std.mem.eql(u8, ctx.sub, "list")) return history(ctx);108    if (std.mem.eql(u8, ctx.sub, "current")) {109        const n = try exec.getCurrentGeneration(ctx.gpa, ctx.io, &ctx.machine);110        output.genCurrent(n);111        return;112    }113    if (std.mem.eql(u8, ctx.sub, "delete")) {114        if (ctx.args.len == 0) return error.Internal;115        if (std.mem.eql(u8, ctx.args[0], "old")) {116            if (ctx.cfg.confirm and !output.confirm("")) return;117            _ = try exec.deleteOldGenerations(ctx.io, &ctx.machine, ctx.gpa);118        } else {119            const n = std.fmt.parseInt(u32, ctx.args[0], 10) catch return error.Internal;120            if (ctx.cfg.confirm and !output.confirm("")) return;121            _ = try exec.deleteGeneration(ctx.io, &ctx.machine, ctx.gpa, n);122        }123        output.done();124        return;125    }126    return error.Internal;127}128129pub fn clean(ctx: Ctx) !void {130    const gens = try exec.getGenerations(ctx.gpa, ctx.io, &ctx.machine);131    defer ctx.gpa.free(gens);132    output.cleanStart(ctx.machine.name, ctx.cfg.generations, @intCast(gens.len));133    if (ctx.cfg.confirm and !output.confirm("removing — cannot be undone")) return;134    const all = ctx.all or std.mem.eql(u8, ctx.sub, "all");135    _ = try exec.collectGarbage(ctx.io, &ctx.machine, ctx.gpa, all, ctx.cfg.generations);136    output.cleanDone("?");137}138139pub fn statusCmd(ctx: Ctx) !void {140    output.statusHeader();141    const gen = exec.getCurrentGeneration(ctx.gpa, ctx.io, &ctx.machine) catch 0;142    output.statusRow(ctx.machine.name, gen, "", "", true);143}144145pub fn diff(ctx: Ctx) !void {146    const gens = try exec.getGenerations(ctx.gpa, ctx.io, &ctx.machine);147    defer ctx.gpa.free(gens);148    var current: u32 = 0;149    var prev: u32 = 0;150    for (gens) |g| {151        if (g.current) current = g.number;152        if (!g.current and g.number > prev and g.number < current) prev = g.number;153    }154    if (ctx.args.len >= 2) {155        const a = std.fmt.parseInt(u32, ctx.args[0], 10) catch current - 1;156        const b = std.fmt.parseInt(u32, ctx.args[1], 10) catch current;157        output.diffHeader(a, b, ctx.machine.name);158        if (try exec.diffClosures(ctx.io, &ctx.machine, ctx.gpa, a, b) != 0) return error.ProcessFailed;159    } else {160        output.diffHeader(if (prev > 0) prev else current - 1, current, ctx.machine.name);161        if (try exec.diffClosures(ctx.io, &ctx.machine, ctx.gpa, if (prev > 0) prev else current - 1, current) != 0) return error.ProcessFailed;162    }163}164165pub fn doctor(ctx: Ctx) !void {166    output.doctorHeader(ctx.machine.name);167    const checks = try exec.runDoctorChecks(ctx.gpa, ctx.io, &ctx.machine);168    defer ctx.gpa.free(checks);169    var warnings: u32 = 0;170    var failures: u32 = 0;171    for (checks) |ch| {172        output.doctorRow(ch);173        if (ch.status == .warn) warnings += 1;174        if (ch.status == .fail) failures += 1;175    }176    output.doctorSummary(warnings, failures);177}178179pub fn update(ctx: Ctx) !void {180    output.updateStart(ctx.machine.name);181    _ = try exec.channelUpdate(ctx.io, &ctx.machine, ctx.gpa);182    output.updateDone();183}184185pub fn upgrade(ctx: Ctx) !void {186    output.upgradeStart(ctx.machine.name);187    output.printSubstep("updating channels", .{});188    _ = exec.channelUpdate(ctx.io, &ctx.machine, ctx.gpa) catch {};189    output.printSubstep("rebuilding", .{});190    const start = std.Io.Clock.awake.now(ctx.io);191    const code = try exec.nixosRebuildSwitch(ctx.io, &ctx.machine, ctx.gpa);192    if (code != 0) {193        output.applyFailed("");194        return error.BuildFailed;195    }196    const elapsed_ns = @max(0, start.untilNow(ctx.io, .awake).toNanoseconds());197    const elapsed: u64 = @intCast(@divFloor(elapsed_ns, 1_000_000));198    const gen = exec.getCurrentGeneration(ctx.gpa, ctx.io, &ctx.machine) catch 0;199    output.applyDone(gen, elapsed);200}201202pub fn log(ctx: Ctx) !void {203    output.logHeader(ctx.machine.name);204    const content = exec.readNinaLog(ctx.gpa, ctx.io, ctx.environ, ctx.last) catch {205        // No log yet — that's fine206        return;207    };208    defer ctx.gpa.free(content);209    var lines = std.mem.splitScalar(u8, content, '\n');210    while (lines.next()) |line| {211        if (line.len == 0) continue;212        var parts = std.mem.tokenizeScalar(u8, line, '\t');213        const date = parts.next() orelse continue;214        const time = parts.next() orelse "";215        const action = parts.next() orelse "";216        const detail = parts.next() orelse "";217        output.logRow(date, time, action, detail);218    }219}220221pub fn edit(ctx: Ctx) !void {222    const hardware = ctx.args.len > 0 and std.mem.eql(u8, ctx.args[0], "hardware");223    const filename = if (hardware) "hardware-configuration.nix" else "configuration.nix";224    const path = try std.fmt.allocPrint(ctx.gpa, "{s}/{s}", .{ ctx.machine.config_path, filename });225    defer ctx.gpa.free(path);226    output.editOpen(path, ctx.cfg.editor);227    _ = try exec.openEditor(ctx.io, ctx.gpa, ctx.cfg.editor, path, null);228}229230pub fn fmt(ctx: Ctx) !void {231    const path = try std.fmt.allocPrint(ctx.gpa, "{s}/configuration.nix", .{ctx.machine.config_path});232    defer ctx.gpa.free(path);233    if (ctx.check) {234        const code = try exec.nixpkgsFmt(ctx.io, &ctx.machine, ctx.gpa, path, true);235        if (code != 0) output.fmtNeedsFormat() else output.fmtDone();236        return;237    }238    output.fmtStart(path);239    _ = try exec.nixpkgsFmt(ctx.io, &ctx.machine, ctx.gpa, path, false);240    output.fmtDone();241}242243pub fn repl(ctx: Ctx) !void {244    output.replStart();245    _ = try exec.nixRepl(ctx.io, &ctx.machine, ctx.gpa);246}247248pub fn develop(ctx: Ctx) !void {249    if (std.mem.eql(u8, ctx.sub, "show")) {250        _ = try exec.nixBuild(ctx.io, &ctx.machine, ctx.gpa, null);251        return;252    }253    const run_cmd: ?[]const u8 = if (ctx.args.len > 0 and std.mem.eql(u8, ctx.sub, "run"))254        ctx.args[0]255    else256        null;257    output.developStart(&.{});258    _ = try exec.nixDevelop(ctx.io, &ctx.machine, ctx.gpa, run_cmd);259    output.developDone();260}261262pub fn info(ctx: Ctx) !void {263    output.infoHeader(ctx.machine.name);264    const nixos_ver = exec.systemInfo(ctx.gpa, ctx.io, &ctx.machine) catch try ctx.gpa.dupe(u8, "unknown");265    defer ctx.gpa.free(nixos_ver);266    output.infoRow("nixos", std.mem.trim(u8, nixos_ver, " \t\n\r"));267268    const kernel = exec.kernelInfo(ctx.gpa, ctx.io, &ctx.machine) catch try ctx.gpa.dupe(u8, "unknown");269    defer ctx.gpa.free(kernel);270    output.infoRow("kernel", std.mem.trim(u8, kernel, " \t\n\r"));271272    const uptime = exec.uptimeInfo(ctx.gpa, ctx.io, &ctx.machine) catch try ctx.gpa.dupe(u8, "unknown");273    defer ctx.gpa.free(uptime);274    output.infoRow("uptime", std.mem.trim(u8, uptime, " \t\n\r"));275}276277pub fn boot(ctx: Ctx) !void {278    output.bootHeader(ctx.machine.name);279    const entries = exec.bootEntries(ctx.gpa, ctx.io, &ctx.machine) catch &.{};280    defer ctx.gpa.free(entries);281    for (entries, 0..) |entry, i| output.bootEntry(entry, i);282}283284pub fn searchCmd(ctx: Ctx) !void {285    const query = if (ctx.args.len > 0) ctx.args[0] else "";286    const result = try search_tui.run(ctx.gpa, ctx.io, query, .packages);287    if (result) |r| {288        installFromResult(ctx, r) catch |e| return e;289    }290}291292pub fn optionCmd(ctx: Ctx) !void {293    const query = if (ctx.args.len > 0) ctx.args[0] else "";294    _ = try search_tui.run(ctx.gpa, ctx.io, query, .options);295}296297pub fn install(ctx: Ctx) !void {298    const pkg_name = if (ctx.args.len > 0) ctx.args[0] else return error.PackageNotFound;299300    // Search for the package to get the attr301    const pkgs = api.searchPackages(ctx.gpa, ctx.io, pkg_name) catch {302        errors.error_info.setMessage("'{s}' not found in nixpkgs", .{pkg_name});303        errors.error_info.setSuggestion("nina search {s}", .{pkg_name});304        return error.PackageNotFound;305    };306    defer ctx.gpa.free(pkgs);307308    if (pkgs.len == 0) {309        errors.error_info.setMessage("'{s}' not found in nixpkgs", .{pkg_name});310        errors.error_info.setSuggestion("nina search {s}", .{pkg_name});311        return error.PackageNotFound;312    }313314    // Prefer an exact name match over the first search result — `install ripgrep`315    // must pick "ripgrep", not the alphabetically-first match like "repgrep".316    var found = pkgs[0];317    for (pkgs) |p| {318        if (std.mem.eql(u8, p.pname, pkg_name)) {319            found = p;320            break;321        }322    }323    output.installSelectPrompt(found.pname, found.version);324325    var choice_buf: [8]u8 = undefined;326    var choice_len: usize = 0;327    while (choice_len < choice_buf.len) {328        const n = std.posix.read(std.posix.STDIN_FILENO, choice_buf[choice_len .. choice_len + 1]) catch break;329        if (n == 0) break;330        if (choice_buf[choice_len] == '\n') break;331        choice_len += 1;332    }333    const choice = std.mem.trim(u8, choice_buf[0..choice_len], " \t");334335    if (choice.len == 0 or choice[0] == 'i' or choice[0] == 'I') {336        try profileInstallPkg(ctx, found);337    } else if (choice[0] == 's' or choice[0] == 'S') {338        try systemInstallPkg(ctx, found);339    } else if (choice[0] == 't' or choice[0] == 'T') {340        try tryPkg(ctx, found);341    }342}343344fn installFromResult(ctx: Ctx, result: types.SearchResult) !void {345    switch (result.action) {346        .profile => try profileInstallPkg(ctx, result.package),347        .system => try systemInstallPkg(ctx, result.package),348    }349}350351fn profileInstallPkg(ctx: Ctx, npkg: types.NixPackage) !void {352    output.installStart(npkg.pname, .profile);353    output.printSubstep("nix profile install nixpkgs#{s}", .{npkg.attr});354    output.printTeach(try std.fmt.allocPrint(ctx.gpa, "nix profile install nixpkgs#{s}", .{npkg.attr}));355    const code = try exec.profileInstall(ctx.io, &ctx.machine, ctx.gpa, npkg.attr);356    if (code != 0) return error.ProcessFailed;357    output.installDone(npkg.pname);358}359360fn systemInstallPkg(ctx: Ctx, npkg: types.NixPackage) !void {361    const config_path = try std.fmt.allocPrint(ctx.gpa, "{s}/configuration.nix", .{ctx.machine.config_path});362    defer ctx.gpa.free(config_path);363    const line = api.findSystemPackagesLine(ctx.io, config_path) orelse 1;364    output.installEditorPrompt(npkg.pname, config_path, line, ctx.cfg.editor);365    _ = try exec.openEditor(ctx.io, ctx.gpa, ctx.cfg.editor, config_path, line);366    if (ctx.cfg.confirm) {367        if (output.confirm("apply now? ")) {368            try apply(ctx);369        }370    }371}372373fn tryPkg(ctx: Ctx, npkg: types.NixPackage) !void {374    output.tryStart(npkg.pname);375    output.printSubstep("nix shell nixpkgs#{s}", .{npkg.attr});376    _ = try exec.nixShell(ctx.io, &ctx.machine, ctx.gpa, npkg.attr);377    output.tryDone();378}379380pub fn remove(ctx: Ctx) !void {381    const pkg_name = if (ctx.args.len > 0) ctx.args[0] else return error.PackageNotFound;382    output.installStart(pkg_name, .profile); // reuse installStart for remove UI383    const code = try exec.profileRemove(ctx.io, &ctx.machine, ctx.gpa, pkg_name);384    if (code != 0) return error.ProcessFailed;385    output.done();386}387388pub fn list(ctx: Ctx) !void {389    output.listHeader();390    const profile_out = exec.profileList(ctx.gpa, ctx.io, &ctx.machine) catch null;391    if (profile_out) |out| {392        defer ctx.gpa.free(out);393        output.listSection("profile");394        var lines = std.mem.splitScalar(u8, out, '\n');395        while (lines.next()) |line| {396            const t = std.mem.trim(u8, line, " \t\r");397            if (t.len == 0) continue;398            output.listPkg(t, "");399        }400    }401}402403pub fn tryCmd(ctx: Ctx) !void {404    const pkg_name_arg = if (ctx.args.len > 0) ctx.args[0] else return error.PackageNotFound;405    const shell_pkg = types.NixPackage{ .attr = pkg_name_arg, .pname = pkg_name_arg, .version = "", .description = "" };406    try tryPkg(ctx, shell_pkg);407}408409// --- Service commands ---410411pub fn service(ctx: Ctx) !void {412    if (std.mem.eql(u8, ctx.sub, "list") or ctx.sub.len == 0) {413        output.serviceListHeader(ctx.machine.name);414        const svcs = exec.serviceList(ctx.gpa, ctx.io, &ctx.machine) catch &.{};415        defer ctx.gpa.free(svcs);416        for (svcs) |s| output.serviceRow(s);417        return;418    }419    if (std.mem.eql(u8, ctx.sub, "logs")) {420        const name = if (ctx.args.len > 0) ctx.args[0] else return error.Internal;421        const follow = ctx.args.len > 1 and std.mem.eql(u8, ctx.args[1], "-f");422        output.serviceLogsHeader(name, ctx.last, ctx.machine.name);423        _ = try exec.serviceLogs(ctx.io, &ctx.machine, ctx.gpa, name, ctx.last, follow);424        return;425    }426    if (std.mem.eql(u8, ctx.sub, "status")) {427        const name = if (ctx.args.len > 0) ctx.args[0] else return error.Internal;428        output.serviceVerb("status", name, ctx.machine.name);429        _ = try exec.serviceCtl(ctx.io, &ctx.machine, ctx.gpa, "status", name);430        return;431    }432    // start/stop/restart/enable/disable433    const name = if (ctx.args.len > 0) ctx.args[0] else return error.Internal;434    if (ctx.cfg.confirm and (std.mem.eql(u8, ctx.sub, "stop") or std.mem.eql(u8, ctx.sub, "disable"))) {435        if (!output.confirm("")) return;436    }437    output.serviceVerb(ctx.sub, name, ctx.machine.name);438    _ = try exec.serviceCtl(ctx.io, &ctx.machine, ctx.gpa, ctx.sub, name);439    output.serviceDone();440}441442// --- Flake commands ---443444pub fn flake(ctx: Ctx) !void {445    if (std.mem.eql(u8, ctx.sub, "update")) {446        output.flakeUpdateStart();447        const input: ?[]const u8 = if (ctx.args.len > 0) ctx.args[0] else null;448        _ = try exec.flakeUpdate(ctx.io, &ctx.machine, ctx.gpa, input);449        output.flakeLockWritten();450        return;451    }452    if (std.mem.eql(u8, ctx.sub, "check")) {453        output.flakeCheckStart();454        const code = try exec.flakeCheck(ctx.io, &ctx.machine, ctx.gpa);455        if (code == 0) output.flakeValid() else output.checkFailed("");456        return;457    }458    if (std.mem.eql(u8, ctx.sub, "show")) {459        const json_out = exec.flakeShow(ctx.gpa, ctx.io, &ctx.machine) catch null;460        if (json_out) |j| {461            defer ctx.gpa.free(j);462            output.flakeShowHeader(".");463            // Parse and display the flake outputs464            const parsed = std.json.parseFromSlice(std.json.Value, ctx.gpa, j, .{}) catch return;465            defer parsed.deinit();466            if (parsed.value == .object) {467                var it = parsed.value.object.iterator();468                while (it.next()) |entry| {469                    if (entry.value_ptr.* == .object) {470                        var inner = entry.value_ptr.object.iterator();471                        while (inner.next()) |e2| {472                            const desc = if (e2.value_ptr.* == .object)473                                if (e2.value_ptr.object.get("description")) |d|474                                    if (d == .string) d.string else ""475                                else ""476                            else "";477                            output.flakeShowEntry(entry.key_ptr.*, e2.key_ptr.*, desc);478                        }479                    }480                }481            }482        }483        return;484    }485    if (std.mem.eql(u8, ctx.sub, "init")) {486        _ = try exec.flakeInit(ctx.io, &ctx.machine, ctx.gpa);487        return;488    }489    if (std.mem.eql(u8, ctx.sub, "lock")) {490        _ = try exec.flakeLock(ctx.io, &ctx.machine, ctx.gpa);491        return;492    }493    if (std.mem.eql(u8, ctx.sub, "clone")) {494        const url = if (ctx.args.len > 0) ctx.args[0] else return error.Internal;495        _ = try exec.flakeClone(ctx.io, &ctx.machine, ctx.gpa, url);496        return;497    }498}499500// --- Store commands ---501502pub fn store(ctx: Ctx) !void {503    if (std.mem.eql(u8, ctx.sub, "gc")) {504        _ = try exec.storeGc(ctx.io, &ctx.machine, ctx.gpa);505        return;506    }507    if (std.mem.eql(u8, ctx.sub, "verify")) {508        output.storeVerifyStart();509        _ = try exec.storeVerify(ctx.io, &ctx.machine, ctx.gpa);510        return;511    }512    if (std.mem.eql(u8, ctx.sub, "repair")) {513        if (ctx.cfg.confirm and !output.confirm("")) return;514        output.storeRepairStart();515        _ = try exec.storeRepair(ctx.io, &ctx.machine, ctx.gpa);516        return;517    }518    if (std.mem.eql(u8, ctx.sub, "info")) {519        const si = try exec.storeInfo(ctx.gpa, ctx.io, &ctx.machine);520        output.storeInfo(si);521        return;522    }523    if (std.mem.eql(u8, ctx.sub, "path")) {524        const attr = if (ctx.args.len > 0) ctx.args[0] else return error.Internal;525        const path = try exec.storePath(ctx.gpa, ctx.io, &ctx.machine, attr);526        defer ctx.gpa.free(path);527        output.storePath(path);528        return;529    }530    // default: info531    const si = try exec.storeInfo(ctx.gpa, ctx.io, &ctx.machine);532    output.storeInfo(si);533}534535// --- Channel commands ---536537pub fn channel(ctx: Ctx) !void {538    if (std.mem.eql(u8, ctx.sub, "list") or ctx.sub.len == 0) {539        output.channelListHeader(ctx.machine.name);540        const chans = try exec.channelList(ctx.gpa, ctx.io, &ctx.machine);541        defer ctx.gpa.free(chans);542        for (chans) |ch| output.channelRow(ch);543        return;544    }545    if (std.mem.eql(u8, ctx.sub, "add")) {546        const url = if (ctx.args.len > 0) ctx.args[0] else return error.Internal;547        const name = if (ctx.args.len > 1) ctx.args[1] else "";548        _ = try exec.channelAdd(ctx.io, &ctx.machine, ctx.gpa, url, name);549        output.channelDone();550        return;551    }552    if (std.mem.eql(u8, ctx.sub, "remove")) {553        const name = if (ctx.args.len > 0) ctx.args[0] else return error.Internal;554        _ = try exec.channelRemove(ctx.io, &ctx.machine, ctx.gpa, name);555        output.channelDone();556        return;557    }558    if (std.mem.eql(u8, ctx.sub, "update")) {559        return update(ctx);560    }561}562563// --- Profile commands ---564565pub fn profile(ctx: Ctx) !void {566    if (std.mem.eql(u8, ctx.sub, "list") or ctx.sub.len == 0) {567        output.profileListHeader();568        const out = exec.profileList(ctx.gpa, ctx.io, &ctx.machine) catch null;569        if (out) |o| {570            defer ctx.gpa.free(o);571            var lines = std.mem.splitScalar(u8, o, '\n');572            while (lines.next()) |line| {573                const t = std.mem.trim(u8, line, " \t\r");574                if (t.len == 0) continue;575                output.profileRow(t, "", "");576            }577        }578        return;579    }580    if (std.mem.eql(u8, ctx.sub, "install")) {581        const attr = if (ctx.args.len > 0) ctx.args[0] else return error.PackageNotFound;582        _ = try exec.profileInstall(ctx.io, &ctx.machine, ctx.gpa, attr);583        output.done();584        return;585    }586    if (std.mem.eql(u8, ctx.sub, "remove")) {587        const attr = if (ctx.args.len > 0) ctx.args[0] else return error.PackageNotFound;588        _ = try exec.profileRemove(ctx.io, &ctx.machine, ctx.gpa, attr);589        output.done();590        return;591    }592    if (std.mem.eql(u8, ctx.sub, "upgrade")) {593        _ = try exec.profileUpgrade(ctx.io, &ctx.machine, ctx.gpa);594        output.done();595        return;596    }597}598599// --- Pkg commands ---600601pub fn pkg(ctx: Ctx) !void {602    const name = if (ctx.args.len > 0) ctx.args[0] else return error.PackageNotFound;603    if (std.mem.eql(u8, ctx.sub, "why")) {604        _ = try exec.pkgWhy(ctx.io, &ctx.machine, ctx.gpa, name);605        return;606    }607    if (std.mem.eql(u8, ctx.sub, "deps")) {608        _ = try exec.pkgDeps(ctx.io, &ctx.machine, ctx.gpa, name);609        return;610    }611    if (std.mem.eql(u8, ctx.sub, "size")) {612        const s = exec.pkgSize(ctx.gpa, ctx.io, &ctx.machine, name) catch null;613        if (s) |sz| {614            defer ctx.gpa.free(sz);615            output.pkgResult("size", sz);616        }617        return;618    }619    if (std.mem.eql(u8, ctx.sub, "path")) {620        const p = try exec.storePath(ctx.gpa, ctx.io, &ctx.machine, name);621        defer ctx.gpa.free(p);622        output.pkgResult("path", p);623        return;624    }625    if (std.mem.eql(u8, ctx.sub, "closure")) {626        _ = try exec.pkgClosure(ctx.io, &ctx.machine, ctx.gpa, name);627        return;628    }629}630631// --- Pin / Unpin ---632633pub fn pin(ctx: Ctx) !void {634    const input = if (ctx.args.len > 0) ctx.args[0] else return error.Internal;635    const commit = if (ctx.args.len > 1) ctx.args[1] else return error.Internal;636    exec.editFlakeLock(ctx.gpa, input, commit) catch return error.ProcessFailed;637    output.pinDone(input, commit);638}639640pub fn unpin(ctx: Ctx) !void {641    const input = if (ctx.args.len > 0) ctx.args[0] else return error.Internal;642    _ = try exec.flakeUpdate(ctx.io, &ctx.machine, ctx.gpa, input);643    output.unpinDone(input);644}645646// --- Hash / Fetch ---647648pub fn hash(ctx: Ctx) !void {649    const path = if (ctx.args.len > 0) ctx.args[0] else return error.Internal;650    const h = try exec.nixHash(ctx.gpa, ctx.io, &ctx.machine, path);651    defer ctx.gpa.free(h);652    output.hashResult(std.mem.trim(u8, h, " \t\n\r"));653}654655pub fn fetch(ctx: Ctx) !void {656    const url = if (ctx.args.len > 0) ctx.args[0] else return error.Internal;657    const h = try exec.nixFetch(ctx.gpa, ctx.io, &ctx.machine, url);658    defer ctx.gpa.free(h);659    output.fetchResult(std.mem.trim(u8, h, " \t\n\r"), url);660}661662// --- Build / Run ---663664pub fn build(ctx: Ctx) !void {665    const attr: ?[]const u8 = if (ctx.args.len > 0) ctx.args[0] else null;666    output.buildStart(attr orelse "default");667    const code = try exec.nixBuild(ctx.io, &ctx.machine, ctx.gpa, attr);668    if (code != 0) return error.BuildFailed;669    output.buildDone();670}671672pub fn runCmd(ctx: Ctx) !void {673    const pkg_arg = if (ctx.args.len > 0) ctx.args[0] else return error.PackageNotFound;674    output.runStart(pkg_arg);675    _ = try exec.nixRun(ctx.io, &ctx.machine, ctx.gpa, pkg_arg);676}677678// --- Hello ---679680pub fn hello(ctx: Ctx) !void {681    output.hello(ctx.cfg.machines);682}683684pub fn setup(gpa: std.mem.Allocator, io: std.Io, environ: *const std.process.Environ.Map) !void {685    output.setupCheck();686    const status = exec.checkNixFeatures(gpa, io, environ);687    output.setupFeatures(status.nix_command, status.flakes);688689    if (status.allEnabled()) {690        output.setupAllSet();691        return;692    }693694    output.setupEnabling();695    const path = try exec.enableNixFeatures(gpa, io, environ);696    defer gpa.free(path);697698    output.setupDone(path, exec.isNixOS(io));699}700701// --- Log helper ---702703fn logAction(ctx: Ctx, action: []const u8, gen: u32) void {704    var buf: [128]u8 = undefined;705    const entry = std.fmt.bufPrint(&buf, "{s}\t{s}\t{s}\tgen {d}\n", .{706        "2026", "00:00", action, gen,707    }) catch return;708    exec.appendNinaLog(ctx.io, ctx.environ, entry);709}