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; };8384 85 86 var prev: u32 = 0;87 var has_prev = false;88 for (gens) |g| {89 if (g.number < current and (!has_prev or g.number > prev)) {90 prev = g.number;91 has_prev = true;92 }93 }94 if (!has_prev) {95 output.alreadyOldest();96 return;97 }9899 output.backStart(ctx.machine.name, current, prev);100 if (ctx.cfg.confirm and !output.confirm("")) return;101102 const code = try exec.rollback(ctx.io, &ctx.machine, ctx.gpa);103 if (code != 0) return error.BuildFailed;104 output.backDone(prev);105}106107pub fn go(ctx: Ctx) !void {108 if (ctx.args.len == 0) return error.Internal;109 const target = std.fmt.parseInt(u32, ctx.args[0], 10) catch return error.Internal;110 const current = try exec.getCurrentGeneration(ctx.gpa, ctx.io, &ctx.machine);111112 output.goStart(ctx.machine.name, current, target);113 if (ctx.cfg.confirm and !output.confirm("")) return;114115 const code = try exec.switchGeneration(ctx.io, &ctx.machine, ctx.gpa, target);116 if (code != 0) return error.ProcessFailed;117 output.generationDone(target);118}119120pub fn history(ctx: Ctx) !void {121 const gens = try exec.getGenerations(ctx.gpa, ctx.io, &ctx.machine);122 defer ctx.gpa.free(gens);123 output.historyHeader(ctx.machine.name);124 for (gens) |g| output.historyRow(g);125}126127pub fn genCmd(ctx: Ctx) !void {128 if (std.mem.eql(u8, ctx.sub, "list")) return history(ctx);129 if (std.mem.eql(u8, ctx.sub, "current")) {130 const n = try exec.getCurrentGeneration(ctx.gpa, ctx.io, &ctx.machine);131 output.genCurrent(n);132 return;133 }134 if (std.mem.eql(u8, ctx.sub, "delete")) {135 if (ctx.args.len == 0) return error.Internal;136 if (std.mem.eql(u8, ctx.args[0], "old")) {137 if (ctx.cfg.confirm and !output.confirm("")) return;138 _ = try exec.deleteOldGenerations(ctx.io, &ctx.machine, ctx.gpa);139 } else {140 const n = std.fmt.parseInt(u32, ctx.args[0], 10) catch return error.Internal;141 if (ctx.cfg.confirm and !output.confirm("")) return;142 _ = try exec.deleteGeneration(ctx.io, &ctx.machine, ctx.gpa, n);143 }144 output.done();145 return;146 }147 return error.Internal;148}149150pub fn clean(ctx: Ctx) !void {151 const gens = try exec.getGenerations(ctx.gpa, ctx.io, &ctx.machine);152 defer ctx.gpa.free(gens);153 output.cleanStart(ctx.machine.name, ctx.cfg.generations, @intCast(gens.len));154 if (ctx.cfg.confirm and !output.confirm("removing — cannot be undone")) return;155 const all = ctx.all or std.mem.eql(u8, ctx.sub, "all");156 _ = try exec.collectGarbage(ctx.io, &ctx.machine, ctx.gpa, all, ctx.cfg.generations);157 output.cleanDone("?");158}159160pub fn statusCmd(ctx: Ctx) !void {161 output.statusHeader();162 163 164 if (ctx.all and ctx.cfg.machines.len > 0) {165 for (ctx.cfg.machines) |m| statusRowFor(ctx, &m);166 return;167 }168 statusRowFor(ctx, &ctx.machine);169}170171fn statusRowFor(ctx: Ctx, machine: *const types.Machine) void {172 173 174 const gen = exec.getCurrentGeneration(ctx.gpa, ctx.io, machine) catch {175 output.statusRow(machine.name, 0, "", "", false);176 return;177 };178 const st = exec.systemStatus(ctx.gpa, ctx.io, machine);179 defer ctx.gpa.free(st.size);180 defer ctx.gpa.free(st.age);181 output.statusRow(machine.name, gen, st.age, st.size, true);182}183184pub fn diff(ctx: Ctx) !void {185 const gens = try exec.getGenerations(ctx.gpa, ctx.io, &ctx.machine);186 defer ctx.gpa.free(gens);187 var current: u32 = 0;188 var prev: u32 = 0;189 for (gens) |g| {190 if (g.current) current = g.number;191 if (!g.current and g.number > prev and g.number < current) prev = g.number;192 }193 if (ctx.args.len >= 2) {194 const a = std.fmt.parseInt(u32, ctx.args[0], 10) catch current - 1;195 const b = std.fmt.parseInt(u32, ctx.args[1], 10) catch current;196 output.diffHeader(a, b, ctx.machine.name);197 if (try exec.diffClosures(ctx.io, &ctx.machine, ctx.gpa, a, b) != 0) return error.ProcessFailed;198 } else {199 output.diffHeader(if (prev > 0) prev else current - 1, current, ctx.machine.name);200 if (try exec.diffClosures(ctx.io, &ctx.machine, ctx.gpa, if (prev > 0) prev else current - 1, current) != 0) return error.ProcessFailed;201 }202}203204pub fn mood(ctx: Ctx) !void {205 const gen = exec.getCurrentGeneration(ctx.gpa, ctx.io, &ctx.machine) catch 0;206 const st = exec.systemStatus(ctx.gpa, ctx.io, &ctx.machine);207 defer ctx.gpa.free(st.size);208 defer ctx.gpa.free(st.age);209 const state = exec.systemRunningState(ctx.gpa, ctx.io, &ctx.machine) catch try ctx.gpa.dupe(u8, "unknown");210 defer ctx.gpa.free(state);211 output.mood(ctx.machine.name, state, gen, st.age, st.size);212}213214pub fn doctor(ctx: Ctx) !void {215 output.doctorHeader(ctx.machine.name);216 const checks = try exec.runDoctorChecks(ctx.gpa, ctx.io, &ctx.machine);217 defer ctx.gpa.free(checks);218 var warnings: u32 = 0;219 var failures: u32 = 0;220 for (checks) |ch| {221 output.doctorRow(ch);222 if (ch.status == .warn) warnings += 1;223 if (ch.status == .fail) failures += 1;224 }225 output.doctorSummary(warnings, failures);226}227228229pub fn update(ctx: Ctx) !void {230 output.updateSelfStart();231232 if (comptime std.mem.eql(u8, version.PLATFORM, "unsupported")) {233 output.updateSelfNoPlatform(@tagName(builtin.cpu.arch) ++ "-" ++ @tagName(builtin.os.tag));234 return error.Internal;235 }236237 const env_url = ctx.environ.get("NINA_UPDATE_SOURCE");238 const source_url = env_url orelse UPDATE_URL;239240 const body = exec.fetchUrl(ctx.gpa, ctx.io, source_url) catch |e| switch (e) {241 error.NetworkError, error.HttpError => {242 output.updateSelfNetworkError();243 return;244 },245 else => return e,246 };247 defer ctx.gpa.free(body);248249 const parsed = std.json.parseFromSlice(std.json.Value, ctx.gpa, body, .{}) catch {250 output.printWarning("could not parse release metadata");251 return;252 };253 defer parsed.deinit();254255 const root = parsed.value;256 if (root != .object) { output.printWarning("malformed release metadata"); return; }257258 const remote_ver = blk: {259 const v = root.object.get("version") orelse { output.printWarning("malformed release metadata"); return; };260 break :blk switch (v) { .string => |s| s, else => { output.printWarning("malformed release metadata"); return; } };261 };262263 if (std.mem.eql(u8, remote_ver, version.VERSION)) {264 output.updateSelfCurrent(version.VERSION);265 return;266 }267268 output.updateSelfAvailable(version.VERSION, remote_ver);269 if (ctx.check) return;270271 var exe_buf: [std.Io.Dir.max_path_bytes]u8 = undefined;272 const install_path = exec.selfExePath(ctx.io, &exe_buf) catch {273 errors.error_info.setMessage("could not determine install path", .{});274 return error.Internal;275 };276277 278 279 if (std.mem.startsWith(u8, install_path, "/nix/store/")) {280 output.updateSelfNixManaged(remote_ver);281 if (ctx.cfg.confirm and !output.confirm("")) return;282 const ec = exec.streamLocal(ctx.io, &.{ "nix", "profile", "upgrade", ".*nina.*" }) catch {283 output.updateSelfNixFailed();284 return;285 };286 if (ec != 0) output.updateSelfNixSystemManaged();287 return;288 }289290 const plat_info = blk: {291 const pv = root.object.get("platforms") orelse break :blk null;292 if (pv != .object) break :blk null;293 break :blk pv.object.get(version.PLATFORM);294 };295 if (plat_info == null) { output.updateSelfNoPlatform(version.PLATFORM); return; }296297 const dl_url = blk: {298 const v = plat_info.?.object.get("url") orelse { output.printWarning("malformed release metadata"); return; };299 break :blk switch (v) { .string => |s| s, else => { output.printWarning("malformed release metadata"); return; } };300 };301302 const raw_cksum = blk: {303 if (plat_info.?.object.get("blake3")) |v|304 break :blk switch (v) { .string => |s| s, else => { output.printWarning("malformed release metadata"); return; } };305 if (plat_info.?.object.get("checksum")) |v|306 break :blk switch (v) { .string => |s| s, else => { output.printWarning("malformed release metadata"); return; } };307 output.printWarning("missing checksum in release metadata");308 return;309 };310 const expected_hex = if (std.mem.startsWith(u8, raw_cksum, "blake3:")) raw_cksum["blake3:".len..] else raw_cksum;311312 if (ctx.cfg.confirm and !output.confirm("")) return;313314 output.updateSelfDownloading(remote_ver, version.PLATFORM);315 const tmp_path = exec.downloadToTemp(ctx.gpa, ctx.io, dl_url) catch {316 output.printWarning("download failed — check your connection and try again");317 return;318 };319 defer ctx.gpa.free(tmp_path);320321 output.updateSelfVerifying();322 exec.verifyAndInstall(ctx.gpa, ctx.io, tmp_path, expected_hex, install_path) catch |e| switch (e) {323 error.ChecksumMismatch => { output.updateSelfChecksumFailed(); return; },324 else => return e,325 };326327 output.updateSelfDone(version.VERSION, remote_ver);328}329330331pub fn goodbye(ctx: Ctx) !void {332 output.goodbyeStart();333334 var exe_buf: [std.Io.Dir.max_path_bytes]u8 = undefined;335 const install_path = exec.selfExePath(ctx.io, &exe_buf) catch {336 errors.error_info.setMessage("could not determine install path", .{});337 return error.Internal;338 };339340 if (std.mem.startsWith(u8, install_path, "/nix/store/")) {341 output.goodbyeNix();342 if (ctx.cfg.confirm and !output.confirm("")) return;343 const ec = exec.streamLocal(ctx.io, &.{ "nix", "profile", "remove", ".*nina.*" }) catch {344 output.goodbyeFailed("nix profile remove failed");345 return;346 };347 if (ec != 0) {348 output.goodbyeNixSystemManaged();349 return;350 }351 } else {352 output.goodbyeFile(install_path);353 if (ctx.cfg.confirm and !output.confirm("")) return;354 std.Io.Dir.deleteFileAbsolute(ctx.io, install_path) catch |e| {355 errors.error_info.setMessage("could not remove {s}", .{install_path});356 return e;357 };358 }359360 361 if (ctx.environ.get("HOME")) |home| {362 const nina_nix: ?[]u8 = std.fmt.allocPrint(ctx.gpa, "{s}/nina.nix", .{home}) catch null;363 if (nina_nix) |path| {364 defer ctx.gpa.free(path);365 const exists = blk: {366 const bytes = std.Io.Dir.readFileAlloc(.cwd(), ctx.io, path, ctx.gpa, .unlimited) catch break :blk false;367 ctx.gpa.free(bytes);368 break :blk true;369 };370 if (exists) {371 output.goodbyeNixFileFound(path);372 if (!ctx.cfg.confirm or output.confirm("")) {373 std.Io.Dir.deleteFileAbsolute(ctx.io, path) catch {374 output.printWarning("could not remove ~/nina.nix — remove it manually to clean up");375 };376 }377 }378 }379 }380381 output.goodbyeDone();382}383384fn channelUpdateCmd(ctx: Ctx) !void {385 output.updateStart(ctx.machine.name);386 _ = try exec.channelUpdate(ctx.io, &ctx.machine, ctx.gpa);387 output.updateDone();388}389390pub fn upgrade(ctx: Ctx) !void {391 output.upgradeStart(ctx.machine.name);392 output.printSubstep("updating channels", .{});393 _ = exec.channelUpdate(ctx.io, &ctx.machine, ctx.gpa) catch {};394 output.printSubstep("rebuilding", .{});395 const start = std.Io.Clock.awake.now(ctx.io);396 const code = try exec.nixosRebuildSwitch(ctx.io, &ctx.machine, ctx.gpa);397 if (code != 0) {398 output.applyFailed("");399 return error.BuildFailed;400 }401 const elapsed_ns = @max(0, start.untilNow(ctx.io, .awake).toNanoseconds());402 const elapsed: u64 = @intCast(@divFloor(elapsed_ns, 1_000_000));403 const gen = exec.getCurrentGeneration(ctx.gpa, ctx.io, &ctx.machine) catch 0;404 output.applyDone(gen, elapsed, false);405}406407pub fn log(ctx: Ctx) !void {408 output.logHeader(ctx.machine.name);409 const content = exec.readNinaLog(ctx.gpa, ctx.io, ctx.environ, ctx.last) catch {410 411 return;412 };413 defer ctx.gpa.free(content);414 var lines = std.mem.splitScalar(u8, content, '\n');415 while (lines.next()) |line| {416 const t = std.mem.trim(u8, line, " \t\r");417 if (t.len == 0) continue;418 if (t[0] == '{') {419 renderJsonLogLine(ctx.gpa, t);420 } else {421 422 var parts = std.mem.tokenizeScalar(u8, t, '\t');423 const date = parts.next() orelse continue;424 const time = parts.next() orelse "";425 const action = parts.next() orelse "";426 const detail = parts.next() orelse "";427 output.logRow(date, time, action, detail);428 }429 }430}431432433434fn renderJsonLogLine(gpa: std.mem.Allocator, line: []const u8) void {435 const parsed = std.json.parseFromSlice(std.json.Value, gpa, line, .{}) catch {436 output.logRow(line, "", "", ""); 437 return;438 };439 defer parsed.deinit();440 if (parsed.value != .object) return;441 const o = parsed.value.object;442443 const ts = jsonStr(o, "ts");444 const date = if (ts.len >= 10) ts[0..10] else ts;445 const time = if (ts.len >= 16) ts[11..16] else "";446 const action = jsonStr(o, "command");447 const outcome = jsonStr(o, "outcome");448449 var buf: [160]u8 = undefined;450 var detail: []const u8 = outcome;451 if (o.get("gen_after")) |g| {452 if (g == .integer) {453 detail = std.fmt.bufPrint(&buf, "{s} -> gen {d}", .{ outcome, g.integer }) catch outcome;454 }455 }456 output.logRow(date, time, action, detail);457}458459fn jsonStr(o: std.json.ObjectMap, key: []const u8) []const u8 {460 if (o.get(key)) |v| {461 if (v == .string) return v.string;462 }463 return "";464}465466pub fn edit(ctx: Ctx) !void {467 const hardware = ctx.args.len > 0 and std.mem.eql(u8, ctx.args[0], "hardware");468 const filename = if (hardware) "hardware-configuration.nix" else "configuration.nix";469 const path = try std.fmt.allocPrint(ctx.gpa, "{s}/{s}", .{ ctx.machine.config_path, filename });470 defer ctx.gpa.free(path);471 output.editOpen(path, ctx.cfg.editor);472 _ = try exec.openEditor(ctx.io, ctx.gpa, ctx.cfg.editor, path, null);473}474475pub fn fmt(ctx: Ctx) !void {476 const path = try std.fmt.allocPrint(ctx.gpa, "{s}/configuration.nix", .{ctx.machine.config_path});477 defer ctx.gpa.free(path);478 if (ctx.check) {479 const code = try exec.nixpkgsFmt(ctx.io, &ctx.machine, ctx.gpa, path, true);480 if (code != 0) output.fmtNeedsFormat() else output.fmtDone();481 return;482 }483 output.fmtStart(path);484 _ = try exec.nixpkgsFmt(ctx.io, &ctx.machine, ctx.gpa, path, false);485 output.fmtDone();486}487488pub fn repl(ctx: Ctx) !void {489 output.replStart();490 _ = try exec.nixRepl(ctx.io, &ctx.machine, ctx.gpa);491}492493pub fn develop(ctx: Ctx) !void {494 if (std.mem.eql(u8, ctx.sub, "show")) {495 _ = try exec.nixBuild(ctx.io, &ctx.machine, ctx.gpa, null);496 return;497 }498 const run_cmd: ?[]const u8 = if (ctx.args.len > 0 and std.mem.eql(u8, ctx.sub, "run"))499 ctx.args[0]500 else501 null;502 output.developStart(&.{});503 _ = try exec.nixDevelop(ctx.io, &ctx.machine, ctx.gpa, run_cmd);504 output.developDone();505}506507pub fn info(ctx: Ctx) !void {508 output.infoHeader(ctx.machine.name);509 const nixos_ver = exec.systemInfo(ctx.gpa, ctx.io, &ctx.machine) catch try ctx.gpa.dupe(u8, "unknown");510 defer ctx.gpa.free(nixos_ver);511 output.infoRow("nixos", std.mem.trim(u8, nixos_ver, " \t\n\r"));512513 const kernel = exec.kernelInfo(ctx.gpa, ctx.io, &ctx.machine) catch try ctx.gpa.dupe(u8, "unknown");514 defer ctx.gpa.free(kernel);515 output.infoRow("kernel", std.mem.trim(u8, kernel, " \t\n\r"));516517 const uptime = exec.uptimeInfo(ctx.gpa, ctx.io, &ctx.machine) catch try ctx.gpa.dupe(u8, "unknown");518 defer ctx.gpa.free(uptime);519 output.infoRow("uptime", std.mem.trim(u8, uptime, " \t\n\r"));520}521522pub fn boot(ctx: Ctx) !void {523 output.bootHeader(ctx.machine.name);524 const entries = exec.bootEntries(ctx.gpa, ctx.io, &ctx.machine) catch &.{};525 defer ctx.gpa.free(entries);526 for (entries, 0..) |entry, i| output.bootEntry(entry, i);527}528529pub fn searchCmd(ctx: Ctx) !void {530 const query = if (ctx.args.len > 0) ctx.args[0] else "";531 api.ensureRegistryPinned(ctx.gpa, ctx.io, ctx.environ);532 const result = try search_tui.run(ctx.gpa, ctx.io, query, .packages);533 if (result) |r| {534 installFromResult(ctx, r) catch |e| return e;535 }536}537538pub fn optionCmd(ctx: Ctx) !void {539 const query = if (ctx.args.len > 0) ctx.args[0] else "";540 api.ensureRegistryPinned(ctx.gpa, ctx.io, ctx.environ);541 _ = try search_tui.run(ctx.gpa, ctx.io, query, .options);542}543544pub fn install(ctx: Ctx) !void {545 const pkg_name = if (ctx.args.len > 0) ctx.args[0] else return error.PackageNotFound;546547 api.ensureRegistryPinned(ctx.gpa, ctx.io, ctx.environ);548549 550 const pkgs = api.searchPackages(ctx.gpa, ctx.io, pkg_name) catch {551 errors.error_info.setMessage("'{s}' not found in nixpkgs", .{pkg_name});552 errors.error_info.setSuggestion("nina search {s}", .{pkg_name});553 return error.PackageNotFound;554 };555 defer ctx.gpa.free(pkgs);556557 if (pkgs.len == 0) {558 errors.error_info.setMessage("'{s}' not found in nixpkgs", .{pkg_name});559 errors.error_info.setSuggestion("nina search {s}", .{pkg_name});560 return error.PackageNotFound;561 }562563 564 565 var found = pkgs[0];566 for (pkgs) |p| {567 if (std.mem.eql(u8, p.pname, pkg_name)) {568 found = p;569 break;570 }571 }572 output.installSelectPrompt(found.pname, found.version);573574 var choice_buf: [8]u8 = undefined;575 var choice_len: usize = 0;576 while (choice_len < choice_buf.len) {577 const n = std.posix.read(std.posix.STDIN_FILENO, choice_buf[choice_len .. choice_len + 1]) catch break;578 if (n == 0) break;579 if (choice_buf[choice_len] == '\n') break;580 choice_len += 1;581 }582 const choice = std.mem.trim(u8, choice_buf[0..choice_len], " \t");583584 if (choice.len == 0 or choice[0] == 'i' or choice[0] == 'I') {585 try profileInstallPkg(ctx, found);586 } else if (choice[0] == 's' or choice[0] == 'S') {587 try systemInstallPkg(ctx, found);588 } else if (choice[0] == 't' or choice[0] == 'T') {589 try tryPkg(ctx, found);590 }591}592593fn installFromResult(ctx: Ctx, result: types.SearchResult) !void {594 switch (result.action) {595 .profile => try profileInstallPkg(ctx, result.package),596 .system => try systemInstallPkg(ctx, result.package),597 }598}599600fn profileInstallPkg(ctx: Ctx, npkg: types.NixPackage) !void {601 output.installStart(npkg.pname, .profile);602 output.printSubstep("nix profile install nixpkgs#{s}", .{npkg.attr});603 output.printTeach(try std.fmt.allocPrint(ctx.gpa, "nix profile install nixpkgs#{s}", .{npkg.attr}));604 const code = try exec.profileInstall(ctx.io, &ctx.machine, ctx.gpa, npkg.attr);605 if (code != 0) return error.ProcessFailed;606 output.installDone(npkg.pname);607}608609fn systemInstallPkg(ctx: Ctx, npkg: types.NixPackage) !void {610 const config_path = try std.fmt.allocPrint(ctx.gpa, "{s}/configuration.nix", .{ctx.machine.config_path});611 defer ctx.gpa.free(config_path);612 const line = api.findSystemPackagesLine(ctx.io, config_path) orelse 1;613 output.installEditorPrompt(npkg.pname, config_path, line, ctx.cfg.editor);614 _ = try exec.openEditor(ctx.io, ctx.gpa, ctx.cfg.editor, config_path, line);615 if (ctx.cfg.confirm) {616 if (output.confirm("apply now? ")) {617 try apply(ctx);618 }619 }620}621622fn tryPkg(ctx: Ctx, npkg: types.NixPackage) !void {623 output.tryStart(npkg.pname);624 output.printSubstep("nix shell nixpkgs#{s}", .{npkg.attr});625 _ = try exec.nixShell(ctx.io, &ctx.machine, ctx.gpa, npkg.attr);626 output.tryDone();627}628629pub fn remove(ctx: Ctx) !void {630 const pkg_name = if (ctx.args.len > 0) ctx.args[0] else return error.PackageNotFound;631 output.removeStart(pkg_name);632 const code = try exec.profileRemove(ctx.io, &ctx.machine, ctx.gpa, pkg_name);633 if (code != 0) return error.ProcessFailed;634 output.done();635}636637pub fn list(ctx: Ctx) !void {638 output.listHeader(ctx.machine.name);639640 641 642 643 if (exec.profileListJson(ctx.gpa, ctx.io, &ctx.machine) catch null) |json| {644 defer ctx.gpa.free(json);645 output.listSection("profile");646 renderProfileJson(ctx.gpa, json);647 }648649 650 651 if (exec.systemPackagePaths(ctx.gpa, ctx.io, &ctx.machine) catch null) |paths| {652 defer ctx.gpa.free(paths);653 output.blankLine();654 output.listSection("system");655 var lines = std.mem.splitScalar(u8, paths, '\n');656 while (lines.next()) |line| {657 const t = std.mem.trim(u8, line, " \t\r");658 if (t.len == 0) continue;659 const row = pkgFromStorePath(t);660 661 if (row.version.len == 0) continue;662 output.listPkg(row.name, row.version);663 }664 }665}666667const PkgRow = struct { name: []const u8, version: []const u8 };668669670671672673fn pkgFromStorePath(path: []const u8) PkgRow {674 const base = std.fs.path.basename(path);675 const after_hash = if (base.len > 33 and base[32] == '-') base[33..] else base;676 var i: usize = 0;677 while (i + 1 < after_hash.len) : (i += 1) {678 if (after_hash[i] == '-' and std.ascii.isDigit(after_hash[i + 1])) {679 return .{ .name = after_hash[0..i], .version = after_hash[i + 1 ..] };680 }681 }682 return .{ .name = after_hash, .version = "" };683}684685test "pkgFromStorePath splits name and version at first dash-digit" {686 const a = pkgFromStorePath("/nix/store/abcdefghijklmnopqrstuvwxyz012345-ripgrep-15.1.0");687 try std.testing.expectEqualStrings("ripgrep", a.name);688 try std.testing.expectEqualStrings("15.1.0", a.version);689690 const b = pkgFromStorePath("/nix/store/abcdefghijklmnopqrstuvwxyz012345-helix-25.01");691 try std.testing.expectEqualStrings("helix", b.name);692 try std.testing.expectEqualStrings("25.01", b.version);693}694695fn firstStorePath(el: std.json.Value) ?[]const u8 {696 if (el != .object) return null;697 const sp = el.object.get("storePaths") orelse return null;698 if (sp != .array or sp.array.items.len == 0) return null;699 const first = sp.array.items[0];700 return if (first == .string) first.string else null;701}702703704705fn renderProfileJson(gpa: std.mem.Allocator, json: []const u8) void {706 const parsed = std.json.parseFromSlice(std.json.Value, gpa, json, .{}) catch return;707 defer parsed.deinit();708 if (parsed.value != .object) return;709 const elements = parsed.value.object.get("elements") orelse return;710 switch (elements) {711 .object => |obj| {712 var it = obj.iterator();713 while (it.next()) |entry| {714 const ver = if (firstStorePath(entry.value_ptr.*)) |p| pkgFromStorePath(p).version else "";715 output.listPkg(entry.key_ptr.*, ver);716 }717 },718 .array => |arr| {719 for (arr.items) |el| {720 const path = firstStorePath(el) orelse continue;721 const row = pkgFromStorePath(path);722 output.listPkg(row.name, row.version);723 }724 },725 else => {},726 }727}728729pub fn tryCmd(ctx: Ctx) !void {730 const pkg_name_arg = if (ctx.args.len > 0) ctx.args[0] else return error.PackageNotFound;731 const shell_pkg = types.NixPackage{ .attr = pkg_name_arg, .pname = pkg_name_arg, .version = "", .description = "" };732 try tryPkg(ctx, shell_pkg);733}734735736737pub fn service(ctx: Ctx) !void {738 if (std.mem.eql(u8, ctx.sub, "list") or ctx.sub.len == 0) {739 output.serviceListHeader(ctx.machine.name);740 const svcs = exec.serviceList(ctx.gpa, ctx.io, &ctx.machine) catch &.{};741 defer ctx.gpa.free(svcs);742 for (svcs) |s| output.serviceRow(s);743 return;744 }745 if (std.mem.eql(u8, ctx.sub, "logs")) {746 const name = if (ctx.args.len > 0) ctx.args[0] else return error.Internal;747 const follow = ctx.args.len > 1 and std.mem.eql(u8, ctx.args[1], "-f");748 output.serviceLogsHeader(name, ctx.last, ctx.machine.name);749 _ = try exec.serviceLogs(ctx.io, &ctx.machine, ctx.gpa, name, ctx.last, follow);750 return;751 }752 if (std.mem.eql(u8, ctx.sub, "status")) {753 const name = if (ctx.args.len > 0) ctx.args[0] else return error.Internal;754 output.serviceVerb("status", name, ctx.machine.name);755 _ = try exec.serviceCtl(ctx.io, &ctx.machine, ctx.gpa, "status", name);756 return;757 }758 759 const name = if (ctx.args.len > 0) ctx.args[0] else return error.Internal;760 if (ctx.cfg.confirm and (std.mem.eql(u8, ctx.sub, "stop") or std.mem.eql(u8, ctx.sub, "disable"))) {761 if (!output.confirm("")) return;762 }763 output.serviceVerb(ctx.sub, name, ctx.machine.name);764 _ = try exec.serviceCtl(ctx.io, &ctx.machine, ctx.gpa, ctx.sub, name);765 output.serviceDone();766}767768769770pub fn flake(ctx: Ctx) !void {771 if (std.mem.eql(u8, ctx.sub, "update")) {772 output.flakeUpdateStart();773 const input: ?[]const u8 = if (ctx.args.len > 0) ctx.args[0] else null;774 _ = try exec.flakeUpdate(ctx.io, &ctx.machine, ctx.gpa, input);775 output.flakeLockWritten();776 return;777 }778 if (std.mem.eql(u8, ctx.sub, "check")) {779 output.flakeCheckStart();780 const code = try exec.flakeCheck(ctx.io, &ctx.machine, ctx.gpa);781 if (code == 0) output.flakeValid() else output.checkFailed("");782 return;783 }784 if (std.mem.eql(u8, ctx.sub, "show")) {785 const json_out = exec.flakeShow(ctx.gpa, ctx.io, &ctx.machine) catch null;786 if (json_out) |j| {787 defer ctx.gpa.free(j);788 output.flakeShowHeader("flake outputs");789 790 const parsed = std.json.parseFromSlice(std.json.Value, ctx.gpa, j, .{}) catch return;791 defer parsed.deinit();792 if (parsed.value == .object) {793 var it = parsed.value.object.iterator();794 while (it.next()) |entry| {795 if (entry.value_ptr.* == .object) {796 var inner = entry.value_ptr.object.iterator();797 while (inner.next()) |e2| {798 const desc = if (e2.value_ptr.* == .object)799 if (e2.value_ptr.object.get("description")) |d|800 if (d == .string) d.string else ""801 else ""802 else "";803 output.flakeShowEntry(entry.key_ptr.*, e2.key_ptr.*, desc);804 }805 }806 }807 }808 }809 return;810 }811 if (std.mem.eql(u8, ctx.sub, "init")) {812 _ = try exec.flakeInit(ctx.io, &ctx.machine, ctx.gpa);813 return;814 }815 if (std.mem.eql(u8, ctx.sub, "lock")) {816 _ = try exec.flakeLock(ctx.io, &ctx.machine, ctx.gpa);817 return;818 }819 if (std.mem.eql(u8, ctx.sub, "clone")) {820 const url = if (ctx.args.len > 0) ctx.args[0] else return error.Internal;821 _ = try exec.flakeClone(ctx.io, &ctx.machine, ctx.gpa, url);822 return;823 }824}825826827828pub fn store(ctx: Ctx) !void {829 if (std.mem.eql(u8, ctx.sub, "gc")) {830 _ = try exec.storeGc(ctx.io, &ctx.machine, ctx.gpa);831 return;832 }833 if (std.mem.eql(u8, ctx.sub, "verify")) {834 output.storeVerifyStart();835 _ = try exec.storeVerify(ctx.io, &ctx.machine, ctx.gpa);836 return;837 }838 if (std.mem.eql(u8, ctx.sub, "repair")) {839 if (ctx.cfg.confirm and !output.confirm("")) return;840 output.storeRepairStart();841 _ = try exec.storeRepair(ctx.io, &ctx.machine, ctx.gpa);842 return;843 }844 if (std.mem.eql(u8, ctx.sub, "info")) {845 const si = try exec.storeInfo(ctx.gpa, ctx.io, &ctx.machine);846 output.storeInfo(si);847 return;848 }849 if (std.mem.eql(u8, ctx.sub, "path")) {850 const attr = if (ctx.args.len > 0) ctx.args[0] else return error.Internal;851 const path = try exec.storePath(ctx.gpa, ctx.io, &ctx.machine, attr);852 defer ctx.gpa.free(path);853 output.storePath(path);854 return;855 }856 857 const si = try exec.storeInfo(ctx.gpa, ctx.io, &ctx.machine);858 output.storeInfo(si);859}860861862863pub fn channel(ctx: Ctx) !void {864 if (std.mem.eql(u8, ctx.sub, "list") or ctx.sub.len == 0) {865 output.channelListHeader(ctx.machine.name);866 const chans = try exec.channelList(ctx.gpa, ctx.io, &ctx.machine);867 defer ctx.gpa.free(chans);868 for (chans) |ch| output.channelRow(ch);869 return;870 }871 if (std.mem.eql(u8, ctx.sub, "add")) {872 const url = if (ctx.args.len > 0) ctx.args[0] else return error.Internal;873 const name = if (ctx.args.len > 1) ctx.args[1] else "";874 _ = try exec.channelAdd(ctx.io, &ctx.machine, ctx.gpa, url, name);875 output.channelDone();876 return;877 }878 if (std.mem.eql(u8, ctx.sub, "remove")) {879 const name = if (ctx.args.len > 0) ctx.args[0] else return error.Internal;880 _ = try exec.channelRemove(ctx.io, &ctx.machine, ctx.gpa, name);881 output.channelDone();882 return;883 }884 if (std.mem.eql(u8, ctx.sub, "update")) {885 return channelUpdateCmd(ctx);886 }887}888889890891pub fn profile(ctx: Ctx) !void {892 if (std.mem.eql(u8, ctx.sub, "list") or ctx.sub.len == 0) {893 output.profileListHeader();894 const out = exec.profileList(ctx.gpa, ctx.io, &ctx.machine) catch null;895 if (out) |o| {896 defer ctx.gpa.free(o);897 var lines = std.mem.splitScalar(u8, o, '\n');898 while (lines.next()) |line| {899 const t = std.mem.trim(u8, line, " \t\r");900 if (t.len == 0) continue;901 output.profileRow(t, "", "");902 }903 }904 return;905 }906 if (std.mem.eql(u8, ctx.sub, "install")) {907 const attr = if (ctx.args.len > 0) ctx.args[0] else return error.PackageNotFound;908 _ = try exec.profileInstall(ctx.io, &ctx.machine, ctx.gpa, attr);909 output.done();910 return;911 }912 if (std.mem.eql(u8, ctx.sub, "remove")) {913 const attr = if (ctx.args.len > 0) ctx.args[0] else return error.PackageNotFound;914 _ = try exec.profileRemove(ctx.io, &ctx.machine, ctx.gpa, attr);915 output.done();916 return;917 }918 if (std.mem.eql(u8, ctx.sub, "upgrade")) {919 _ = try exec.profileUpgrade(ctx.io, &ctx.machine, ctx.gpa);920 output.done();921 return;922 }923}924925926927pub fn pkg(ctx: Ctx) !void {928 const name = if (ctx.args.len > 0) ctx.args[0] else return error.PackageNotFound;929 if (std.mem.eql(u8, ctx.sub, "why")) {930 _ = try exec.pkgWhy(ctx.io, &ctx.machine, ctx.gpa, name);931 return;932 }933 if (std.mem.eql(u8, ctx.sub, "deps")) {934 _ = try exec.pkgDeps(ctx.io, &ctx.machine, ctx.gpa, name);935 return;936 }937 if (std.mem.eql(u8, ctx.sub, "size")) {938 const s = exec.pkgSize(ctx.gpa, ctx.io, &ctx.machine, name) catch null;939 if (s) |sz| {940 defer ctx.gpa.free(sz);941 output.pkgResult("size", sz);942 }943 return;944 }945 if (std.mem.eql(u8, ctx.sub, "path")) {946 const p = try exec.storePath(ctx.gpa, ctx.io, &ctx.machine, name);947 defer ctx.gpa.free(p);948 output.pkgResult("path", p);949 return;950 }951 if (std.mem.eql(u8, ctx.sub, "closure")) {952 _ = try exec.pkgClosure(ctx.io, &ctx.machine, ctx.gpa, name);953 return;954 }955}956957958959pub fn pin(ctx: Ctx) !void {960 const input = if (ctx.args.len > 0) ctx.args[0] else return error.Internal;961 const commit = if (ctx.args.len > 1) ctx.args[1] else return error.Internal;962 exec.editFlakeLock(ctx.gpa, input, commit) catch return error.ProcessFailed;963 output.pinDone(input, commit);964}965966pub fn unpin(ctx: Ctx) !void {967 const input = if (ctx.args.len > 0) ctx.args[0] else return error.Internal;968 _ = try exec.flakeUpdate(ctx.io, &ctx.machine, ctx.gpa, input);969 output.unpinDone(input);970}971972973974pub fn hash(ctx: Ctx) !void {975 const path = if (ctx.args.len > 0) ctx.args[0] else return error.Internal;976 const h = try exec.nixHash(ctx.gpa, ctx.io, &ctx.machine, path);977 defer ctx.gpa.free(h);978 output.hashResult(std.mem.trim(u8, h, " \t\n\r"));979}980981pub fn fetch(ctx: Ctx) !void {982 const url = if (ctx.args.len > 0) ctx.args[0] else return error.Internal;983 const h = try exec.nixFetch(ctx.gpa, ctx.io, &ctx.machine, url);984 defer ctx.gpa.free(h);985 output.fetchResult(std.mem.trim(u8, h, " \t\n\r"), url);986}987988989990pub fn build(ctx: Ctx) !void {991 const attr: ?[]const u8 = if (ctx.args.len > 0) ctx.args[0] else null;992 output.buildStart(attr orelse "default");993 const code = try exec.nixBuild(ctx.io, &ctx.machine, ctx.gpa, attr);994 if (code != 0) return error.BuildFailed;995 output.buildDone();996}997998pub fn runCmd(ctx: Ctx) !void {999 const pkg_arg = if (ctx.args.len > 0) ctx.args[0] else return error.PackageNotFound;1000 output.runStart(pkg_arg);1001 _ = try exec.nixRun(ctx.io, &ctx.machine, ctx.gpa, pkg_arg);1002}1003100410051006pub fn hello(ctx: Ctx) !void {1007 output.hello(ctx.cfg.machines);1008}10091010pub fn setup(gpa: std.mem.Allocator, io: std.Io, environ: *const std.process.Environ.Map) !void {1011 output.setupCheck();1012 const status = exec.checkNixFeatures(gpa, io, environ);1013 output.setupFeatures(status.nix_command, status.flakes);10141015 if (status.allEnabled()) {1016 output.setupAllSet();1017 api.ensureRegistryPinned(gpa, io, environ);1018 return;1019 }10201021 output.setupEnabling();1022 const path = try exec.enableNixFeatures(gpa, io, environ);1023 defer gpa.free(path);10241025 output.setupDone(path, exec.isNixOS(io));1026 api.ensureRegistryPinned(gpa, io, environ);1027}1028102910301031fn logAction(ctx: Ctx, action: []const u8, gen: u32) void {1032 var ts_buf: [32]u8 = undefined;1033 const ts = nowIso(ctx.io, &ts_buf);1034 var buf: [256]u8 = undefined;1035 const entry = std.fmt.bufPrint(&buf, "{{\"ts\":\"{s}\",\"machine\":\"{s}\",\"command\":\"{s}\",\"outcome\":\"success\",\"gen_before\":null,\"gen_after\":{d},\"duration_ms\":0}}\n", .{1036 ts, ctx.machine.name, action, gen,1037 }) catch return;1038 exec.appendNinaLog(ctx.io, ctx.environ, entry);1039}1040104110421043fn nowIso(io: std.Io, buf: []u8) []const u8 {1044 const fallback = "1970-01-01T00:00:00Z";1045 const ns = std.Io.Clock.real.now(io).toNanoseconds();1046 if (ns <= 0) return fallback;1047 const secs: u64 = @intCast(@divFloor(ns, 1_000_000_000));1048 const es = std.time.epoch.EpochSeconds{ .secs = secs };1049 const ed = es.getEpochDay();1050 const yd = ed.calculateYearDay();1051 const md = yd.calculateMonthDay();1052 const ds = es.getDaySeconds();1053 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", .{1054 yd.year, md.month.numeric(), @as(u32, md.day_index) + 1,1055 ds.getHoursIntoDay(), ds.getMinutesIntoHour(), ds.getSecondsIntoMinute(),1056 }) catch return fallback;1057}