123const std = @import("std");4const builtin = @import("builtin");5const types = @import("types.zig");6const errors = @import("errors.zig");7const config = @import("config.zig");8const output = @import("output.zig");9const exec = @import("exec.zig");10const api = @import("api.zig");11const search_tui = @import("search.zig");12const version = @import("version.zig");1314const UPDATE_URL = "https://kepr.uk/api/nina/releases/latest";1516pub const Ctx = struct {17 gpa: std.mem.Allocator,18 io: std.Io,19 environ: *const std.process.Environ.Map,20 machine: types.Machine,21 cfg: types.NinaConfig,22 args: []const []const u8, 23 sub: []const u8, 24 dry: bool,25 check: bool,26 no_apply: bool,27 all: bool,28 last: u32,29 json: bool,30};3132pub fn apply(ctx: Ctx) !void {33 if (ctx.dry) {34 output.applyDry(ctx.machine.name);35 const code = try exec.nixosRebuildDryActivate(ctx.io, &ctx.machine, ctx.gpa);36 if (code != 0) return error.BuildFailed;37 output.applyDryDone();38 return;39 }40 if (ctx.check) {41 return check(ctx);42 }4344 output.applyStart(ctx.machine.name);45 const start = std.Io.Clock.awake.now(ctx.io);4647 const code = exec.nixosRebuildSwitch(ctx.io, &ctx.machine, ctx.gpa) catch |e| {48 output.applyFailed("");49 return e;50 };5152 if (code != 0) {53 output.applyFailed("");54 return error.BuildFailed;55 }5657 const elapsed_ns = @max(0, start.untilNow(ctx.io, .awake).toNanoseconds());58 const elapsed: u64 = @intCast(@divFloor(elapsed_ns, 1_000_000));59 const gen = exec.getCurrentGeneration(ctx.gpa, ctx.io, &ctx.machine) catch 0;60 output.applyDone(gen, elapsed);61 logAction(ctx, "apply", gen);62}6364pub fn check(ctx: Ctx) !void {65 output.checkStart(ctx.machine.name);66 const code = try exec.nixosRebuildBuild(ctx.io, &ctx.machine, ctx.gpa);67 if (code != 0) {68 output.checkFailed("");69 return error.BuildFailed;70 }71 output.checkOk();72}7374pub fn back(ctx: Ctx) !void {75 const gens = try exec.getGenerations(ctx.gpa, ctx.io, &ctx.machine);76 defer ctx.gpa.free(gens);7778 var current: u32 = 0;79 for (gens) |g| if (g.current) { current = g.number; };80 const prev = if (current > 1) current - 1 else current;8182 output.backStart(ctx.machine.name, current, prev);83 if (ctx.cfg.confirm and !output.confirm("")) return;8485 const code = try exec.rollback(ctx.io, &ctx.machine, ctx.gpa);86 if (code != 0) return error.BuildFailed;87 output.generationDone(prev);88}8990pub fn go(ctx: Ctx) !void {91 if (ctx.args.len == 0) return error.Internal;92 const target = std.fmt.parseInt(u32, ctx.args[0], 10) catch return error.Internal;93 const current = try exec.getCurrentGeneration(ctx.gpa, ctx.io, &ctx.machine);9495 output.goStart(ctx.machine.name, current, target);96 if (ctx.cfg.confirm and !output.confirm("")) return;9798 const code = try exec.switchGeneration(ctx.io, &ctx.machine, ctx.gpa, target);99 if (code != 0) return error.ProcessFailed;100 output.generationDone(target);101}102103pub fn history(ctx: Ctx) !void {104 const gens = try exec.getGenerations(ctx.gpa, ctx.io, &ctx.machine);105 defer ctx.gpa.free(gens);106 output.historyHeader(ctx.machine.name);107 for (gens) |g| output.historyRow(g);108}109110pub fn genCmd(ctx: Ctx) !void {111 if (std.mem.eql(u8, ctx.sub, "list")) return history(ctx);112 if (std.mem.eql(u8, ctx.sub, "current")) {113 const n = try exec.getCurrentGeneration(ctx.gpa, ctx.io, &ctx.machine);114 output.genCurrent(n);115 return;116 }117 if (std.mem.eql(u8, ctx.sub, "delete")) {118 if (ctx.args.len == 0) return error.Internal;119 if (std.mem.eql(u8, ctx.args[0], "old")) {120 if (ctx.cfg.confirm and !output.confirm("")) return;121 _ = try exec.deleteOldGenerations(ctx.io, &ctx.machine, ctx.gpa);122 } else {123 const n = std.fmt.parseInt(u32, ctx.args[0], 10) catch return error.Internal;124 if (ctx.cfg.confirm and !output.confirm("")) return;125 _ = try exec.deleteGeneration(ctx.io, &ctx.machine, ctx.gpa, n);126 }127 output.done();128 return;129 }130 return error.Internal;131}132133pub fn clean(ctx: Ctx) !void {134 const gens = try exec.getGenerations(ctx.gpa, ctx.io, &ctx.machine);135 defer ctx.gpa.free(gens);136 output.cleanStart(ctx.machine.name, ctx.cfg.generations, @intCast(gens.len));137 if (ctx.cfg.confirm and !output.confirm("removing — cannot be undone")) return;138 const all = ctx.all or std.mem.eql(u8, ctx.sub, "all");139 _ = try exec.collectGarbage(ctx.io, &ctx.machine, ctx.gpa, all, ctx.cfg.generations);140 output.cleanDone("?");141}142143pub fn statusCmd(ctx: Ctx) !void {144 output.statusHeader();145 const gen = exec.getCurrentGeneration(ctx.gpa, ctx.io, &ctx.machine) catch 0;146 output.statusRow(ctx.machine.name, gen, "", "", true);147}148149pub fn diff(ctx: Ctx) !void {150 const gens = try exec.getGenerations(ctx.gpa, ctx.io, &ctx.machine);151 defer ctx.gpa.free(gens);152 var current: u32 = 0;153 var prev: u32 = 0;154 for (gens) |g| {155 if (g.current) current = g.number;156 if (!g.current and g.number > prev and g.number < current) prev = g.number;157 }158 if (ctx.args.len >= 2) {159 const a = std.fmt.parseInt(u32, ctx.args[0], 10) catch current - 1;160 const b = std.fmt.parseInt(u32, ctx.args[1], 10) catch current;161 output.diffHeader(a, b, ctx.machine.name);162 if (try exec.diffClosures(ctx.io, &ctx.machine, ctx.gpa, a, b) != 0) return error.ProcessFailed;163 } else {164 output.diffHeader(if (prev > 0) prev else current - 1, current, ctx.machine.name);165 if (try exec.diffClosures(ctx.io, &ctx.machine, ctx.gpa, if (prev > 0) prev else current - 1, current) != 0) return error.ProcessFailed;166 }167}168169pub fn doctor(ctx: Ctx) !void {170 output.doctorHeader(ctx.machine.name);171 const checks = try exec.runDoctorChecks(ctx.gpa, ctx.io, &ctx.machine);172 defer ctx.gpa.free(checks);173 var warnings: u32 = 0;174 var failures: u32 = 0;175 for (checks) |ch| {176 output.doctorRow(ch);177 if (ch.status == .warn) warnings += 1;178 if (ch.status == .fail) failures += 1;179 }180 output.doctorSummary(warnings, failures);181}182183184pub fn update(ctx: Ctx) !void {185 output.updateSelfStart();186187 if (comptime std.mem.eql(u8, version.PLATFORM, "unsupported")) {188 output.updateSelfNoPlatform(@tagName(builtin.cpu.arch) ++ "-" ++ @tagName(builtin.os.tag));189 return error.Internal;190 }191192 const env_url = ctx.environ.get("NINA_UPDATE_SOURCE");193 const source_url = env_url orelse UPDATE_URL;194195 const body = exec.fetchUrl(ctx.gpa, ctx.io, source_url) catch |e| switch (e) {196 error.NetworkError, error.HttpError => {197 output.updateSelfNetworkError();198 return;199 },200 else => return e,201 };202 defer ctx.gpa.free(body);203204 const parsed = std.json.parseFromSlice(std.json.Value, ctx.gpa, body, .{}) catch {205 output.printWarning("could not parse release metadata");206 return;207 };208 defer parsed.deinit();209210 const root = parsed.value;211 if (root != .object) { output.printWarning("malformed release metadata"); return; }212213 const remote_ver = blk: {214 const v = root.object.get("version") orelse { output.printWarning("malformed release metadata"); return; };215 break :blk switch (v) { .string => |s| s, else => { output.printWarning("malformed release metadata"); return; } };216 };217218 if (std.mem.eql(u8, remote_ver, version.VERSION)) {219 output.updateSelfCurrent(version.VERSION);220 return;221 }222223 output.updateSelfAvailable(version.VERSION, remote_ver);224 if (ctx.check) return;225226 const plat_info = blk: {227 const pv = root.object.get("platforms") orelse break :blk null;228 if (pv != .object) break :blk null;229 break :blk pv.object.get(version.PLATFORM);230 };231 if (plat_info == null) { output.updateSelfNoPlatform(version.PLATFORM); return; }232233 const dl_url = blk: {234 const v = plat_info.?.object.get("url") orelse { output.printWarning("malformed release metadata"); return; };235 break :blk switch (v) { .string => |s| s, else => { output.printWarning("malformed release metadata"); return; } };236 };237238 const raw_cksum = blk: {239 if (plat_info.?.object.get("blake3")) |v|240 break :blk switch (v) { .string => |s| s, else => { output.printWarning("malformed release metadata"); return; } };241 if (plat_info.?.object.get("checksum")) |v|242 break :blk switch (v) { .string => |s| s, else => { output.printWarning("malformed release metadata"); return; } };243 output.printWarning("missing checksum in release metadata");244 return;245 };246 const expected_hex = if (std.mem.startsWith(u8, raw_cksum, "blake3:")) raw_cksum["blake3:".len..] else raw_cksum;247248 if (ctx.cfg.confirm and !output.confirm("")) return;249250 var exe_buf: [std.Io.Dir.max_path_bytes]u8 = undefined;251 const install_path = exec.selfExePath(ctx.io, &exe_buf) catch {252 errors.error_info.setMessage("could not determine install path", .{});253 return error.Internal;254 };255256 output.updateSelfDownloading(remote_ver, version.PLATFORM);257 const tmp_path = exec.downloadToTemp(ctx.gpa, ctx.io, dl_url) catch {258 output.printWarning("download failed — check your connection and try again");259 return;260 };261 defer ctx.gpa.free(tmp_path);262263 output.updateSelfVerifying();264 exec.verifyAndInstall(ctx.gpa, ctx.io, tmp_path, expected_hex, install_path) catch |e| switch (e) {265 error.ChecksumMismatch => { output.updateSelfChecksumFailed(); return; },266 else => return e,267 };268269 output.updateSelfDone(version.VERSION, remote_ver);270}271272fn channelUpdateCmd(ctx: Ctx) !void {273 output.updateStart(ctx.machine.name);274 _ = try exec.channelUpdate(ctx.io, &ctx.machine, ctx.gpa);275 output.updateDone();276}277278pub fn upgrade(ctx: Ctx) !void {279 output.upgradeStart(ctx.machine.name);280 output.printSubstep("updating channels", .{});281 _ = exec.channelUpdate(ctx.io, &ctx.machine, ctx.gpa) catch {};282 output.printSubstep("rebuilding", .{});283 const start = std.Io.Clock.awake.now(ctx.io);284 const code = try exec.nixosRebuildSwitch(ctx.io, &ctx.machine, ctx.gpa);285 if (code != 0) {286 output.applyFailed("");287 return error.BuildFailed;288 }289 const elapsed_ns = @max(0, start.untilNow(ctx.io, .awake).toNanoseconds());290 const elapsed: u64 = @intCast(@divFloor(elapsed_ns, 1_000_000));291 const gen = exec.getCurrentGeneration(ctx.gpa, ctx.io, &ctx.machine) catch 0;292 output.applyDone(gen, elapsed);293}294295pub fn log(ctx: Ctx) !void {296 output.logHeader(ctx.machine.name);297 const content = exec.readNinaLog(ctx.gpa, ctx.io, ctx.environ, ctx.last) catch {298 299 return;300 };301 defer ctx.gpa.free(content);302 var lines = std.mem.splitScalar(u8, content, '\n');303 while (lines.next()) |line| {304 if (line.len == 0) continue;305 var parts = std.mem.tokenizeScalar(u8, line, '\t');306 const date = parts.next() orelse continue;307 const time = parts.next() orelse "";308 const action = parts.next() orelse "";309 const detail = parts.next() orelse "";310 output.logRow(date, time, action, detail);311 }312}313314pub fn edit(ctx: Ctx) !void {315 const hardware = ctx.args.len > 0 and std.mem.eql(u8, ctx.args[0], "hardware");316 const filename = if (hardware) "hardware-configuration.nix" else "configuration.nix";317 const path = try std.fmt.allocPrint(ctx.gpa, "{s}/{s}", .{ ctx.machine.config_path, filename });318 defer ctx.gpa.free(path);319 output.editOpen(path, ctx.cfg.editor);320 _ = try exec.openEditor(ctx.io, ctx.gpa, ctx.cfg.editor, path, null);321}322323pub fn fmt(ctx: Ctx) !void {324 const path = try std.fmt.allocPrint(ctx.gpa, "{s}/configuration.nix", .{ctx.machine.config_path});325 defer ctx.gpa.free(path);326 if (ctx.check) {327 const code = try exec.nixpkgsFmt(ctx.io, &ctx.machine, ctx.gpa, path, true);328 if (code != 0) output.fmtNeedsFormat() else output.fmtDone();329 return;330 }331 output.fmtStart(path);332 _ = try exec.nixpkgsFmt(ctx.io, &ctx.machine, ctx.gpa, path, false);333 output.fmtDone();334}335336pub fn repl(ctx: Ctx) !void {337 output.replStart();338 _ = try exec.nixRepl(ctx.io, &ctx.machine, ctx.gpa);339}340341pub fn develop(ctx: Ctx) !void {342 if (std.mem.eql(u8, ctx.sub, "show")) {343 _ = try exec.nixBuild(ctx.io, &ctx.machine, ctx.gpa, null);344 return;345 }346 const run_cmd: ?[]const u8 = if (ctx.args.len > 0 and std.mem.eql(u8, ctx.sub, "run"))347 ctx.args[0]348 else349 null;350 output.developStart(&.{});351 _ = try exec.nixDevelop(ctx.io, &ctx.machine, ctx.gpa, run_cmd);352 output.developDone();353}354355pub fn info(ctx: Ctx) !void {356 output.infoHeader(ctx.machine.name);357 const nixos_ver = exec.systemInfo(ctx.gpa, ctx.io, &ctx.machine) catch try ctx.gpa.dupe(u8, "unknown");358 defer ctx.gpa.free(nixos_ver);359 output.infoRow("nixos", std.mem.trim(u8, nixos_ver, " \t\n\r"));360361 const kernel = exec.kernelInfo(ctx.gpa, ctx.io, &ctx.machine) catch try ctx.gpa.dupe(u8, "unknown");362 defer ctx.gpa.free(kernel);363 output.infoRow("kernel", std.mem.trim(u8, kernel, " \t\n\r"));364365 const uptime = exec.uptimeInfo(ctx.gpa, ctx.io, &ctx.machine) catch try ctx.gpa.dupe(u8, "unknown");366 defer ctx.gpa.free(uptime);367 output.infoRow("uptime", std.mem.trim(u8, uptime, " \t\n\r"));368}369370pub fn boot(ctx: Ctx) !void {371 output.bootHeader(ctx.machine.name);372 const entries = exec.bootEntries(ctx.gpa, ctx.io, &ctx.machine) catch &.{};373 defer ctx.gpa.free(entries);374 for (entries, 0..) |entry, i| output.bootEntry(entry, i);375}376377pub fn searchCmd(ctx: Ctx) !void {378 const query = if (ctx.args.len > 0) ctx.args[0] else "";379 const result = try search_tui.run(ctx.gpa, ctx.io, query, .packages);380 if (result) |r| {381 installFromResult(ctx, r) catch |e| return e;382 }383}384385pub fn optionCmd(ctx: Ctx) !void {386 const query = if (ctx.args.len > 0) ctx.args[0] else "";387 _ = try search_tui.run(ctx.gpa, ctx.io, query, .options);388}389390pub fn install(ctx: Ctx) !void {391 const pkg_name = if (ctx.args.len > 0) ctx.args[0] else return error.PackageNotFound;392393 394 const pkgs = api.searchPackages(ctx.gpa, ctx.io, pkg_name) catch {395 errors.error_info.setMessage("'{s}' not found in nixpkgs", .{pkg_name});396 errors.error_info.setSuggestion("nina search {s}", .{pkg_name});397 return error.PackageNotFound;398 };399 defer ctx.gpa.free(pkgs);400401 if (pkgs.len == 0) {402 errors.error_info.setMessage("'{s}' not found in nixpkgs", .{pkg_name});403 errors.error_info.setSuggestion("nina search {s}", .{pkg_name});404 return error.PackageNotFound;405 }406407 408 409 var found = pkgs[0];410 for (pkgs) |p| {411 if (std.mem.eql(u8, p.pname, pkg_name)) {412 found = p;413 break;414 }415 }416 output.installSelectPrompt(found.pname, found.version);417418 var choice_buf: [8]u8 = undefined;419 var choice_len: usize = 0;420 while (choice_len < choice_buf.len) {421 const n = std.posix.read(std.posix.STDIN_FILENO, choice_buf[choice_len .. choice_len + 1]) catch break;422 if (n == 0) break;423 if (choice_buf[choice_len] == '\n') break;424 choice_len += 1;425 }426 const choice = std.mem.trim(u8, choice_buf[0..choice_len], " \t");427428 if (choice.len == 0 or choice[0] == 'i' or choice[0] == 'I') {429 try profileInstallPkg(ctx, found);430 } else if (choice[0] == 's' or choice[0] == 'S') {431 try systemInstallPkg(ctx, found);432 } else if (choice[0] == 't' or choice[0] == 'T') {433 try tryPkg(ctx, found);434 }435}436437fn installFromResult(ctx: Ctx, result: types.SearchResult) !void {438 switch (result.action) {439 .profile => try profileInstallPkg(ctx, result.package),440 .system => try systemInstallPkg(ctx, result.package),441 }442}443444fn profileInstallPkg(ctx: Ctx, npkg: types.NixPackage) !void {445 output.installStart(npkg.pname, .profile);446 output.printSubstep("nix profile install nixpkgs#{s}", .{npkg.attr});447 output.printTeach(try std.fmt.allocPrint(ctx.gpa, "nix profile install nixpkgs#{s}", .{npkg.attr}));448 const code = try exec.profileInstall(ctx.io, &ctx.machine, ctx.gpa, npkg.attr);449 if (code != 0) return error.ProcessFailed;450 output.installDone(npkg.pname);451}452453fn systemInstallPkg(ctx: Ctx, npkg: types.NixPackage) !void {454 const config_path = try std.fmt.allocPrint(ctx.gpa, "{s}/configuration.nix", .{ctx.machine.config_path});455 defer ctx.gpa.free(config_path);456 const line = api.findSystemPackagesLine(ctx.io, config_path) orelse 1;457 output.installEditorPrompt(npkg.pname, config_path, line, ctx.cfg.editor);458 _ = try exec.openEditor(ctx.io, ctx.gpa, ctx.cfg.editor, config_path, line);459 if (ctx.cfg.confirm) {460 if (output.confirm("apply now? ")) {461 try apply(ctx);462 }463 }464}465466fn tryPkg(ctx: Ctx, npkg: types.NixPackage) !void {467 output.tryStart(npkg.pname);468 output.printSubstep("nix shell nixpkgs#{s}", .{npkg.attr});469 _ = try exec.nixShell(ctx.io, &ctx.machine, ctx.gpa, npkg.attr);470 output.tryDone();471}472473pub fn remove(ctx: Ctx) !void {474 const pkg_name = if (ctx.args.len > 0) ctx.args[0] else return error.PackageNotFound;475 output.installStart(pkg_name, .profile); 476 const code = try exec.profileRemove(ctx.io, &ctx.machine, ctx.gpa, pkg_name);477 if (code != 0) return error.ProcessFailed;478 output.done();479}480481pub fn list(ctx: Ctx) !void {482 output.listHeader();483 const profile_out = exec.profileList(ctx.gpa, ctx.io, &ctx.machine) catch null;484 if (profile_out) |out| {485 defer ctx.gpa.free(out);486 output.listSection("profile");487 var lines = std.mem.splitScalar(u8, out, '\n');488 while (lines.next()) |line| {489 const t = std.mem.trim(u8, line, " \t\r");490 if (t.len == 0) continue;491 output.listPkg(t, "");492 }493 }494}495496pub fn tryCmd(ctx: Ctx) !void {497 const pkg_name_arg = if (ctx.args.len > 0) ctx.args[0] else return error.PackageNotFound;498 const shell_pkg = types.NixPackage{ .attr = pkg_name_arg, .pname = pkg_name_arg, .version = "", .description = "" };499 try tryPkg(ctx, shell_pkg);500}501502503504pub fn service(ctx: Ctx) !void {505 if (std.mem.eql(u8, ctx.sub, "list") or ctx.sub.len == 0) {506 output.serviceListHeader(ctx.machine.name);507 const svcs = exec.serviceList(ctx.gpa, ctx.io, &ctx.machine) catch &.{};508 defer ctx.gpa.free(svcs);509 for (svcs) |s| output.serviceRow(s);510 return;511 }512 if (std.mem.eql(u8, ctx.sub, "logs")) {513 const name = if (ctx.args.len > 0) ctx.args[0] else return error.Internal;514 const follow = ctx.args.len > 1 and std.mem.eql(u8, ctx.args[1], "-f");515 output.serviceLogsHeader(name, ctx.last, ctx.machine.name);516 _ = try exec.serviceLogs(ctx.io, &ctx.machine, ctx.gpa, name, ctx.last, follow);517 return;518 }519 if (std.mem.eql(u8, ctx.sub, "status")) {520 const name = if (ctx.args.len > 0) ctx.args[0] else return error.Internal;521 output.serviceVerb("status", name, ctx.machine.name);522 _ = try exec.serviceCtl(ctx.io, &ctx.machine, ctx.gpa, "status", name);523 return;524 }525 526 const name = if (ctx.args.len > 0) ctx.args[0] else return error.Internal;527 if (ctx.cfg.confirm and (std.mem.eql(u8, ctx.sub, "stop") or std.mem.eql(u8, ctx.sub, "disable"))) {528 if (!output.confirm("")) return;529 }530 output.serviceVerb(ctx.sub, name, ctx.machine.name);531 _ = try exec.serviceCtl(ctx.io, &ctx.machine, ctx.gpa, ctx.sub, name);532 output.serviceDone();533}534535536537pub fn flake(ctx: Ctx) !void {538 if (std.mem.eql(u8, ctx.sub, "update")) {539 output.flakeUpdateStart();540 const input: ?[]const u8 = if (ctx.args.len > 0) ctx.args[0] else null;541 _ = try exec.flakeUpdate(ctx.io, &ctx.machine, ctx.gpa, input);542 output.flakeLockWritten();543 return;544 }545 if (std.mem.eql(u8, ctx.sub, "check")) {546 output.flakeCheckStart();547 const code = try exec.flakeCheck(ctx.io, &ctx.machine, ctx.gpa);548 if (code == 0) output.flakeValid() else output.checkFailed("");549 return;550 }551 if (std.mem.eql(u8, ctx.sub, "show")) {552 const json_out = exec.flakeShow(ctx.gpa, ctx.io, &ctx.machine) catch null;553 if (json_out) |j| {554 defer ctx.gpa.free(j);555 output.flakeShowHeader(".");556 557 const parsed = std.json.parseFromSlice(std.json.Value, ctx.gpa, j, .{}) catch return;558 defer parsed.deinit();559 if (parsed.value == .object) {560 var it = parsed.value.object.iterator();561 while (it.next()) |entry| {562 if (entry.value_ptr.* == .object) {563 var inner = entry.value_ptr.object.iterator();564 while (inner.next()) |e2| {565 const desc = if (e2.value_ptr.* == .object)566 if (e2.value_ptr.object.get("description")) |d|567 if (d == .string) d.string else ""568 else ""569 else "";570 output.flakeShowEntry(entry.key_ptr.*, e2.key_ptr.*, desc);571 }572 }573 }574 }575 }576 return;577 }578 if (std.mem.eql(u8, ctx.sub, "init")) {579 _ = try exec.flakeInit(ctx.io, &ctx.machine, ctx.gpa);580 return;581 }582 if (std.mem.eql(u8, ctx.sub, "lock")) {583 _ = try exec.flakeLock(ctx.io, &ctx.machine, ctx.gpa);584 return;585 }586 if (std.mem.eql(u8, ctx.sub, "clone")) {587 const url = if (ctx.args.len > 0) ctx.args[0] else return error.Internal;588 _ = try exec.flakeClone(ctx.io, &ctx.machine, ctx.gpa, url);589 return;590 }591}592593594595pub fn store(ctx: Ctx) !void {596 if (std.mem.eql(u8, ctx.sub, "gc")) {597 _ = try exec.storeGc(ctx.io, &ctx.machine, ctx.gpa);598 return;599 }600 if (std.mem.eql(u8, ctx.sub, "verify")) {601 output.storeVerifyStart();602 _ = try exec.storeVerify(ctx.io, &ctx.machine, ctx.gpa);603 return;604 }605 if (std.mem.eql(u8, ctx.sub, "repair")) {606 if (ctx.cfg.confirm and !output.confirm("")) return;607 output.storeRepairStart();608 _ = try exec.storeRepair(ctx.io, &ctx.machine, ctx.gpa);609 return;610 }611 if (std.mem.eql(u8, ctx.sub, "info")) {612 const si = try exec.storeInfo(ctx.gpa, ctx.io, &ctx.machine);613 output.storeInfo(si);614 return;615 }616 if (std.mem.eql(u8, ctx.sub, "path")) {617 const attr = if (ctx.args.len > 0) ctx.args[0] else return error.Internal;618 const path = try exec.storePath(ctx.gpa, ctx.io, &ctx.machine, attr);619 defer ctx.gpa.free(path);620 output.storePath(path);621 return;622 }623 624 const si = try exec.storeInfo(ctx.gpa, ctx.io, &ctx.machine);625 output.storeInfo(si);626}627628629630pub fn channel(ctx: Ctx) !void {631 if (std.mem.eql(u8, ctx.sub, "list") or ctx.sub.len == 0) {632 output.channelListHeader(ctx.machine.name);633 const chans = try exec.channelList(ctx.gpa, ctx.io, &ctx.machine);634 defer ctx.gpa.free(chans);635 for (chans) |ch| output.channelRow(ch);636 return;637 }638 if (std.mem.eql(u8, ctx.sub, "add")) {639 const url = if (ctx.args.len > 0) ctx.args[0] else return error.Internal;640 const name = if (ctx.args.len > 1) ctx.args[1] else "";641 _ = try exec.channelAdd(ctx.io, &ctx.machine, ctx.gpa, url, name);642 output.channelDone();643 return;644 }645 if (std.mem.eql(u8, ctx.sub, "remove")) {646 const name = if (ctx.args.len > 0) ctx.args[0] else return error.Internal;647 _ = try exec.channelRemove(ctx.io, &ctx.machine, ctx.gpa, name);648 output.channelDone();649 return;650 }651 if (std.mem.eql(u8, ctx.sub, "update")) {652 return channelUpdateCmd(ctx);653 }654}655656657658pub fn profile(ctx: Ctx) !void {659 if (std.mem.eql(u8, ctx.sub, "list") or ctx.sub.len == 0) {660 output.profileListHeader();661 const out = exec.profileList(ctx.gpa, ctx.io, &ctx.machine) catch null;662 if (out) |o| {663 defer ctx.gpa.free(o);664 var lines = std.mem.splitScalar(u8, o, '\n');665 while (lines.next()) |line| {666 const t = std.mem.trim(u8, line, " \t\r");667 if (t.len == 0) continue;668 output.profileRow(t, "", "");669 }670 }671 return;672 }673 if (std.mem.eql(u8, ctx.sub, "install")) {674 const attr = if (ctx.args.len > 0) ctx.args[0] else return error.PackageNotFound;675 _ = try exec.profileInstall(ctx.io, &ctx.machine, ctx.gpa, attr);676 output.done();677 return;678 }679 if (std.mem.eql(u8, ctx.sub, "remove")) {680 const attr = if (ctx.args.len > 0) ctx.args[0] else return error.PackageNotFound;681 _ = try exec.profileRemove(ctx.io, &ctx.machine, ctx.gpa, attr);682 output.done();683 return;684 }685 if (std.mem.eql(u8, ctx.sub, "upgrade")) {686 _ = try exec.profileUpgrade(ctx.io, &ctx.machine, ctx.gpa);687 output.done();688 return;689 }690}691692693694pub fn pkg(ctx: Ctx) !void {695 const name = if (ctx.args.len > 0) ctx.args[0] else return error.PackageNotFound;696 if (std.mem.eql(u8, ctx.sub, "why")) {697 _ = try exec.pkgWhy(ctx.io, &ctx.machine, ctx.gpa, name);698 return;699 }700 if (std.mem.eql(u8, ctx.sub, "deps")) {701 _ = try exec.pkgDeps(ctx.io, &ctx.machine, ctx.gpa, name);702 return;703 }704 if (std.mem.eql(u8, ctx.sub, "size")) {705 const s = exec.pkgSize(ctx.gpa, ctx.io, &ctx.machine, name) catch null;706 if (s) |sz| {707 defer ctx.gpa.free(sz);708 output.pkgResult("size", sz);709 }710 return;711 }712 if (std.mem.eql(u8, ctx.sub, "path")) {713 const p = try exec.storePath(ctx.gpa, ctx.io, &ctx.machine, name);714 defer ctx.gpa.free(p);715 output.pkgResult("path", p);716 return;717 }718 if (std.mem.eql(u8, ctx.sub, "closure")) {719 _ = try exec.pkgClosure(ctx.io, &ctx.machine, ctx.gpa, name);720 return;721 }722}723724725726pub fn pin(ctx: Ctx) !void {727 const input = if (ctx.args.len > 0) ctx.args[0] else return error.Internal;728 const commit = if (ctx.args.len > 1) ctx.args[1] else return error.Internal;729 exec.editFlakeLock(ctx.gpa, input, commit) catch return error.ProcessFailed;730 output.pinDone(input, commit);731}732733pub fn unpin(ctx: Ctx) !void {734 const input = if (ctx.args.len > 0) ctx.args[0] else return error.Internal;735 _ = try exec.flakeUpdate(ctx.io, &ctx.machine, ctx.gpa, input);736 output.unpinDone(input);737}738739740741pub fn hash(ctx: Ctx) !void {742 const path = if (ctx.args.len > 0) ctx.args[0] else return error.Internal;743 const h = try exec.nixHash(ctx.gpa, ctx.io, &ctx.machine, path);744 defer ctx.gpa.free(h);745 output.hashResult(std.mem.trim(u8, h, " \t\n\r"));746}747748pub fn fetch(ctx: Ctx) !void {749 const url = if (ctx.args.len > 0) ctx.args[0] else return error.Internal;750 const h = try exec.nixFetch(ctx.gpa, ctx.io, &ctx.machine, url);751 defer ctx.gpa.free(h);752 output.fetchResult(std.mem.trim(u8, h, " \t\n\r"), url);753}754755756757pub fn build(ctx: Ctx) !void {758 const attr: ?[]const u8 = if (ctx.args.len > 0) ctx.args[0] else null;759 output.buildStart(attr orelse "default");760 const code = try exec.nixBuild(ctx.io, &ctx.machine, ctx.gpa, attr);761 if (code != 0) return error.BuildFailed;762 output.buildDone();763}764765pub fn runCmd(ctx: Ctx) !void {766 const pkg_arg = if (ctx.args.len > 0) ctx.args[0] else return error.PackageNotFound;767 output.runStart(pkg_arg);768 _ = try exec.nixRun(ctx.io, &ctx.machine, ctx.gpa, pkg_arg);769}770771772773pub fn hello(ctx: Ctx) !void {774 output.hello(ctx.cfg.machines);775}776777pub fn setup(gpa: std.mem.Allocator, io: std.Io, environ: *const std.process.Environ.Map) !void {778 output.setupCheck();779 const status = exec.checkNixFeatures(gpa, io, environ);780 output.setupFeatures(status.nix_command, status.flakes);781782 if (status.allEnabled()) {783 output.setupAllSet();784 return;785 }786787 output.setupEnabling();788 const path = try exec.enableNixFeatures(gpa, io, environ);789 defer gpa.free(path);790791 output.setupDone(path, exec.isNixOS(io));792}793794795796fn logAction(ctx: Ctx, action: []const u8, gen: u32) void {797 var buf: [128]u8 = undefined;798 const entry = std.fmt.bufPrint(&buf, "{s}\t{s}\t{s}\tgen {d}\n", .{799 "2026", "00:00", action, gen,800 }) catch return;801 exec.appendNinaLog(ctx.io, ctx.environ, entry);802}