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 const st = exec.systemStatus(ctx.gpa, ctx.io, &ctx.machine);147 defer ctx.gpa.free(st.size);148 defer ctx.gpa.free(st.age);149 output.statusRow(ctx.machine.name, gen, st.age, st.size, true);150}151152pub fn diff(ctx: Ctx) !void {153 const gens = try exec.getGenerations(ctx.gpa, ctx.io, &ctx.machine);154 defer ctx.gpa.free(gens);155 var current: u32 = 0;156 var prev: u32 = 0;157 for (gens) |g| {158 if (g.current) current = g.number;159 if (!g.current and g.number > prev and g.number < current) prev = g.number;160 }161 if (ctx.args.len >= 2) {162 const a = std.fmt.parseInt(u32, ctx.args[0], 10) catch current - 1;163 const b = std.fmt.parseInt(u32, ctx.args[1], 10) catch current;164 output.diffHeader(a, b, ctx.machine.name);165 if (try exec.diffClosures(ctx.io, &ctx.machine, ctx.gpa, a, b) != 0) return error.ProcessFailed;166 } else {167 output.diffHeader(if (prev > 0) prev else current - 1, current, ctx.machine.name);168 if (try exec.diffClosures(ctx.io, &ctx.machine, ctx.gpa, if (prev > 0) prev else current - 1, current) != 0) return error.ProcessFailed;169 }170}171172pub fn mood(ctx: Ctx) !void {173 const gen = exec.getCurrentGeneration(ctx.gpa, ctx.io, &ctx.machine) catch 0;174 const st = exec.systemStatus(ctx.gpa, ctx.io, &ctx.machine);175 defer ctx.gpa.free(st.size);176 defer ctx.gpa.free(st.age);177 const state = exec.systemRunningState(ctx.gpa, ctx.io, &ctx.machine) catch try ctx.gpa.dupe(u8, "unknown");178 defer ctx.gpa.free(state);179 output.mood(ctx.machine.name, state, gen, st.age, st.size);180}181182pub fn doctor(ctx: Ctx) !void {183 output.doctorHeader(ctx.machine.name);184 const checks = try exec.runDoctorChecks(ctx.gpa, ctx.io, &ctx.machine);185 defer ctx.gpa.free(checks);186 var warnings: u32 = 0;187 var failures: u32 = 0;188 for (checks) |ch| {189 output.doctorRow(ch);190 if (ch.status == .warn) warnings += 1;191 if (ch.status == .fail) failures += 1;192 }193 output.doctorSummary(warnings, failures);194}195196197pub fn update(ctx: Ctx) !void {198 output.updateSelfStart();199200 if (comptime std.mem.eql(u8, version.PLATFORM, "unsupported")) {201 output.updateSelfNoPlatform(@tagName(builtin.cpu.arch) ++ "-" ++ @tagName(builtin.os.tag));202 return error.Internal;203 }204205 const env_url = ctx.environ.get("NINA_UPDATE_SOURCE");206 const source_url = env_url orelse UPDATE_URL;207208 const body = exec.fetchUrl(ctx.gpa, ctx.io, source_url) catch |e| switch (e) {209 error.NetworkError, error.HttpError => {210 output.updateSelfNetworkError();211 return;212 },213 else => return e,214 };215 defer ctx.gpa.free(body);216217 const parsed = std.json.parseFromSlice(std.json.Value, ctx.gpa, body, .{}) catch {218 output.printWarning("could not parse release metadata");219 return;220 };221 defer parsed.deinit();222223 const root = parsed.value;224 if (root != .object) { output.printWarning("malformed release metadata"); return; }225226 const remote_ver = blk: {227 const v = root.object.get("version") orelse { output.printWarning("malformed release metadata"); return; };228 break :blk switch (v) { .string => |s| s, else => { output.printWarning("malformed release metadata"); return; } };229 };230231 if (std.mem.eql(u8, remote_ver, version.VERSION)) {232 output.updateSelfCurrent(version.VERSION);233 return;234 }235236 output.updateSelfAvailable(version.VERSION, remote_ver);237 if (ctx.check) return;238239 var exe_buf: [std.Io.Dir.max_path_bytes]u8 = undefined;240 const install_path = exec.selfExePath(ctx.io, &exe_buf) catch {241 errors.error_info.setMessage("could not determine install path", .{});242 return error.Internal;243 };244245 246 247 if (std.mem.startsWith(u8, install_path, "/nix/store/")) {248 output.updateSelfNixManaged(remote_ver);249 if (ctx.cfg.confirm and !output.confirm("")) return;250 const ec = exec.streamLocal(ctx.io, &.{ "nix", "profile", "upgrade", ".*nina.*" }) catch {251 output.updateSelfNixFailed();252 return;253 };254 if (ec != 0) output.updateSelfNixSystemManaged();255 return;256 }257258 const plat_info = blk: {259 const pv = root.object.get("platforms") orelse break :blk null;260 if (pv != .object) break :blk null;261 break :blk pv.object.get(version.PLATFORM);262 };263 if (plat_info == null) { output.updateSelfNoPlatform(version.PLATFORM); return; }264265 const dl_url = blk: {266 const v = plat_info.?.object.get("url") orelse { output.printWarning("malformed release metadata"); return; };267 break :blk switch (v) { .string => |s| s, else => { output.printWarning("malformed release metadata"); return; } };268 };269270 const raw_cksum = blk: {271 if (plat_info.?.object.get("blake3")) |v|272 break :blk switch (v) { .string => |s| s, else => { output.printWarning("malformed release metadata"); return; } };273 if (plat_info.?.object.get("checksum")) |v|274 break :blk switch (v) { .string => |s| s, else => { output.printWarning("malformed release metadata"); return; } };275 output.printWarning("missing checksum in release metadata");276 return;277 };278 const expected_hex = if (std.mem.startsWith(u8, raw_cksum, "blake3:")) raw_cksum["blake3:".len..] else raw_cksum;279280 if (ctx.cfg.confirm and !output.confirm("")) return;281282 output.updateSelfDownloading(remote_ver, version.PLATFORM);283 const tmp_path = exec.downloadToTemp(ctx.gpa, ctx.io, dl_url) catch {284 output.printWarning("download failed — check your connection and try again");285 return;286 };287 defer ctx.gpa.free(tmp_path);288289 output.updateSelfVerifying();290 exec.verifyAndInstall(ctx.gpa, ctx.io, tmp_path, expected_hex, install_path) catch |e| switch (e) {291 error.ChecksumMismatch => { output.updateSelfChecksumFailed(); return; },292 else => return e,293 };294295 output.updateSelfDone(version.VERSION, remote_ver);296}297298299pub fn goodbye(ctx: Ctx) !void {300 output.goodbyeStart();301302 var exe_buf: [std.Io.Dir.max_path_bytes]u8 = undefined;303 const install_path = exec.selfExePath(ctx.io, &exe_buf) catch {304 errors.error_info.setMessage("could not determine install path", .{});305 return error.Internal;306 };307308 if (std.mem.startsWith(u8, install_path, "/nix/store/")) {309 output.goodbyeNix();310 if (ctx.cfg.confirm and !output.confirm("")) return;311 const ec = exec.streamLocal(ctx.io, &.{ "nix", "profile", "remove", ".*nina.*" }) catch {312 output.goodbyeFailed("nix profile remove failed");313 return;314 };315 if (ec != 0) {316 output.goodbyeNixSystemManaged();317 return;318 }319 } else {320 output.goodbyeFile(install_path);321 if (ctx.cfg.confirm and !output.confirm("")) return;322 std.Io.Dir.deleteFileAbsolute(ctx.io, install_path) catch |e| {323 errors.error_info.setMessage("could not remove {s}", .{install_path});324 return e;325 };326 }327328 329 if (ctx.environ.get("HOME")) |home| {330 const nina_nix: ?[]u8 = std.fmt.allocPrint(ctx.gpa, "{s}/nina.nix", .{home}) catch null;331 if (nina_nix) |path| {332 defer ctx.gpa.free(path);333 const exists = blk: {334 const bytes = std.Io.Dir.readFileAlloc(.cwd(), ctx.io, path, ctx.gpa, .unlimited) catch break :blk false;335 ctx.gpa.free(bytes);336 break :blk true;337 };338 if (exists) {339 output.goodbyeNixFileFound(path);340 if (!ctx.cfg.confirm or output.confirm("")) {341 std.Io.Dir.deleteFileAbsolute(ctx.io, path) catch {342 output.printWarning("could not remove ~/nina.nix — remove it manually to clean up");343 };344 }345 }346 }347 }348349 output.goodbyeDone();350}351352fn channelUpdateCmd(ctx: Ctx) !void {353 output.updateStart(ctx.machine.name);354 _ = try exec.channelUpdate(ctx.io, &ctx.machine, ctx.gpa);355 output.updateDone();356}357358pub fn upgrade(ctx: Ctx) !void {359 output.upgradeStart(ctx.machine.name);360 output.printSubstep("updating channels", .{});361 _ = exec.channelUpdate(ctx.io, &ctx.machine, ctx.gpa) catch {};362 output.printSubstep("rebuilding", .{});363 const start = std.Io.Clock.awake.now(ctx.io);364 const code = try exec.nixosRebuildSwitch(ctx.io, &ctx.machine, ctx.gpa);365 if (code != 0) {366 output.applyFailed("");367 return error.BuildFailed;368 }369 const elapsed_ns = @max(0, start.untilNow(ctx.io, .awake).toNanoseconds());370 const elapsed: u64 = @intCast(@divFloor(elapsed_ns, 1_000_000));371 const gen = exec.getCurrentGeneration(ctx.gpa, ctx.io, &ctx.machine) catch 0;372 output.applyDone(gen, elapsed);373}374375pub fn log(ctx: Ctx) !void {376 output.logHeader(ctx.machine.name);377 const content = exec.readNinaLog(ctx.gpa, ctx.io, ctx.environ, ctx.last) catch {378 379 return;380 };381 defer ctx.gpa.free(content);382 var lines = std.mem.splitScalar(u8, content, '\n');383 while (lines.next()) |line| {384 const t = std.mem.trim(u8, line, " \t\r");385 if (t.len == 0) continue;386 if (t[0] == '{') {387 renderJsonLogLine(ctx.gpa, t);388 } else {389 390 var parts = std.mem.tokenizeScalar(u8, t, '\t');391 const date = parts.next() orelse continue;392 const time = parts.next() orelse "";393 const action = parts.next() orelse "";394 const detail = parts.next() orelse "";395 output.logRow(date, time, action, detail);396 }397 }398}399400401402fn renderJsonLogLine(gpa: std.mem.Allocator, line: []const u8) void {403 const parsed = std.json.parseFromSlice(std.json.Value, gpa, line, .{}) catch {404 output.logRow(line, "", "", ""); 405 return;406 };407 defer parsed.deinit();408 if (parsed.value != .object) return;409 const o = parsed.value.object;410411 const ts = jsonStr(o, "ts");412 const date = if (ts.len >= 10) ts[0..10] else ts;413 const time = if (ts.len >= 16) ts[11..16] else "";414 const action = jsonStr(o, "command");415 const outcome = jsonStr(o, "outcome");416417 var buf: [160]u8 = undefined;418 var detail: []const u8 = outcome;419 if (o.get("gen_after")) |g| {420 if (g == .integer) {421 detail = std.fmt.bufPrint(&buf, "{s} -> gen {d}", .{ outcome, g.integer }) catch outcome;422 }423 }424 output.logRow(date, time, action, detail);425}426427fn jsonStr(o: std.json.ObjectMap, key: []const u8) []const u8 {428 if (o.get(key)) |v| {429 if (v == .string) return v.string;430 }431 return "";432}433434pub fn edit(ctx: Ctx) !void {435 const hardware = ctx.args.len > 0 and std.mem.eql(u8, ctx.args[0], "hardware");436 const filename = if (hardware) "hardware-configuration.nix" else "configuration.nix";437 const path = try std.fmt.allocPrint(ctx.gpa, "{s}/{s}", .{ ctx.machine.config_path, filename });438 defer ctx.gpa.free(path);439 output.editOpen(path, ctx.cfg.editor);440 _ = try exec.openEditor(ctx.io, ctx.gpa, ctx.cfg.editor, path, null);441}442443pub fn fmt(ctx: Ctx) !void {444 const path = try std.fmt.allocPrint(ctx.gpa, "{s}/configuration.nix", .{ctx.machine.config_path});445 defer ctx.gpa.free(path);446 if (ctx.check) {447 const code = try exec.nixpkgsFmt(ctx.io, &ctx.machine, ctx.gpa, path, true);448 if (code != 0) output.fmtNeedsFormat() else output.fmtDone();449 return;450 }451 output.fmtStart(path);452 _ = try exec.nixpkgsFmt(ctx.io, &ctx.machine, ctx.gpa, path, false);453 output.fmtDone();454}455456pub fn repl(ctx: Ctx) !void {457 output.replStart();458 _ = try exec.nixRepl(ctx.io, &ctx.machine, ctx.gpa);459}460461pub fn develop(ctx: Ctx) !void {462 if (std.mem.eql(u8, ctx.sub, "show")) {463 _ = try exec.nixBuild(ctx.io, &ctx.machine, ctx.gpa, null);464 return;465 }466 const run_cmd: ?[]const u8 = if (ctx.args.len > 0 and std.mem.eql(u8, ctx.sub, "run"))467 ctx.args[0]468 else469 null;470 output.developStart(&.{});471 _ = try exec.nixDevelop(ctx.io, &ctx.machine, ctx.gpa, run_cmd);472 output.developDone();473}474475pub fn info(ctx: Ctx) !void {476 output.infoHeader(ctx.machine.name);477 const nixos_ver = exec.systemInfo(ctx.gpa, ctx.io, &ctx.machine) catch try ctx.gpa.dupe(u8, "unknown");478 defer ctx.gpa.free(nixos_ver);479 output.infoRow("nixos", std.mem.trim(u8, nixos_ver, " \t\n\r"));480481 const kernel = exec.kernelInfo(ctx.gpa, ctx.io, &ctx.machine) catch try ctx.gpa.dupe(u8, "unknown");482 defer ctx.gpa.free(kernel);483 output.infoRow("kernel", std.mem.trim(u8, kernel, " \t\n\r"));484485 const uptime = exec.uptimeInfo(ctx.gpa, ctx.io, &ctx.machine) catch try ctx.gpa.dupe(u8, "unknown");486 defer ctx.gpa.free(uptime);487 output.infoRow("uptime", std.mem.trim(u8, uptime, " \t\n\r"));488}489490pub fn boot(ctx: Ctx) !void {491 output.bootHeader(ctx.machine.name);492 const entries = exec.bootEntries(ctx.gpa, ctx.io, &ctx.machine) catch &.{};493 defer ctx.gpa.free(entries);494 for (entries, 0..) |entry, i| output.bootEntry(entry, i);495}496497pub fn searchCmd(ctx: Ctx) !void {498 const query = if (ctx.args.len > 0) ctx.args[0] else "";499 api.ensureRegistryPinned(ctx.gpa, ctx.io, ctx.environ);500 const result = try search_tui.run(ctx.gpa, ctx.io, query, .packages);501 if (result) |r| {502 installFromResult(ctx, r) catch |e| return e;503 }504}505506pub fn optionCmd(ctx: Ctx) !void {507 const query = if (ctx.args.len > 0) ctx.args[0] else "";508 api.ensureRegistryPinned(ctx.gpa, ctx.io, ctx.environ);509 _ = try search_tui.run(ctx.gpa, ctx.io, query, .options);510}511512pub fn install(ctx: Ctx) !void {513 const pkg_name = if (ctx.args.len > 0) ctx.args[0] else return error.PackageNotFound;514515 api.ensureRegistryPinned(ctx.gpa, ctx.io, ctx.environ);516517 518 const pkgs = api.searchPackages(ctx.gpa, ctx.io, pkg_name) catch {519 errors.error_info.setMessage("'{s}' not found in nixpkgs", .{pkg_name});520 errors.error_info.setSuggestion("nina search {s}", .{pkg_name});521 return error.PackageNotFound;522 };523 defer ctx.gpa.free(pkgs);524525 if (pkgs.len == 0) {526 errors.error_info.setMessage("'{s}' not found in nixpkgs", .{pkg_name});527 errors.error_info.setSuggestion("nina search {s}", .{pkg_name});528 return error.PackageNotFound;529 }530531 532 533 var found = pkgs[0];534 for (pkgs) |p| {535 if (std.mem.eql(u8, p.pname, pkg_name)) {536 found = p;537 break;538 }539 }540 output.installSelectPrompt(found.pname, found.version);541542 var choice_buf: [8]u8 = undefined;543 var choice_len: usize = 0;544 while (choice_len < choice_buf.len) {545 const n = std.posix.read(std.posix.STDIN_FILENO, choice_buf[choice_len .. choice_len + 1]) catch break;546 if (n == 0) break;547 if (choice_buf[choice_len] == '\n') break;548 choice_len += 1;549 }550 const choice = std.mem.trim(u8, choice_buf[0..choice_len], " \t");551552 if (choice.len == 0 or choice[0] == 'i' or choice[0] == 'I') {553 try profileInstallPkg(ctx, found);554 } else if (choice[0] == 's' or choice[0] == 'S') {555 try systemInstallPkg(ctx, found);556 } else if (choice[0] == 't' or choice[0] == 'T') {557 try tryPkg(ctx, found);558 }559}560561fn installFromResult(ctx: Ctx, result: types.SearchResult) !void {562 switch (result.action) {563 .profile => try profileInstallPkg(ctx, result.package),564 .system => try systemInstallPkg(ctx, result.package),565 }566}567568fn profileInstallPkg(ctx: Ctx, npkg: types.NixPackage) !void {569 output.installStart(npkg.pname, .profile);570 output.printSubstep("nix profile install nixpkgs#{s}", .{npkg.attr});571 output.printTeach(try std.fmt.allocPrint(ctx.gpa, "nix profile install nixpkgs#{s}", .{npkg.attr}));572 const code = try exec.profileInstall(ctx.io, &ctx.machine, ctx.gpa, npkg.attr);573 if (code != 0) return error.ProcessFailed;574 output.installDone(npkg.pname);575}576577fn systemInstallPkg(ctx: Ctx, npkg: types.NixPackage) !void {578 const config_path = try std.fmt.allocPrint(ctx.gpa, "{s}/configuration.nix", .{ctx.machine.config_path});579 defer ctx.gpa.free(config_path);580 const line = api.findSystemPackagesLine(ctx.io, config_path) orelse 1;581 output.installEditorPrompt(npkg.pname, config_path, line, ctx.cfg.editor);582 _ = try exec.openEditor(ctx.io, ctx.gpa, ctx.cfg.editor, config_path, line);583 if (ctx.cfg.confirm) {584 if (output.confirm("apply now? ")) {585 try apply(ctx);586 }587 }588}589590fn tryPkg(ctx: Ctx, npkg: types.NixPackage) !void {591 output.tryStart(npkg.pname);592 output.printSubstep("nix shell nixpkgs#{s}", .{npkg.attr});593 _ = try exec.nixShell(ctx.io, &ctx.machine, ctx.gpa, npkg.attr);594 output.tryDone();595}596597pub fn remove(ctx: Ctx) !void {598 const pkg_name = if (ctx.args.len > 0) ctx.args[0] else return error.PackageNotFound;599 output.removeStart(pkg_name);600 const code = try exec.profileRemove(ctx.io, &ctx.machine, ctx.gpa, pkg_name);601 if (code != 0) return error.ProcessFailed;602 output.done();603}604605pub fn list(ctx: Ctx) !void {606 output.listHeader();607 const profile_out = exec.profileList(ctx.gpa, ctx.io, &ctx.machine) catch null;608 if (profile_out) |out| {609 defer ctx.gpa.free(out);610 output.listSection("profile");611 var lines = std.mem.splitScalar(u8, out, '\n');612 while (lines.next()) |line| {613 const t = std.mem.trim(u8, line, " \t\r");614 if (t.len == 0) continue;615 output.listPkg(t, "");616 }617 }618}619620pub fn tryCmd(ctx: Ctx) !void {621 const pkg_name_arg = if (ctx.args.len > 0) ctx.args[0] else return error.PackageNotFound;622 const shell_pkg = types.NixPackage{ .attr = pkg_name_arg, .pname = pkg_name_arg, .version = "", .description = "" };623 try tryPkg(ctx, shell_pkg);624}625626627628pub fn service(ctx: Ctx) !void {629 if (std.mem.eql(u8, ctx.sub, "list") or ctx.sub.len == 0) {630 output.serviceListHeader(ctx.machine.name);631 const svcs = exec.serviceList(ctx.gpa, ctx.io, &ctx.machine) catch &.{};632 defer ctx.gpa.free(svcs);633 for (svcs) |s| output.serviceRow(s);634 return;635 }636 if (std.mem.eql(u8, ctx.sub, "logs")) {637 const name = if (ctx.args.len > 0) ctx.args[0] else return error.Internal;638 const follow = ctx.args.len > 1 and std.mem.eql(u8, ctx.args[1], "-f");639 output.serviceLogsHeader(name, ctx.last, ctx.machine.name);640 _ = try exec.serviceLogs(ctx.io, &ctx.machine, ctx.gpa, name, ctx.last, follow);641 return;642 }643 if (std.mem.eql(u8, ctx.sub, "status")) {644 const name = if (ctx.args.len > 0) ctx.args[0] else return error.Internal;645 output.serviceVerb("status", name, ctx.machine.name);646 _ = try exec.serviceCtl(ctx.io, &ctx.machine, ctx.gpa, "status", name);647 return;648 }649 650 const name = if (ctx.args.len > 0) ctx.args[0] else return error.Internal;651 if (ctx.cfg.confirm and (std.mem.eql(u8, ctx.sub, "stop") or std.mem.eql(u8, ctx.sub, "disable"))) {652 if (!output.confirm("")) return;653 }654 output.serviceVerb(ctx.sub, name, ctx.machine.name);655 _ = try exec.serviceCtl(ctx.io, &ctx.machine, ctx.gpa, ctx.sub, name);656 output.serviceDone();657}658659660661pub fn flake(ctx: Ctx) !void {662 if (std.mem.eql(u8, ctx.sub, "update")) {663 output.flakeUpdateStart();664 const input: ?[]const u8 = if (ctx.args.len > 0) ctx.args[0] else null;665 _ = try exec.flakeUpdate(ctx.io, &ctx.machine, ctx.gpa, input);666 output.flakeLockWritten();667 return;668 }669 if (std.mem.eql(u8, ctx.sub, "check")) {670 output.flakeCheckStart();671 const code = try exec.flakeCheck(ctx.io, &ctx.machine, ctx.gpa);672 if (code == 0) output.flakeValid() else output.checkFailed("");673 return;674 }675 if (std.mem.eql(u8, ctx.sub, "show")) {676 const json_out = exec.flakeShow(ctx.gpa, ctx.io, &ctx.machine) catch null;677 if (json_out) |j| {678 defer ctx.gpa.free(j);679 output.flakeShowHeader("flake outputs");680 681 const parsed = std.json.parseFromSlice(std.json.Value, ctx.gpa, j, .{}) catch return;682 defer parsed.deinit();683 if (parsed.value == .object) {684 var it = parsed.value.object.iterator();685 while (it.next()) |entry| {686 if (entry.value_ptr.* == .object) {687 var inner = entry.value_ptr.object.iterator();688 while (inner.next()) |e2| {689 const desc = if (e2.value_ptr.* == .object)690 if (e2.value_ptr.object.get("description")) |d|691 if (d == .string) d.string else ""692 else ""693 else "";694 output.flakeShowEntry(entry.key_ptr.*, e2.key_ptr.*, desc);695 }696 }697 }698 }699 }700 return;701 }702 if (std.mem.eql(u8, ctx.sub, "init")) {703 _ = try exec.flakeInit(ctx.io, &ctx.machine, ctx.gpa);704 return;705 }706 if (std.mem.eql(u8, ctx.sub, "lock")) {707 _ = try exec.flakeLock(ctx.io, &ctx.machine, ctx.gpa);708 return;709 }710 if (std.mem.eql(u8, ctx.sub, "clone")) {711 const url = if (ctx.args.len > 0) ctx.args[0] else return error.Internal;712 _ = try exec.flakeClone(ctx.io, &ctx.machine, ctx.gpa, url);713 return;714 }715}716717718719pub fn store(ctx: Ctx) !void {720 if (std.mem.eql(u8, ctx.sub, "gc")) {721 _ = try exec.storeGc(ctx.io, &ctx.machine, ctx.gpa);722 return;723 }724 if (std.mem.eql(u8, ctx.sub, "verify")) {725 output.storeVerifyStart();726 _ = try exec.storeVerify(ctx.io, &ctx.machine, ctx.gpa);727 return;728 }729 if (std.mem.eql(u8, ctx.sub, "repair")) {730 if (ctx.cfg.confirm and !output.confirm("")) return;731 output.storeRepairStart();732 _ = try exec.storeRepair(ctx.io, &ctx.machine, ctx.gpa);733 return;734 }735 if (std.mem.eql(u8, ctx.sub, "info")) {736 const si = try exec.storeInfo(ctx.gpa, ctx.io, &ctx.machine);737 output.storeInfo(si);738 return;739 }740 if (std.mem.eql(u8, ctx.sub, "path")) {741 const attr = if (ctx.args.len > 0) ctx.args[0] else return error.Internal;742 const path = try exec.storePath(ctx.gpa, ctx.io, &ctx.machine, attr);743 defer ctx.gpa.free(path);744 output.storePath(path);745 return;746 }747 748 const si = try exec.storeInfo(ctx.gpa, ctx.io, &ctx.machine);749 output.storeInfo(si);750}751752753754pub fn channel(ctx: Ctx) !void {755 if (std.mem.eql(u8, ctx.sub, "list") or ctx.sub.len == 0) {756 output.channelListHeader(ctx.machine.name);757 const chans = try exec.channelList(ctx.gpa, ctx.io, &ctx.machine);758 defer ctx.gpa.free(chans);759 for (chans) |ch| output.channelRow(ch);760 return;761 }762 if (std.mem.eql(u8, ctx.sub, "add")) {763 const url = if (ctx.args.len > 0) ctx.args[0] else return error.Internal;764 const name = if (ctx.args.len > 1) ctx.args[1] else "";765 _ = try exec.channelAdd(ctx.io, &ctx.machine, ctx.gpa, url, name);766 output.channelDone();767 return;768 }769 if (std.mem.eql(u8, ctx.sub, "remove")) {770 const name = if (ctx.args.len > 0) ctx.args[0] else return error.Internal;771 _ = try exec.channelRemove(ctx.io, &ctx.machine, ctx.gpa, name);772 output.channelDone();773 return;774 }775 if (std.mem.eql(u8, ctx.sub, "update")) {776 return channelUpdateCmd(ctx);777 }778}779780781782pub fn profile(ctx: Ctx) !void {783 if (std.mem.eql(u8, ctx.sub, "list") or ctx.sub.len == 0) {784 output.profileListHeader();785 const out = exec.profileList(ctx.gpa, ctx.io, &ctx.machine) catch null;786 if (out) |o| {787 defer ctx.gpa.free(o);788 var lines = std.mem.splitScalar(u8, o, '\n');789 while (lines.next()) |line| {790 const t = std.mem.trim(u8, line, " \t\r");791 if (t.len == 0) continue;792 output.profileRow(t, "", "");793 }794 }795 return;796 }797 if (std.mem.eql(u8, ctx.sub, "install")) {798 const attr = if (ctx.args.len > 0) ctx.args[0] else return error.PackageNotFound;799 _ = try exec.profileInstall(ctx.io, &ctx.machine, ctx.gpa, attr);800 output.done();801 return;802 }803 if (std.mem.eql(u8, ctx.sub, "remove")) {804 const attr = if (ctx.args.len > 0) ctx.args[0] else return error.PackageNotFound;805 _ = try exec.profileRemove(ctx.io, &ctx.machine, ctx.gpa, attr);806 output.done();807 return;808 }809 if (std.mem.eql(u8, ctx.sub, "upgrade")) {810 _ = try exec.profileUpgrade(ctx.io, &ctx.machine, ctx.gpa);811 output.done();812 return;813 }814}815816817818pub fn pkg(ctx: Ctx) !void {819 const name = if (ctx.args.len > 0) ctx.args[0] else return error.PackageNotFound;820 if (std.mem.eql(u8, ctx.sub, "why")) {821 _ = try exec.pkgWhy(ctx.io, &ctx.machine, ctx.gpa, name);822 return;823 }824 if (std.mem.eql(u8, ctx.sub, "deps")) {825 _ = try exec.pkgDeps(ctx.io, &ctx.machine, ctx.gpa, name);826 return;827 }828 if (std.mem.eql(u8, ctx.sub, "size")) {829 const s = exec.pkgSize(ctx.gpa, ctx.io, &ctx.machine, name) catch null;830 if (s) |sz| {831 defer ctx.gpa.free(sz);832 output.pkgResult("size", sz);833 }834 return;835 }836 if (std.mem.eql(u8, ctx.sub, "path")) {837 const p = try exec.storePath(ctx.gpa, ctx.io, &ctx.machine, name);838 defer ctx.gpa.free(p);839 output.pkgResult("path", p);840 return;841 }842 if (std.mem.eql(u8, ctx.sub, "closure")) {843 _ = try exec.pkgClosure(ctx.io, &ctx.machine, ctx.gpa, name);844 return;845 }846}847848849850pub fn pin(ctx: Ctx) !void {851 const input = if (ctx.args.len > 0) ctx.args[0] else return error.Internal;852 const commit = if (ctx.args.len > 1) ctx.args[1] else return error.Internal;853 exec.editFlakeLock(ctx.gpa, input, commit) catch return error.ProcessFailed;854 output.pinDone(input, commit);855}856857pub fn unpin(ctx: Ctx) !void {858 const input = if (ctx.args.len > 0) ctx.args[0] else return error.Internal;859 _ = try exec.flakeUpdate(ctx.io, &ctx.machine, ctx.gpa, input);860 output.unpinDone(input);861}862863864865pub fn hash(ctx: Ctx) !void {866 const path = if (ctx.args.len > 0) ctx.args[0] else return error.Internal;867 const h = try exec.nixHash(ctx.gpa, ctx.io, &ctx.machine, path);868 defer ctx.gpa.free(h);869 output.hashResult(std.mem.trim(u8, h, " \t\n\r"));870}871872pub fn fetch(ctx: Ctx) !void {873 const url = if (ctx.args.len > 0) ctx.args[0] else return error.Internal;874 const h = try exec.nixFetch(ctx.gpa, ctx.io, &ctx.machine, url);875 defer ctx.gpa.free(h);876 output.fetchResult(std.mem.trim(u8, h, " \t\n\r"), url);877}878879880881pub fn build(ctx: Ctx) !void {882 const attr: ?[]const u8 = if (ctx.args.len > 0) ctx.args[0] else null;883 output.buildStart(attr orelse "default");884 const code = try exec.nixBuild(ctx.io, &ctx.machine, ctx.gpa, attr);885 if (code != 0) return error.BuildFailed;886 output.buildDone();887}888889pub fn runCmd(ctx: Ctx) !void {890 const pkg_arg = if (ctx.args.len > 0) ctx.args[0] else return error.PackageNotFound;891 output.runStart(pkg_arg);892 _ = try exec.nixRun(ctx.io, &ctx.machine, ctx.gpa, pkg_arg);893}894895896897pub fn hello(ctx: Ctx) !void {898 output.hello(ctx.cfg.machines);899}900901pub fn setup(gpa: std.mem.Allocator, io: std.Io, environ: *const std.process.Environ.Map) !void {902 output.setupCheck();903 const status = exec.checkNixFeatures(gpa, io, environ);904 output.setupFeatures(status.nix_command, status.flakes);905906 if (status.allEnabled()) {907 output.setupAllSet();908 api.ensureRegistryPinned(gpa, io, environ);909 return;910 }911912 output.setupEnabling();913 const path = try exec.enableNixFeatures(gpa, io, environ);914 defer gpa.free(path);915916 output.setupDone(path, exec.isNixOS(io));917 api.ensureRegistryPinned(gpa, io, environ);918}919920921922fn logAction(ctx: Ctx, action: []const u8, gen: u32) void {923 var ts_buf: [32]u8 = undefined;924 const ts = nowIso(ctx.io, &ts_buf);925 var buf: [256]u8 = undefined;926 const entry = std.fmt.bufPrint(&buf, "{{\"ts\":\"{s}\",\"machine\":\"{s}\",\"command\":\"{s}\",\"outcome\":\"success\",\"gen_before\":null,\"gen_after\":{d},\"duration_ms\":0}}\n", .{927 ts, ctx.machine.name, action, gen,928 }) catch return;929 exec.appendNinaLog(ctx.io, ctx.environ, entry);930}931932933934fn nowIso(io: std.Io, buf: []u8) []const u8 {935 const fallback = "1970-01-01T00:00:00Z";936 const ns = std.Io.Clock.real.now(io).toNanoseconds();937 if (ns <= 0) return fallback;938 const secs: u64 = @intCast(@divFloor(ns, 1_000_000_000));939 const es = std.time.epoch.EpochSeconds{ .secs = secs };940 const ed = es.getEpochDay();941 const yd = ed.calculateYearDay();942 const md = yd.calculateMonthDay();943 const ds = es.getDaySeconds();944 return std.fmt.bufPrint(buf, "{d:0>4}-{d:0>2}-{d:0>2}T{d:0>2}:{d:0>2}:{d:0>2}Z", .{945 yd.year, md.month.numeric(), @as(u32, md.day_index) + 1,946 ds.getHoursIntoDay(), ds.getMinutesIntoHour(), ds.getSecondsIntoMinute(),947 }) catch return fallback;948}