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 doctor(ctx: Ctx) !void {173 output.doctorHeader(ctx.machine.name);174 const checks = try exec.runDoctorChecks(ctx.gpa, ctx.io, &ctx.machine);175 defer ctx.gpa.free(checks);176 var warnings: u32 = 0;177 var failures: u32 = 0;178 for (checks) |ch| {179 output.doctorRow(ch);180 if (ch.status == .warn) warnings += 1;181 if (ch.status == .fail) failures += 1;182 }183 output.doctorSummary(warnings, failures);184}185186187pub fn update(ctx: Ctx) !void {188 output.updateSelfStart();189190 if (comptime std.mem.eql(u8, version.PLATFORM, "unsupported")) {191 output.updateSelfNoPlatform(@tagName(builtin.cpu.arch) ++ "-" ++ @tagName(builtin.os.tag));192 return error.Internal;193 }194195 const env_url = ctx.environ.get("NINA_UPDATE_SOURCE");196 const source_url = env_url orelse UPDATE_URL;197198 const body = exec.fetchUrl(ctx.gpa, ctx.io, source_url) catch |e| switch (e) {199 error.NetworkError, error.HttpError => {200 output.updateSelfNetworkError();201 return;202 },203 else => return e,204 };205 defer ctx.gpa.free(body);206207 const parsed = std.json.parseFromSlice(std.json.Value, ctx.gpa, body, .{}) catch {208 output.printWarning("could not parse release metadata");209 return;210 };211 defer parsed.deinit();212213 const root = parsed.value;214 if (root != .object) { output.printWarning("malformed release metadata"); return; }215216 const remote_ver = blk: {217 const v = root.object.get("version") orelse { output.printWarning("malformed release metadata"); return; };218 break :blk switch (v) { .string => |s| s, else => { output.printWarning("malformed release metadata"); return; } };219 };220221 if (std.mem.eql(u8, remote_ver, version.VERSION)) {222 output.updateSelfCurrent(version.VERSION);223 return;224 }225226 output.updateSelfAvailable(version.VERSION, remote_ver);227 if (ctx.check) return;228229 var exe_buf: [std.Io.Dir.max_path_bytes]u8 = undefined;230 const install_path = exec.selfExePath(ctx.io, &exe_buf) catch {231 errors.error_info.setMessage("could not determine install path", .{});232 return error.Internal;233 };234235 236 237 if (std.mem.startsWith(u8, install_path, "/nix/store/")) {238 output.updateSelfNixManaged(remote_ver);239 if (ctx.cfg.confirm and !output.confirm("")) return;240 const ec = exec.streamLocal(ctx.io, &.{ "nix", "profile", "upgrade", ".*nina.*" }) catch {241 output.updateSelfNixFailed();242 return;243 };244 if (ec != 0) output.updateSelfNixSystemManaged();245 return;246 }247248 const plat_info = blk: {249 const pv = root.object.get("platforms") orelse break :blk null;250 if (pv != .object) break :blk null;251 break :blk pv.object.get(version.PLATFORM);252 };253 if (plat_info == null) { output.updateSelfNoPlatform(version.PLATFORM); return; }254255 const dl_url = blk: {256 const v = plat_info.?.object.get("url") orelse { output.printWarning("malformed release metadata"); return; };257 break :blk switch (v) { .string => |s| s, else => { output.printWarning("malformed release metadata"); return; } };258 };259260 const raw_cksum = blk: {261 if (plat_info.?.object.get("blake3")) |v|262 break :blk switch (v) { .string => |s| s, else => { output.printWarning("malformed release metadata"); return; } };263 if (plat_info.?.object.get("checksum")) |v|264 break :blk switch (v) { .string => |s| s, else => { output.printWarning("malformed release metadata"); return; } };265 output.printWarning("missing checksum in release metadata");266 return;267 };268 const expected_hex = if (std.mem.startsWith(u8, raw_cksum, "blake3:")) raw_cksum["blake3:".len..] else raw_cksum;269270 if (ctx.cfg.confirm and !output.confirm("")) return;271272 output.updateSelfDownloading(remote_ver, version.PLATFORM);273 const tmp_path = exec.downloadToTemp(ctx.gpa, ctx.io, dl_url) catch {274 output.printWarning("download failed — check your connection and try again");275 return;276 };277 defer ctx.gpa.free(tmp_path);278279 output.updateSelfVerifying();280 exec.verifyAndInstall(ctx.gpa, ctx.io, tmp_path, expected_hex, install_path) catch |e| switch (e) {281 error.ChecksumMismatch => { output.updateSelfChecksumFailed(); return; },282 else => return e,283 };284285 output.updateSelfDone(version.VERSION, remote_ver);286}287288289pub fn goodbye(ctx: Ctx) !void {290 output.goodbyeStart();291292 var exe_buf: [std.Io.Dir.max_path_bytes]u8 = undefined;293 const install_path = exec.selfExePath(ctx.io, &exe_buf) catch {294 errors.error_info.setMessage("could not determine install path", .{});295 return error.Internal;296 };297298 if (std.mem.startsWith(u8, install_path, "/nix/store/")) {299 output.goodbyeNix();300 if (ctx.cfg.confirm and !output.confirm("")) return;301 const ec = exec.streamLocal(ctx.io, &.{ "nix", "profile", "remove", ".*nina.*" }) catch {302 output.goodbyeFailed("nix profile remove failed");303 return;304 };305 if (ec != 0) {306 output.goodbyeNixSystemManaged();307 return;308 }309 } else {310 output.goodbyeFile(install_path);311 if (ctx.cfg.confirm and !output.confirm("")) return;312 std.Io.Dir.deleteFileAbsolute(ctx.io, install_path) catch |e| {313 errors.error_info.setMessage("could not remove {s}", .{install_path});314 return e;315 };316 }317318 319 if (ctx.environ.get("HOME")) |home| {320 const nina_nix: ?[]u8 = std.fmt.allocPrint(ctx.gpa, "{s}/nina.nix", .{home}) catch null;321 if (nina_nix) |path| {322 defer ctx.gpa.free(path);323 const exists = blk: {324 const bytes = std.Io.Dir.readFileAlloc(.cwd(), ctx.io, path, ctx.gpa, .unlimited) catch break :blk false;325 ctx.gpa.free(bytes);326 break :blk true;327 };328 if (exists) {329 output.goodbyeNixFileFound(path);330 if (!ctx.cfg.confirm or output.confirm("")) {331 std.Io.Dir.deleteFileAbsolute(ctx.io, path) catch {332 output.printWarning("could not remove ~/nina.nix — remove it manually to clean up");333 };334 }335 }336 }337 }338339 output.goodbyeDone();340}341342fn channelUpdateCmd(ctx: Ctx) !void {343 output.updateStart(ctx.machine.name);344 _ = try exec.channelUpdate(ctx.io, &ctx.machine, ctx.gpa);345 output.updateDone();346}347348pub fn upgrade(ctx: Ctx) !void {349 output.upgradeStart(ctx.machine.name);350 output.printSubstep("updating channels", .{});351 _ = exec.channelUpdate(ctx.io, &ctx.machine, ctx.gpa) catch {};352 output.printSubstep("rebuilding", .{});353 const start = std.Io.Clock.awake.now(ctx.io);354 const code = try exec.nixosRebuildSwitch(ctx.io, &ctx.machine, ctx.gpa);355 if (code != 0) {356 output.applyFailed("");357 return error.BuildFailed;358 }359 const elapsed_ns = @max(0, start.untilNow(ctx.io, .awake).toNanoseconds());360 const elapsed: u64 = @intCast(@divFloor(elapsed_ns, 1_000_000));361 const gen = exec.getCurrentGeneration(ctx.gpa, ctx.io, &ctx.machine) catch 0;362 output.applyDone(gen, elapsed);363}364365pub fn log(ctx: Ctx) !void {366 output.logHeader(ctx.machine.name);367 const content = exec.readNinaLog(ctx.gpa, ctx.io, ctx.environ, ctx.last) catch {368 369 return;370 };371 defer ctx.gpa.free(content);372 var lines = std.mem.splitScalar(u8, content, '\n');373 while (lines.next()) |line| {374 const t = std.mem.trim(u8, line, " \t\r");375 if (t.len == 0) continue;376 if (t[0] == '{') {377 renderJsonLogLine(ctx.gpa, t);378 } else {379 380 var parts = std.mem.tokenizeScalar(u8, t, '\t');381 const date = parts.next() orelse continue;382 const time = parts.next() orelse "";383 const action = parts.next() orelse "";384 const detail = parts.next() orelse "";385 output.logRow(date, time, action, detail);386 }387 }388}389390391392fn renderJsonLogLine(gpa: std.mem.Allocator, line: []const u8) void {393 const parsed = std.json.parseFromSlice(std.json.Value, gpa, line, .{}) catch {394 output.logRow(line, "", "", ""); 395 return;396 };397 defer parsed.deinit();398 if (parsed.value != .object) return;399 const o = parsed.value.object;400401 const ts = jsonStr(o, "ts");402 const date = if (ts.len >= 10) ts[0..10] else ts;403 const time = if (ts.len >= 16) ts[11..16] else "";404 const action = jsonStr(o, "command");405 const outcome = jsonStr(o, "outcome");406407 var buf: [160]u8 = undefined;408 var detail: []const u8 = outcome;409 if (o.get("gen_after")) |g| {410 if (g == .integer) {411 detail = std.fmt.bufPrint(&buf, "{s} -> gen {d}", .{ outcome, g.integer }) catch outcome;412 }413 }414 output.logRow(date, time, action, detail);415}416417fn jsonStr(o: std.json.ObjectMap, key: []const u8) []const u8 {418 if (o.get(key)) |v| {419 if (v == .string) return v.string;420 }421 return "";422}423424pub fn edit(ctx: Ctx) !void {425 const hardware = ctx.args.len > 0 and std.mem.eql(u8, ctx.args[0], "hardware");426 const filename = if (hardware) "hardware-configuration.nix" else "configuration.nix";427 const path = try std.fmt.allocPrint(ctx.gpa, "{s}/{s}", .{ ctx.machine.config_path, filename });428 defer ctx.gpa.free(path);429 output.editOpen(path, ctx.cfg.editor);430 _ = try exec.openEditor(ctx.io, ctx.gpa, ctx.cfg.editor, path, null);431}432433pub fn fmt(ctx: Ctx) !void {434 const path = try std.fmt.allocPrint(ctx.gpa, "{s}/configuration.nix", .{ctx.machine.config_path});435 defer ctx.gpa.free(path);436 if (ctx.check) {437 const code = try exec.nixpkgsFmt(ctx.io, &ctx.machine, ctx.gpa, path, true);438 if (code != 0) output.fmtNeedsFormat() else output.fmtDone();439 return;440 }441 output.fmtStart(path);442 _ = try exec.nixpkgsFmt(ctx.io, &ctx.machine, ctx.gpa, path, false);443 output.fmtDone();444}445446pub fn repl(ctx: Ctx) !void {447 output.replStart();448 _ = try exec.nixRepl(ctx.io, &ctx.machine, ctx.gpa);449}450451pub fn develop(ctx: Ctx) !void {452 if (std.mem.eql(u8, ctx.sub, "show")) {453 _ = try exec.nixBuild(ctx.io, &ctx.machine, ctx.gpa, null);454 return;455 }456 const run_cmd: ?[]const u8 = if (ctx.args.len > 0 and std.mem.eql(u8, ctx.sub, "run"))457 ctx.args[0]458 else459 null;460 output.developStart(&.{});461 _ = try exec.nixDevelop(ctx.io, &ctx.machine, ctx.gpa, run_cmd);462 output.developDone();463}464465pub fn info(ctx: Ctx) !void {466 output.infoHeader(ctx.machine.name);467 const nixos_ver = exec.systemInfo(ctx.gpa, ctx.io, &ctx.machine) catch try ctx.gpa.dupe(u8, "unknown");468 defer ctx.gpa.free(nixos_ver);469 output.infoRow("nixos", std.mem.trim(u8, nixos_ver, " \t\n\r"));470471 const kernel = exec.kernelInfo(ctx.gpa, ctx.io, &ctx.machine) catch try ctx.gpa.dupe(u8, "unknown");472 defer ctx.gpa.free(kernel);473 output.infoRow("kernel", std.mem.trim(u8, kernel, " \t\n\r"));474475 const uptime = exec.uptimeInfo(ctx.gpa, ctx.io, &ctx.machine) catch try ctx.gpa.dupe(u8, "unknown");476 defer ctx.gpa.free(uptime);477 output.infoRow("uptime", std.mem.trim(u8, uptime, " \t\n\r"));478}479480pub fn boot(ctx: Ctx) !void {481 output.bootHeader(ctx.machine.name);482 const entries = exec.bootEntries(ctx.gpa, ctx.io, &ctx.machine) catch &.{};483 defer ctx.gpa.free(entries);484 for (entries, 0..) |entry, i| output.bootEntry(entry, i);485}486487pub fn searchCmd(ctx: Ctx) !void {488 const query = if (ctx.args.len > 0) ctx.args[0] else "";489 api.ensureRegistryPinned(ctx.gpa, ctx.io, ctx.environ);490 const result = try search_tui.run(ctx.gpa, ctx.io, query, .packages);491 if (result) |r| {492 installFromResult(ctx, r) catch |e| return e;493 }494}495496pub fn optionCmd(ctx: Ctx) !void {497 const query = if (ctx.args.len > 0) ctx.args[0] else "";498 api.ensureRegistryPinned(ctx.gpa, ctx.io, ctx.environ);499 _ = try search_tui.run(ctx.gpa, ctx.io, query, .options);500}501502pub fn install(ctx: Ctx) !void {503 const pkg_name = if (ctx.args.len > 0) ctx.args[0] else return error.PackageNotFound;504505 api.ensureRegistryPinned(ctx.gpa, ctx.io, ctx.environ);506507 508 const pkgs = api.searchPackages(ctx.gpa, ctx.io, pkg_name) catch {509 errors.error_info.setMessage("'{s}' not found in nixpkgs", .{pkg_name});510 errors.error_info.setSuggestion("nina search {s}", .{pkg_name});511 return error.PackageNotFound;512 };513 defer ctx.gpa.free(pkgs);514515 if (pkgs.len == 0) {516 errors.error_info.setMessage("'{s}' not found in nixpkgs", .{pkg_name});517 errors.error_info.setSuggestion("nina search {s}", .{pkg_name});518 return error.PackageNotFound;519 }520521 522 523 var found = pkgs[0];524 for (pkgs) |p| {525 if (std.mem.eql(u8, p.pname, pkg_name)) {526 found = p;527 break;528 }529 }530 output.installSelectPrompt(found.pname, found.version);531532 var choice_buf: [8]u8 = undefined;533 var choice_len: usize = 0;534 while (choice_len < choice_buf.len) {535 const n = std.posix.read(std.posix.STDIN_FILENO, choice_buf[choice_len .. choice_len + 1]) catch break;536 if (n == 0) break;537 if (choice_buf[choice_len] == '\n') break;538 choice_len += 1;539 }540 const choice = std.mem.trim(u8, choice_buf[0..choice_len], " \t");541542 if (choice.len == 0 or choice[0] == 'i' or choice[0] == 'I') {543 try profileInstallPkg(ctx, found);544 } else if (choice[0] == 's' or choice[0] == 'S') {545 try systemInstallPkg(ctx, found);546 } else if (choice[0] == 't' or choice[0] == 'T') {547 try tryPkg(ctx, found);548 }549}550551fn installFromResult(ctx: Ctx, result: types.SearchResult) !void {552 switch (result.action) {553 .profile => try profileInstallPkg(ctx, result.package),554 .system => try systemInstallPkg(ctx, result.package),555 }556}557558fn profileInstallPkg(ctx: Ctx, npkg: types.NixPackage) !void {559 output.installStart(npkg.pname, .profile);560 output.printSubstep("nix profile install nixpkgs#{s}", .{npkg.attr});561 output.printTeach(try std.fmt.allocPrint(ctx.gpa, "nix profile install nixpkgs#{s}", .{npkg.attr}));562 const code = try exec.profileInstall(ctx.io, &ctx.machine, ctx.gpa, npkg.attr);563 if (code != 0) return error.ProcessFailed;564 output.installDone(npkg.pname);565}566567fn systemInstallPkg(ctx: Ctx, npkg: types.NixPackage) !void {568 const config_path = try std.fmt.allocPrint(ctx.gpa, "{s}/configuration.nix", .{ctx.machine.config_path});569 defer ctx.gpa.free(config_path);570 const line = api.findSystemPackagesLine(ctx.io, config_path) orelse 1;571 output.installEditorPrompt(npkg.pname, config_path, line, ctx.cfg.editor);572 _ = try exec.openEditor(ctx.io, ctx.gpa, ctx.cfg.editor, config_path, line);573 if (ctx.cfg.confirm) {574 if (output.confirm("apply now? ")) {575 try apply(ctx);576 }577 }578}579580fn tryPkg(ctx: Ctx, npkg: types.NixPackage) !void {581 output.tryStart(npkg.pname);582 output.printSubstep("nix shell nixpkgs#{s}", .{npkg.attr});583 _ = try exec.nixShell(ctx.io, &ctx.machine, ctx.gpa, npkg.attr);584 output.tryDone();585}586587pub fn remove(ctx: Ctx) !void {588 const pkg_name = if (ctx.args.len > 0) ctx.args[0] else return error.PackageNotFound;589 output.removeStart(pkg_name);590 const code = try exec.profileRemove(ctx.io, &ctx.machine, ctx.gpa, pkg_name);591 if (code != 0) return error.ProcessFailed;592 output.done();593}594595pub fn list(ctx: Ctx) !void {596 output.listHeader();597 const profile_out = exec.profileList(ctx.gpa, ctx.io, &ctx.machine) catch null;598 if (profile_out) |out| {599 defer ctx.gpa.free(out);600 output.listSection("profile");601 var lines = std.mem.splitScalar(u8, out, '\n');602 while (lines.next()) |line| {603 const t = std.mem.trim(u8, line, " \t\r");604 if (t.len == 0) continue;605 output.listPkg(t, "");606 }607 }608}609610pub fn tryCmd(ctx: Ctx) !void {611 const pkg_name_arg = if (ctx.args.len > 0) ctx.args[0] else return error.PackageNotFound;612 const shell_pkg = types.NixPackage{ .attr = pkg_name_arg, .pname = pkg_name_arg, .version = "", .description = "" };613 try tryPkg(ctx, shell_pkg);614}615616617618pub fn service(ctx: Ctx) !void {619 if (std.mem.eql(u8, ctx.sub, "list") or ctx.sub.len == 0) {620 output.serviceListHeader(ctx.machine.name);621 const svcs = exec.serviceList(ctx.gpa, ctx.io, &ctx.machine) catch &.{};622 defer ctx.gpa.free(svcs);623 for (svcs) |s| output.serviceRow(s);624 return;625 }626 if (std.mem.eql(u8, ctx.sub, "logs")) {627 const name = if (ctx.args.len > 0) ctx.args[0] else return error.Internal;628 const follow = ctx.args.len > 1 and std.mem.eql(u8, ctx.args[1], "-f");629 output.serviceLogsHeader(name, ctx.last, ctx.machine.name);630 _ = try exec.serviceLogs(ctx.io, &ctx.machine, ctx.gpa, name, ctx.last, follow);631 return;632 }633 if (std.mem.eql(u8, ctx.sub, "status")) {634 const name = if (ctx.args.len > 0) ctx.args[0] else return error.Internal;635 output.serviceVerb("status", name, ctx.machine.name);636 _ = try exec.serviceCtl(ctx.io, &ctx.machine, ctx.gpa, "status", name);637 return;638 }639 640 const name = if (ctx.args.len > 0) ctx.args[0] else return error.Internal;641 if (ctx.cfg.confirm and (std.mem.eql(u8, ctx.sub, "stop") or std.mem.eql(u8, ctx.sub, "disable"))) {642 if (!output.confirm("")) return;643 }644 output.serviceVerb(ctx.sub, name, ctx.machine.name);645 _ = try exec.serviceCtl(ctx.io, &ctx.machine, ctx.gpa, ctx.sub, name);646 output.serviceDone();647}648649650651pub fn flake(ctx: Ctx) !void {652 if (std.mem.eql(u8, ctx.sub, "update")) {653 output.flakeUpdateStart();654 const input: ?[]const u8 = if (ctx.args.len > 0) ctx.args[0] else null;655 _ = try exec.flakeUpdate(ctx.io, &ctx.machine, ctx.gpa, input);656 output.flakeLockWritten();657 return;658 }659 if (std.mem.eql(u8, ctx.sub, "check")) {660 output.flakeCheckStart();661 const code = try exec.flakeCheck(ctx.io, &ctx.machine, ctx.gpa);662 if (code == 0) output.flakeValid() else output.checkFailed("");663 return;664 }665 if (std.mem.eql(u8, ctx.sub, "show")) {666 const json_out = exec.flakeShow(ctx.gpa, ctx.io, &ctx.machine) catch null;667 if (json_out) |j| {668 defer ctx.gpa.free(j);669 output.flakeShowHeader("flake outputs");670 671 const parsed = std.json.parseFromSlice(std.json.Value, ctx.gpa, j, .{}) catch return;672 defer parsed.deinit();673 if (parsed.value == .object) {674 var it = parsed.value.object.iterator();675 while (it.next()) |entry| {676 if (entry.value_ptr.* == .object) {677 var inner = entry.value_ptr.object.iterator();678 while (inner.next()) |e2| {679 const desc = if (e2.value_ptr.* == .object)680 if (e2.value_ptr.object.get("description")) |d|681 if (d == .string) d.string else ""682 else ""683 else "";684 output.flakeShowEntry(entry.key_ptr.*, e2.key_ptr.*, desc);685 }686 }687 }688 }689 }690 return;691 }692 if (std.mem.eql(u8, ctx.sub, "init")) {693 _ = try exec.flakeInit(ctx.io, &ctx.machine, ctx.gpa);694 return;695 }696 if (std.mem.eql(u8, ctx.sub, "lock")) {697 _ = try exec.flakeLock(ctx.io, &ctx.machine, ctx.gpa);698 return;699 }700 if (std.mem.eql(u8, ctx.sub, "clone")) {701 const url = if (ctx.args.len > 0) ctx.args[0] else return error.Internal;702 _ = try exec.flakeClone(ctx.io, &ctx.machine, ctx.gpa, url);703 return;704 }705}706707708709pub fn store(ctx: Ctx) !void {710 if (std.mem.eql(u8, ctx.sub, "gc")) {711 _ = try exec.storeGc(ctx.io, &ctx.machine, ctx.gpa);712 return;713 }714 if (std.mem.eql(u8, ctx.sub, "verify")) {715 output.storeVerifyStart();716 _ = try exec.storeVerify(ctx.io, &ctx.machine, ctx.gpa);717 return;718 }719 if (std.mem.eql(u8, ctx.sub, "repair")) {720 if (ctx.cfg.confirm and !output.confirm("")) return;721 output.storeRepairStart();722 _ = try exec.storeRepair(ctx.io, &ctx.machine, ctx.gpa);723 return;724 }725 if (std.mem.eql(u8, ctx.sub, "info")) {726 const si = try exec.storeInfo(ctx.gpa, ctx.io, &ctx.machine);727 output.storeInfo(si);728 return;729 }730 if (std.mem.eql(u8, ctx.sub, "path")) {731 const attr = if (ctx.args.len > 0) ctx.args[0] else return error.Internal;732 const path = try exec.storePath(ctx.gpa, ctx.io, &ctx.machine, attr);733 defer ctx.gpa.free(path);734 output.storePath(path);735 return;736 }737 738 const si = try exec.storeInfo(ctx.gpa, ctx.io, &ctx.machine);739 output.storeInfo(si);740}741742743744pub fn channel(ctx: Ctx) !void {745 if (std.mem.eql(u8, ctx.sub, "list") or ctx.sub.len == 0) {746 output.channelListHeader(ctx.machine.name);747 const chans = try exec.channelList(ctx.gpa, ctx.io, &ctx.machine);748 defer ctx.gpa.free(chans);749 for (chans) |ch| output.channelRow(ch);750 return;751 }752 if (std.mem.eql(u8, ctx.sub, "add")) {753 const url = if (ctx.args.len > 0) ctx.args[0] else return error.Internal;754 const name = if (ctx.args.len > 1) ctx.args[1] else "";755 _ = try exec.channelAdd(ctx.io, &ctx.machine, ctx.gpa, url, name);756 output.channelDone();757 return;758 }759 if (std.mem.eql(u8, ctx.sub, "remove")) {760 const name = if (ctx.args.len > 0) ctx.args[0] else return error.Internal;761 _ = try exec.channelRemove(ctx.io, &ctx.machine, ctx.gpa, name);762 output.channelDone();763 return;764 }765 if (std.mem.eql(u8, ctx.sub, "update")) {766 return channelUpdateCmd(ctx);767 }768}769770771772pub fn profile(ctx: Ctx) !void {773 if (std.mem.eql(u8, ctx.sub, "list") or ctx.sub.len == 0) {774 output.profileListHeader();775 const out = exec.profileList(ctx.gpa, ctx.io, &ctx.machine) catch null;776 if (out) |o| {777 defer ctx.gpa.free(o);778 var lines = std.mem.splitScalar(u8, o, '\n');779 while (lines.next()) |line| {780 const t = std.mem.trim(u8, line, " \t\r");781 if (t.len == 0) continue;782 output.profileRow(t, "", "");783 }784 }785 return;786 }787 if (std.mem.eql(u8, ctx.sub, "install")) {788 const attr = if (ctx.args.len > 0) ctx.args[0] else return error.PackageNotFound;789 _ = try exec.profileInstall(ctx.io, &ctx.machine, ctx.gpa, attr);790 output.done();791 return;792 }793 if (std.mem.eql(u8, ctx.sub, "remove")) {794 const attr = if (ctx.args.len > 0) ctx.args[0] else return error.PackageNotFound;795 _ = try exec.profileRemove(ctx.io, &ctx.machine, ctx.gpa, attr);796 output.done();797 return;798 }799 if (std.mem.eql(u8, ctx.sub, "upgrade")) {800 _ = try exec.profileUpgrade(ctx.io, &ctx.machine, ctx.gpa);801 output.done();802 return;803 }804}805806807808pub fn pkg(ctx: Ctx) !void {809 const name = if (ctx.args.len > 0) ctx.args[0] else return error.PackageNotFound;810 if (std.mem.eql(u8, ctx.sub, "why")) {811 _ = try exec.pkgWhy(ctx.io, &ctx.machine, ctx.gpa, name);812 return;813 }814 if (std.mem.eql(u8, ctx.sub, "deps")) {815 _ = try exec.pkgDeps(ctx.io, &ctx.machine, ctx.gpa, name);816 return;817 }818 if (std.mem.eql(u8, ctx.sub, "size")) {819 const s = exec.pkgSize(ctx.gpa, ctx.io, &ctx.machine, name) catch null;820 if (s) |sz| {821 defer ctx.gpa.free(sz);822 output.pkgResult("size", sz);823 }824 return;825 }826 if (std.mem.eql(u8, ctx.sub, "path")) {827 const p = try exec.storePath(ctx.gpa, ctx.io, &ctx.machine, name);828 defer ctx.gpa.free(p);829 output.pkgResult("path", p);830 return;831 }832 if (std.mem.eql(u8, ctx.sub, "closure")) {833 _ = try exec.pkgClosure(ctx.io, &ctx.machine, ctx.gpa, name);834 return;835 }836}837838839840pub fn pin(ctx: Ctx) !void {841 const input = if (ctx.args.len > 0) ctx.args[0] else return error.Internal;842 const commit = if (ctx.args.len > 1) ctx.args[1] else return error.Internal;843 exec.editFlakeLock(ctx.gpa, input, commit) catch return error.ProcessFailed;844 output.pinDone(input, commit);845}846847pub fn unpin(ctx: Ctx) !void {848 const input = if (ctx.args.len > 0) ctx.args[0] else return error.Internal;849 _ = try exec.flakeUpdate(ctx.io, &ctx.machine, ctx.gpa, input);850 output.unpinDone(input);851}852853854855pub fn hash(ctx: Ctx) !void {856 const path = if (ctx.args.len > 0) ctx.args[0] else return error.Internal;857 const h = try exec.nixHash(ctx.gpa, ctx.io, &ctx.machine, path);858 defer ctx.gpa.free(h);859 output.hashResult(std.mem.trim(u8, h, " \t\n\r"));860}861862pub fn fetch(ctx: Ctx) !void {863 const url = if (ctx.args.len > 0) ctx.args[0] else return error.Internal;864 const h = try exec.nixFetch(ctx.gpa, ctx.io, &ctx.machine, url);865 defer ctx.gpa.free(h);866 output.fetchResult(std.mem.trim(u8, h, " \t\n\r"), url);867}868869870871pub fn build(ctx: Ctx) !void {872 const attr: ?[]const u8 = if (ctx.args.len > 0) ctx.args[0] else null;873 output.buildStart(attr orelse "default");874 const code = try exec.nixBuild(ctx.io, &ctx.machine, ctx.gpa, attr);875 if (code != 0) return error.BuildFailed;876 output.buildDone();877}878879pub fn runCmd(ctx: Ctx) !void {880 const pkg_arg = if (ctx.args.len > 0) ctx.args[0] else return error.PackageNotFound;881 output.runStart(pkg_arg);882 _ = try exec.nixRun(ctx.io, &ctx.machine, ctx.gpa, pkg_arg);883}884885886887pub fn hello(ctx: Ctx) !void {888 output.hello(ctx.cfg.machines);889}890891pub fn setup(gpa: std.mem.Allocator, io: std.Io, environ: *const std.process.Environ.Map) !void {892 output.setupCheck();893 const status = exec.checkNixFeatures(gpa, io, environ);894 output.setupFeatures(status.nix_command, status.flakes);895896 if (status.allEnabled()) {897 output.setupAllSet();898 api.ensureRegistryPinned(gpa, io, environ);899 return;900 }901902 output.setupEnabling();903 const path = try exec.enableNixFeatures(gpa, io, environ);904 defer gpa.free(path);905906 output.setupDone(path, exec.isNixOS(io));907 api.ensureRegistryPinned(gpa, io, environ);908}909910911912fn logAction(ctx: Ctx, action: []const u8, gen: u32) void {913 var ts_buf: [32]u8 = undefined;914 const ts = nowIso(ctx.io, &ts_buf);915 var buf: [256]u8 = undefined;916 const entry = std.fmt.bufPrint(&buf, "{{\"ts\":\"{s}\",\"machine\":\"{s}\",\"command\":\"{s}\",\"outcome\":\"success\",\"gen_before\":null,\"gen_after\":{d},\"duration_ms\":0}}\n", .{917 ts, ctx.machine.name, action, gen,918 }) catch return;919 exec.appendNinaLog(ctx.io, ctx.environ, entry);920}921922923924fn nowIso(io: std.Io, buf: []u8) []const u8 {925 const fallback = "1970-01-01T00:00:00Z";926 const ns = std.Io.Clock.real.now(io).toNanoseconds();927 if (ns <= 0) return fallback;928 const secs: u64 = @intCast(@divFloor(ns, 1_000_000_000));929 const es = std.time.epoch.EpochSeconds{ .secs = secs };930 const ed = es.getEpochDay();931 const yd = ed.calculateYearDay();932 const md = yd.calculateMonthDay();933 const ds = es.getDaySeconds();934 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", .{935 yd.year, md.month.numeric(), @as(u32, md.day_index) + 1,936 ds.getHoursIntoDay(), ds.getMinutesIntoHour(), ds.getSecondsIntoMinute(),937 }) catch return fallback;938}