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 var exe_buf: [std.Io.Dir.max_path_bytes]u8 = undefined;227 const install_path = exec.selfExePath(ctx.io, &exe_buf) catch {228 errors.error_info.setMessage("could not determine install path", .{});229 return error.Internal;230 };231232 233 234 if (std.mem.startsWith(u8, install_path, "/nix/store/")) {235 output.updateSelfNixManaged(remote_ver);236 if (ctx.cfg.confirm and !output.confirm("")) return;237 const ec = exec.streamLocal(ctx.io, &.{ "nix", "profile", "upgrade", ".*nina.*" }) catch {238 output.updateSelfNixFailed();239 return;240 };241 if (ec != 0) output.updateSelfNixSystemManaged();242 return;243 }244245 const plat_info = blk: {246 const pv = root.object.get("platforms") orelse break :blk null;247 if (pv != .object) break :blk null;248 break :blk pv.object.get(version.PLATFORM);249 };250 if (plat_info == null) { output.updateSelfNoPlatform(version.PLATFORM); return; }251252 const dl_url = blk: {253 const v = plat_info.?.object.get("url") orelse { output.printWarning("malformed release metadata"); return; };254 break :blk switch (v) { .string => |s| s, else => { output.printWarning("malformed release metadata"); return; } };255 };256257 const raw_cksum = blk: {258 if (plat_info.?.object.get("blake3")) |v|259 break :blk switch (v) { .string => |s| s, else => { output.printWarning("malformed release metadata"); return; } };260 if (plat_info.?.object.get("checksum")) |v|261 break :blk switch (v) { .string => |s| s, else => { output.printWarning("malformed release metadata"); return; } };262 output.printWarning("missing checksum in release metadata");263 return;264 };265 const expected_hex = if (std.mem.startsWith(u8, raw_cksum, "blake3:")) raw_cksum["blake3:".len..] else raw_cksum;266267 if (ctx.cfg.confirm and !output.confirm("")) return;268269 output.updateSelfDownloading(remote_ver, version.PLATFORM);270 const tmp_path = exec.downloadToTemp(ctx.gpa, ctx.io, dl_url) catch {271 output.printWarning("download failed — check your connection and try again");272 return;273 };274 defer ctx.gpa.free(tmp_path);275276 output.updateSelfVerifying();277 exec.verifyAndInstall(ctx.gpa, ctx.io, tmp_path, expected_hex, install_path) catch |e| switch (e) {278 error.ChecksumMismatch => { output.updateSelfChecksumFailed(); return; },279 else => return e,280 };281282 output.updateSelfDone(version.VERSION, remote_ver);283}284285286pub fn goodbye(ctx: Ctx) !void {287 output.goodbyeStart();288289 var exe_buf: [std.Io.Dir.max_path_bytes]u8 = undefined;290 const install_path = exec.selfExePath(ctx.io, &exe_buf) catch {291 errors.error_info.setMessage("could not determine install path", .{});292 return error.Internal;293 };294295 if (std.mem.startsWith(u8, install_path, "/nix/store/")) {296 output.goodbyeNix();297 if (ctx.cfg.confirm and !output.confirm("")) return;298 const ec = exec.streamLocal(ctx.io, &.{ "nix", "profile", "remove", ".*nina.*" }) catch {299 output.goodbyeFailed("nix profile remove failed");300 return;301 };302 if (ec != 0) {303 output.goodbyeNixSystemManaged();304 return;305 }306 } else {307 output.goodbyeFile(install_path);308 if (ctx.cfg.confirm and !output.confirm("")) return;309 std.Io.Dir.deleteFileAbsolute(ctx.io, install_path) catch |e| {310 errors.error_info.setMessage("could not remove {s}", .{install_path});311 return e;312 };313 }314315 316 if (ctx.environ.get("HOME")) |home| {317 const nina_nix: ?[]u8 = std.fmt.allocPrint(ctx.gpa, "{s}/nina.nix", .{home}) catch null;318 if (nina_nix) |path| {319 defer ctx.gpa.free(path);320 const exists = blk: {321 const bytes = std.Io.Dir.readFileAlloc(.cwd(), ctx.io, path, ctx.gpa, .unlimited) catch break :blk false;322 ctx.gpa.free(bytes);323 break :blk true;324 };325 if (exists) {326 output.goodbyeNixFileFound(path);327 if (!ctx.cfg.confirm or output.confirm("")) {328 std.Io.Dir.deleteFileAbsolute(ctx.io, path) catch {329 output.printWarning("could not remove ~/nina.nix — remove it manually to clean up");330 };331 }332 }333 }334 }335336 output.goodbyeDone();337}338339fn channelUpdateCmd(ctx: Ctx) !void {340 output.updateStart(ctx.machine.name);341 _ = try exec.channelUpdate(ctx.io, &ctx.machine, ctx.gpa);342 output.updateDone();343}344345pub fn upgrade(ctx: Ctx) !void {346 output.upgradeStart(ctx.machine.name);347 output.printSubstep("updating channels", .{});348 _ = exec.channelUpdate(ctx.io, &ctx.machine, ctx.gpa) catch {};349 output.printSubstep("rebuilding", .{});350 const start = std.Io.Clock.awake.now(ctx.io);351 const code = try exec.nixosRebuildSwitch(ctx.io, &ctx.machine, ctx.gpa);352 if (code != 0) {353 output.applyFailed("");354 return error.BuildFailed;355 }356 const elapsed_ns = @max(0, start.untilNow(ctx.io, .awake).toNanoseconds());357 const elapsed: u64 = @intCast(@divFloor(elapsed_ns, 1_000_000));358 const gen = exec.getCurrentGeneration(ctx.gpa, ctx.io, &ctx.machine) catch 0;359 output.applyDone(gen, elapsed);360}361362pub fn log(ctx: Ctx) !void {363 output.logHeader(ctx.machine.name);364 const content = exec.readNinaLog(ctx.gpa, ctx.io, ctx.environ, ctx.last) catch {365 366 return;367 };368 defer ctx.gpa.free(content);369 var lines = std.mem.splitScalar(u8, content, '\n');370 while (lines.next()) |line| {371 const t = std.mem.trim(u8, line, " \t\r");372 if (t.len == 0) continue;373 if (t[0] == '{') {374 renderJsonLogLine(ctx.gpa, t);375 } else {376 377 var parts = std.mem.tokenizeScalar(u8, t, '\t');378 const date = parts.next() orelse continue;379 const time = parts.next() orelse "";380 const action = parts.next() orelse "";381 const detail = parts.next() orelse "";382 output.logRow(date, time, action, detail);383 }384 }385}386387388389fn renderJsonLogLine(gpa: std.mem.Allocator, line: []const u8) void {390 const parsed = std.json.parseFromSlice(std.json.Value, gpa, line, .{}) catch {391 output.logRow(line, "", "", ""); 392 return;393 };394 defer parsed.deinit();395 if (parsed.value != .object) return;396 const o = parsed.value.object;397398 const ts = jsonStr(o, "ts");399 const date = if (ts.len >= 10) ts[0..10] else ts;400 const time = if (ts.len >= 16) ts[11..16] else "";401 const action = jsonStr(o, "command");402 const outcome = jsonStr(o, "outcome");403404 var buf: [160]u8 = undefined;405 var detail: []const u8 = outcome;406 if (o.get("gen_after")) |g| {407 if (g == .integer) {408 detail = std.fmt.bufPrint(&buf, "{s} -> gen {d}", .{ outcome, g.integer }) catch outcome;409 }410 }411 output.logRow(date, time, action, detail);412}413414fn jsonStr(o: std.json.ObjectMap, key: []const u8) []const u8 {415 if (o.get(key)) |v| {416 if (v == .string) return v.string;417 }418 return "";419}420421pub fn edit(ctx: Ctx) !void {422 const hardware = ctx.args.len > 0 and std.mem.eql(u8, ctx.args[0], "hardware");423 const filename = if (hardware) "hardware-configuration.nix" else "configuration.nix";424 const path = try std.fmt.allocPrint(ctx.gpa, "{s}/{s}", .{ ctx.machine.config_path, filename });425 defer ctx.gpa.free(path);426 output.editOpen(path, ctx.cfg.editor);427 _ = try exec.openEditor(ctx.io, ctx.gpa, ctx.cfg.editor, path, null);428}429430pub fn fmt(ctx: Ctx) !void {431 const path = try std.fmt.allocPrint(ctx.gpa, "{s}/configuration.nix", .{ctx.machine.config_path});432 defer ctx.gpa.free(path);433 if (ctx.check) {434 const code = try exec.nixpkgsFmt(ctx.io, &ctx.machine, ctx.gpa, path, true);435 if (code != 0) output.fmtNeedsFormat() else output.fmtDone();436 return;437 }438 output.fmtStart(path);439 _ = try exec.nixpkgsFmt(ctx.io, &ctx.machine, ctx.gpa, path, false);440 output.fmtDone();441}442443pub fn repl(ctx: Ctx) !void {444 output.replStart();445 _ = try exec.nixRepl(ctx.io, &ctx.machine, ctx.gpa);446}447448pub fn develop(ctx: Ctx) !void {449 if (std.mem.eql(u8, ctx.sub, "show")) {450 _ = try exec.nixBuild(ctx.io, &ctx.machine, ctx.gpa, null);451 return;452 }453 const run_cmd: ?[]const u8 = if (ctx.args.len > 0 and std.mem.eql(u8, ctx.sub, "run"))454 ctx.args[0]455 else456 null;457 output.developStart(&.{});458 _ = try exec.nixDevelop(ctx.io, &ctx.machine, ctx.gpa, run_cmd);459 output.developDone();460}461462pub fn info(ctx: Ctx) !void {463 output.infoHeader(ctx.machine.name);464 const nixos_ver = exec.systemInfo(ctx.gpa, ctx.io, &ctx.machine) catch try ctx.gpa.dupe(u8, "unknown");465 defer ctx.gpa.free(nixos_ver);466 output.infoRow("nixos", std.mem.trim(u8, nixos_ver, " \t\n\r"));467468 const kernel = exec.kernelInfo(ctx.gpa, ctx.io, &ctx.machine) catch try ctx.gpa.dupe(u8, "unknown");469 defer ctx.gpa.free(kernel);470 output.infoRow("kernel", std.mem.trim(u8, kernel, " \t\n\r"));471472 const uptime = exec.uptimeInfo(ctx.gpa, ctx.io, &ctx.machine) catch try ctx.gpa.dupe(u8, "unknown");473 defer ctx.gpa.free(uptime);474 output.infoRow("uptime", std.mem.trim(u8, uptime, " \t\n\r"));475}476477pub fn boot(ctx: Ctx) !void {478 output.bootHeader(ctx.machine.name);479 const entries = exec.bootEntries(ctx.gpa, ctx.io, &ctx.machine) catch &.{};480 defer ctx.gpa.free(entries);481 for (entries, 0..) |entry, i| output.bootEntry(entry, i);482}483484pub fn searchCmd(ctx: Ctx) !void {485 const query = if (ctx.args.len > 0) ctx.args[0] else "";486 api.ensureRegistryPinned(ctx.gpa, ctx.io, ctx.environ);487 const result = try search_tui.run(ctx.gpa, ctx.io, query, .packages);488 if (result) |r| {489 installFromResult(ctx, r) catch |e| return e;490 }491}492493pub fn optionCmd(ctx: Ctx) !void {494 const query = if (ctx.args.len > 0) ctx.args[0] else "";495 api.ensureRegistryPinned(ctx.gpa, ctx.io, ctx.environ);496 _ = try search_tui.run(ctx.gpa, ctx.io, query, .options);497}498499pub fn install(ctx: Ctx) !void {500 const pkg_name = if (ctx.args.len > 0) ctx.args[0] else return error.PackageNotFound;501502 api.ensureRegistryPinned(ctx.gpa, ctx.io, ctx.environ);503504 505 const pkgs = api.searchPackages(ctx.gpa, ctx.io, pkg_name) catch {506 errors.error_info.setMessage("'{s}' not found in nixpkgs", .{pkg_name});507 errors.error_info.setSuggestion("nina search {s}", .{pkg_name});508 return error.PackageNotFound;509 };510 defer ctx.gpa.free(pkgs);511512 if (pkgs.len == 0) {513 errors.error_info.setMessage("'{s}' not found in nixpkgs", .{pkg_name});514 errors.error_info.setSuggestion("nina search {s}", .{pkg_name});515 return error.PackageNotFound;516 }517518 519 520 var found = pkgs[0];521 for (pkgs) |p| {522 if (std.mem.eql(u8, p.pname, pkg_name)) {523 found = p;524 break;525 }526 }527 output.installSelectPrompt(found.pname, found.version);528529 var choice_buf: [8]u8 = undefined;530 var choice_len: usize = 0;531 while (choice_len < choice_buf.len) {532 const n = std.posix.read(std.posix.STDIN_FILENO, choice_buf[choice_len .. choice_len + 1]) catch break;533 if (n == 0) break;534 if (choice_buf[choice_len] == '\n') break;535 choice_len += 1;536 }537 const choice = std.mem.trim(u8, choice_buf[0..choice_len], " \t");538539 if (choice.len == 0 or choice[0] == 'i' or choice[0] == 'I') {540 try profileInstallPkg(ctx, found);541 } else if (choice[0] == 's' or choice[0] == 'S') {542 try systemInstallPkg(ctx, found);543 } else if (choice[0] == 't' or choice[0] == 'T') {544 try tryPkg(ctx, found);545 }546}547548fn installFromResult(ctx: Ctx, result: types.SearchResult) !void {549 switch (result.action) {550 .profile => try profileInstallPkg(ctx, result.package),551 .system => try systemInstallPkg(ctx, result.package),552 }553}554555fn profileInstallPkg(ctx: Ctx, npkg: types.NixPackage) !void {556 output.installStart(npkg.pname, .profile);557 output.printSubstep("nix profile install nixpkgs#{s}", .{npkg.attr});558 output.printTeach(try std.fmt.allocPrint(ctx.gpa, "nix profile install nixpkgs#{s}", .{npkg.attr}));559 const code = try exec.profileInstall(ctx.io, &ctx.machine, ctx.gpa, npkg.attr);560 if (code != 0) return error.ProcessFailed;561 output.installDone(npkg.pname);562}563564fn systemInstallPkg(ctx: Ctx, npkg: types.NixPackage) !void {565 const config_path = try std.fmt.allocPrint(ctx.gpa, "{s}/configuration.nix", .{ctx.machine.config_path});566 defer ctx.gpa.free(config_path);567 const line = api.findSystemPackagesLine(ctx.io, config_path) orelse 1;568 output.installEditorPrompt(npkg.pname, config_path, line, ctx.cfg.editor);569 _ = try exec.openEditor(ctx.io, ctx.gpa, ctx.cfg.editor, config_path, line);570 if (ctx.cfg.confirm) {571 if (output.confirm("apply now? ")) {572 try apply(ctx);573 }574 }575}576577fn tryPkg(ctx: Ctx, npkg: types.NixPackage) !void {578 output.tryStart(npkg.pname);579 output.printSubstep("nix shell nixpkgs#{s}", .{npkg.attr});580 _ = try exec.nixShell(ctx.io, &ctx.machine, ctx.gpa, npkg.attr);581 output.tryDone();582}583584pub fn remove(ctx: Ctx) !void {585 const pkg_name = if (ctx.args.len > 0) ctx.args[0] else return error.PackageNotFound;586 output.removeStart(pkg_name);587 const code = try exec.profileRemove(ctx.io, &ctx.machine, ctx.gpa, pkg_name);588 if (code != 0) return error.ProcessFailed;589 output.done();590}591592pub fn list(ctx: Ctx) !void {593 output.listHeader();594 const profile_out = exec.profileList(ctx.gpa, ctx.io, &ctx.machine) catch null;595 if (profile_out) |out| {596 defer ctx.gpa.free(out);597 output.listSection("profile");598 var lines = std.mem.splitScalar(u8, out, '\n');599 while (lines.next()) |line| {600 const t = std.mem.trim(u8, line, " \t\r");601 if (t.len == 0) continue;602 output.listPkg(t, "");603 }604 }605}606607pub fn tryCmd(ctx: Ctx) !void {608 const pkg_name_arg = if (ctx.args.len > 0) ctx.args[0] else return error.PackageNotFound;609 const shell_pkg = types.NixPackage{ .attr = pkg_name_arg, .pname = pkg_name_arg, .version = "", .description = "" };610 try tryPkg(ctx, shell_pkg);611}612613614615pub fn service(ctx: Ctx) !void {616 if (std.mem.eql(u8, ctx.sub, "list") or ctx.sub.len == 0) {617 output.serviceListHeader(ctx.machine.name);618 const svcs = exec.serviceList(ctx.gpa, ctx.io, &ctx.machine) catch &.{};619 defer ctx.gpa.free(svcs);620 for (svcs) |s| output.serviceRow(s);621 return;622 }623 if (std.mem.eql(u8, ctx.sub, "logs")) {624 const name = if (ctx.args.len > 0) ctx.args[0] else return error.Internal;625 const follow = ctx.args.len > 1 and std.mem.eql(u8, ctx.args[1], "-f");626 output.serviceLogsHeader(name, ctx.last, ctx.machine.name);627 _ = try exec.serviceLogs(ctx.io, &ctx.machine, ctx.gpa, name, ctx.last, follow);628 return;629 }630 if (std.mem.eql(u8, ctx.sub, "status")) {631 const name = if (ctx.args.len > 0) ctx.args[0] else return error.Internal;632 output.serviceVerb("status", name, ctx.machine.name);633 _ = try exec.serviceCtl(ctx.io, &ctx.machine, ctx.gpa, "status", name);634 return;635 }636 637 const name = if (ctx.args.len > 0) ctx.args[0] else return error.Internal;638 if (ctx.cfg.confirm and (std.mem.eql(u8, ctx.sub, "stop") or std.mem.eql(u8, ctx.sub, "disable"))) {639 if (!output.confirm("")) return;640 }641 output.serviceVerb(ctx.sub, name, ctx.machine.name);642 _ = try exec.serviceCtl(ctx.io, &ctx.machine, ctx.gpa, ctx.sub, name);643 output.serviceDone();644}645646647648pub fn flake(ctx: Ctx) !void {649 if (std.mem.eql(u8, ctx.sub, "update")) {650 output.flakeUpdateStart();651 const input: ?[]const u8 = if (ctx.args.len > 0) ctx.args[0] else null;652 _ = try exec.flakeUpdate(ctx.io, &ctx.machine, ctx.gpa, input);653 output.flakeLockWritten();654 return;655 }656 if (std.mem.eql(u8, ctx.sub, "check")) {657 output.flakeCheckStart();658 const code = try exec.flakeCheck(ctx.io, &ctx.machine, ctx.gpa);659 if (code == 0) output.flakeValid() else output.checkFailed("");660 return;661 }662 if (std.mem.eql(u8, ctx.sub, "show")) {663 const json_out = exec.flakeShow(ctx.gpa, ctx.io, &ctx.machine) catch null;664 if (json_out) |j| {665 defer ctx.gpa.free(j);666 output.flakeShowHeader("flake outputs");667 668 const parsed = std.json.parseFromSlice(std.json.Value, ctx.gpa, j, .{}) catch return;669 defer parsed.deinit();670 if (parsed.value == .object) {671 var it = parsed.value.object.iterator();672 while (it.next()) |entry| {673 if (entry.value_ptr.* == .object) {674 var inner = entry.value_ptr.object.iterator();675 while (inner.next()) |e2| {676 const desc = if (e2.value_ptr.* == .object)677 if (e2.value_ptr.object.get("description")) |d|678 if (d == .string) d.string else ""679 else ""680 else "";681 output.flakeShowEntry(entry.key_ptr.*, e2.key_ptr.*, desc);682 }683 }684 }685 }686 }687 return;688 }689 if (std.mem.eql(u8, ctx.sub, "init")) {690 _ = try exec.flakeInit(ctx.io, &ctx.machine, ctx.gpa);691 return;692 }693 if (std.mem.eql(u8, ctx.sub, "lock")) {694 _ = try exec.flakeLock(ctx.io, &ctx.machine, ctx.gpa);695 return;696 }697 if (std.mem.eql(u8, ctx.sub, "clone")) {698 const url = if (ctx.args.len > 0) ctx.args[0] else return error.Internal;699 _ = try exec.flakeClone(ctx.io, &ctx.machine, ctx.gpa, url);700 return;701 }702}703704705706pub fn store(ctx: Ctx) !void {707 if (std.mem.eql(u8, ctx.sub, "gc")) {708 _ = try exec.storeGc(ctx.io, &ctx.machine, ctx.gpa);709 return;710 }711 if (std.mem.eql(u8, ctx.sub, "verify")) {712 output.storeVerifyStart();713 _ = try exec.storeVerify(ctx.io, &ctx.machine, ctx.gpa);714 return;715 }716 if (std.mem.eql(u8, ctx.sub, "repair")) {717 if (ctx.cfg.confirm and !output.confirm("")) return;718 output.storeRepairStart();719 _ = try exec.storeRepair(ctx.io, &ctx.machine, ctx.gpa);720 return;721 }722 if (std.mem.eql(u8, ctx.sub, "info")) {723 const si = try exec.storeInfo(ctx.gpa, ctx.io, &ctx.machine);724 output.storeInfo(si);725 return;726 }727 if (std.mem.eql(u8, ctx.sub, "path")) {728 const attr = if (ctx.args.len > 0) ctx.args[0] else return error.Internal;729 const path = try exec.storePath(ctx.gpa, ctx.io, &ctx.machine, attr);730 defer ctx.gpa.free(path);731 output.storePath(path);732 return;733 }734 735 const si = try exec.storeInfo(ctx.gpa, ctx.io, &ctx.machine);736 output.storeInfo(si);737}738739740741pub fn channel(ctx: Ctx) !void {742 if (std.mem.eql(u8, ctx.sub, "list") or ctx.sub.len == 0) {743 output.channelListHeader(ctx.machine.name);744 const chans = try exec.channelList(ctx.gpa, ctx.io, &ctx.machine);745 defer ctx.gpa.free(chans);746 for (chans) |ch| output.channelRow(ch);747 return;748 }749 if (std.mem.eql(u8, ctx.sub, "add")) {750 const url = if (ctx.args.len > 0) ctx.args[0] else return error.Internal;751 const name = if (ctx.args.len > 1) ctx.args[1] else "";752 _ = try exec.channelAdd(ctx.io, &ctx.machine, ctx.gpa, url, name);753 output.channelDone();754 return;755 }756 if (std.mem.eql(u8, ctx.sub, "remove")) {757 const name = if (ctx.args.len > 0) ctx.args[0] else return error.Internal;758 _ = try exec.channelRemove(ctx.io, &ctx.machine, ctx.gpa, name);759 output.channelDone();760 return;761 }762 if (std.mem.eql(u8, ctx.sub, "update")) {763 return channelUpdateCmd(ctx);764 }765}766767768769pub fn profile(ctx: Ctx) !void {770 if (std.mem.eql(u8, ctx.sub, "list") or ctx.sub.len == 0) {771 output.profileListHeader();772 const out = exec.profileList(ctx.gpa, ctx.io, &ctx.machine) catch null;773 if (out) |o| {774 defer ctx.gpa.free(o);775 var lines = std.mem.splitScalar(u8, o, '\n');776 while (lines.next()) |line| {777 const t = std.mem.trim(u8, line, " \t\r");778 if (t.len == 0) continue;779 output.profileRow(t, "", "");780 }781 }782 return;783 }784 if (std.mem.eql(u8, ctx.sub, "install")) {785 const attr = if (ctx.args.len > 0) ctx.args[0] else return error.PackageNotFound;786 _ = try exec.profileInstall(ctx.io, &ctx.machine, ctx.gpa, attr);787 output.done();788 return;789 }790 if (std.mem.eql(u8, ctx.sub, "remove")) {791 const attr = if (ctx.args.len > 0) ctx.args[0] else return error.PackageNotFound;792 _ = try exec.profileRemove(ctx.io, &ctx.machine, ctx.gpa, attr);793 output.done();794 return;795 }796 if (std.mem.eql(u8, ctx.sub, "upgrade")) {797 _ = try exec.profileUpgrade(ctx.io, &ctx.machine, ctx.gpa);798 output.done();799 return;800 }801}802803804805pub fn pkg(ctx: Ctx) !void {806 const name = if (ctx.args.len > 0) ctx.args[0] else return error.PackageNotFound;807 if (std.mem.eql(u8, ctx.sub, "why")) {808 _ = try exec.pkgWhy(ctx.io, &ctx.machine, ctx.gpa, name);809 return;810 }811 if (std.mem.eql(u8, ctx.sub, "deps")) {812 _ = try exec.pkgDeps(ctx.io, &ctx.machine, ctx.gpa, name);813 return;814 }815 if (std.mem.eql(u8, ctx.sub, "size")) {816 const s = exec.pkgSize(ctx.gpa, ctx.io, &ctx.machine, name) catch null;817 if (s) |sz| {818 defer ctx.gpa.free(sz);819 output.pkgResult("size", sz);820 }821 return;822 }823 if (std.mem.eql(u8, ctx.sub, "path")) {824 const p = try exec.storePath(ctx.gpa, ctx.io, &ctx.machine, name);825 defer ctx.gpa.free(p);826 output.pkgResult("path", p);827 return;828 }829 if (std.mem.eql(u8, ctx.sub, "closure")) {830 _ = try exec.pkgClosure(ctx.io, &ctx.machine, ctx.gpa, name);831 return;832 }833}834835836837pub fn pin(ctx: Ctx) !void {838 const input = if (ctx.args.len > 0) ctx.args[0] else return error.Internal;839 const commit = if (ctx.args.len > 1) ctx.args[1] else return error.Internal;840 exec.editFlakeLock(ctx.gpa, input, commit) catch return error.ProcessFailed;841 output.pinDone(input, commit);842}843844pub fn unpin(ctx: Ctx) !void {845 const input = if (ctx.args.len > 0) ctx.args[0] else return error.Internal;846 _ = try exec.flakeUpdate(ctx.io, &ctx.machine, ctx.gpa, input);847 output.unpinDone(input);848}849850851852pub fn hash(ctx: Ctx) !void {853 const path = if (ctx.args.len > 0) ctx.args[0] else return error.Internal;854 const h = try exec.nixHash(ctx.gpa, ctx.io, &ctx.machine, path);855 defer ctx.gpa.free(h);856 output.hashResult(std.mem.trim(u8, h, " \t\n\r"));857}858859pub fn fetch(ctx: Ctx) !void {860 const url = if (ctx.args.len > 0) ctx.args[0] else return error.Internal;861 const h = try exec.nixFetch(ctx.gpa, ctx.io, &ctx.machine, url);862 defer ctx.gpa.free(h);863 output.fetchResult(std.mem.trim(u8, h, " \t\n\r"), url);864}865866867868pub fn build(ctx: Ctx) !void {869 const attr: ?[]const u8 = if (ctx.args.len > 0) ctx.args[0] else null;870 output.buildStart(attr orelse "default");871 const code = try exec.nixBuild(ctx.io, &ctx.machine, ctx.gpa, attr);872 if (code != 0) return error.BuildFailed;873 output.buildDone();874}875876pub fn runCmd(ctx: Ctx) !void {877 const pkg_arg = if (ctx.args.len > 0) ctx.args[0] else return error.PackageNotFound;878 output.runStart(pkg_arg);879 _ = try exec.nixRun(ctx.io, &ctx.machine, ctx.gpa, pkg_arg);880}881882883884pub fn hello(ctx: Ctx) !void {885 output.hello(ctx.cfg.machines);886}887888pub fn setup(gpa: std.mem.Allocator, io: std.Io, environ: *const std.process.Environ.Map) !void {889 output.setupCheck();890 const status = exec.checkNixFeatures(gpa, io, environ);891 output.setupFeatures(status.nix_command, status.flakes);892893 if (status.allEnabled()) {894 output.setupAllSet();895 api.ensureRegistryPinned(gpa, io, environ);896 return;897 }898899 output.setupEnabling();900 const path = try exec.enableNixFeatures(gpa, io, environ);901 defer gpa.free(path);902903 output.setupDone(path, exec.isNixOS(io));904 api.ensureRegistryPinned(gpa, io, environ);905}906907908909fn logAction(ctx: Ctx, action: []const u8, gen: u32) void {910 var ts_buf: [32]u8 = undefined;911 const ts = nowIso(ctx.io, &ts_buf);912 var buf: [256]u8 = undefined;913 const entry = std.fmt.bufPrint(&buf, "{{\"ts\":\"{s}\",\"machine\":\"{s}\",\"command\":\"{s}\",\"outcome\":\"success\",\"gen_before\":null,\"gen_after\":{d},\"duration_ms\":0}}\n", .{914 ts, ctx.machine.name, action, gen,915 }) catch return;916 exec.appendNinaLog(ctx.io, ctx.environ, entry);917}918919920921fn nowIso(io: std.Io, buf: []u8) []const u8 {922 const fallback = "1970-01-01T00:00:00Z";923 const ns = std.Io.Clock.real.now(io).toNanoseconds();924 if (ns <= 0) return fallback;925 const secs: u64 = @intCast(@divFloor(ns, 1_000_000_000));926 const es = std.time.epoch.EpochSeconds{ .secs = secs };927 const ed = es.getEpochDay();928 const yd = ed.calculateYearDay();929 const md = yd.calculateMonthDay();930 const ds = es.getDaySeconds();931 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", .{932 yd.year, md.month.numeric(), @as(u32, md.day_index) + 1,933 ds.getHoursIntoDay(), ds.getMinutesIntoHour(), ds.getSecondsIntoMinute(),934 }) catch return fallback;935}