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 exec.markApplyFailed(ctx.io, ctx.environ);49 output.applyFailed("");50 return e;51 };5253 if (code != 0) {54 exec.markApplyFailed(ctx.io, ctx.environ);55 output.applyFailed("");56 return error.BuildFailed;57 }5859 const elapsed_ns = @max(0, start.untilNow(ctx.io, .awake).toNanoseconds());60 const elapsed: u64 = @intCast(@divFloor(elapsed_ns, 1_000_000));61 const gen = exec.getCurrentGeneration(ctx.gpa, ctx.io, &ctx.machine) catch 0;62 const after_failure = exec.takeApplyFailed(ctx.io, ctx.environ);63 output.applyDone(gen, elapsed, after_failure);64 logAction(ctx, "apply", gen);65}6667pub fn check(ctx: Ctx) !void {68 output.checkStart(ctx.machine.name);69 const code = try exec.nixosRebuildBuild(ctx.io, &ctx.machine, ctx.gpa);70 if (code != 0) {71 output.checkFailed("");72 return error.BuildFailed;73 }74 output.checkOk();75}7677pub fn back(ctx: Ctx) !void {78 const gens = try exec.getGenerations(ctx.gpa, ctx.io, &ctx.machine);79 defer ctx.gpa.free(gens);8081 var current: u32 = 0;82 for (gens) |g| if (g.current) { current = g.number; };83 const prev = if (current > 1) current - 1 else current;8485 output.backStart(ctx.machine.name, current, prev);86 if (ctx.cfg.confirm and !output.confirm("")) return;8788 const code = try exec.rollback(ctx.io, &ctx.machine, ctx.gpa);89 if (code != 0) return error.BuildFailed;90 output.backDone(prev);91}9293pub fn go(ctx: Ctx) !void {94 if (ctx.args.len == 0) return error.Internal;95 const target = std.fmt.parseInt(u32, ctx.args[0], 10) catch return error.Internal;96 const current = try exec.getCurrentGeneration(ctx.gpa, ctx.io, &ctx.machine);9798 output.goStart(ctx.machine.name, current, target);99 if (ctx.cfg.confirm and !output.confirm("")) return;100101 const code = try exec.switchGeneration(ctx.io, &ctx.machine, ctx.gpa, target);102 if (code != 0) return error.ProcessFailed;103 output.generationDone(target);104}105106pub fn history(ctx: Ctx) !void {107 const gens = try exec.getGenerations(ctx.gpa, ctx.io, &ctx.machine);108 defer ctx.gpa.free(gens);109 output.historyHeader(ctx.machine.name);110 for (gens) |g| output.historyRow(g);111}112113pub fn genCmd(ctx: Ctx) !void {114 if (std.mem.eql(u8, ctx.sub, "list")) return history(ctx);115 if (std.mem.eql(u8, ctx.sub, "current")) {116 const n = try exec.getCurrentGeneration(ctx.gpa, ctx.io, &ctx.machine);117 output.genCurrent(n);118 return;119 }120 if (std.mem.eql(u8, ctx.sub, "delete")) {121 if (ctx.args.len == 0) return error.Internal;122 if (std.mem.eql(u8, ctx.args[0], "old")) {123 if (ctx.cfg.confirm and !output.confirm("")) return;124 _ = try exec.deleteOldGenerations(ctx.io, &ctx.machine, ctx.gpa);125 } else {126 const n = std.fmt.parseInt(u32, ctx.args[0], 10) catch return error.Internal;127 if (ctx.cfg.confirm and !output.confirm("")) return;128 _ = try exec.deleteGeneration(ctx.io, &ctx.machine, ctx.gpa, n);129 }130 output.done();131 return;132 }133 return error.Internal;134}135136pub fn clean(ctx: Ctx) !void {137 const gens = try exec.getGenerations(ctx.gpa, ctx.io, &ctx.machine);138 defer ctx.gpa.free(gens);139 output.cleanStart(ctx.machine.name, ctx.cfg.generations, @intCast(gens.len));140 if (ctx.cfg.confirm and !output.confirm("removing — cannot be undone")) return;141 const all = ctx.all or std.mem.eql(u8, ctx.sub, "all");142 _ = try exec.collectGarbage(ctx.io, &ctx.machine, ctx.gpa, all, ctx.cfg.generations);143 output.cleanDone("?");144}145146pub fn statusCmd(ctx: Ctx) !void {147 output.statusHeader();148 const gen = exec.getCurrentGeneration(ctx.gpa, ctx.io, &ctx.machine) catch 0;149 const st = exec.systemStatus(ctx.gpa, ctx.io, &ctx.machine);150 defer ctx.gpa.free(st.size);151 defer ctx.gpa.free(st.age);152 output.statusRow(ctx.machine.name, gen, st.age, st.size, true);153}154155pub fn diff(ctx: Ctx) !void {156 const gens = try exec.getGenerations(ctx.gpa, ctx.io, &ctx.machine);157 defer ctx.gpa.free(gens);158 var current: u32 = 0;159 var prev: u32 = 0;160 for (gens) |g| {161 if (g.current) current = g.number;162 if (!g.current and g.number > prev and g.number < current) prev = g.number;163 }164 if (ctx.args.len >= 2) {165 const a = std.fmt.parseInt(u32, ctx.args[0], 10) catch current - 1;166 const b = std.fmt.parseInt(u32, ctx.args[1], 10) catch current;167 output.diffHeader(a, b, ctx.machine.name);168 if (try exec.diffClosures(ctx.io, &ctx.machine, ctx.gpa, a, b) != 0) return error.ProcessFailed;169 } else {170 output.diffHeader(if (prev > 0) prev else current - 1, current, ctx.machine.name);171 if (try exec.diffClosures(ctx.io, &ctx.machine, ctx.gpa, if (prev > 0) prev else current - 1, current) != 0) return error.ProcessFailed;172 }173}174175pub fn mood(ctx: Ctx) !void {176 const gen = exec.getCurrentGeneration(ctx.gpa, ctx.io, &ctx.machine) catch 0;177 const st = exec.systemStatus(ctx.gpa, ctx.io, &ctx.machine);178 defer ctx.gpa.free(st.size);179 defer ctx.gpa.free(st.age);180 const state = exec.systemRunningState(ctx.gpa, ctx.io, &ctx.machine) catch try ctx.gpa.dupe(u8, "unknown");181 defer ctx.gpa.free(state);182 output.mood(ctx.machine.name, state, gen, st.age, st.size);183}184185pub fn doctor(ctx: Ctx) !void {186 output.doctorHeader(ctx.machine.name);187 const checks = try exec.runDoctorChecks(ctx.gpa, ctx.io, &ctx.machine);188 defer ctx.gpa.free(checks);189 var warnings: u32 = 0;190 var failures: u32 = 0;191 for (checks) |ch| {192 output.doctorRow(ch);193 if (ch.status == .warn) warnings += 1;194 if (ch.status == .fail) failures += 1;195 }196 output.doctorSummary(warnings, failures);197}198199200pub fn update(ctx: Ctx) !void {201 output.updateSelfStart();202203 if (comptime std.mem.eql(u8, version.PLATFORM, "unsupported")) {204 output.updateSelfNoPlatform(@tagName(builtin.cpu.arch) ++ "-" ++ @tagName(builtin.os.tag));205 return error.Internal;206 }207208 const env_url = ctx.environ.get("NINA_UPDATE_SOURCE");209 const source_url = env_url orelse UPDATE_URL;210211 const body = exec.fetchUrl(ctx.gpa, ctx.io, source_url) catch |e| switch (e) {212 error.NetworkError, error.HttpError => {213 output.updateSelfNetworkError();214 return;215 },216 else => return e,217 };218 defer ctx.gpa.free(body);219220 const parsed = std.json.parseFromSlice(std.json.Value, ctx.gpa, body, .{}) catch {221 output.printWarning("could not parse release metadata");222 return;223 };224 defer parsed.deinit();225226 const root = parsed.value;227 if (root != .object) { output.printWarning("malformed release metadata"); return; }228229 const remote_ver = blk: {230 const v = root.object.get("version") orelse { output.printWarning("malformed release metadata"); return; };231 break :blk switch (v) { .string => |s| s, else => { output.printWarning("malformed release metadata"); return; } };232 };233234 if (std.mem.eql(u8, remote_ver, version.VERSION)) {235 output.updateSelfCurrent(version.VERSION);236 return;237 }238239 output.updateSelfAvailable(version.VERSION, remote_ver);240 if (ctx.check) return;241242 var exe_buf: [std.Io.Dir.max_path_bytes]u8 = undefined;243 const install_path = exec.selfExePath(ctx.io, &exe_buf) catch {244 errors.error_info.setMessage("could not determine install path", .{});245 return error.Internal;246 };247248 249 250 if (std.mem.startsWith(u8, install_path, "/nix/store/")) {251 output.updateSelfNixManaged(remote_ver);252 if (ctx.cfg.confirm and !output.confirm("")) return;253 const ec = exec.streamLocal(ctx.io, &.{ "nix", "profile", "upgrade", ".*nina.*" }) catch {254 output.updateSelfNixFailed();255 return;256 };257 if (ec != 0) output.updateSelfNixSystemManaged();258 return;259 }260261 const plat_info = blk: {262 const pv = root.object.get("platforms") orelse break :blk null;263 if (pv != .object) break :blk null;264 break :blk pv.object.get(version.PLATFORM);265 };266 if (plat_info == null) { output.updateSelfNoPlatform(version.PLATFORM); return; }267268 const dl_url = blk: {269 const v = plat_info.?.object.get("url") orelse { output.printWarning("malformed release metadata"); return; };270 break :blk switch (v) { .string => |s| s, else => { output.printWarning("malformed release metadata"); return; } };271 };272273 const raw_cksum = blk: {274 if (plat_info.?.object.get("blake3")) |v|275 break :blk switch (v) { .string => |s| s, else => { output.printWarning("malformed release metadata"); return; } };276 if (plat_info.?.object.get("checksum")) |v|277 break :blk switch (v) { .string => |s| s, else => { output.printWarning("malformed release metadata"); return; } };278 output.printWarning("missing checksum in release metadata");279 return;280 };281 const expected_hex = if (std.mem.startsWith(u8, raw_cksum, "blake3:")) raw_cksum["blake3:".len..] else raw_cksum;282283 if (ctx.cfg.confirm and !output.confirm("")) return;284285 output.updateSelfDownloading(remote_ver, version.PLATFORM);286 const tmp_path = exec.downloadToTemp(ctx.gpa, ctx.io, dl_url) catch {287 output.printWarning("download failed — check your connection and try again");288 return;289 };290 defer ctx.gpa.free(tmp_path);291292 output.updateSelfVerifying();293 exec.verifyAndInstall(ctx.gpa, ctx.io, tmp_path, expected_hex, install_path) catch |e| switch (e) {294 error.ChecksumMismatch => { output.updateSelfChecksumFailed(); return; },295 else => return e,296 };297298 output.updateSelfDone(version.VERSION, remote_ver);299}300301302pub fn goodbye(ctx: Ctx) !void {303 output.goodbyeStart();304305 var exe_buf: [std.Io.Dir.max_path_bytes]u8 = undefined;306 const install_path = exec.selfExePath(ctx.io, &exe_buf) catch {307 errors.error_info.setMessage("could not determine install path", .{});308 return error.Internal;309 };310311 if (std.mem.startsWith(u8, install_path, "/nix/store/")) {312 output.goodbyeNix();313 if (ctx.cfg.confirm and !output.confirm("")) return;314 const ec = exec.streamLocal(ctx.io, &.{ "nix", "profile", "remove", ".*nina.*" }) catch {315 output.goodbyeFailed("nix profile remove failed");316 return;317 };318 if (ec != 0) {319 output.goodbyeNixSystemManaged();320 return;321 }322 } else {323 output.goodbyeFile(install_path);324 if (ctx.cfg.confirm and !output.confirm("")) return;325 std.Io.Dir.deleteFileAbsolute(ctx.io, install_path) catch |e| {326 errors.error_info.setMessage("could not remove {s}", .{install_path});327 return e;328 };329 }330331 332 if (ctx.environ.get("HOME")) |home| {333 const nina_nix: ?[]u8 = std.fmt.allocPrint(ctx.gpa, "{s}/nina.nix", .{home}) catch null;334 if (nina_nix) |path| {335 defer ctx.gpa.free(path);336 const exists = blk: {337 const bytes = std.Io.Dir.readFileAlloc(.cwd(), ctx.io, path, ctx.gpa, .unlimited) catch break :blk false;338 ctx.gpa.free(bytes);339 break :blk true;340 };341 if (exists) {342 output.goodbyeNixFileFound(path);343 if (!ctx.cfg.confirm or output.confirm("")) {344 std.Io.Dir.deleteFileAbsolute(ctx.io, path) catch {345 output.printWarning("could not remove ~/nina.nix — remove it manually to clean up");346 };347 }348 }349 }350 }351352 output.goodbyeDone();353}354355fn channelUpdateCmd(ctx: Ctx) !void {356 output.updateStart(ctx.machine.name);357 _ = try exec.channelUpdate(ctx.io, &ctx.machine, ctx.gpa);358 output.updateDone();359}360361pub fn upgrade(ctx: Ctx) !void {362 output.upgradeStart(ctx.machine.name);363 output.printSubstep("updating channels", .{});364 _ = exec.channelUpdate(ctx.io, &ctx.machine, ctx.gpa) catch {};365 output.printSubstep("rebuilding", .{});366 const start = std.Io.Clock.awake.now(ctx.io);367 const code = try exec.nixosRebuildSwitch(ctx.io, &ctx.machine, ctx.gpa);368 if (code != 0) {369 output.applyFailed("");370 return error.BuildFailed;371 }372 const elapsed_ns = @max(0, start.untilNow(ctx.io, .awake).toNanoseconds());373 const elapsed: u64 = @intCast(@divFloor(elapsed_ns, 1_000_000));374 const gen = exec.getCurrentGeneration(ctx.gpa, ctx.io, &ctx.machine) catch 0;375 output.applyDone(gen, elapsed, false);376}377378pub fn log(ctx: Ctx) !void {379 output.logHeader(ctx.machine.name);380 const content = exec.readNinaLog(ctx.gpa, ctx.io, ctx.environ, ctx.last) catch {381 382 return;383 };384 defer ctx.gpa.free(content);385 var lines = std.mem.splitScalar(u8, content, '\n');386 while (lines.next()) |line| {387 const t = std.mem.trim(u8, line, " \t\r");388 if (t.len == 0) continue;389 if (t[0] == '{') {390 renderJsonLogLine(ctx.gpa, t);391 } else {392 393 var parts = std.mem.tokenizeScalar(u8, t, '\t');394 const date = parts.next() orelse continue;395 const time = parts.next() orelse "";396 const action = parts.next() orelse "";397 const detail = parts.next() orelse "";398 output.logRow(date, time, action, detail);399 }400 }401}402403404405fn renderJsonLogLine(gpa: std.mem.Allocator, line: []const u8) void {406 const parsed = std.json.parseFromSlice(std.json.Value, gpa, line, .{}) catch {407 output.logRow(line, "", "", ""); 408 return;409 };410 defer parsed.deinit();411 if (parsed.value != .object) return;412 const o = parsed.value.object;413414 const ts = jsonStr(o, "ts");415 const date = if (ts.len >= 10) ts[0..10] else ts;416 const time = if (ts.len >= 16) ts[11..16] else "";417 const action = jsonStr(o, "command");418 const outcome = jsonStr(o, "outcome");419420 var buf: [160]u8 = undefined;421 var detail: []const u8 = outcome;422 if (o.get("gen_after")) |g| {423 if (g == .integer) {424 detail = std.fmt.bufPrint(&buf, "{s} -> gen {d}", .{ outcome, g.integer }) catch outcome;425 }426 }427 output.logRow(date, time, action, detail);428}429430fn jsonStr(o: std.json.ObjectMap, key: []const u8) []const u8 {431 if (o.get(key)) |v| {432 if (v == .string) return v.string;433 }434 return "";435}436437pub fn edit(ctx: Ctx) !void {438 const hardware = ctx.args.len > 0 and std.mem.eql(u8, ctx.args[0], "hardware");439 const filename = if (hardware) "hardware-configuration.nix" else "configuration.nix";440 const path = try std.fmt.allocPrint(ctx.gpa, "{s}/{s}", .{ ctx.machine.config_path, filename });441 defer ctx.gpa.free(path);442 output.editOpen(path, ctx.cfg.editor);443 _ = try exec.openEditor(ctx.io, ctx.gpa, ctx.cfg.editor, path, null);444}445446pub fn fmt(ctx: Ctx) !void {447 const path = try std.fmt.allocPrint(ctx.gpa, "{s}/configuration.nix", .{ctx.machine.config_path});448 defer ctx.gpa.free(path);449 if (ctx.check) {450 const code = try exec.nixpkgsFmt(ctx.io, &ctx.machine, ctx.gpa, path, true);451 if (code != 0) output.fmtNeedsFormat() else output.fmtDone();452 return;453 }454 output.fmtStart(path);455 _ = try exec.nixpkgsFmt(ctx.io, &ctx.machine, ctx.gpa, path, false);456 output.fmtDone();457}458459pub fn repl(ctx: Ctx) !void {460 output.replStart();461 _ = try exec.nixRepl(ctx.io, &ctx.machine, ctx.gpa);462}463464pub fn develop(ctx: Ctx) !void {465 if (std.mem.eql(u8, ctx.sub, "show")) {466 _ = try exec.nixBuild(ctx.io, &ctx.machine, ctx.gpa, null);467 return;468 }469 const run_cmd: ?[]const u8 = if (ctx.args.len > 0 and std.mem.eql(u8, ctx.sub, "run"))470 ctx.args[0]471 else472 null;473 output.developStart(&.{});474 _ = try exec.nixDevelop(ctx.io, &ctx.machine, ctx.gpa, run_cmd);475 output.developDone();476}477478pub fn info(ctx: Ctx) !void {479 output.infoHeader(ctx.machine.name);480 const nixos_ver = exec.systemInfo(ctx.gpa, ctx.io, &ctx.machine) catch try ctx.gpa.dupe(u8, "unknown");481 defer ctx.gpa.free(nixos_ver);482 output.infoRow("nixos", std.mem.trim(u8, nixos_ver, " \t\n\r"));483484 const kernel = exec.kernelInfo(ctx.gpa, ctx.io, &ctx.machine) catch try ctx.gpa.dupe(u8, "unknown");485 defer ctx.gpa.free(kernel);486 output.infoRow("kernel", std.mem.trim(u8, kernel, " \t\n\r"));487488 const uptime = exec.uptimeInfo(ctx.gpa, ctx.io, &ctx.machine) catch try ctx.gpa.dupe(u8, "unknown");489 defer ctx.gpa.free(uptime);490 output.infoRow("uptime", std.mem.trim(u8, uptime, " \t\n\r"));491}492493pub fn boot(ctx: Ctx) !void {494 output.bootHeader(ctx.machine.name);495 const entries = exec.bootEntries(ctx.gpa, ctx.io, &ctx.machine) catch &.{};496 defer ctx.gpa.free(entries);497 for (entries, 0..) |entry, i| output.bootEntry(entry, i);498}499500pub fn searchCmd(ctx: Ctx) !void {501 const query = if (ctx.args.len > 0) ctx.args[0] else "";502 api.ensureRegistryPinned(ctx.gpa, ctx.io, ctx.environ);503 const result = try search_tui.run(ctx.gpa, ctx.io, query, .packages);504 if (result) |r| {505 installFromResult(ctx, r) catch |e| return e;506 }507}508509pub fn optionCmd(ctx: Ctx) !void {510 const query = if (ctx.args.len > 0) ctx.args[0] else "";511 api.ensureRegistryPinned(ctx.gpa, ctx.io, ctx.environ);512 _ = try search_tui.run(ctx.gpa, ctx.io, query, .options);513}514515pub fn install(ctx: Ctx) !void {516 const pkg_name = if (ctx.args.len > 0) ctx.args[0] else return error.PackageNotFound;517518 api.ensureRegistryPinned(ctx.gpa, ctx.io, ctx.environ);519520 521 const pkgs = api.searchPackages(ctx.gpa, ctx.io, pkg_name) catch {522 errors.error_info.setMessage("'{s}' not found in nixpkgs", .{pkg_name});523 errors.error_info.setSuggestion("nina search {s}", .{pkg_name});524 return error.PackageNotFound;525 };526 defer ctx.gpa.free(pkgs);527528 if (pkgs.len == 0) {529 errors.error_info.setMessage("'{s}' not found in nixpkgs", .{pkg_name});530 errors.error_info.setSuggestion("nina search {s}", .{pkg_name});531 return error.PackageNotFound;532 }533534 535 536 var found = pkgs[0];537 for (pkgs) |p| {538 if (std.mem.eql(u8, p.pname, pkg_name)) {539 found = p;540 break;541 }542 }543 output.installSelectPrompt(found.pname, found.version);544545 var choice_buf: [8]u8 = undefined;546 var choice_len: usize = 0;547 while (choice_len < choice_buf.len) {548 const n = std.posix.read(std.posix.STDIN_FILENO, choice_buf[choice_len .. choice_len + 1]) catch break;549 if (n == 0) break;550 if (choice_buf[choice_len] == '\n') break;551 choice_len += 1;552 }553 const choice = std.mem.trim(u8, choice_buf[0..choice_len], " \t");554555 if (choice.len == 0 or choice[0] == 'i' or choice[0] == 'I') {556 try profileInstallPkg(ctx, found);557 } else if (choice[0] == 's' or choice[0] == 'S') {558 try systemInstallPkg(ctx, found);559 } else if (choice[0] == 't' or choice[0] == 'T') {560 try tryPkg(ctx, found);561 }562}563564fn installFromResult(ctx: Ctx, result: types.SearchResult) !void {565 switch (result.action) {566 .profile => try profileInstallPkg(ctx, result.package),567 .system => try systemInstallPkg(ctx, result.package),568 }569}570571fn profileInstallPkg(ctx: Ctx, npkg: types.NixPackage) !void {572 output.installStart(npkg.pname, .profile);573 output.printSubstep("nix profile install nixpkgs#{s}", .{npkg.attr});574 output.printTeach(try std.fmt.allocPrint(ctx.gpa, "nix profile install nixpkgs#{s}", .{npkg.attr}));575 const code = try exec.profileInstall(ctx.io, &ctx.machine, ctx.gpa, npkg.attr);576 if (code != 0) return error.ProcessFailed;577 output.installDone(npkg.pname);578}579580fn systemInstallPkg(ctx: Ctx, npkg: types.NixPackage) !void {581 const config_path = try std.fmt.allocPrint(ctx.gpa, "{s}/configuration.nix", .{ctx.machine.config_path});582 defer ctx.gpa.free(config_path);583 const line = api.findSystemPackagesLine(ctx.io, config_path) orelse 1;584 output.installEditorPrompt(npkg.pname, config_path, line, ctx.cfg.editor);585 _ = try exec.openEditor(ctx.io, ctx.gpa, ctx.cfg.editor, config_path, line);586 if (ctx.cfg.confirm) {587 if (output.confirm("apply now? ")) {588 try apply(ctx);589 }590 }591}592593fn tryPkg(ctx: Ctx, npkg: types.NixPackage) !void {594 output.tryStart(npkg.pname);595 output.printSubstep("nix shell nixpkgs#{s}", .{npkg.attr});596 _ = try exec.nixShell(ctx.io, &ctx.machine, ctx.gpa, npkg.attr);597 output.tryDone();598}599600pub fn remove(ctx: Ctx) !void {601 const pkg_name = if (ctx.args.len > 0) ctx.args[0] else return error.PackageNotFound;602 output.removeStart(pkg_name);603 const code = try exec.profileRemove(ctx.io, &ctx.machine, ctx.gpa, pkg_name);604 if (code != 0) return error.ProcessFailed;605 output.done();606}607608pub fn list(ctx: Ctx) !void {609 output.listHeader();610 const profile_out = exec.profileList(ctx.gpa, ctx.io, &ctx.machine) catch null;611 if (profile_out) |out| {612 defer ctx.gpa.free(out);613 output.listSection("profile");614 var lines = std.mem.splitScalar(u8, out, '\n');615 while (lines.next()) |line| {616 const t = std.mem.trim(u8, line, " \t\r");617 if (t.len == 0) continue;618 output.listPkg(t, "");619 }620 }621}622623pub fn tryCmd(ctx: Ctx) !void {624 const pkg_name_arg = if (ctx.args.len > 0) ctx.args[0] else return error.PackageNotFound;625 const shell_pkg = types.NixPackage{ .attr = pkg_name_arg, .pname = pkg_name_arg, .version = "", .description = "" };626 try tryPkg(ctx, shell_pkg);627}628629630631pub fn service(ctx: Ctx) !void {632 if (std.mem.eql(u8, ctx.sub, "list") or ctx.sub.len == 0) {633 output.serviceListHeader(ctx.machine.name);634 const svcs = exec.serviceList(ctx.gpa, ctx.io, &ctx.machine) catch &.{};635 defer ctx.gpa.free(svcs);636 for (svcs) |s| output.serviceRow(s);637 return;638 }639 if (std.mem.eql(u8, ctx.sub, "logs")) {640 const name = if (ctx.args.len > 0) ctx.args[0] else return error.Internal;641 const follow = ctx.args.len > 1 and std.mem.eql(u8, ctx.args[1], "-f");642 output.serviceLogsHeader(name, ctx.last, ctx.machine.name);643 _ = try exec.serviceLogs(ctx.io, &ctx.machine, ctx.gpa, name, ctx.last, follow);644 return;645 }646 if (std.mem.eql(u8, ctx.sub, "status")) {647 const name = if (ctx.args.len > 0) ctx.args[0] else return error.Internal;648 output.serviceVerb("status", name, ctx.machine.name);649 _ = try exec.serviceCtl(ctx.io, &ctx.machine, ctx.gpa, "status", name);650 return;651 }652 653 const name = if (ctx.args.len > 0) ctx.args[0] else return error.Internal;654 if (ctx.cfg.confirm and (std.mem.eql(u8, ctx.sub, "stop") or std.mem.eql(u8, ctx.sub, "disable"))) {655 if (!output.confirm("")) return;656 }657 output.serviceVerb(ctx.sub, name, ctx.machine.name);658 _ = try exec.serviceCtl(ctx.io, &ctx.machine, ctx.gpa, ctx.sub, name);659 output.serviceDone();660}661662663664pub fn flake(ctx: Ctx) !void {665 if (std.mem.eql(u8, ctx.sub, "update")) {666 output.flakeUpdateStart();667 const input: ?[]const u8 = if (ctx.args.len > 0) ctx.args[0] else null;668 _ = try exec.flakeUpdate(ctx.io, &ctx.machine, ctx.gpa, input);669 output.flakeLockWritten();670 return;671 }672 if (std.mem.eql(u8, ctx.sub, "check")) {673 output.flakeCheckStart();674 const code = try exec.flakeCheck(ctx.io, &ctx.machine, ctx.gpa);675 if (code == 0) output.flakeValid() else output.checkFailed("");676 return;677 }678 if (std.mem.eql(u8, ctx.sub, "show")) {679 const json_out = exec.flakeShow(ctx.gpa, ctx.io, &ctx.machine) catch null;680 if (json_out) |j| {681 defer ctx.gpa.free(j);682 output.flakeShowHeader("flake outputs");683 684 const parsed = std.json.parseFromSlice(std.json.Value, ctx.gpa, j, .{}) catch return;685 defer parsed.deinit();686 if (parsed.value == .object) {687 var it = parsed.value.object.iterator();688 while (it.next()) |entry| {689 if (entry.value_ptr.* == .object) {690 var inner = entry.value_ptr.object.iterator();691 while (inner.next()) |e2| {692 const desc = if (e2.value_ptr.* == .object)693 if (e2.value_ptr.object.get("description")) |d|694 if (d == .string) d.string else ""695 else ""696 else "";697 output.flakeShowEntry(entry.key_ptr.*, e2.key_ptr.*, desc);698 }699 }700 }701 }702 }703 return;704 }705 if (std.mem.eql(u8, ctx.sub, "init")) {706 _ = try exec.flakeInit(ctx.io, &ctx.machine, ctx.gpa);707 return;708 }709 if (std.mem.eql(u8, ctx.sub, "lock")) {710 _ = try exec.flakeLock(ctx.io, &ctx.machine, ctx.gpa);711 return;712 }713 if (std.mem.eql(u8, ctx.sub, "clone")) {714 const url = if (ctx.args.len > 0) ctx.args[0] else return error.Internal;715 _ = try exec.flakeClone(ctx.io, &ctx.machine, ctx.gpa, url);716 return;717 }718}719720721722pub fn store(ctx: Ctx) !void {723 if (std.mem.eql(u8, ctx.sub, "gc")) {724 _ = try exec.storeGc(ctx.io, &ctx.machine, ctx.gpa);725 return;726 }727 if (std.mem.eql(u8, ctx.sub, "verify")) {728 output.storeVerifyStart();729 _ = try exec.storeVerify(ctx.io, &ctx.machine, ctx.gpa);730 return;731 }732 if (std.mem.eql(u8, ctx.sub, "repair")) {733 if (ctx.cfg.confirm and !output.confirm("")) return;734 output.storeRepairStart();735 _ = try exec.storeRepair(ctx.io, &ctx.machine, ctx.gpa);736 return;737 }738 if (std.mem.eql(u8, ctx.sub, "info")) {739 const si = try exec.storeInfo(ctx.gpa, ctx.io, &ctx.machine);740 output.storeInfo(si);741 return;742 }743 if (std.mem.eql(u8, ctx.sub, "path")) {744 const attr = if (ctx.args.len > 0) ctx.args[0] else return error.Internal;745 const path = try exec.storePath(ctx.gpa, ctx.io, &ctx.machine, attr);746 defer ctx.gpa.free(path);747 output.storePath(path);748 return;749 }750 751 const si = try exec.storeInfo(ctx.gpa, ctx.io, &ctx.machine);752 output.storeInfo(si);753}754755756757pub fn channel(ctx: Ctx) !void {758 if (std.mem.eql(u8, ctx.sub, "list") or ctx.sub.len == 0) {759 output.channelListHeader(ctx.machine.name);760 const chans = try exec.channelList(ctx.gpa, ctx.io, &ctx.machine);761 defer ctx.gpa.free(chans);762 for (chans) |ch| output.channelRow(ch);763 return;764 }765 if (std.mem.eql(u8, ctx.sub, "add")) {766 const url = if (ctx.args.len > 0) ctx.args[0] else return error.Internal;767 const name = if (ctx.args.len > 1) ctx.args[1] else "";768 _ = try exec.channelAdd(ctx.io, &ctx.machine, ctx.gpa, url, name);769 output.channelDone();770 return;771 }772 if (std.mem.eql(u8, ctx.sub, "remove")) {773 const name = if (ctx.args.len > 0) ctx.args[0] else return error.Internal;774 _ = try exec.channelRemove(ctx.io, &ctx.machine, ctx.gpa, name);775 output.channelDone();776 return;777 }778 if (std.mem.eql(u8, ctx.sub, "update")) {779 return channelUpdateCmd(ctx);780 }781}782783784785pub fn profile(ctx: Ctx) !void {786 if (std.mem.eql(u8, ctx.sub, "list") or ctx.sub.len == 0) {787 output.profileListHeader();788 const out = exec.profileList(ctx.gpa, ctx.io, &ctx.machine) catch null;789 if (out) |o| {790 defer ctx.gpa.free(o);791 var lines = std.mem.splitScalar(u8, o, '\n');792 while (lines.next()) |line| {793 const t = std.mem.trim(u8, line, " \t\r");794 if (t.len == 0) continue;795 output.profileRow(t, "", "");796 }797 }798 return;799 }800 if (std.mem.eql(u8, ctx.sub, "install")) {801 const attr = if (ctx.args.len > 0) ctx.args[0] else return error.PackageNotFound;802 _ = try exec.profileInstall(ctx.io, &ctx.machine, ctx.gpa, attr);803 output.done();804 return;805 }806 if (std.mem.eql(u8, ctx.sub, "remove")) {807 const attr = if (ctx.args.len > 0) ctx.args[0] else return error.PackageNotFound;808 _ = try exec.profileRemove(ctx.io, &ctx.machine, ctx.gpa, attr);809 output.done();810 return;811 }812 if (std.mem.eql(u8, ctx.sub, "upgrade")) {813 _ = try exec.profileUpgrade(ctx.io, &ctx.machine, ctx.gpa);814 output.done();815 return;816 }817}818819820821pub fn pkg(ctx: Ctx) !void {822 const name = if (ctx.args.len > 0) ctx.args[0] else return error.PackageNotFound;823 if (std.mem.eql(u8, ctx.sub, "why")) {824 _ = try exec.pkgWhy(ctx.io, &ctx.machine, ctx.gpa, name);825 return;826 }827 if (std.mem.eql(u8, ctx.sub, "deps")) {828 _ = try exec.pkgDeps(ctx.io, &ctx.machine, ctx.gpa, name);829 return;830 }831 if (std.mem.eql(u8, ctx.sub, "size")) {832 const s = exec.pkgSize(ctx.gpa, ctx.io, &ctx.machine, name) catch null;833 if (s) |sz| {834 defer ctx.gpa.free(sz);835 output.pkgResult("size", sz);836 }837 return;838 }839 if (std.mem.eql(u8, ctx.sub, "path")) {840 const p = try exec.storePath(ctx.gpa, ctx.io, &ctx.machine, name);841 defer ctx.gpa.free(p);842 output.pkgResult("path", p);843 return;844 }845 if (std.mem.eql(u8, ctx.sub, "closure")) {846 _ = try exec.pkgClosure(ctx.io, &ctx.machine, ctx.gpa, name);847 return;848 }849}850851852853pub fn pin(ctx: Ctx) !void {854 const input = if (ctx.args.len > 0) ctx.args[0] else return error.Internal;855 const commit = if (ctx.args.len > 1) ctx.args[1] else return error.Internal;856 exec.editFlakeLock(ctx.gpa, input, commit) catch return error.ProcessFailed;857 output.pinDone(input, commit);858}859860pub fn unpin(ctx: Ctx) !void {861 const input = if (ctx.args.len > 0) ctx.args[0] else return error.Internal;862 _ = try exec.flakeUpdate(ctx.io, &ctx.machine, ctx.gpa, input);863 output.unpinDone(input);864}865866867868pub fn hash(ctx: Ctx) !void {869 const path = if (ctx.args.len > 0) ctx.args[0] else return error.Internal;870 const h = try exec.nixHash(ctx.gpa, ctx.io, &ctx.machine, path);871 defer ctx.gpa.free(h);872 output.hashResult(std.mem.trim(u8, h, " \t\n\r"));873}874875pub fn fetch(ctx: Ctx) !void {876 const url = if (ctx.args.len > 0) ctx.args[0] else return error.Internal;877 const h = try exec.nixFetch(ctx.gpa, ctx.io, &ctx.machine, url);878 defer ctx.gpa.free(h);879 output.fetchResult(std.mem.trim(u8, h, " \t\n\r"), url);880}881882883884pub fn build(ctx: Ctx) !void {885 const attr: ?[]const u8 = if (ctx.args.len > 0) ctx.args[0] else null;886 output.buildStart(attr orelse "default");887 const code = try exec.nixBuild(ctx.io, &ctx.machine, ctx.gpa, attr);888 if (code != 0) return error.BuildFailed;889 output.buildDone();890}891892pub fn runCmd(ctx: Ctx) !void {893 const pkg_arg = if (ctx.args.len > 0) ctx.args[0] else return error.PackageNotFound;894 output.runStart(pkg_arg);895 _ = try exec.nixRun(ctx.io, &ctx.machine, ctx.gpa, pkg_arg);896}897898899900pub fn hello(ctx: Ctx) !void {901 output.hello(ctx.cfg.machines);902}903904pub fn setup(gpa: std.mem.Allocator, io: std.Io, environ: *const std.process.Environ.Map) !void {905 output.setupCheck();906 const status = exec.checkNixFeatures(gpa, io, environ);907 output.setupFeatures(status.nix_command, status.flakes);908909 if (status.allEnabled()) {910 output.setupAllSet();911 api.ensureRegistryPinned(gpa, io, environ);912 return;913 }914915 output.setupEnabling();916 const path = try exec.enableNixFeatures(gpa, io, environ);917 defer gpa.free(path);918919 output.setupDone(path, exec.isNixOS(io));920 api.ensureRegistryPinned(gpa, io, environ);921}922923924925fn logAction(ctx: Ctx, action: []const u8, gen: u32) void {926 var ts_buf: [32]u8 = undefined;927 const ts = nowIso(ctx.io, &ts_buf);928 var buf: [256]u8 = undefined;929 const entry = std.fmt.bufPrint(&buf, "{{\"ts\":\"{s}\",\"machine\":\"{s}\",\"command\":\"{s}\",\"outcome\":\"success\",\"gen_before\":null,\"gen_after\":{d},\"duration_ms\":0}}\n", .{930 ts, ctx.machine.name, action, gen,931 }) catch return;932 exec.appendNinaLog(ctx.io, ctx.environ, entry);933}934935936937fn nowIso(io: std.Io, buf: []u8) []const u8 {938 const fallback = "1970-01-01T00:00:00Z";939 const ns = std.Io.Clock.real.now(io).toNanoseconds();940 if (ns <= 0) return fallback;941 const secs: u64 = @intCast(@divFloor(ns, 1_000_000_000));942 const es = std.time.epoch.EpochSeconds{ .secs = secs };943 const ed = es.getEpochDay();944 const yd = ed.calculateYearDay();945 const md = yd.calculateMonthDay();946 const ds = es.getDaySeconds();947 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", .{948 yd.year, md.month.numeric(), @as(u32, md.day_index) + 1,949 ds.getHoursIntoDay(), ds.getMinutesIntoHour(), ds.getSecondsIntoMinute(),950 }) catch return fallback;951}