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 149 150 if (ctx.all and ctx.cfg.machines.len > 0) {151 for (ctx.cfg.machines) |m| statusRowFor(ctx, &m);152 return;153 }154 statusRowFor(ctx, &ctx.machine);155}156157fn statusRowFor(ctx: Ctx, machine: *const types.Machine) void {158 159 160 const gen = exec.getCurrentGeneration(ctx.gpa, ctx.io, machine) catch {161 output.statusRow(machine.name, 0, "", "", false);162 return;163 };164 const st = exec.systemStatus(ctx.gpa, ctx.io, machine);165 defer ctx.gpa.free(st.size);166 defer ctx.gpa.free(st.age);167 output.statusRow(machine.name, gen, st.age, st.size, true);168}169170pub fn diff(ctx: Ctx) !void {171 const gens = try exec.getGenerations(ctx.gpa, ctx.io, &ctx.machine);172 defer ctx.gpa.free(gens);173 var current: u32 = 0;174 var prev: u32 = 0;175 for (gens) |g| {176 if (g.current) current = g.number;177 if (!g.current and g.number > prev and g.number < current) prev = g.number;178 }179 if (ctx.args.len >= 2) {180 const a = std.fmt.parseInt(u32, ctx.args[0], 10) catch current - 1;181 const b = std.fmt.parseInt(u32, ctx.args[1], 10) catch current;182 output.diffHeader(a, b, ctx.machine.name);183 if (try exec.diffClosures(ctx.io, &ctx.machine, ctx.gpa, a, b) != 0) return error.ProcessFailed;184 } else {185 output.diffHeader(if (prev > 0) prev else current - 1, current, ctx.machine.name);186 if (try exec.diffClosures(ctx.io, &ctx.machine, ctx.gpa, if (prev > 0) prev else current - 1, current) != 0) return error.ProcessFailed;187 }188}189190pub fn mood(ctx: Ctx) !void {191 const gen = exec.getCurrentGeneration(ctx.gpa, ctx.io, &ctx.machine) catch 0;192 const st = exec.systemStatus(ctx.gpa, ctx.io, &ctx.machine);193 defer ctx.gpa.free(st.size);194 defer ctx.gpa.free(st.age);195 const state = exec.systemRunningState(ctx.gpa, ctx.io, &ctx.machine) catch try ctx.gpa.dupe(u8, "unknown");196 defer ctx.gpa.free(state);197 output.mood(ctx.machine.name, state, gen, st.age, st.size);198}199200pub fn doctor(ctx: Ctx) !void {201 output.doctorHeader(ctx.machine.name);202 const checks = try exec.runDoctorChecks(ctx.gpa, ctx.io, &ctx.machine);203 defer ctx.gpa.free(checks);204 var warnings: u32 = 0;205 var failures: u32 = 0;206 for (checks) |ch| {207 output.doctorRow(ch);208 if (ch.status == .warn) warnings += 1;209 if (ch.status == .fail) failures += 1;210 }211 output.doctorSummary(warnings, failures);212}213214215pub fn update(ctx: Ctx) !void {216 output.updateSelfStart();217218 if (comptime std.mem.eql(u8, version.PLATFORM, "unsupported")) {219 output.updateSelfNoPlatform(@tagName(builtin.cpu.arch) ++ "-" ++ @tagName(builtin.os.tag));220 return error.Internal;221 }222223 const env_url = ctx.environ.get("NINA_UPDATE_SOURCE");224 const source_url = env_url orelse UPDATE_URL;225226 const body = exec.fetchUrl(ctx.gpa, ctx.io, source_url) catch |e| switch (e) {227 error.NetworkError, error.HttpError => {228 output.updateSelfNetworkError();229 return;230 },231 else => return e,232 };233 defer ctx.gpa.free(body);234235 const parsed = std.json.parseFromSlice(std.json.Value, ctx.gpa, body, .{}) catch {236 output.printWarning("could not parse release metadata");237 return;238 };239 defer parsed.deinit();240241 const root = parsed.value;242 if (root != .object) { output.printWarning("malformed release metadata"); return; }243244 const remote_ver = blk: {245 const v = root.object.get("version") orelse { output.printWarning("malformed release metadata"); return; };246 break :blk switch (v) { .string => |s| s, else => { output.printWarning("malformed release metadata"); return; } };247 };248249 if (std.mem.eql(u8, remote_ver, version.VERSION)) {250 output.updateSelfCurrent(version.VERSION);251 return;252 }253254 output.updateSelfAvailable(version.VERSION, remote_ver);255 if (ctx.check) return;256257 var exe_buf: [std.Io.Dir.max_path_bytes]u8 = undefined;258 const install_path = exec.selfExePath(ctx.io, &exe_buf) catch {259 errors.error_info.setMessage("could not determine install path", .{});260 return error.Internal;261 };262263 264 265 if (std.mem.startsWith(u8, install_path, "/nix/store/")) {266 output.updateSelfNixManaged(remote_ver);267 if (ctx.cfg.confirm and !output.confirm("")) return;268 const ec = exec.streamLocal(ctx.io, &.{ "nix", "profile", "upgrade", ".*nina.*" }) catch {269 output.updateSelfNixFailed();270 return;271 };272 if (ec != 0) output.updateSelfNixSystemManaged();273 return;274 }275276 const plat_info = blk: {277 const pv = root.object.get("platforms") orelse break :blk null;278 if (pv != .object) break :blk null;279 break :blk pv.object.get(version.PLATFORM);280 };281 if (plat_info == null) { output.updateSelfNoPlatform(version.PLATFORM); return; }282283 const dl_url = blk: {284 const v = plat_info.?.object.get("url") orelse { output.printWarning("malformed release metadata"); return; };285 break :blk switch (v) { .string => |s| s, else => { output.printWarning("malformed release metadata"); return; } };286 };287288 const raw_cksum = blk: {289 if (plat_info.?.object.get("blake3")) |v|290 break :blk switch (v) { .string => |s| s, else => { output.printWarning("malformed release metadata"); return; } };291 if (plat_info.?.object.get("checksum")) |v|292 break :blk switch (v) { .string => |s| s, else => { output.printWarning("malformed release metadata"); return; } };293 output.printWarning("missing checksum in release metadata");294 return;295 };296 const expected_hex = if (std.mem.startsWith(u8, raw_cksum, "blake3:")) raw_cksum["blake3:".len..] else raw_cksum;297298 if (ctx.cfg.confirm and !output.confirm("")) return;299300 output.updateSelfDownloading(remote_ver, version.PLATFORM);301 const tmp_path = exec.downloadToTemp(ctx.gpa, ctx.io, dl_url) catch {302 output.printWarning("download failed — check your connection and try again");303 return;304 };305 defer ctx.gpa.free(tmp_path);306307 output.updateSelfVerifying();308 exec.verifyAndInstall(ctx.gpa, ctx.io, tmp_path, expected_hex, install_path) catch |e| switch (e) {309 error.ChecksumMismatch => { output.updateSelfChecksumFailed(); return; },310 else => return e,311 };312313 output.updateSelfDone(version.VERSION, remote_ver);314}315316317pub fn goodbye(ctx: Ctx) !void {318 output.goodbyeStart();319320 var exe_buf: [std.Io.Dir.max_path_bytes]u8 = undefined;321 const install_path = exec.selfExePath(ctx.io, &exe_buf) catch {322 errors.error_info.setMessage("could not determine install path", .{});323 return error.Internal;324 };325326 if (std.mem.startsWith(u8, install_path, "/nix/store/")) {327 output.goodbyeNix();328 if (ctx.cfg.confirm and !output.confirm("")) return;329 const ec = exec.streamLocal(ctx.io, &.{ "nix", "profile", "remove", ".*nina.*" }) catch {330 output.goodbyeFailed("nix profile remove failed");331 return;332 };333 if (ec != 0) {334 output.goodbyeNixSystemManaged();335 return;336 }337 } else {338 output.goodbyeFile(install_path);339 if (ctx.cfg.confirm and !output.confirm("")) return;340 std.Io.Dir.deleteFileAbsolute(ctx.io, install_path) catch |e| {341 errors.error_info.setMessage("could not remove {s}", .{install_path});342 return e;343 };344 }345346 347 if (ctx.environ.get("HOME")) |home| {348 const nina_nix: ?[]u8 = std.fmt.allocPrint(ctx.gpa, "{s}/nina.nix", .{home}) catch null;349 if (nina_nix) |path| {350 defer ctx.gpa.free(path);351 const exists = blk: {352 const bytes = std.Io.Dir.readFileAlloc(.cwd(), ctx.io, path, ctx.gpa, .unlimited) catch break :blk false;353 ctx.gpa.free(bytes);354 break :blk true;355 };356 if (exists) {357 output.goodbyeNixFileFound(path);358 if (!ctx.cfg.confirm or output.confirm("")) {359 std.Io.Dir.deleteFileAbsolute(ctx.io, path) catch {360 output.printWarning("could not remove ~/nina.nix — remove it manually to clean up");361 };362 }363 }364 }365 }366367 output.goodbyeDone();368}369370fn channelUpdateCmd(ctx: Ctx) !void {371 output.updateStart(ctx.machine.name);372 _ = try exec.channelUpdate(ctx.io, &ctx.machine, ctx.gpa);373 output.updateDone();374}375376pub fn upgrade(ctx: Ctx) !void {377 output.upgradeStart(ctx.machine.name);378 output.printSubstep("updating channels", .{});379 _ = exec.channelUpdate(ctx.io, &ctx.machine, ctx.gpa) catch {};380 output.printSubstep("rebuilding", .{});381 const start = std.Io.Clock.awake.now(ctx.io);382 const code = try exec.nixosRebuildSwitch(ctx.io, &ctx.machine, ctx.gpa);383 if (code != 0) {384 output.applyFailed("");385 return error.BuildFailed;386 }387 const elapsed_ns = @max(0, start.untilNow(ctx.io, .awake).toNanoseconds());388 const elapsed: u64 = @intCast(@divFloor(elapsed_ns, 1_000_000));389 const gen = exec.getCurrentGeneration(ctx.gpa, ctx.io, &ctx.machine) catch 0;390 output.applyDone(gen, elapsed, false);391}392393pub fn log(ctx: Ctx) !void {394 output.logHeader(ctx.machine.name);395 const content = exec.readNinaLog(ctx.gpa, ctx.io, ctx.environ, ctx.last) catch {396 397 return;398 };399 defer ctx.gpa.free(content);400 var lines = std.mem.splitScalar(u8, content, '\n');401 while (lines.next()) |line| {402 const t = std.mem.trim(u8, line, " \t\r");403 if (t.len == 0) continue;404 if (t[0] == '{') {405 renderJsonLogLine(ctx.gpa, t);406 } else {407 408 var parts = std.mem.tokenizeScalar(u8, t, '\t');409 const date = parts.next() orelse continue;410 const time = parts.next() orelse "";411 const action = parts.next() orelse "";412 const detail = parts.next() orelse "";413 output.logRow(date, time, action, detail);414 }415 }416}417418419420fn renderJsonLogLine(gpa: std.mem.Allocator, line: []const u8) void {421 const parsed = std.json.parseFromSlice(std.json.Value, gpa, line, .{}) catch {422 output.logRow(line, "", "", ""); 423 return;424 };425 defer parsed.deinit();426 if (parsed.value != .object) return;427 const o = parsed.value.object;428429 const ts = jsonStr(o, "ts");430 const date = if (ts.len >= 10) ts[0..10] else ts;431 const time = if (ts.len >= 16) ts[11..16] else "";432 const action = jsonStr(o, "command");433 const outcome = jsonStr(o, "outcome");434435 var buf: [160]u8 = undefined;436 var detail: []const u8 = outcome;437 if (o.get("gen_after")) |g| {438 if (g == .integer) {439 detail = std.fmt.bufPrint(&buf, "{s} -> gen {d}", .{ outcome, g.integer }) catch outcome;440 }441 }442 output.logRow(date, time, action, detail);443}444445fn jsonStr(o: std.json.ObjectMap, key: []const u8) []const u8 {446 if (o.get(key)) |v| {447 if (v == .string) return v.string;448 }449 return "";450}451452pub fn edit(ctx: Ctx) !void {453 const hardware = ctx.args.len > 0 and std.mem.eql(u8, ctx.args[0], "hardware");454 const filename = if (hardware) "hardware-configuration.nix" else "configuration.nix";455 const path = try std.fmt.allocPrint(ctx.gpa, "{s}/{s}", .{ ctx.machine.config_path, filename });456 defer ctx.gpa.free(path);457 output.editOpen(path, ctx.cfg.editor);458 _ = try exec.openEditor(ctx.io, ctx.gpa, ctx.cfg.editor, path, null);459}460461pub fn fmt(ctx: Ctx) !void {462 const path = try std.fmt.allocPrint(ctx.gpa, "{s}/configuration.nix", .{ctx.machine.config_path});463 defer ctx.gpa.free(path);464 if (ctx.check) {465 const code = try exec.nixpkgsFmt(ctx.io, &ctx.machine, ctx.gpa, path, true);466 if (code != 0) output.fmtNeedsFormat() else output.fmtDone();467 return;468 }469 output.fmtStart(path);470 _ = try exec.nixpkgsFmt(ctx.io, &ctx.machine, ctx.gpa, path, false);471 output.fmtDone();472}473474pub fn repl(ctx: Ctx) !void {475 output.replStart();476 _ = try exec.nixRepl(ctx.io, &ctx.machine, ctx.gpa);477}478479pub fn develop(ctx: Ctx) !void {480 if (std.mem.eql(u8, ctx.sub, "show")) {481 _ = try exec.nixBuild(ctx.io, &ctx.machine, ctx.gpa, null);482 return;483 }484 const run_cmd: ?[]const u8 = if (ctx.args.len > 0 and std.mem.eql(u8, ctx.sub, "run"))485 ctx.args[0]486 else487 null;488 output.developStart(&.{});489 _ = try exec.nixDevelop(ctx.io, &ctx.machine, ctx.gpa, run_cmd);490 output.developDone();491}492493pub fn info(ctx: Ctx) !void {494 output.infoHeader(ctx.machine.name);495 const nixos_ver = exec.systemInfo(ctx.gpa, ctx.io, &ctx.machine) catch try ctx.gpa.dupe(u8, "unknown");496 defer ctx.gpa.free(nixos_ver);497 output.infoRow("nixos", std.mem.trim(u8, nixos_ver, " \t\n\r"));498499 const kernel = exec.kernelInfo(ctx.gpa, ctx.io, &ctx.machine) catch try ctx.gpa.dupe(u8, "unknown");500 defer ctx.gpa.free(kernel);501 output.infoRow("kernel", std.mem.trim(u8, kernel, " \t\n\r"));502503 const uptime = exec.uptimeInfo(ctx.gpa, ctx.io, &ctx.machine) catch try ctx.gpa.dupe(u8, "unknown");504 defer ctx.gpa.free(uptime);505 output.infoRow("uptime", std.mem.trim(u8, uptime, " \t\n\r"));506}507508pub fn boot(ctx: Ctx) !void {509 output.bootHeader(ctx.machine.name);510 const entries = exec.bootEntries(ctx.gpa, ctx.io, &ctx.machine) catch &.{};511 defer ctx.gpa.free(entries);512 for (entries, 0..) |entry, i| output.bootEntry(entry, i);513}514515pub fn searchCmd(ctx: Ctx) !void {516 const query = if (ctx.args.len > 0) ctx.args[0] else "";517 api.ensureRegistryPinned(ctx.gpa, ctx.io, ctx.environ);518 const result = try search_tui.run(ctx.gpa, ctx.io, query, .packages);519 if (result) |r| {520 installFromResult(ctx, r) catch |e| return e;521 }522}523524pub fn optionCmd(ctx: Ctx) !void {525 const query = if (ctx.args.len > 0) ctx.args[0] else "";526 api.ensureRegistryPinned(ctx.gpa, ctx.io, ctx.environ);527 _ = try search_tui.run(ctx.gpa, ctx.io, query, .options);528}529530pub fn install(ctx: Ctx) !void {531 const pkg_name = if (ctx.args.len > 0) ctx.args[0] else return error.PackageNotFound;532533 api.ensureRegistryPinned(ctx.gpa, ctx.io, ctx.environ);534535 536 const pkgs = api.searchPackages(ctx.gpa, ctx.io, pkg_name) catch {537 errors.error_info.setMessage("'{s}' not found in nixpkgs", .{pkg_name});538 errors.error_info.setSuggestion("nina search {s}", .{pkg_name});539 return error.PackageNotFound;540 };541 defer ctx.gpa.free(pkgs);542543 if (pkgs.len == 0) {544 errors.error_info.setMessage("'{s}' not found in nixpkgs", .{pkg_name});545 errors.error_info.setSuggestion("nina search {s}", .{pkg_name});546 return error.PackageNotFound;547 }548549 550 551 var found = pkgs[0];552 for (pkgs) |p| {553 if (std.mem.eql(u8, p.pname, pkg_name)) {554 found = p;555 break;556 }557 }558 output.installSelectPrompt(found.pname, found.version);559560 var choice_buf: [8]u8 = undefined;561 var choice_len: usize = 0;562 while (choice_len < choice_buf.len) {563 const n = std.posix.read(std.posix.STDIN_FILENO, choice_buf[choice_len .. choice_len + 1]) catch break;564 if (n == 0) break;565 if (choice_buf[choice_len] == '\n') break;566 choice_len += 1;567 }568 const choice = std.mem.trim(u8, choice_buf[0..choice_len], " \t");569570 if (choice.len == 0 or choice[0] == 'i' or choice[0] == 'I') {571 try profileInstallPkg(ctx, found);572 } else if (choice[0] == 's' or choice[0] == 'S') {573 try systemInstallPkg(ctx, found);574 } else if (choice[0] == 't' or choice[0] == 'T') {575 try tryPkg(ctx, found);576 }577}578579fn installFromResult(ctx: Ctx, result: types.SearchResult) !void {580 switch (result.action) {581 .profile => try profileInstallPkg(ctx, result.package),582 .system => try systemInstallPkg(ctx, result.package),583 }584}585586fn profileInstallPkg(ctx: Ctx, npkg: types.NixPackage) !void {587 output.installStart(npkg.pname, .profile);588 output.printSubstep("nix profile install nixpkgs#{s}", .{npkg.attr});589 output.printTeach(try std.fmt.allocPrint(ctx.gpa, "nix profile install nixpkgs#{s}", .{npkg.attr}));590 const code = try exec.profileInstall(ctx.io, &ctx.machine, ctx.gpa, npkg.attr);591 if (code != 0) return error.ProcessFailed;592 output.installDone(npkg.pname);593}594595fn systemInstallPkg(ctx: Ctx, npkg: types.NixPackage) !void {596 const config_path = try std.fmt.allocPrint(ctx.gpa, "{s}/configuration.nix", .{ctx.machine.config_path});597 defer ctx.gpa.free(config_path);598 const line = api.findSystemPackagesLine(ctx.io, config_path) orelse 1;599 output.installEditorPrompt(npkg.pname, config_path, line, ctx.cfg.editor);600 _ = try exec.openEditor(ctx.io, ctx.gpa, ctx.cfg.editor, config_path, line);601 if (ctx.cfg.confirm) {602 if (output.confirm("apply now? ")) {603 try apply(ctx);604 }605 }606}607608fn tryPkg(ctx: Ctx, npkg: types.NixPackage) !void {609 output.tryStart(npkg.pname);610 output.printSubstep("nix shell nixpkgs#{s}", .{npkg.attr});611 _ = try exec.nixShell(ctx.io, &ctx.machine, ctx.gpa, npkg.attr);612 output.tryDone();613}614615pub fn remove(ctx: Ctx) !void {616 const pkg_name = if (ctx.args.len > 0) ctx.args[0] else return error.PackageNotFound;617 output.removeStart(pkg_name);618 const code = try exec.profileRemove(ctx.io, &ctx.machine, ctx.gpa, pkg_name);619 if (code != 0) return error.ProcessFailed;620 output.done();621}622623pub fn list(ctx: Ctx) !void {624 output.listHeader(ctx.machine.name);625626 627 628 629 if (exec.profileListJson(ctx.gpa, ctx.io, &ctx.machine) catch null) |json| {630 defer ctx.gpa.free(json);631 output.listSection("profile");632 renderProfileJson(ctx.gpa, json);633 }634635 636 637 if (exec.systemPackagePaths(ctx.gpa, ctx.io, &ctx.machine) catch null) |paths| {638 defer ctx.gpa.free(paths);639 output.blankLine();640 output.listSection("system");641 var lines = std.mem.splitScalar(u8, paths, '\n');642 while (lines.next()) |line| {643 const t = std.mem.trim(u8, line, " \t\r");644 if (t.len == 0) continue;645 const row = pkgFromStorePath(t);646 647 if (row.version.len == 0) continue;648 output.listPkg(row.name, row.version);649 }650 }651}652653const PkgRow = struct { name: []const u8, version: []const u8 };654655656657658659fn pkgFromStorePath(path: []const u8) PkgRow {660 const base = std.fs.path.basename(path);661 const after_hash = if (base.len > 33 and base[32] == '-') base[33..] else base;662 var i: usize = 0;663 while (i + 1 < after_hash.len) : (i += 1) {664 if (after_hash[i] == '-' and std.ascii.isDigit(after_hash[i + 1])) {665 return .{ .name = after_hash[0..i], .version = after_hash[i + 1 ..] };666 }667 }668 return .{ .name = after_hash, .version = "" };669}670671test "pkgFromStorePath splits name and version at first dash-digit" {672 const a = pkgFromStorePath("/nix/store/abcdefghijklmnopqrstuvwxyz012345-ripgrep-15.1.0");673 try std.testing.expectEqualStrings("ripgrep", a.name);674 try std.testing.expectEqualStrings("15.1.0", a.version);675676 const b = pkgFromStorePath("/nix/store/abcdefghijklmnopqrstuvwxyz012345-helix-25.01");677 try std.testing.expectEqualStrings("helix", b.name);678 try std.testing.expectEqualStrings("25.01", b.version);679}680681fn firstStorePath(el: std.json.Value) ?[]const u8 {682 if (el != .object) return null;683 const sp = el.object.get("storePaths") orelse return null;684 if (sp != .array or sp.array.items.len == 0) return null;685 const first = sp.array.items[0];686 return if (first == .string) first.string else null;687}688689690691fn renderProfileJson(gpa: std.mem.Allocator, json: []const u8) void {692 const parsed = std.json.parseFromSlice(std.json.Value, gpa, json, .{}) catch return;693 defer parsed.deinit();694 if (parsed.value != .object) return;695 const elements = parsed.value.object.get("elements") orelse return;696 switch (elements) {697 .object => |obj| {698 var it = obj.iterator();699 while (it.next()) |entry| {700 const ver = if (firstStorePath(entry.value_ptr.*)) |p| pkgFromStorePath(p).version else "";701 output.listPkg(entry.key_ptr.*, ver);702 }703 },704 .array => |arr| {705 for (arr.items) |el| {706 const path = firstStorePath(el) orelse continue;707 const row = pkgFromStorePath(path);708 output.listPkg(row.name, row.version);709 }710 },711 else => {},712 }713}714715pub fn tryCmd(ctx: Ctx) !void {716 const pkg_name_arg = if (ctx.args.len > 0) ctx.args[0] else return error.PackageNotFound;717 const shell_pkg = types.NixPackage{ .attr = pkg_name_arg, .pname = pkg_name_arg, .version = "", .description = "" };718 try tryPkg(ctx, shell_pkg);719}720721722723pub fn service(ctx: Ctx) !void {724 if (std.mem.eql(u8, ctx.sub, "list") or ctx.sub.len == 0) {725 output.serviceListHeader(ctx.machine.name);726 const svcs = exec.serviceList(ctx.gpa, ctx.io, &ctx.machine) catch &.{};727 defer ctx.gpa.free(svcs);728 for (svcs) |s| output.serviceRow(s);729 return;730 }731 if (std.mem.eql(u8, ctx.sub, "logs")) {732 const name = if (ctx.args.len > 0) ctx.args[0] else return error.Internal;733 const follow = ctx.args.len > 1 and std.mem.eql(u8, ctx.args[1], "-f");734 output.serviceLogsHeader(name, ctx.last, ctx.machine.name);735 _ = try exec.serviceLogs(ctx.io, &ctx.machine, ctx.gpa, name, ctx.last, follow);736 return;737 }738 if (std.mem.eql(u8, ctx.sub, "status")) {739 const name = if (ctx.args.len > 0) ctx.args[0] else return error.Internal;740 output.serviceVerb("status", name, ctx.machine.name);741 _ = try exec.serviceCtl(ctx.io, &ctx.machine, ctx.gpa, "status", name);742 return;743 }744 745 const name = if (ctx.args.len > 0) ctx.args[0] else return error.Internal;746 if (ctx.cfg.confirm and (std.mem.eql(u8, ctx.sub, "stop") or std.mem.eql(u8, ctx.sub, "disable"))) {747 if (!output.confirm("")) return;748 }749 output.serviceVerb(ctx.sub, name, ctx.machine.name);750 _ = try exec.serviceCtl(ctx.io, &ctx.machine, ctx.gpa, ctx.sub, name);751 output.serviceDone();752}753754755756pub fn flake(ctx: Ctx) !void {757 if (std.mem.eql(u8, ctx.sub, "update")) {758 output.flakeUpdateStart();759 const input: ?[]const u8 = if (ctx.args.len > 0) ctx.args[0] else null;760 _ = try exec.flakeUpdate(ctx.io, &ctx.machine, ctx.gpa, input);761 output.flakeLockWritten();762 return;763 }764 if (std.mem.eql(u8, ctx.sub, "check")) {765 output.flakeCheckStart();766 const code = try exec.flakeCheck(ctx.io, &ctx.machine, ctx.gpa);767 if (code == 0) output.flakeValid() else output.checkFailed("");768 return;769 }770 if (std.mem.eql(u8, ctx.sub, "show")) {771 const json_out = exec.flakeShow(ctx.gpa, ctx.io, &ctx.machine) catch null;772 if (json_out) |j| {773 defer ctx.gpa.free(j);774 output.flakeShowHeader("flake outputs");775 776 const parsed = std.json.parseFromSlice(std.json.Value, ctx.gpa, j, .{}) catch return;777 defer parsed.deinit();778 if (parsed.value == .object) {779 var it = parsed.value.object.iterator();780 while (it.next()) |entry| {781 if (entry.value_ptr.* == .object) {782 var inner = entry.value_ptr.object.iterator();783 while (inner.next()) |e2| {784 const desc = if (e2.value_ptr.* == .object)785 if (e2.value_ptr.object.get("description")) |d|786 if (d == .string) d.string else ""787 else ""788 else "";789 output.flakeShowEntry(entry.key_ptr.*, e2.key_ptr.*, desc);790 }791 }792 }793 }794 }795 return;796 }797 if (std.mem.eql(u8, ctx.sub, "init")) {798 _ = try exec.flakeInit(ctx.io, &ctx.machine, ctx.gpa);799 return;800 }801 if (std.mem.eql(u8, ctx.sub, "lock")) {802 _ = try exec.flakeLock(ctx.io, &ctx.machine, ctx.gpa);803 return;804 }805 if (std.mem.eql(u8, ctx.sub, "clone")) {806 const url = if (ctx.args.len > 0) ctx.args[0] else return error.Internal;807 _ = try exec.flakeClone(ctx.io, &ctx.machine, ctx.gpa, url);808 return;809 }810}811812813814pub fn store(ctx: Ctx) !void {815 if (std.mem.eql(u8, ctx.sub, "gc")) {816 _ = try exec.storeGc(ctx.io, &ctx.machine, ctx.gpa);817 return;818 }819 if (std.mem.eql(u8, ctx.sub, "verify")) {820 output.storeVerifyStart();821 _ = try exec.storeVerify(ctx.io, &ctx.machine, ctx.gpa);822 return;823 }824 if (std.mem.eql(u8, ctx.sub, "repair")) {825 if (ctx.cfg.confirm and !output.confirm("")) return;826 output.storeRepairStart();827 _ = try exec.storeRepair(ctx.io, &ctx.machine, ctx.gpa);828 return;829 }830 if (std.mem.eql(u8, ctx.sub, "info")) {831 const si = try exec.storeInfo(ctx.gpa, ctx.io, &ctx.machine);832 output.storeInfo(si);833 return;834 }835 if (std.mem.eql(u8, ctx.sub, "path")) {836 const attr = if (ctx.args.len > 0) ctx.args[0] else return error.Internal;837 const path = try exec.storePath(ctx.gpa, ctx.io, &ctx.machine, attr);838 defer ctx.gpa.free(path);839 output.storePath(path);840 return;841 }842 843 const si = try exec.storeInfo(ctx.gpa, ctx.io, &ctx.machine);844 output.storeInfo(si);845}846847848849pub fn channel(ctx: Ctx) !void {850 if (std.mem.eql(u8, ctx.sub, "list") or ctx.sub.len == 0) {851 output.channelListHeader(ctx.machine.name);852 const chans = try exec.channelList(ctx.gpa, ctx.io, &ctx.machine);853 defer ctx.gpa.free(chans);854 for (chans) |ch| output.channelRow(ch);855 return;856 }857 if (std.mem.eql(u8, ctx.sub, "add")) {858 const url = if (ctx.args.len > 0) ctx.args[0] else return error.Internal;859 const name = if (ctx.args.len > 1) ctx.args[1] else "";860 _ = try exec.channelAdd(ctx.io, &ctx.machine, ctx.gpa, url, name);861 output.channelDone();862 return;863 }864 if (std.mem.eql(u8, ctx.sub, "remove")) {865 const name = if (ctx.args.len > 0) ctx.args[0] else return error.Internal;866 _ = try exec.channelRemove(ctx.io, &ctx.machine, ctx.gpa, name);867 output.channelDone();868 return;869 }870 if (std.mem.eql(u8, ctx.sub, "update")) {871 return channelUpdateCmd(ctx);872 }873}874875876877pub fn profile(ctx: Ctx) !void {878 if (std.mem.eql(u8, ctx.sub, "list") or ctx.sub.len == 0) {879 output.profileListHeader();880 const out = exec.profileList(ctx.gpa, ctx.io, &ctx.machine) catch null;881 if (out) |o| {882 defer ctx.gpa.free(o);883 var lines = std.mem.splitScalar(u8, o, '\n');884 while (lines.next()) |line| {885 const t = std.mem.trim(u8, line, " \t\r");886 if (t.len == 0) continue;887 output.profileRow(t, "", "");888 }889 }890 return;891 }892 if (std.mem.eql(u8, ctx.sub, "install")) {893 const attr = if (ctx.args.len > 0) ctx.args[0] else return error.PackageNotFound;894 _ = try exec.profileInstall(ctx.io, &ctx.machine, ctx.gpa, attr);895 output.done();896 return;897 }898 if (std.mem.eql(u8, ctx.sub, "remove")) {899 const attr = if (ctx.args.len > 0) ctx.args[0] else return error.PackageNotFound;900 _ = try exec.profileRemove(ctx.io, &ctx.machine, ctx.gpa, attr);901 output.done();902 return;903 }904 if (std.mem.eql(u8, ctx.sub, "upgrade")) {905 _ = try exec.profileUpgrade(ctx.io, &ctx.machine, ctx.gpa);906 output.done();907 return;908 }909}910911912913pub fn pkg(ctx: Ctx) !void {914 const name = if (ctx.args.len > 0) ctx.args[0] else return error.PackageNotFound;915 if (std.mem.eql(u8, ctx.sub, "why")) {916 _ = try exec.pkgWhy(ctx.io, &ctx.machine, ctx.gpa, name);917 return;918 }919 if (std.mem.eql(u8, ctx.sub, "deps")) {920 _ = try exec.pkgDeps(ctx.io, &ctx.machine, ctx.gpa, name);921 return;922 }923 if (std.mem.eql(u8, ctx.sub, "size")) {924 const s = exec.pkgSize(ctx.gpa, ctx.io, &ctx.machine, name) catch null;925 if (s) |sz| {926 defer ctx.gpa.free(sz);927 output.pkgResult("size", sz);928 }929 return;930 }931 if (std.mem.eql(u8, ctx.sub, "path")) {932 const p = try exec.storePath(ctx.gpa, ctx.io, &ctx.machine, name);933 defer ctx.gpa.free(p);934 output.pkgResult("path", p);935 return;936 }937 if (std.mem.eql(u8, ctx.sub, "closure")) {938 _ = try exec.pkgClosure(ctx.io, &ctx.machine, ctx.gpa, name);939 return;940 }941}942943944945pub fn pin(ctx: Ctx) !void {946 const input = if (ctx.args.len > 0) ctx.args[0] else return error.Internal;947 const commit = if (ctx.args.len > 1) ctx.args[1] else return error.Internal;948 exec.editFlakeLock(ctx.gpa, input, commit) catch return error.ProcessFailed;949 output.pinDone(input, commit);950}951952pub fn unpin(ctx: Ctx) !void {953 const input = if (ctx.args.len > 0) ctx.args[0] else return error.Internal;954 _ = try exec.flakeUpdate(ctx.io, &ctx.machine, ctx.gpa, input);955 output.unpinDone(input);956}957958959960pub fn hash(ctx: Ctx) !void {961 const path = if (ctx.args.len > 0) ctx.args[0] else return error.Internal;962 const h = try exec.nixHash(ctx.gpa, ctx.io, &ctx.machine, path);963 defer ctx.gpa.free(h);964 output.hashResult(std.mem.trim(u8, h, " \t\n\r"));965}966967pub fn fetch(ctx: Ctx) !void {968 const url = if (ctx.args.len > 0) ctx.args[0] else return error.Internal;969 const h = try exec.nixFetch(ctx.gpa, ctx.io, &ctx.machine, url);970 defer ctx.gpa.free(h);971 output.fetchResult(std.mem.trim(u8, h, " \t\n\r"), url);972}973974975976pub fn build(ctx: Ctx) !void {977 const attr: ?[]const u8 = if (ctx.args.len > 0) ctx.args[0] else null;978 output.buildStart(attr orelse "default");979 const code = try exec.nixBuild(ctx.io, &ctx.machine, ctx.gpa, attr);980 if (code != 0) return error.BuildFailed;981 output.buildDone();982}983984pub fn runCmd(ctx: Ctx) !void {985 const pkg_arg = if (ctx.args.len > 0) ctx.args[0] else return error.PackageNotFound;986 output.runStart(pkg_arg);987 _ = try exec.nixRun(ctx.io, &ctx.machine, ctx.gpa, pkg_arg);988}989990991992pub fn hello(ctx: Ctx) !void {993 output.hello(ctx.cfg.machines);994}995996pub fn setup(gpa: std.mem.Allocator, io: std.Io, environ: *const std.process.Environ.Map) !void {997 output.setupCheck();998 const status = exec.checkNixFeatures(gpa, io, environ);999 output.setupFeatures(status.nix_command, status.flakes);10001001 if (status.allEnabled()) {1002 output.setupAllSet();1003 api.ensureRegistryPinned(gpa, io, environ);1004 return;1005 }10061007 output.setupEnabling();1008 const path = try exec.enableNixFeatures(gpa, io, environ);1009 defer gpa.free(path);10101011 output.setupDone(path, exec.isNixOS(io));1012 api.ensureRegistryPinned(gpa, io, environ);1013}1014101510161017fn logAction(ctx: Ctx, action: []const u8, gen: u32) void {1018 var ts_buf: [32]u8 = undefined;1019 const ts = nowIso(ctx.io, &ts_buf);1020 var buf: [256]u8 = undefined;1021 const entry = std.fmt.bufPrint(&buf, "{{\"ts\":\"{s}\",\"machine\":\"{s}\",\"command\":\"{s}\",\"outcome\":\"success\",\"gen_before\":null,\"gen_after\":{d},\"duration_ms\":0}}\n", .{1022 ts, ctx.machine.name, action, gen,1023 }) catch return;1024 exec.appendNinaLog(ctx.io, ctx.environ, entry);1025}1026102710281029fn nowIso(io: std.Io, buf: []u8) []const u8 {1030 const fallback = "1970-01-01T00:00:00Z";1031 const ns = std.Io.Clock.real.now(io).toNanoseconds();1032 if (ns <= 0) return fallback;1033 const secs: u64 = @intCast(@divFloor(ns, 1_000_000_000));1034 const es = std.time.epoch.EpochSeconds{ .secs = secs };1035 const ed = es.getEpochDay();1036 const yd = ed.calculateYearDay();1037 const md = yd.calculateMonthDay();1038 const ds = es.getDaySeconds();1039 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", .{1040 yd.year, md.month.numeric(), @as(u32, md.day_index) + 1,1041 ds.getHoursIntoDay(), ds.getMinutesIntoHour(), ds.getSecondsIntoMinute(),1042 }) catch return fallback;1043}