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 user: bool, 31};3233343536fn flakeSystem(ctx: Ctx) bool {37 return ctx.cfg.flake or exec.configHasFlake(ctx.gpa, ctx.io, &ctx.machine);38}3940fn runPreHook(ctx: Ctx, hook_name: []const u8) !bool {41 const hook = try exec.runHook(ctx.gpa, ctx.io, ctx.environ, hook_name);42 defer ctx.gpa.free(hook.output);43 switch (hook.result) {44 .not_found, .ok => return true,45 .failed => {46 output.hookFailed(hook_name, hook.exit_code, hook.output);47 return output.confirmDefaultNo("continue anyway?");48 },49 }50}5152fn runPostHook(ctx: Ctx, hook_name: []const u8) void {53 const hook = exec.runHook(ctx.gpa, ctx.io, ctx.environ, hook_name) catch return;54 defer ctx.gpa.free(hook.output);55 if (hook.result == .failed) {56 output.hookWarning(hook_name, hook.exit_code, hook.output);57 }58}5960pub fn apply(ctx: Ctx) !void {61 if (ctx.dry) {62 output.applyDry(ctx.machine.name);63 const code = try exec.nixosRebuildDryActivate(ctx.io, &ctx.machine, ctx.gpa);64 if (code != 0) return error.BuildFailed;65 output.applyDryDone();66 return;67 }68 if (ctx.check) {69 return check(ctx);70 }7172 if (!try runPreHook(ctx, "pre-apply")) {73 output.aborted();74 return;75 }7677 output.applyStart(ctx.machine.name);78 const start = std.Io.Clock.awake.now(ctx.io);7980 const code = exec.nixosRebuildSwitch(ctx.io, &ctx.machine, ctx.gpa) catch |e| {81 exec.markApplyFailed(ctx.io, ctx.environ);82 output.applyFailed("");83 return e;84 };8586 if (code != 0) {87 exec.markApplyFailed(ctx.io, ctx.environ);88 output.applyFailed("");89 return error.BuildFailed;90 }9192 const elapsed_ns = @max(0, start.untilNow(ctx.io, .awake).toNanoseconds());93 const elapsed: u64 = @intCast(@divFloor(elapsed_ns, 1_000_000));94 const gen = exec.getCurrentGeneration(ctx.gpa, ctx.io, &ctx.machine) catch 0;95 const after_failure = exec.takeApplyFailed(ctx.io, ctx.environ);96 output.applyDone(gen, elapsed, after_failure);97 runPostHook(ctx, "post-apply");98 logAction(ctx, "apply", gen);99}100101pub fn check(ctx: Ctx) !void {102 output.checkStart(ctx.machine.name);103 const code = try exec.nixosRebuildBuild(ctx.io, &ctx.machine, ctx.gpa);104 if (code != 0) {105 output.checkFailed("");106 return error.BuildFailed;107 }108 output.checkOk();109}110111pub fn back(ctx: Ctx) !void {112 const gens = try exec.getGenerations(ctx.gpa, ctx.io, &ctx.machine);113 defer ctx.gpa.free(gens);114115 var current: u32 = 0;116 for (gens) |g| if (g.current) {117 current = g.number;118 };119120 121 122 var prev: u32 = 0;123 var has_prev = false;124 for (gens) |g| {125 if (g.number < current and (!has_prev or g.number > prev)) {126 prev = g.number;127 has_prev = true;128 }129 }130 if (!has_prev) {131 output.alreadyOldest();132 return;133 }134135 if (!try runPreHook(ctx, "pre-back")) {136 output.aborted();137 return;138 }139140 output.backStart(ctx.machine.name, current, prev);141 if (ctx.cfg.confirm and !output.confirm("")) return;142143 const code = try exec.rollback(ctx.io, &ctx.machine, ctx.gpa);144 if (code != 0) return error.BuildFailed;145 output.backDone(prev);146 runPostHook(ctx, "post-back");147}148149pub fn go(ctx: Ctx) !void {150 if (ctx.args.len == 0) return error.Internal;151 const target = std.fmt.parseInt(u32, ctx.args[0], 10) catch return error.Internal;152 const current = try exec.getCurrentGeneration(ctx.gpa, ctx.io, &ctx.machine);153154 output.goStart(ctx.machine.name, current, target);155 if (ctx.cfg.confirm and !output.confirm("")) return;156157 const code = try exec.switchGeneration(ctx.io, &ctx.machine, ctx.gpa, target);158 if (code != 0) return error.ProcessFailed;159 output.generationDone(target);160}161162pub fn history(ctx: Ctx) !void {163 const gens = try exec.getGenerations(ctx.gpa, ctx.io, &ctx.machine);164 defer ctx.gpa.free(gens);165 output.historyHeader(ctx.machine.name);166 for (gens) |g| output.historyRow(g);167}168169const gen_help = [_][2][]const u8{170 .{ "list", "list generations" },171 .{ "current", "show current generation" },172 .{ "delete <n>", "delete generation n" },173 .{ "delete old", "delete old generations" },174};175176pub fn genCmd(ctx: Ctx) !void {177 if (ctx.sub.len == 0 or std.mem.eql(u8, ctx.sub, "help")) return output.subcommandHelp("gen", &gen_help, null);178 if (std.mem.eql(u8, ctx.sub, "list")) return history(ctx);179 if (std.mem.eql(u8, ctx.sub, "current")) {180 const n = try exec.getCurrentGeneration(ctx.gpa, ctx.io, &ctx.machine);181 output.genCurrent(n);182 return;183 }184 if (std.mem.eql(u8, ctx.sub, "delete")) {185 if (ctx.args.len == 0) return error.Internal;186 if (std.mem.eql(u8, ctx.args[0], "old")) {187 if (ctx.cfg.confirm and !output.confirm("")) return;188 _ = try exec.deleteOldGenerations(ctx.io, &ctx.machine, ctx.gpa);189 } else {190 const n = std.fmt.parseInt(u32, ctx.args[0], 10) catch return error.Internal;191 if (ctx.cfg.confirm and !output.confirm("")) return;192 _ = try exec.deleteGeneration(ctx.io, &ctx.machine, ctx.gpa, n);193 }194 output.done();195 return;196 }197 return error.Internal;198}199200pub fn clean(ctx: Ctx) !void {201 const gens = try exec.getGenerations(ctx.gpa, ctx.io, &ctx.machine);202 defer ctx.gpa.free(gens);203 const all = ctx.all or std.mem.eql(u8, ctx.sub, "all");204 const total: u32 = @intCast(gens.len);205 206 207 const keep: u32 = if (all) 1 else ctx.cfg.generations;208 if (total <= keep) {209 output.cleanNothing(ctx.machine.name, total);210 return;211 }212 output.cleanStart(ctx.machine.name, keep, total);213 if (ctx.cfg.confirm and !output.confirm("removing — cannot be undone")) return;214 _ = try exec.collectGarbage(ctx.io, &ctx.machine, ctx.gpa, all, ctx.cfg.generations);215 output.cleanDone();216}217218pub fn statusCmd(ctx: Ctx) !void {219 output.statusHeader();220 221 222 if (ctx.all and ctx.cfg.machines.len > 0) {223 for (ctx.cfg.machines) |m| statusRowFor(ctx, &m);224 } else {225 statusRowFor(ctx, &ctx.machine);226 }227 homeStatusLine(ctx);228}229230231232233fn homeStatusLine(ctx: Ctx) void {234 if (exec.detectHomeManager(ctx.gpa, ctx.io, ctx.environ) != .standalone) return;235 const out = exec.homeGenerations(ctx.gpa, ctx.io, &ctx.machine) catch return;236 defer ctx.gpa.free(out);237 const gens = parseHomeGenerations(ctx.gpa, out) catch return;238 defer freeHomeGens(ctx.gpa, gens);239 if (gens.len == 0) return;240 output.statusHomeRow(gens[0].number, gens[0].date);241}242243fn statusRowFor(ctx: Ctx, machine: *const types.Machine) void {244 245 246 const gen = exec.getCurrentGeneration(ctx.gpa, ctx.io, machine) catch {247 output.statusRow(machine.name, 0, "", "", false);248 return;249 };250 const st = exec.systemStatus(ctx.gpa, ctx.io, machine);251 defer ctx.gpa.free(st.size);252 defer ctx.gpa.free(st.age);253 output.statusRow(machine.name, gen, st.age, st.size, true);254}255256pub fn diff(ctx: Ctx) !void {257 const gens = try exec.getGenerations(ctx.gpa, ctx.io, &ctx.machine);258 defer ctx.gpa.free(gens);259 var current: u32 = 0;260 var prev: u32 = 0;261 for (gens) |g| {262 if (g.current) current = g.number;263 if (!g.current and g.number > prev and g.number < current) prev = g.number;264 }265 if (ctx.args.len >= 2) {266 const a = std.fmt.parseInt(u32, ctx.args[0], 10) catch current - 1;267 const b = std.fmt.parseInt(u32, ctx.args[1], 10) catch current;268 output.diffHeader(a, b, ctx.machine.name);269 if (try exec.diffClosures(ctx.io, &ctx.machine, ctx.gpa, a, b) != 0) return error.ProcessFailed;270 } else {271 output.diffHeader(if (prev > 0) prev else current - 1, current, ctx.machine.name);272 if (try exec.diffClosures(ctx.io, &ctx.machine, ctx.gpa, if (prev > 0) prev else current - 1, current) != 0) return error.ProcessFailed;273 }274}275276pub fn mood(ctx: Ctx) !void {277 const gen = exec.getCurrentGeneration(ctx.gpa, ctx.io, &ctx.machine) catch 0;278 const st = exec.systemStatus(ctx.gpa, ctx.io, &ctx.machine);279 defer ctx.gpa.free(st.size);280 defer ctx.gpa.free(st.age);281 const state = exec.systemRunningState(ctx.gpa, ctx.io, &ctx.machine) catch try ctx.gpa.dupe(u8, "unknown");282 defer ctx.gpa.free(state);283 output.mood(ctx.machine.name, state, gen, st.age, st.size);284}285286pub fn doctor(ctx: Ctx) !void {287 output.doctorHeader(ctx.machine.name);288 const checks = try exec.runDoctorChecks(ctx.gpa, ctx.io, &ctx.machine);289 defer ctx.gpa.free(checks);290 var warnings: u32 = 0;291 var failures: u32 = 0;292 for (checks) |ch| {293 output.doctorRow(ch);294 if (ch.status == .warn) warnings += 1;295 if (ch.status == .fail) failures += 1;296 }297298 299 300 const hm_mode = exec.detectHomeManager(ctx.gpa, ctx.io, ctx.environ);301 if (hm_mode != .none) {302 const note = homeDoctorNote(ctx, hm_mode);303 defer ctx.gpa.free(note);304 output.doctorRow(.{ .name = "home manager", .status = .ok, .note = note });305 }306307 output.doctorSummary(warnings, failures);308}309310311312fn homeDoctorNote(ctx: Ctx, mode: exec.HomeManagerMode) []const u8 {313 const label = switch (mode) {314 .standalone => "standalone",315 .module => "module",316 .none => "",317 };318 if (mode == .standalone) {319 const gen = homeCurrentGen(ctx);320 if (gen != 0)321 return std.fmt.allocPrint(ctx.gpa, "{s} gen {d}", .{ label, gen }) catch322 (ctx.gpa.dupe(u8, label) catch "");323 }324 return ctx.gpa.dupe(u8, label) catch "";325}326327328pub fn update(ctx: Ctx) !void {329 output.updateSelfStart();330331 if (comptime std.mem.eql(u8, version.PLATFORM, "unsupported")) {332 output.updateSelfNoPlatform(@tagName(builtin.cpu.arch) ++ "-" ++ @tagName(builtin.os.tag));333 return error.Internal;334 }335336 const env_url = ctx.environ.get("NINA_UPDATE_SOURCE");337 const source_url = env_url orelse UPDATE_URL;338339 const body = exec.fetchUrl(ctx.gpa, ctx.io, source_url) catch |e| switch (e) {340 error.NetworkError, error.HttpError => {341 output.updateSelfNetworkError();342 return;343 },344 else => return e,345 };346 defer ctx.gpa.free(body);347348 const parsed = std.json.parseFromSlice(std.json.Value, ctx.gpa, body, .{}) catch {349 output.printWarning("could not parse release metadata");350 return;351 };352 defer parsed.deinit();353354 const root = parsed.value;355 if (root != .object) {356 output.printWarning("malformed release metadata");357 return;358 }359360 const remote_ver = blk: {361 const v = root.object.get("version") orelse {362 output.printWarning("malformed release metadata");363 return;364 };365 break :blk switch (v) {366 .string => |s| s,367 else => {368 output.printWarning("malformed release metadata");369 return;370 },371 };372 };373 const release_notes = blk: {374 const v = root.object.get("release_notes") orelse break :blk "";375 break :blk switch (v) {376 .string => |s| s,377 else => "",378 };379 };380381 if (std.mem.eql(u8, remote_ver, version.VERSION)) {382 output.updateSelfCurrent(version.VERSION);383 return;384 }385386 output.updateSelfAvailable(version.VERSION, remote_ver);387 output.updateSelfReleaseNotes(release_notes);388 if (ctx.check) return;389390 var exe_buf: [std.Io.Dir.max_path_bytes]u8 = undefined;391 const install_path = exec.selfExePath(ctx.io, &exe_buf) catch {392 errors.error_info.setMessage("could not determine install path", .{});393 return error.Internal;394 };395396 397 398 399 400 401 if (std.mem.startsWith(u8, install_path, "/nix/store/")) {402 const nina_element = exec.profileNinaElement(ctx.gpa, ctx.io, &ctx.machine);403 defer {404 if (nina_element) |n| ctx.gpa.free(n);405 }406 if (nina_element == null) {407 output.updateSelfNotInProfile();408 return;409 }410 const element = nina_element.?;411 output.updateSelfNixManaged(remote_ver);412 if (ctx.cfg.confirm and !output.confirm("")) return;413 const ec = exec.streamLocal(ctx.io, &.{ "nix", "profile", "upgrade", element }) catch {414 output.updateSelfNixFailed();415 return;416 };417 if (ec != 0) {418 output.updateSelfNixFailed();419 return;420 }421 output.updateSelfDone(version.VERSION, remote_ver);422 return;423 }424425 const plat_info = blk: {426 const pv = root.object.get("platforms") orelse break :blk null;427 if (pv != .object) break :blk null;428 break :blk pv.object.get(version.PLATFORM);429 };430 if (plat_info == null) {431 output.updateSelfNoPlatform(version.PLATFORM);432 return;433 }434435 const dl_url = blk: {436 const v = plat_info.?.object.get("url") orelse {437 output.printWarning("malformed release metadata");438 return;439 };440 break :blk switch (v) {441 .string => |s| s,442 else => {443 output.printWarning("malformed release metadata");444 return;445 },446 };447 };448449 const raw_cksum = blk: {450 if (plat_info.?.object.get("blake3")) |v|451 break :blk switch (v) {452 .string => |s| s,453 else => {454 output.printWarning("malformed release metadata");455 return;456 },457 };458 if (plat_info.?.object.get("checksum")) |v|459 break :blk switch (v) {460 .string => |s| s,461 else => {462 output.printWarning("malformed release metadata");463 return;464 },465 };466 output.printWarning("missing checksum in release metadata");467 return;468 };469 const expected_hex = if (std.mem.startsWith(u8, raw_cksum, "blake3:")) raw_cksum["blake3:".len..] else raw_cksum;470471 if (ctx.cfg.confirm and !output.confirm("")) return;472473 output.updateSelfDownloading(remote_ver, version.PLATFORM);474 const tmp_path = exec.downloadToTemp(ctx.gpa, ctx.io, dl_url) catch {475 output.printWarning("download failed — check your connection and try again");476 return;477 };478 defer ctx.gpa.free(tmp_path);479480 output.updateSelfVerifying();481 exec.verifyAndInstall(ctx.gpa, ctx.io, tmp_path, expected_hex, install_path) catch |e| switch (e) {482 error.ChecksumMismatch => {483 output.updateSelfChecksumFailed();484 return;485 },486 else => return e,487 };488489 output.updateSelfDone(version.VERSION, remote_ver);490}491492493494495pub fn goodbye(ctx: Ctx) !void {496 output.goodbyeStart();497498 var exe_buf: [std.Io.Dir.max_path_bytes]u8 = undefined;499 const install_path = exec.selfExePath(ctx.io, &exe_buf) catch {500 errors.error_info.setMessage("could not determine install path", .{});501 return error.Internal;502 };503504 505 if (!std.mem.startsWith(u8, install_path, "/nix/store/")) {506 output.goodbyeFile(install_path);507 if (ctx.cfg.confirm and !output.confirm("")) return;508 std.Io.Dir.deleteFileAbsolute(ctx.io, install_path) catch |e| {509 errors.error_info.setMessage("could not remove {s}", .{install_path});510 return e;511 };512 output.goodbyeDone();513 return;514 }515516 517 const nina_element = exec.profileNinaElement(ctx.gpa, ctx.io, &ctx.machine);518 defer {519 if (nina_element) |n| ctx.gpa.free(n);520 }521522 if (nina_element == null) {523 errors.error_info.setMessage("nina not found in nix profile — was it installed via 'nix profile add'?", .{});524 return error.Internal;525 }526527 output.goodbyeNix();528 if (ctx.cfg.confirm and !output.confirm("")) return;529 const ec = exec.streamLocal(ctx.io, &.{ "nix", "profile", "remove", nina_element.? }) catch {530 output.goodbyeFailed("nix profile remove failed");531 return;532 };533 if (ec != 0) {534 output.goodbyeFailed("nix profile remove failed");535 return;536 }537 output.goodbyeDone();538}539540fn channelUpdateCmd(ctx: Ctx) !void {541 output.updateStart(ctx.machine.name);542 _ = try exec.channelUpdate(ctx.io, &ctx.machine, ctx.gpa);543 output.updateDone();544}545546pub fn upgrade(ctx: Ctx) !void {547 if (!try runPreHook(ctx, "pre-upgrade")) {548 output.aborted();549 return;550 }551552 output.upgradeStart(ctx.machine.name);553 554 555 556 if (flakeSystem(ctx)) {557 output.printSubstep("updating flake inputs", .{});558 _ = exec.flakeUpdateAt(ctx.io, &ctx.machine, ctx.gpa, ctx.machine.config_path) catch {};559 } else {560 output.printSubstep("updating channels", .{});561 _ = exec.channelUpdate(ctx.io, &ctx.machine, ctx.gpa) catch {};562 }563 output.printSubstep("rebuilding", .{});564 const start = std.Io.Clock.awake.now(ctx.io);565 const code = try exec.nixosRebuildSwitch(ctx.io, &ctx.machine, ctx.gpa);566 if (code != 0) {567 output.applyFailed("");568 return error.BuildFailed;569 }570 const elapsed_ns = @max(0, start.untilNow(ctx.io, .awake).toNanoseconds());571 const elapsed: u64 = @intCast(@divFloor(elapsed_ns, 1_000_000));572 const gen = exec.getCurrentGeneration(ctx.gpa, ctx.io, &ctx.machine) catch 0;573 output.applyDone(gen, elapsed, false);574 runPostHook(ctx, "post-upgrade");575}576577pub fn log(ctx: Ctx) !void {578 output.logHeader(ctx.machine.name);579 const content = exec.readNinaLog(ctx.gpa, ctx.io, ctx.environ, ctx.last) catch {580 581 return;582 };583 defer ctx.gpa.free(content);584 var lines = std.mem.splitScalar(u8, content, '\n');585 while (lines.next()) |line| {586 const t = std.mem.trim(u8, line, " \t\r");587 if (t.len == 0) continue;588 if (t[0] == '{') {589 renderJsonLogLine(ctx.gpa, t);590 } else {591 592 var parts = std.mem.tokenizeScalar(u8, t, '\t');593 const date = parts.next() orelse continue;594 const time = parts.next() orelse "";595 const action = parts.next() orelse "";596 const detail = parts.next() orelse "";597 output.logRow(date, time, action, detail);598 }599 }600}601602603604fn renderJsonLogLine(gpa: std.mem.Allocator, line: []const u8) void {605 const parsed = std.json.parseFromSlice(std.json.Value, gpa, line, .{}) catch {606 output.logRow(line, "", "", ""); 607 return;608 };609 defer parsed.deinit();610 if (parsed.value != .object) return;611 const o = parsed.value.object;612613 const ts = jsonStr(o, "ts");614 const date = if (ts.len >= 10) ts[0..10] else ts;615 const time = if (ts.len >= 16) ts[11..16] else "";616 const action = jsonStr(o, "command");617 const outcome = jsonStr(o, "outcome");618619 var buf: [160]u8 = undefined;620 var detail: []const u8 = outcome;621 if (o.get("gen_after")) |g| {622 if (g == .integer) {623 detail = std.fmt.bufPrint(&buf, "{s} -> gen {d}", .{ outcome, g.integer }) catch outcome;624 }625 }626 output.logRow(date, time, action, detail);627}628629fn jsonStr(o: std.json.ObjectMap, key: []const u8) []const u8 {630 if (o.get(key)) |v| {631 if (v == .string) return v.string;632 }633 return "";634}635636pub fn edit(ctx: Ctx) !void {637 const hardware = ctx.args.len > 0 and std.mem.eql(u8, ctx.args[0], "hardware");638 const filename = if (hardware) "hardware-configuration.nix" else "configuration.nix";639 const path = try std.fmt.allocPrint(ctx.gpa, "{s}/{s}", .{ ctx.machine.config_path, filename });640 defer ctx.gpa.free(path);641 output.editOpen(path, ctx.cfg.editor);642 _ = try exec.openEditor(ctx.io, ctx.gpa, ctx.cfg.editor, path, null);643}644645pub fn fmt(ctx: Ctx) !void {646 const path = try std.fmt.allocPrint(ctx.gpa, "{s}/configuration.nix", .{ctx.machine.config_path});647 defer ctx.gpa.free(path);648 if (ctx.check) {649 const code = try exec.nixpkgsFmt(ctx.io, &ctx.machine, ctx.gpa, path, true);650 if (code != 0) output.fmtNeedsFormat() else output.fmtDone();651 return;652 }653 output.fmtStart(path);654 _ = try exec.nixpkgsFmt(ctx.io, &ctx.machine, ctx.gpa, path, false);655 output.fmtDone();656}657658pub fn repl(ctx: Ctx) !void {659 output.replStart();660 _ = try exec.nixRepl(ctx.io, &ctx.machine, ctx.gpa);661}662663pub fn develop(ctx: Ctx) !void {664 if (std.mem.eql(u8, ctx.sub, "show")) {665 _ = try exec.nixBuild(ctx.io, &ctx.machine, ctx.gpa, null);666 return;667 }668 const run_cmd: ?[]const u8 = if (ctx.args.len > 0 and std.mem.eql(u8, ctx.sub, "run"))669 ctx.args[0]670 else671 null;672 output.developStart(&.{});673 _ = try exec.nixDevelopPs1(ctx.io, &ctx.machine, ctx.gpa, run_cmd, ctx.environ);674 output.developDone();675}676677pub fn info(ctx: Ctx) !void {678 output.infoHeader(ctx.machine.name);679 const nixos_ver = exec.systemInfo(ctx.gpa, ctx.io, &ctx.machine) catch try ctx.gpa.dupe(u8, "unknown");680 defer ctx.gpa.free(nixos_ver);681 output.infoRow("nixos", std.mem.trim(u8, nixos_ver, " \t\n\r"));682683 const kernel = exec.kernelInfo(ctx.gpa, ctx.io, &ctx.machine) catch try ctx.gpa.dupe(u8, "unknown");684 defer ctx.gpa.free(kernel);685 output.infoRow("kernel", std.mem.trim(u8, kernel, " \t\n\r"));686687 const uptime = exec.uptimeInfo(ctx.gpa, ctx.io, &ctx.machine) catch try ctx.gpa.dupe(u8, "unknown");688 defer ctx.gpa.free(uptime);689 output.infoRow("uptime", std.mem.trim(u8, uptime, " \t\n\r"));690}691692pub fn boot(ctx: Ctx) !void {693 output.bootHeader(ctx.machine.name);694 const entries = exec.bootEntries(ctx.gpa, ctx.io, &ctx.machine) catch &.{};695 defer ctx.gpa.free(entries);696 for (entries, 0..) |entry, i| output.bootEntry(entry, i);697}698699pub fn searchCmd(ctx: Ctx) !void {700 const query = if (ctx.args.len > 0) ctx.args[0] else "";701 api.ensureRegistryPinned(ctx.gpa, ctx.io, ctx.environ);702 const selected = try search_tui.run(ctx.gpa, ctx.io, query, .packages);703 if (selected) |npkg| try selectAndInstall(ctx, npkg);704}705706pub fn optionCmd(ctx: Ctx) !void {707 const query = if (ctx.args.len > 0) ctx.args[0] else "";708 api.ensureRegistryPinned(ctx.gpa, ctx.io, ctx.environ);709 _ = try search_tui.run(ctx.gpa, ctx.io, query, .options);710}711712pub fn install(ctx: Ctx) !void {713 const pkg_name = if (ctx.args.len > 0) ctx.args[0] else return error.PackageNotFound;714715 api.ensureRegistryPinned(ctx.gpa, ctx.io, ctx.environ);716717 718 const pkgs = api.searchPackages(ctx.gpa, ctx.io, pkg_name) catch {719 errors.error_info.setMessage("'{s}' not found in nixpkgs", .{pkg_name});720 errors.error_info.setSuggestion("nina search {s}", .{pkg_name});721 return error.PackageNotFound;722 };723724 if (pkgs.len == 0) {725 726 if (closestPackage(ctx, pkg_name)) |sugg| {727 output.didYouMean(pkg_name, sugg.pname);728 if (output.confirm("")) try selectAndInstall(ctx, sugg);729 return;730 }731 errors.error_info.setMessage("'{s}' not found in nixpkgs", .{pkg_name});732 errors.error_info.setSuggestion("nina search {s}", .{pkg_name});733 return error.PackageNotFound;734 }735736 737 738 var found = pkgs[0];739 for (pkgs) |p| {740 if (std.mem.eql(u8, p.pname, pkg_name)) {741 found = p;742 break;743 }744 }745 try selectAndInstall(ctx, found);746}747748749750751fn closestPackage(ctx: Ctx, query: []const u8) ?types.NixPackage {752 if (query.len < 2) return null;753 var candidates = api.searchPackages(ctx.gpa, ctx.io, query[0..@min(query.len, 4)]) catch &.{};754 if (candidates.len == 0 and query.len > 2) {755 candidates = api.searchPackages(ctx.gpa, ctx.io, query[0..2]) catch &.{};756 }757 if (candidates.len == 0) return null;758759 var best: ?types.NixPackage = null;760 var best_dist: usize = std.math.maxInt(usize);761 for (candidates) |p| {762 const d = editDistance(query, p.pname);763 if (d > 0 and d < best_dist) {764 best_dist = d;765 best = p;766 }767 }768 769 const threshold = @max(@as(usize, 2), query.len / 3);770 return if (best_dist <= threshold) best else null;771}772773774775fn editDistance(a: []const u8, b: []const u8) usize {776 const n = b.len;777 if (a.len == 0) return n;778 if (n == 0) return a.len;779 if (n + 1 > 128) return std.math.maxInt(usize);780 var prev: [128]usize = undefined;781 var curr: [128]usize = undefined;782 var j: usize = 0;783 while (j <= n) : (j += 1) prev[j] = j;784 var i: usize = 1;785 while (i <= a.len) : (i += 1) {786 curr[0] = i;787 j = 1;788 while (j <= n) : (j += 1) {789 const cost: usize = if (std.ascii.toLower(a[i - 1]) == std.ascii.toLower(b[j - 1])) 0 else 1;790 curr[j] = @min(prev[j] + 1, @min(curr[j - 1] + 1, prev[j - 1] + cost));791 }792 @memcpy(prev[0 .. n + 1], curr[0 .. n + 1]);793 }794 return prev[n];795}796797test "editDistance is case-insensitive Levenshtein" {798 try std.testing.expectEqual(@as(usize, 1), editDistance("firefux", "firefox"));799 try std.testing.expectEqual(@as(usize, 0), editDistance("git", "GIT"));800 try std.testing.expectEqual(@as(usize, 2), editDistance("fierfox", "firefox"));801 try std.testing.expectEqual(@as(usize, 7), editDistance("", "firefox"));802}803804805806807808fn selectAndInstall(ctx: Ctx, npkg: types.NixPackage) !void {809 output.installSelectPrompt(npkg.pname, npkg.version);810811 var choice_buf: [8]u8 = undefined;812 var choice_len: usize = 0;813 while (choice_len < choice_buf.len) {814 const n = std.posix.read(std.posix.STDIN_FILENO, choice_buf[choice_len .. choice_len + 1]) catch break;815 if (n == 0) break;816 if (choice_buf[choice_len] == '\n') break;817 choice_len += 1;818 }819 const choice = std.mem.trim(u8, choice_buf[0..choice_len], " \t");820821 if (choice.len == 0 or choice[0] == 'i' or choice[0] == 'I') {822 try profileInstallPkg(ctx, npkg);823 } else if (choice[0] == 's' or choice[0] == 'S') {824 try systemInstallPkg(ctx, npkg);825 } else if (choice[0] == 't' or choice[0] == 'T') {826 try tryPkg(ctx, npkg);827 }828}829830fn profileInstallPkg(ctx: Ctx, npkg: types.NixPackage) !void {831 output.installStart(npkg.pname, .profile);832 output.printSubstep("nix profile add nixpkgs#{s}", .{npkg.attr});833 output.printTeach(try std.fmt.allocPrint(ctx.gpa, "nix profile add nixpkgs#{s}", .{npkg.attr}));834 const code = try exec.profileInstall(ctx.io, &ctx.machine, ctx.gpa, npkg.attr);835 if (code != 0) return error.ProcessFailed;836 output.installDone(npkg.pname);837}838839fn systemInstallPkg(ctx: Ctx, npkg: types.NixPackage) !void {840 const config_path = try std.fmt.allocPrint(ctx.gpa, "{s}/configuration.nix", .{ctx.machine.config_path});841 defer ctx.gpa.free(config_path);842 const line = api.findSystemPackagesLine(ctx.io, config_path) orelse 1;843 output.installEditorPrompt(npkg.pname, config_path, line, ctx.cfg.editor);844 _ = try exec.openEditor(ctx.io, ctx.gpa, ctx.cfg.editor, config_path, line);845 if (ctx.cfg.confirm) {846 if (output.confirm("apply now? ")) {847 try apply(ctx);848 }849 }850}851852fn tryPkg(ctx: Ctx, npkg: types.NixPackage) !void {853 output.tryStart(npkg.pname);854 output.printSubstep("nix shell nixpkgs#{s}", .{npkg.attr});855 output.tryExitHint();856 _ = try exec.nixShellPs1(ctx.io, &ctx.machine, ctx.gpa, npkg.attr, ctx.environ);857 output.tryDone();858}859860pub fn remove(ctx: Ctx) !void {861 const pkg_name = if (ctx.args.len > 0) ctx.args[0] else return error.PackageNotFound;862 output.removeStart(pkg_name);863 const code = try exec.profileRemove(ctx.io, &ctx.machine, ctx.gpa, pkg_name);864 if (code != 0) return error.ProcessFailed;865 output.done();866}867868pub fn list(ctx: Ctx) !void {869 output.listHeader(ctx.machine.name);870871 872 873 874 if (exec.profileListJson(ctx.gpa, ctx.io, &ctx.machine) catch null) |json| {875 defer ctx.gpa.free(json);876 output.listSection("profile");877 renderProfileJson(ctx.gpa, json);878 }879880 881 882 if (exec.systemPackagePaths(ctx.gpa, ctx.io, &ctx.machine) catch null) |paths| {883 defer ctx.gpa.free(paths);884 output.blankLine();885 output.listSection("system");886 var lines = std.mem.splitScalar(u8, paths, '\n');887 while (lines.next()) |line| {888 const t = std.mem.trim(u8, line, " \t\r");889 if (t.len == 0) continue;890 const row = pkgFromStorePath(t);891 892 if (row.version.len == 0) continue;893 output.listPkg(row.name, row.version);894 }895 }896}897898const PkgRow = struct { name: []const u8, version: []const u8 };899900901902903904fn pkgFromStorePath(path: []const u8) PkgRow {905 const base = std.fs.path.basename(path);906 const after_hash = if (base.len > 33 and base[32] == '-') base[33..] else base;907 var i: usize = 0;908 while (i + 1 < after_hash.len) : (i += 1) {909 if (after_hash[i] == '-' and std.ascii.isDigit(after_hash[i + 1])) {910 return .{ .name = after_hash[0..i], .version = stripOutputSuffix(after_hash[i + 1 ..]) };911 }912 }913 return .{ .name = after_hash, .version = "" };914}915916917918919fn stripOutputSuffix(ver: []const u8) []const u8 {920 const suffixes = [_][]const u8{ "-bin", "-man", "-dev", "-doc", "-lib", "-info", "-out", "-static", "-devdoc" };921 for (suffixes) |s| {922 if (std.mem.endsWith(u8, ver, s)) return ver[0 .. ver.len - s.len];923 }924 return ver;925}926927test "pkgFromStorePath splits name and version at first dash-digit" {928 const a = pkgFromStorePath("/nix/store/abcdefghijklmnopqrstuvwxyz012345-ripgrep-15.1.0");929 try std.testing.expectEqualStrings("ripgrep", a.name);930 try std.testing.expectEqualStrings("15.1.0", a.version);931932 const b = pkgFromStorePath("/nix/store/abcdefghijklmnopqrstuvwxyz012345-helix-25.01");933 try std.testing.expectEqualStrings("helix", b.name);934 try std.testing.expectEqualStrings("25.01", b.version);935936 937 const c = pkgFromStorePath("/nix/store/abcdefghijklmnopqrstuvwxyz012345-curl-8.20.0-bin");938 try std.testing.expectEqualStrings("curl", c.name);939 try std.testing.expectEqualStrings("8.20.0", c.version);940}941942fn firstStorePath(el: std.json.Value) ?[]const u8 {943 if (el != .object) return null;944 const sp = el.object.get("storePaths") orelse return null;945 if (sp != .array or sp.array.items.len == 0) return null;946 const first = sp.array.items[0];947 return if (first == .string) first.string else null;948}949950951952fn renderProfileJson(gpa: std.mem.Allocator, json: []const u8) void {953 const parsed = std.json.parseFromSlice(std.json.Value, gpa, json, .{}) catch return;954 defer parsed.deinit();955 if (parsed.value != .object) return;956 const elements = parsed.value.object.get("elements") orelse return;957 switch (elements) {958 .object => |obj| {959 var it = obj.iterator();960 while (it.next()) |entry| {961 const ver = if (firstStorePath(entry.value_ptr.*)) |p| pkgFromStorePath(p).version else "";962 output.listPkg(entry.key_ptr.*, ver);963 }964 },965 .array => |arr| {966 for (arr.items) |el| {967 const path = firstStorePath(el) orelse continue;968 const row = pkgFromStorePath(path);969 output.listPkg(row.name, row.version);970 }971 },972 else => {},973 }974}975976pub fn tryCmd(ctx: Ctx) !void {977 const pkg_name_arg = if (ctx.args.len > 0) ctx.args[0] else return error.PackageNotFound;978 const shell_pkg = types.NixPackage{ .attr = pkg_name_arg, .pname = pkg_name_arg, .version = "", .description = "" };979 try tryPkg(ctx, shell_pkg);980}981982983984const home_help = [_][2][]const u8{985 .{ "apply", "apply home-manager configuration" },986 .{ "apply --dry", "preview changes without activating" },987 .{ "back", "roll back one generation" },988 .{ "history", "list all generations" },989 .{ "edit", "open home.nix" },990 .{ "check", "validate without applying" },991 .{ "diff", "compare the latest two generations" },992 .{ "packages", "list managed packages" },993};994995const HomeGen = struct {996 number: u32,997 date: []const u8,998 time: []const u8,999 path: []const u8,1000};10011002100310041005fn parseHomeGenerations(gpa: std.mem.Allocator, out_str: []const u8) ![]HomeGen {1006 var gens: std.ArrayList(HomeGen) = .empty;1007 var lines = std.mem.splitScalar(u8, out_str, '\n');1008 while (lines.next()) |line| {1009 const t = std.mem.trim(u8, line, " \t\r");1010 if (t.len == 0) continue;1011 var it = std.mem.tokenizeAny(u8, t, " \t");1012 const date = it.next() orelse continue;1013 const time = it.next() orelse continue;1014 var number: ?u32 = null;1015 var path: []const u8 = "";1016 while (it.next()) |tok| {1017 if (std.mem.eql(u8, tok, "id")) {1018 if (it.next()) |n| number = std.fmt.parseInt(u32, n, 10) catch null;1019 } else if (std.mem.startsWith(u8, tok, "/nix/store/")) {1020 path = tok;1021 }1022 }1023 const num = number orelse continue;1024 try gens.append(gpa, .{1025 .number = num,1026 .date = try gpa.dupe(u8, date),1027 .time = try gpa.dupe(u8, time),1028 .path = try gpa.dupe(u8, path),1029 });1030 }1031 return gens.toOwnedSlice(gpa);1032}10331034fn freeHomeGens(gpa: std.mem.Allocator, gens: []HomeGen) void {1035 for (gens) |g| {1036 gpa.free(g.date);1037 gpa.free(g.time);1038 gpa.free(g.path);1039 }1040 gpa.free(gens);1041}10421043test "parseHomeGenerations reads id, date and store path" {1044 const a = std.testing.allocator;1045 const sample =1046 \\2026-06-01 11:56 : id 765 -> /nix/store/aaaa-home-manager-generation1047 \\2026-05-31 10:29 : id 764 -> /nix/store/bbbb-home-manager-generation1048 ;1049 const gens = try parseHomeGenerations(a, sample);1050 defer freeHomeGens(a, gens);1051 try std.testing.expectEqual(@as(usize, 2), gens.len);1052 try std.testing.expectEqual(@as(u32, 765), gens[0].number);1053 try std.testing.expectEqualStrings("2026-06-01", gens[0].date);1054 try std.testing.expectEqualStrings("11:56", gens[0].time);1055 try std.testing.expectEqualStrings("/nix/store/aaaa-home-manager-generation", gens[0].path);1056 try std.testing.expectEqual(@as(u32, 764), gens[1].number);1057}105810591060fn homeCurrentGen(ctx: Ctx) u32 {1061 const out = exec.homeGenerations(ctx.gpa, ctx.io, &ctx.machine) catch return 0;1062 defer ctx.gpa.free(out);1063 const gens = parseHomeGenerations(ctx.gpa, out) catch return 0;1064 defer freeHomeGens(ctx.gpa, gens);1065 return if (gens.len > 0) gens[0].number else 0;1066}10671068pub fn home(ctx: Ctx) !void {1069 if (ctx.sub.len == 0 or std.mem.eql(u8, ctx.sub, "help"))1070 return output.subcommandHelp("home", &home_help, null);10711072 1073 1074 1075 1076 1077 if (!ctx.machine.local) {1078 output.homeLocalOnly(ctx.machine.name);1079 return;1080 }10811082 const mode = exec.detectHomeManager(ctx.gpa, ctx.io, ctx.environ);1083 if (mode == .none) {1084 output.homeNotInstalled();1085 return;1086 }10871088 1089 1090 if (std.mem.eql(u8, ctx.sub, "apply")) return homeApply(ctx, mode);1091 1092 if (std.mem.eql(u8, ctx.sub, "edit")) return homeEdit(ctx);10931094 if (mode == .module) {1095 output.homeModuleManaged();1096 return;1097 }10981099 if (std.mem.eql(u8, ctx.sub, "back")) return homeBack(ctx);1100 if (std.mem.eql(u8, ctx.sub, "history")) return homeHistory(ctx);1101 if (std.mem.eql(u8, ctx.sub, "check")) return homeCheck(ctx);1102 if (std.mem.eql(u8, ctx.sub, "diff")) return homeDiff(ctx);1103 if (std.mem.eql(u8, ctx.sub, "packages")) return homePackages(ctx);11041105 return output.subcommandHelp("home", &home_help, null);1106}11071108fn homePackages(ctx: Ctx) !void {1109 const out = try exec.homeGenerations(ctx.gpa, ctx.io, &ctx.machine);1110 defer ctx.gpa.free(out);1111 const gens = try parseHomeGenerations(ctx.gpa, out);1112 defer freeHomeGens(ctx.gpa, gens);11131114 output.homePackagesHeader(ctx.machine.name);1115 if (gens.len == 0 or gens[0].path.len == 0) return;11161117 1118 1119 1120 1121 1122 const gen_refs = exec.homeGenerationReferences(ctx.gpa, ctx.io, &ctx.machine, gens[0].path) catch return;1123 defer ctx.gpa.free(gen_refs);11241125 var refs = gen_refs;1126 var env_refs: ?[]u8 = null;1127 defer if (env_refs) |e| ctx.gpa.free(e);1128 if (findHomeManagerPath(gen_refs)) |pkg_path| {1129 if (exec.homeGenerationReferences(ctx.gpa, ctx.io, &ctx.machine, pkg_path)) |e| {1130 env_refs = e;1131 refs = e;1132 } else |_| {}1133 }11341135 1136 1137 1138 var seen = std.StringHashMap(void).init(ctx.gpa);1139 defer seen.deinit();1140 var lines = std.mem.splitScalar(u8, refs, '\n');1141 while (lines.next()) |line| {1142 const t = std.mem.trim(u8, line, " \t\r");1143 if (t.len == 0) continue;1144 const row = pkgFromStorePath(t);1145 if (row.version.len == 0) continue;1146 if (std.mem.indexOf(u8, row.name, "home-manager") != null) continue;1147 if (seen.contains(row.name)) continue;1148 seen.put(row.name, {}) catch {};1149 output.listPkg(row.name, row.version);1150 }1151}1152115311541155fn findHomeManagerPath(refs: []const u8) ?[]const u8 {1156 var lines = std.mem.splitScalar(u8, refs, '\n');1157 while (lines.next()) |line| {1158 const t = std.mem.trim(u8, line, " \t\r");1159 if (std.mem.indexOf(u8, t, "home-manager-path") != null) return t;1160 }1161 return null;1162}11631164test "findHomeManagerPath picks the env, not the generation or infra" {1165 const refs =1166 \\/nix/store/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa-bash-5.31167 \\/nix/store/bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb-home-manager-path1168 \\/nix/store/cccccccccccccccccccccccccccccccc-coreutils-9.111169 ;1170 try std.testing.expectEqualStrings(1171 "/nix/store/bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb-home-manager-path",1172 findHomeManagerPath(refs).?,1173 );1174 try std.testing.expectEqual(@as(?[]const u8, null), findHomeManagerPath("/nix/store/x-bash-5.3\n"));1175}11761177fn homeDiff(ctx: Ctx) !void {1178 const out = try exec.homeGenerations(ctx.gpa, ctx.io, &ctx.machine);1179 defer ctx.gpa.free(out);1180 const gens = try parseHomeGenerations(ctx.gpa, out);1181 defer freeHomeGens(ctx.gpa, gens);11821183 1184 if (gens.len < 2 or gens[0].path.len == 0 or gens[1].path.len == 0) {1185 output.homeNothingToDiff();1186 return;1187 }1188 const newer = gens[0];1189 const older = gens[1];1190 output.homeDiffHeader(older.number, newer.number, ctx.machine.name);1191 if (try exec.diffClosuresPaths(ctx.io, &ctx.machine, ctx.gpa, older.path, newer.path) != 0)1192 return error.ProcessFailed;1193}11941195fn homeCheck(ctx: Ctx) !void {1196 output.homeCheckStart(ctx.machine.name);1197 const code = try exec.homeManagerBuild(ctx.io, &ctx.machine, ctx.gpa);1198 if (code != 0) {1199 output.checkFailed("");1200 return error.BuildFailed;1201 }1202 output.homeCheckOk();1203}12041205fn homeEdit(ctx: Ctx) !void {1206 1207 1208 const path = exec.homeNixPath(ctx.gpa, ctx.environ) orelse {1209 errors.error_info.setMessage("could not resolve ~/.config/home-manager/home.nix", .{});1210 return error.ConfigNotFound;1211 };1212 defer ctx.gpa.free(path);1213 output.homeEditOpen(path, ctx.cfg.editor);1214 _ = try exec.openEditor(ctx.io, ctx.gpa, ctx.cfg.editor, path, null);1215}12161217fn homeHistory(ctx: Ctx) !void {1218 const out = try exec.homeGenerations(ctx.gpa, ctx.io, &ctx.machine);1219 defer ctx.gpa.free(out);1220 const gens = try parseHomeGenerations(ctx.gpa, out);1221 defer freeHomeGens(ctx.gpa, gens);1222 output.homeHistoryHeader(ctx.machine.name);1223 1224 for (gens, 0..) |g, i| output.homeHistoryRow(g.number, g.date, g.time, i == 0);1225}12261227fn homeBack(ctx: Ctx) !void {1228 const out = try exec.homeGenerations(ctx.gpa, ctx.io, &ctx.machine);1229 defer ctx.gpa.free(out);1230 const gens = try parseHomeGenerations(ctx.gpa, out);1231 defer freeHomeGens(ctx.gpa, gens);12321233 1234 if (gens.len < 2) {1235 output.alreadyOldest();1236 return;1237 }1238 const current = gens[0].number;1239 const prev = gens[1].number;12401241 output.homeBackStart(ctx.machine.name, current, prev);1242 if (ctx.cfg.confirm and !output.confirm("")) return;1243 const code = try exec.homeManagerRollback(ctx.io, &ctx.machine, ctx.gpa);1244 if (code != 0) return error.BuildFailed;1245 output.homeBackDone(prev);1246}12471248fn homeApply(ctx: Ctx, mode: exec.HomeManagerMode) !void {1249 1250 if (mode == .module) {1251 output.homeModuleManaged();1252 return;1253 }1254 if (ctx.dry) {1255 output.homeApplyDry(ctx.machine.name);1256 const code = try exec.homeManagerBuild(ctx.io, &ctx.machine, ctx.gpa);1257 if (code != 0) return error.BuildFailed;1258 output.homeApplyDryDone();1259 return;1260 }12611262 if (!try runPreHook(ctx, "pre-home")) {1263 output.aborted();1264 return;1265 }12661267 output.homeApplyStart(ctx.machine.name);1268 const flake_ref = exec.homeFlakeRef(ctx.gpa, ctx.io, ctx.environ);1269 defer if (flake_ref) |r| ctx.gpa.free(r);1270 if (flake_ref) |ref| {1271 output.printSubstep("home-manager switch --flake {s}", .{ref});1272 } else {1273 output.printSubstep("home-manager switch", .{});1274 }12751276 const start = std.Io.Clock.awake.now(ctx.io);1277 const code = if (flake_ref) |ref|1278 try exec.homeManagerSwitchFlake(ctx.io, &ctx.machine, ctx.gpa, ref)1279 else1280 try exec.homeManagerSwitch(ctx.io, &ctx.machine, ctx.gpa);1281 if (code != 0) return error.BuildFailed;12821283 const elapsed_ns = @max(0, start.untilNow(ctx.io, .awake).toNanoseconds());1284 const elapsed: u64 = @intCast(@divFloor(elapsed_ns, 1_000_000));1285 output.homeApplyDone(homeCurrentGen(ctx), elapsed);1286 runPostHook(ctx, "post-home");1287}1288128912901291const service_help = [_][2][]const u8{1292 .{ "list", "list running services" },1293 .{ "logs <svc>", "show service logs" },1294 .{ "status <svc>", "service status" },1295 .{ "start <svc>", "start a service" },1296 .{ "stop <svc>", "stop a service" },1297 .{ "restart <svc>", "restart a service" },1298 .{ "enable <svc>", "enable at boot" },1299 .{ "disable <svc>", "disable at boot" },1300};13011302pub fn service(ctx: Ctx) !void {1303 if (ctx.sub.len == 0 or std.mem.eql(u8, ctx.sub, "help")) return output.subcommandHelp("service", &service_help, "add --user to act on user services");1304 if (std.mem.eql(u8, ctx.sub, "list")) {1305 output.serviceListHeader(ctx.machine.name);1306 const svcs = exec.serviceList(ctx.gpa, ctx.io, &ctx.machine, ctx.user) catch &.{};1307 defer ctx.gpa.free(svcs);1308 for (svcs) |s| output.serviceRow(s);1309 return;1310 }1311 if (std.mem.eql(u8, ctx.sub, "logs")) {1312 const name = if (ctx.args.len > 0) ctx.args[0] else return error.Internal;1313 const follow = ctx.args.len > 1 and std.mem.eql(u8, ctx.args[1], "-f");1314 output.serviceLogsHeader(name, ctx.last, ctx.machine.name);1315 _ = try exec.serviceLogs(ctx.io, &ctx.machine, ctx.gpa, name, ctx.last, follow, ctx.user);1316 return;1317 }1318 if (std.mem.eql(u8, ctx.sub, "status")) {1319 const name = if (ctx.args.len > 0) ctx.args[0] else return error.Internal;1320 output.serviceVerb("status", name, ctx.machine.name);1321 _ = try exec.serviceCtl(ctx.io, &ctx.machine, ctx.gpa, "status", name, ctx.user);1322 return;1323 }1324 1325 const name = if (ctx.args.len > 0) ctx.args[0] else return error.Internal;1326 if (ctx.cfg.confirm and (std.mem.eql(u8, ctx.sub, "stop") or std.mem.eql(u8, ctx.sub, "disable"))) {1327 if (!output.confirm("")) return;1328 }1329 output.serviceVerb(ctx.sub, name, ctx.machine.name);1330 _ = try exec.serviceCtl(ctx.io, &ctx.machine, ctx.gpa, ctx.sub, name, ctx.user);1331 output.serviceDone();1332}1333133413351336const flake_help = [_][2][]const u8{1337 .{ "init", "create a new flake.nix" },1338 .{ "update", "update flake inputs" },1339 .{ "update <in>", "update a specific input" },1340 .{ "check", "validate the flake" },1341 .{ "show", "inspect flake outputs" },1342 .{ "lock", "regenerate flake.lock" },1343 .{ "clone <url>", "clone a flake repository" },1344};13451346pub fn flake(ctx: Ctx) !void {1347 if (ctx.sub.len == 0 or std.mem.eql(u8, ctx.sub, "help")) return output.subcommandHelp("flake", &flake_help, null);1348 if (std.mem.eql(u8, ctx.sub, "update")) {1349 output.flakeUpdateStart();1350 const input: ?[]const u8 = if (ctx.args.len > 0) ctx.args[0] else null;1351 _ = try exec.flakeUpdate(ctx.io, &ctx.machine, ctx.gpa, input);1352 output.flakeLockWritten();1353 return;1354 }1355 if (std.mem.eql(u8, ctx.sub, "check")) {1356 output.flakeCheckStart();1357 const code = try exec.flakeCheck(ctx.io, &ctx.machine, ctx.gpa);1358 if (code == 0) output.flakeValid() else output.checkFailed("");1359 return;1360 }1361 if (std.mem.eql(u8, ctx.sub, "show")) {1362 const json_out = exec.flakeShow(ctx.gpa, ctx.io, &ctx.machine) catch null;1363 if (json_out) |j| {1364 defer ctx.gpa.free(j);1365 output.flakeShowHeader("flake outputs");1366 1367 const parsed = std.json.parseFromSlice(std.json.Value, ctx.gpa, j, .{}) catch return;1368 defer parsed.deinit();1369 if (parsed.value == .object) {1370 var it = parsed.value.object.iterator();1371 while (it.next()) |entry| {1372 if (entry.value_ptr.* == .object) {1373 var inner = entry.value_ptr.object.iterator();1374 while (inner.next()) |e2| {1375 const desc = if (e2.value_ptr.* == .object)1376 if (e2.value_ptr.object.get("description")) |d|1377 if (d == .string) d.string else ""1378 else1379 ""1380 else1381 "";1382 output.flakeShowEntry(entry.key_ptr.*, e2.key_ptr.*, desc);1383 }1384 }1385 }1386 }1387 }1388 return;1389 }1390 if (std.mem.eql(u8, ctx.sub, "init")) {1391 _ = try exec.flakeInit(ctx.io, &ctx.machine, ctx.gpa);1392 return;1393 }1394 if (std.mem.eql(u8, ctx.sub, "lock")) {1395 _ = try exec.flakeLock(ctx.io, &ctx.machine, ctx.gpa);1396 return;1397 }1398 if (std.mem.eql(u8, ctx.sub, "clone")) {1399 const url = if (ctx.args.len > 0) ctx.args[0] else return error.Internal;1400 _ = try exec.flakeClone(ctx.io, &ctx.machine, ctx.gpa, url);1401 return;1402 }1403}1404140514061407const store_help = [_][2][]const u8{1408 .{ "info", "store size and counts" },1409 .{ "path <attr>", "store path of a package" },1410 .{ "gc", "collect garbage" },1411 .{ "verify", "verify store integrity" },1412 .{ "repair", "repair corrupted paths" },1413};14141415pub fn store(ctx: Ctx) !void {1416 if (ctx.sub.len == 0 or std.mem.eql(u8, ctx.sub, "help")) return output.subcommandHelp("store", &store_help, null);1417 if (std.mem.eql(u8, ctx.sub, "gc")) {1418 _ = try exec.storeGc(ctx.io, &ctx.machine, ctx.gpa);1419 return;1420 }1421 if (std.mem.eql(u8, ctx.sub, "verify")) {1422 output.storeVerifyStart();1423 _ = try exec.storeVerify(ctx.io, &ctx.machine, ctx.gpa);1424 return;1425 }1426 if (std.mem.eql(u8, ctx.sub, "repair")) {1427 if (ctx.cfg.confirm and !output.confirm("")) return;1428 output.storeRepairStart();1429 _ = try exec.storeRepair(ctx.io, &ctx.machine, ctx.gpa);1430 return;1431 }1432 if (std.mem.eql(u8, ctx.sub, "info")) {1433 const si = try exec.storeInfo(ctx.gpa, ctx.io, &ctx.machine);1434 output.storeInfo(si);1435 return;1436 }1437 if (std.mem.eql(u8, ctx.sub, "path")) {1438 const attr = if (ctx.args.len > 0) ctx.args[0] else return error.Internal;1439 const path = try exec.storePath(ctx.gpa, ctx.io, &ctx.machine, attr);1440 defer ctx.gpa.free(path);1441 output.storePath(path);1442 return;1443 }1444 1445 const si = try exec.storeInfo(ctx.gpa, ctx.io, &ctx.machine);1446 output.storeInfo(si);1447}1448144914501451const channel_help = [_][2][]const u8{1452 .{ "list", "list channels" },1453 .{ "add <url> [name]", "add a channel" },1454 .{ "remove <name>", "remove a channel" },1455 .{ "update", "update all channels" },1456};14571458pub fn channel(ctx: Ctx) !void {1459 if (ctx.sub.len == 0 or std.mem.eql(u8, ctx.sub, "help")) return output.subcommandHelp("channel", &channel_help, null);1460 1461 if (flakeSystem(ctx)) output.flakeChannelNote();1462 if (std.mem.eql(u8, ctx.sub, "list")) {1463 output.channelListHeader(ctx.machine.name);1464 const chans = try exec.channelList(ctx.gpa, ctx.io, &ctx.machine);1465 defer ctx.gpa.free(chans);1466 for (chans) |ch| output.channelRow(ch);1467 return;1468 }1469 if (std.mem.eql(u8, ctx.sub, "add")) {1470 const url = if (ctx.args.len > 0) ctx.args[0] else return error.Internal;1471 const name = if (ctx.args.len > 1) ctx.args[1] else "";1472 _ = try exec.channelAdd(ctx.io, &ctx.machine, ctx.gpa, url, name);1473 output.channelDone();1474 return;1475 }1476 if (std.mem.eql(u8, ctx.sub, "remove")) {1477 const name = if (ctx.args.len > 0) ctx.args[0] else return error.Internal;1478 _ = try exec.channelRemove(ctx.io, &ctx.machine, ctx.gpa, name);1479 output.channelDone();1480 return;1481 }1482 if (std.mem.eql(u8, ctx.sub, "update")) {1483 return channelUpdateCmd(ctx);1484 }1485}1486148714881489pub fn profile(ctx: Ctx) !void {1490 if (std.mem.eql(u8, ctx.sub, "list") or ctx.sub.len == 0) {1491 output.profileListHeader();1492 const out = exec.profileList(ctx.gpa, ctx.io, &ctx.machine) catch null;1493 if (out) |o| {1494 defer ctx.gpa.free(o);1495 var lines = std.mem.splitScalar(u8, o, '\n');1496 while (lines.next()) |line| {1497 const t = std.mem.trim(u8, line, " \t\r");1498 if (t.len == 0) continue;1499 output.profileRow(t, "", "");1500 }1501 }1502 return;1503 }1504 if (std.mem.eql(u8, ctx.sub, "install")) {1505 const attr = if (ctx.args.len > 0) ctx.args[0] else return error.PackageNotFound;1506 _ = try exec.profileInstall(ctx.io, &ctx.machine, ctx.gpa, attr);1507 output.done();1508 return;1509 }1510 if (std.mem.eql(u8, ctx.sub, "remove")) {1511 const attr = if (ctx.args.len > 0) ctx.args[0] else return error.PackageNotFound;1512 _ = try exec.profileRemove(ctx.io, &ctx.machine, ctx.gpa, attr);1513 output.done();1514 return;1515 }1516 if (std.mem.eql(u8, ctx.sub, "upgrade")) {1517 _ = try exec.profileUpgrade(ctx.io, &ctx.machine, ctx.gpa);1518 output.done();1519 return;1520 }1521}1522152315241525const pkg_help = [_][2][]const u8{1526 .{ "why <pkg>", "what pulled it in" },1527 .{ "deps <pkg>", "direct dependencies" },1528 .{ "size <pkg>", "closure size" },1529 .{ "path <pkg>", "store path" },1530 .{ "closure <pkg>", "full closure" },1531};15321533pub fn pkg(ctx: Ctx) !void {1534 if (ctx.sub.len == 0 or std.mem.eql(u8, ctx.sub, "help")) return output.subcommandHelp("pkg", &pkg_help, null);1535 const name = if (ctx.args.len > 0) ctx.args[0] else return error.PackageNotFound;1536 if (std.mem.eql(u8, ctx.sub, "why")) {1537 if (try exec.pkgWhy(ctx.io, &ctx.machine, ctx.gpa, name) != 0) return error.PackageNotFound;1538 return;1539 }1540 if (std.mem.eql(u8, ctx.sub, "deps")) {1541 if (try exec.pkgDeps(ctx.io, &ctx.machine, ctx.gpa, name) != 0) return error.PackageNotFound;1542 return;1543 }1544 if (std.mem.eql(u8, ctx.sub, "size")) {1545 const s = try exec.pkgSize(ctx.gpa, ctx.io, &ctx.machine, name);1546 defer ctx.gpa.free(s);1547 output.pkgResult("size", s);1548 return;1549 }1550 if (std.mem.eql(u8, ctx.sub, "path")) {1551 const p = try exec.storePath(ctx.gpa, ctx.io, &ctx.machine, name);1552 defer ctx.gpa.free(p);1553 output.pkgResult("path", p);1554 return;1555 }1556 if (std.mem.eql(u8, ctx.sub, "closure")) {1557 if (try exec.pkgClosure(ctx.io, &ctx.machine, ctx.gpa, name) != 0) return error.PackageNotFound;1558 return;1559 }1560}1561156215631564pub fn why(ctx: Ctx) !void {1565 const name = if (ctx.args.len > 0) ctx.args[0] else return error.PackageNotFound;1566 if (try exec.pkgWhy(ctx.io, &ctx.machine, ctx.gpa, name) != 0) return error.PackageNotFound;1567}1568156915701571pub fn pin(ctx: Ctx) !void {1572 const input = if (ctx.args.len > 0) ctx.args[0] else return error.Internal;1573 const commit = if (ctx.args.len > 1) ctx.args[1] else return error.Internal;1574 exec.editFlakeLock(ctx.gpa, input, commit) catch return error.ProcessFailed;1575 output.pinDone(input, commit);1576}15771578pub fn unpin(ctx: Ctx) !void {1579 const input = if (ctx.args.len > 0) ctx.args[0] else return error.Internal;1580 _ = try exec.flakeUpdate(ctx.io, &ctx.machine, ctx.gpa, input);1581 output.unpinDone(input);1582}1583158415851586pub fn hash(ctx: Ctx) !void {1587 const path = if (ctx.args.len > 0) ctx.args[0] else return error.Internal;1588 const h = try exec.nixHash(ctx.gpa, ctx.io, &ctx.machine, path);1589 defer ctx.gpa.free(h);1590 output.hashResult(std.mem.trim(u8, h, " \t\n\r"));1591}15921593pub fn fetch(ctx: Ctx) !void {1594 const url = if (ctx.args.len > 0) ctx.args[0] else return error.Internal;1595 const h = try exec.nixFetch(ctx.gpa, ctx.io, &ctx.machine, url);1596 defer ctx.gpa.free(h);1597 output.fetchResult(std.mem.trim(u8, h, " \t\n\r"), url);1598}1599160016011602pub fn build(ctx: Ctx) !void {1603 const attr: ?[]const u8 = if (ctx.args.len > 0) ctx.args[0] else null;1604 output.buildStart(attr orelse "default");1605 const code = try exec.nixBuild(ctx.io, &ctx.machine, ctx.gpa, attr);1606 if (code != 0) return error.BuildFailed;1607 output.buildDone();1608}16091610pub fn runCmd(ctx: Ctx) !void {1611 const pkg_arg = if (ctx.args.len > 0) ctx.args[0] else return error.PackageNotFound;1612 output.runStart(pkg_arg);1613 _ = try exec.nixRun(ctx.io, &ctx.machine, ctx.gpa, pkg_arg);1614}1615161616171618pub fn hello(ctx: Ctx) !void {1619 output.hello(ctx.cfg.machines);1620}16211622const ConfigKind = enum { flake, channel, none };1623162416251626fn scanConfig(io: std.Io) ConfigKind {1627 if (exec.pathExists(io, "/etc/nixos/flake.nix")) return .flake;1628 if (exec.pathExists(io, "/etc/nixos/configuration.nix")) return .channel;1629 return .none;1630}16311632163316341635pub fn firstRunWizard(gpa: std.mem.Allocator, io: std.Io, environ: *const std.process.Environ.Map) !void {1636 output.firstRunBanner();1637 output.firstRunNoConfig();16381639 var name_buf: [256]u8 = undefined;1640 var path_buf: [256]u8 = undefined;16411642 const machine_name = output.firstRunPrompt("machine name", "my-machine", &name_buf);16431644 output.firstRunScanning();1645 const kind = scanConfig(io);1646 switch (kind) {1647 .flake => output.firstRunDetected("/etc/nixos/flake.nix", true),1648 .channel => output.firstRunDetected("/etc/nixos/configuration.nix", false),1649 .none => output.firstRunNoneFound(),1650 }16511652 1653 1654 const default_path: []const u8 = if (kind == .none) "" else "/etc/nixos";1655 const config_path = output.firstRunPromptDefault("config path", default_path, kind != .none, &path_buf);16561657 1658 1659 const use_flakes = exec.dirHasFlake(gpa, io, config_path);16601661 const conf_path = try config.configPath(gpa, environ);1662 defer gpa.free(conf_path);1663 if (std.fs.path.dirname(conf_path)) |dir| exec.ensureDir(gpa, io, dir);16641665 1666 const flake_line = if (use_flakes) "flake = true\n" else "";1667 var content_buf: [1024]u8 = undefined;1668 const content = try std.fmt.bufPrint(1669 &content_buf,1670 "{s}[machine]\nname = {s}\nconfig = {s}\nlocal = true\n",1671 .{ flake_line, machine_name, config_path },1672 );16731674 const file = std.Io.Dir.createFile(.cwd(), io, conf_path, .{}) catch {1675 errors.error_info.setMessage("could not write {s}", .{conf_path});1676 return error.Internal;1677 };1678 defer file.close(io);1679 file.writePositionalAll(io, content, 0) catch {1680 errors.error_info.setMessage("could not write {s}", .{conf_path});1681 return error.Internal;1682 };16831684 output.firstRunWritten(conf_path);1685}16861687pub fn setup(gpa: std.mem.Allocator, io: std.Io, environ: *const std.process.Environ.Map) !void {1688 1689 1690 1691 config.migrateLegacyConfig(gpa, io, environ);1692 if (config.configPath(gpa, environ)) |conf_path| {1693 defer gpa.free(conf_path);1694 if (std.Io.Dir.openFile(.cwd(), io, conf_path, .{})) |f| {1695 f.close(io);1696 } else |_| {1697 try firstRunWizard(gpa, io, environ);1698 errors.error_info.reset();1699 }1700 } else |_| {}17011702 output.setupCheck();1703 const status = exec.checkNixFeatures(gpa, io, environ);1704 output.setupFeatures(status.nix_command, status.flakes);17051706 if (status.allEnabled()) {1707 output.setupAllSet();1708 api.ensureRegistryPinned(gpa, io, environ);1709 return;1710 }17111712 1713 1714 1715 1716 if (exec.isNixOS(io)) {1717 const conf_path = "/etc/nixos/configuration.nix";1718 if (exec.nixosConfigEnablesFeatures(gpa, io, conf_path)) {1719 output.setupNixosNeedsRebuild(conf_path);1720 return;1721 }1722 output.setupNixosOffer(exec.NIXOS_FEATURES_LINE, conf_path);1723 if (output.confirmDefaultNo("add it now?")) {1724 if (exec.appendFeaturesToNixosConfig(gpa, io, environ, conf_path)) |_| {1725 output.setupNixosWrote(conf_path);1726 } else |_| {1727 output.setupNixosManual(exec.NIXOS_FEATURES_LINE, conf_path);1728 }1729 } else {1730 output.setupNixosManual(exec.NIXOS_FEATURES_LINE, conf_path);1731 }1732 return;1733 }17341735 1736 output.setupEnabling();1737 const path = try exec.enableNixFeatures(gpa, io, environ);1738 defer gpa.free(path);17391740 output.setupDone(path);1741 api.ensureRegistryPinned(gpa, io, environ);1742}1743174417451746fn logAction(ctx: Ctx, action: []const u8, gen: u32) void {1747 var ts_buf: [32]u8 = undefined;1748 const ts = nowIso(ctx.io, &ts_buf);1749 var buf: [256]u8 = undefined;1750 const entry = std.fmt.bufPrint(&buf, "{{\"ts\":\"{s}\",\"machine\":\"{s}\",\"command\":\"{s}\",\"outcome\":\"success\",\"gen_before\":null,\"gen_after\":{d},\"duration_ms\":0}}\n", .{1751 ts, ctx.machine.name, action, gen,1752 }) catch return;1753 exec.appendNinaLog(ctx.io, ctx.environ, entry);1754}1755175617571758fn nowIso(io: std.Io, buf: []u8) []const u8 {1759 const fallback = "1970-01-01T00:00:00Z";1760 const ns = std.Io.Clock.real.now(io).toNanoseconds();1761 if (ns <= 0) return fallback;1762 const secs: u64 = @intCast(@divFloor(ns, 1_000_000_000));1763 const es = std.time.epoch.EpochSeconds{ .secs = secs };1764 const ed = es.getEpochDay();1765 const yd = ed.calculateYearDay();1766 const md = yd.calculateMonthDay();1767 const ds = es.getDaySeconds();1768 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", .{1769 yd.year, md.month.numeric(), @as(u32, md.day_index) + 1,1770 ds.getHoursIntoDay(), ds.getMinutesIntoHour(), ds.getSecondsIntoMinute(),1771 }) catch return fallback;1772}