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 error.HookAborted;75 }7677 output.applyStart(ctx.machine.name);78 output.buildPanelInit();79 const start = std.Io.Clock.awake.now(ctx.io);8081 var panel = output.BuildPanel{};82 const code = exec.nixosRebuildSwitchWithPanel(ctx.io, &ctx.machine, ctx.gpa, &panel) catch |e| {83 output.buildPanelClear();84 exec.markApplyFailed(ctx.io, ctx.environ);85 return e;86 };87 output.buildPanelClear();8889 if (code != 0) {90 exec.markApplyFailed(ctx.io, ctx.environ);91 return error.BuildFailed;92 }9394 const elapsed_ns = @max(0, start.untilNow(ctx.io, .awake).toNanoseconds());95 const elapsed: u64 = @intCast(@divFloor(elapsed_ns, 1_000_000));96 const gen = exec.getCurrentGeneration(ctx.gpa, ctx.io, &ctx.machine) catch 0;97 const after_failure = exec.takeApplyFailed(ctx.io, ctx.environ);98 output.applyDone(gen, elapsed, after_failure);99 100 const warnings = errors.extractWarnings(ctx.gpa, errors.getBuildStderr()) catch &.{};101 defer {102 for (warnings) |w| ctx.gpa.free(w);103 ctx.gpa.free(warnings);104 }105 if (warnings.len > 0) output.buildWarnings(warnings);106 runPostHook(ctx, "post-apply");107 logAction(ctx, "apply", gen);108}109110pub fn check(ctx: Ctx) !void {111 output.checkStart(ctx.machine.name);112 const code = try exec.nixosRebuildBuild(ctx.io, &ctx.machine, ctx.gpa);113 if (code != 0) return error.BuildFailed;114 output.checkOk();115}116117pub fn back(ctx: Ctx) !void {118 const gens = try exec.getGenerations(ctx.gpa, ctx.io, &ctx.machine);119 defer ctx.gpa.free(gens);120121 var current: u32 = 0;122 for (gens) |g| if (g.current) {123 current = g.number;124 };125126 127 128 var prev: u32 = 0;129 var has_prev = false;130 for (gens) |g| {131 if (g.number < current and (!has_prev or g.number > prev)) {132 prev = g.number;133 has_prev = true;134 }135 }136 if (!has_prev) {137 output.alreadyOldest();138 return;139 }140141 if (!try runPreHook(ctx, "pre-back")) {142 output.aborted();143 return error.HookAborted;144 }145146 output.backStart(ctx.machine.name, current, prev);147 if (ctx.cfg.confirm and !output.confirm("")) return;148149 150 151 152 const code = if (flakeSystem(ctx))153 try exec.switchGeneration(ctx.io, &ctx.machine, ctx.gpa, prev)154 else155 try exec.rollback(ctx.io, &ctx.machine, ctx.gpa);156 if (code != 0) return error.BuildFailed;157 output.backDone(prev);158 runPostHook(ctx, "post-back");159}160161pub fn go(ctx: Ctx) !void {162 if (ctx.args.len == 0) return error.Internal;163 const target = std.fmt.parseInt(u32, ctx.args[0], 10) catch return error.Internal;164 const current = try exec.getCurrentGeneration(ctx.gpa, ctx.io, &ctx.machine);165166 output.goStart(ctx.machine.name, current, target);167 if (ctx.cfg.confirm and !output.confirm("")) return;168169 const code = try exec.switchGeneration(ctx.io, &ctx.machine, ctx.gpa, target);170 if (code != 0) return error.ProcessFailed;171 output.generationDone(target);172}173174pub fn history(ctx: Ctx) !void {175 const gens = try exec.getGenerations(ctx.gpa, ctx.io, &ctx.machine);176 defer ctx.gpa.free(gens);177 output.historyHeader(ctx.machine.name);178 for (gens) |g| output.historyRow(g);179}180181const gen_help = [_][2][]const u8{182 .{ "list", "list generations" },183 .{ "current", "show current generation" },184 .{ "delete <n>", "delete generation n" },185 .{ "delete old", "delete old generations" },186};187188pub fn genCmd(ctx: Ctx) !void {189 if (ctx.sub.len == 0 or std.mem.eql(u8, ctx.sub, "help")) return output.subcommandHelp("gen", &gen_help, null);190 if (std.mem.eql(u8, ctx.sub, "list")) return history(ctx);191 if (std.mem.eql(u8, ctx.sub, "current")) {192 const n = try exec.getCurrentGeneration(ctx.gpa, ctx.io, &ctx.machine);193 output.genCurrent(n);194 return;195 }196 if (std.mem.eql(u8, ctx.sub, "delete")) {197 if (ctx.args.len == 0) return error.Internal;198 if (std.mem.eql(u8, ctx.args[0], "old")) {199 if (ctx.cfg.confirm and !output.confirm("")) return;200 _ = try exec.deleteOldGenerations(ctx.io, &ctx.machine, ctx.gpa);201 } else {202 const n = std.fmt.parseInt(u32, ctx.args[0], 10) catch return error.Internal;203 if (ctx.cfg.confirm and !output.confirm("")) return;204 _ = try exec.deleteGeneration(ctx.io, &ctx.machine, ctx.gpa, n);205 }206 output.done();207 return;208 }209 return error.Internal;210}211212pub fn clean(ctx: Ctx) !void {213 const gens = try exec.getGenerations(ctx.gpa, ctx.io, &ctx.machine);214 defer ctx.gpa.free(gens);215 const all = ctx.all or std.mem.eql(u8, ctx.sub, "all");216 const total: u32 = @intCast(gens.len);217 218 219 const keep: u32 = if (all) 1 else ctx.cfg.generations;220 if (total <= keep) {221 output.cleanNothing(ctx.machine.name, total);222 return;223 }224 output.cleanStart(ctx.machine.name, keep, total);225 if (ctx.cfg.confirm and !output.confirm("removing — cannot be undone")) return;226 _ = try exec.collectGarbage(ctx.io, &ctx.machine, ctx.gpa, all, ctx.cfg.generations);227 output.cleanDone();228}229230pub fn statusCmd(ctx: Ctx) !void {231 output.statusHeader();232 233 234 if (ctx.all and ctx.cfg.machines.len > 0) {235 for (ctx.cfg.machines) |m| statusRowFor(ctx, &m);236 } else {237 statusRowFor(ctx, &ctx.machine);238 }239 homeStatusLine(ctx);240}241242243244245fn homeStatusLine(ctx: Ctx) void {246 if (exec.detectHomeManager(ctx.gpa, ctx.io, ctx.environ) != .standalone) return;247 const out = exec.homeGenerations(ctx.gpa, ctx.io, &ctx.machine) catch return;248 defer ctx.gpa.free(out);249 const gens = parseHomeGenerations(ctx.gpa, out) catch return;250 defer freeHomeGens(ctx.gpa, gens);251 if (gens.len == 0) return;252 output.statusHomeRow(gens[0].number, gens[0].date);253}254255fn statusRowFor(ctx: Ctx, machine: *const types.Machine) void {256 257 258 const gen = exec.getCurrentGeneration(ctx.gpa, ctx.io, machine) catch {259 output.statusRow(machine.name, 0, "", "", false);260 return;261 };262 const st = exec.systemStatus(ctx.gpa, ctx.io, machine);263 defer ctx.gpa.free(st.size);264 defer ctx.gpa.free(st.age);265 output.statusRow(machine.name, gen, st.age, st.size, true);266}267268pub fn diff(ctx: Ctx) !void {269 const gens = try exec.getGenerations(ctx.gpa, ctx.io, &ctx.machine);270 defer ctx.gpa.free(gens);271 var current: u32 = 0;272 var prev: u32 = 0;273 for (gens) |g| {274 if (g.current) current = g.number;275 if (!g.current and g.number > prev and g.number < current) prev = g.number;276 }277 if (ctx.args.len >= 2) {278 const a = std.fmt.parseInt(u32, ctx.args[0], 10) catch current - 1;279 const b = std.fmt.parseInt(u32, ctx.args[1], 10) catch current;280 output.diffHeader(a, b, ctx.machine.name);281 if (try exec.diffClosures(ctx.io, &ctx.machine, ctx.gpa, a, b) != 0) return error.ProcessFailed;282 } else {283 output.diffHeader(if (prev > 0) prev else current - 1, current, ctx.machine.name);284 if (try exec.diffClosures(ctx.io, &ctx.machine, ctx.gpa, if (prev > 0) prev else current - 1, current) != 0) return error.ProcessFailed;285 }286}287288pub fn mood(ctx: Ctx) !void {289 const gen = exec.getCurrentGeneration(ctx.gpa, ctx.io, &ctx.machine) catch 0;290 const st = exec.systemStatus(ctx.gpa, ctx.io, &ctx.machine);291 defer ctx.gpa.free(st.size);292 defer ctx.gpa.free(st.age);293 const state = exec.systemRunningState(ctx.gpa, ctx.io, &ctx.machine) catch try ctx.gpa.dupe(u8, "unknown");294 defer ctx.gpa.free(state);295 output.mood(ctx.machine.name, state, gen, st.age, st.size);296}297298pub fn doctor(ctx: Ctx) !void {299 output.doctorHeader(ctx.machine.name);300 const checks = try exec.runDoctorChecks(ctx.gpa, ctx.io, &ctx.machine);301 defer ctx.gpa.free(checks);302 var warnings: u32 = 0;303 var failures: u32 = 0;304 for (checks) |ch| {305 output.doctorRow(ch);306 if (ch.status == .warn) warnings += 1;307 if (ch.status == .fail) failures += 1;308 }309310 311 312 const hm_mode = exec.detectHomeManager(ctx.gpa, ctx.io, ctx.environ);313 if (hm_mode != .none) {314 const note = homeDoctorNote(ctx, hm_mode);315 defer ctx.gpa.free(note);316 output.doctorRow(.{ .name = "home manager", .status = .ok, .note = note });317 }318319 output.doctorSummary(warnings, failures);320}321322323324fn homeDoctorNote(ctx: Ctx, mode: exec.HomeManagerMode) []const u8 {325 const label = switch (mode) {326 .standalone => "standalone",327 .module => "module",328 .none => "",329 };330 if (mode == .standalone) {331 const gen = homeCurrentGen(ctx);332 if (gen != 0)333 return std.fmt.allocPrint(ctx.gpa, "{s} gen {d}", .{ label, gen }) catch334 (ctx.gpa.dupe(u8, label) catch "");335 }336 return ctx.gpa.dupe(u8, label) catch "";337}338339340pub fn update(ctx: Ctx) !void {341 output.updateSelfStart();342343 if (comptime std.mem.eql(u8, version.PLATFORM, "unsupported")) {344 output.updateSelfNoPlatform(@tagName(builtin.cpu.arch) ++ "-" ++ @tagName(builtin.os.tag));345 return error.Internal;346 }347348 const env_url = ctx.environ.get("NINA_UPDATE_SOURCE");349 const source_url = env_url orelse UPDATE_URL;350351 const body = exec.fetchUrl(ctx.gpa, ctx.io, source_url) catch |e| switch (e) {352 error.NetworkError, error.HttpError => {353 output.updateSelfNetworkError();354 return;355 },356 else => return e,357 };358 defer ctx.gpa.free(body);359360 const parsed = std.json.parseFromSlice(std.json.Value, ctx.gpa, body, .{}) catch {361 output.printWarning("could not parse release metadata");362 return;363 };364 defer parsed.deinit();365366 const root = parsed.value;367 if (root != .object) {368 output.printWarning("malformed release metadata");369 return;370 }371372 const remote_ver = blk: {373 const v = root.object.get("version") orelse {374 output.printWarning("malformed release metadata");375 return;376 };377 break :blk switch (v) {378 .string => |s| s,379 else => {380 output.printWarning("malformed release metadata");381 return;382 },383 };384 };385 const release_notes = blk: {386 const v = root.object.get("release_notes") orelse break :blk "";387 break :blk switch (v) {388 .string => |s| s,389 else => "",390 };391 };392393 if (std.mem.eql(u8, remote_ver, version.VERSION)) {394 output.updateSelfCurrent(version.VERSION);395 return;396 }397398 output.updateSelfAvailable(version.VERSION, remote_ver);399 output.updateSelfReleaseNotes(release_notes);400 if (ctx.check) return;401402 var exe_buf: [std.Io.Dir.max_path_bytes]u8 = undefined;403 const install_path = exec.selfExePath(ctx.io, &exe_buf) catch {404 errors.error_info.setMessage("could not determine install path", .{});405 return error.Internal;406 };407408 409 410 411 412 413 if (std.mem.startsWith(u8, install_path, "/nix/store/")) {414 const nina_element = exec.profileNinaElement(ctx.gpa, ctx.io, &ctx.machine);415 defer {416 if (nina_element) |n| ctx.gpa.free(n);417 }418 if (nina_element == null) {419 output.updateSelfNotInProfile();420 return;421 }422 const element = nina_element.?;423 output.updateSelfNixManaged(remote_ver);424 if (ctx.cfg.confirm and !output.confirm("")) return;425 const ec = exec.streamLocal(ctx.io, &.{ "nix", "profile", "upgrade", element }) catch {426 output.updateSelfNixFailed();427 return;428 };429 if (ec != 0) {430 output.updateSelfNixFailed();431 return;432 }433 output.updateSelfDone(version.VERSION, remote_ver);434 return;435 }436437 const plat_info = blk: {438 const pv = root.object.get("platforms") orelse break :blk null;439 if (pv != .object) break :blk null;440 break :blk pv.object.get(version.PLATFORM);441 };442 if (plat_info == null) {443 output.updateSelfNoPlatform(version.PLATFORM);444 return;445 }446447 const dl_url = blk: {448 const v = plat_info.?.object.get("url") orelse {449 output.printWarning("malformed release metadata");450 return;451 };452 break :blk switch (v) {453 .string => |s| s,454 else => {455 output.printWarning("malformed release metadata");456 return;457 },458 };459 };460461 const raw_cksum = blk: {462 if (plat_info.?.object.get("blake3")) |v|463 break :blk switch (v) {464 .string => |s| s,465 else => {466 output.printWarning("malformed release metadata");467 return;468 },469 };470 if (plat_info.?.object.get("checksum")) |v|471 break :blk switch (v) {472 .string => |s| s,473 else => {474 output.printWarning("malformed release metadata");475 return;476 },477 };478 output.printWarning("missing checksum in release metadata");479 return;480 };481 const expected_hex = if (std.mem.startsWith(u8, raw_cksum, "blake3:")) raw_cksum["blake3:".len..] else raw_cksum;482483 if (ctx.cfg.confirm and !output.confirm("")) return;484485 output.updateSelfDownloading(remote_ver, version.PLATFORM);486 const tmp_path = exec.downloadToTemp(ctx.gpa, ctx.io, dl_url) catch {487 output.printWarning("download failed — check your connection and try again");488 return;489 };490 defer ctx.gpa.free(tmp_path);491492 output.updateSelfVerifying();493 exec.verifyAndInstall(ctx.gpa, ctx.io, tmp_path, expected_hex, install_path) catch |e| switch (e) {494 error.ChecksumMismatch => {495 output.updateSelfChecksumFailed();496 return;497 },498 else => return e,499 };500501 output.updateSelfDone(version.VERSION, remote_ver);502}503504505506507pub fn goodbye(ctx: Ctx) !void {508 output.goodbyeStart();509510 var exe_buf: [std.Io.Dir.max_path_bytes]u8 = undefined;511 const install_path = exec.selfExePath(ctx.io, &exe_buf) catch {512 errors.error_info.setMessage("could not determine install path", .{});513 return error.Internal;514 };515516 517 if (!std.mem.startsWith(u8, install_path, "/nix/store/")) {518 output.goodbyeFile(install_path);519 if (ctx.cfg.confirm and !output.confirm("")) return;520 std.Io.Dir.deleteFileAbsolute(ctx.io, install_path) catch |e| {521 errors.error_info.setMessage("could not remove {s}", .{install_path});522 return e;523 };524 output.goodbyeDone();525 return;526 }527528 529 const nina_element = exec.profileNinaElement(ctx.gpa, ctx.io, &ctx.machine);530 defer {531 if (nina_element) |n| ctx.gpa.free(n);532 }533534 if (nina_element == null) {535 errors.error_info.setMessage("nina not found in nix profile — was it installed via 'nix profile add'?", .{});536 return error.Internal;537 }538539 output.goodbyeNix();540 if (ctx.cfg.confirm and !output.confirm("")) return;541 const ec = exec.streamLocal(ctx.io, &.{ "nix", "profile", "remove", nina_element.? }) catch {542 output.goodbyeFailed("nix profile remove failed");543 return;544 };545 if (ec != 0) {546 output.goodbyeFailed("nix profile remove failed");547 return;548 }549 output.goodbyeDone();550}551552fn channelUpdateCmd(ctx: Ctx) !void {553 output.updateStart(ctx.machine.name);554 _ = try exec.channelUpdate(ctx.io, &ctx.machine, ctx.gpa);555 output.updateDone();556}557558pub fn upgrade(ctx: Ctx) !void {559 if (!try runPreHook(ctx, "pre-upgrade")) {560 output.aborted();561 return error.HookAborted;562 }563564 output.upgradeStart(ctx.machine.name);565 566 567 568 if (flakeSystem(ctx)) {569 output.printSubstep("updating flake inputs", .{});570 _ = exec.flakeUpdateAt(ctx.io, &ctx.machine, ctx.gpa, ctx.machine.config_path) catch {};571 } else {572 output.printSubstep("updating channels", .{});573 _ = exec.channelUpdate(ctx.io, &ctx.machine, ctx.gpa) catch {};574 }575 output.printSubstep("rebuilding", .{});576 output.buildPanelInit();577 const start = std.Io.Clock.awake.now(ctx.io);578 var panel = output.BuildPanel{};579 const code = exec.nixosRebuildSwitchWithPanel(ctx.io, &ctx.machine, ctx.gpa, &panel) catch |e| {580 output.buildPanelClear();581 return e;582 };583 output.buildPanelClear();584 if (code != 0) {585 output.applyFailed("");586 return error.BuildFailed;587 }588 const elapsed_ns = @max(0, start.untilNow(ctx.io, .awake).toNanoseconds());589 const elapsed: u64 = @intCast(@divFloor(elapsed_ns, 1_000_000));590 const gen = exec.getCurrentGeneration(ctx.gpa, ctx.io, &ctx.machine) catch 0;591 output.applyDone(gen, elapsed, false);592 runPostHook(ctx, "post-upgrade");593}594595pub fn log(ctx: Ctx) !void {596 output.logHeader(ctx.machine.name);597 const content = exec.readNinaLog(ctx.gpa, ctx.io, ctx.environ, ctx.last) catch {598 599 return;600 };601 defer ctx.gpa.free(content);602 var lines = std.mem.splitScalar(u8, content, '\n');603 while (lines.next()) |line| {604 const t = std.mem.trim(u8, line, " \t\r");605 if (t.len == 0) continue;606 if (t[0] == '{') {607 renderJsonLogLine(ctx.gpa, t);608 } else {609 610 var parts = std.mem.tokenizeScalar(u8, t, '\t');611 const date = parts.next() orelse continue;612 const time = parts.next() orelse "";613 const action = parts.next() orelse "";614 const detail = parts.next() orelse "";615 output.logRow(date, time, action, detail);616 }617 }618}619620621622fn renderJsonLogLine(gpa: std.mem.Allocator, line: []const u8) void {623 const parsed = std.json.parseFromSlice(std.json.Value, gpa, line, .{}) catch {624 output.logRow(line, "", "", ""); 625 return;626 };627 defer parsed.deinit();628 if (parsed.value != .object) return;629 const o = parsed.value.object;630631 const ts = jsonStr(o, "ts");632 const date = if (ts.len >= 10) ts[0..10] else ts;633 const time = if (ts.len >= 16) ts[11..16] else "";634 const action = jsonStr(o, "command");635 const outcome = jsonStr(o, "outcome");636637 var buf: [160]u8 = undefined;638 var detail: []const u8 = outcome;639 if (o.get("gen_after")) |g| {640 if (g == .integer) {641 detail = std.fmt.bufPrint(&buf, "{s} -> gen {d}", .{ outcome, g.integer }) catch outcome;642 }643 }644 output.logRow(date, time, action, detail);645}646647fn jsonStr(o: std.json.ObjectMap, key: []const u8) []const u8 {648 if (o.get(key)) |v| {649 if (v == .string) return v.string;650 }651 return "";652}653654pub fn edit(ctx: Ctx) !void {655 const hardware = ctx.args.len > 0 and std.mem.eql(u8, ctx.args[0], "hardware");656 const filename = if (hardware) "hardware-configuration.nix" else "configuration.nix";657 const path = try std.fmt.allocPrint(ctx.gpa, "{s}/{s}", .{ ctx.machine.config_path, filename });658 defer ctx.gpa.free(path);659 output.editOpen(path, ctx.cfg.editor);660 _ = try exec.openEditor(ctx.io, ctx.gpa, ctx.cfg.editor, path, null);661}662663pub fn fmt(ctx: Ctx) !void {664 const path = try std.fmt.allocPrint(ctx.gpa, "{s}/configuration.nix", .{ctx.machine.config_path});665 defer ctx.gpa.free(path);666 if (ctx.check) {667 const code = try exec.nixpkgsFmt(ctx.io, &ctx.machine, ctx.gpa, path, true);668 if (code != 0) output.fmtNeedsFormat() else output.fmtDone();669 return;670 }671 output.fmtStart(path);672 _ = try exec.nixpkgsFmt(ctx.io, &ctx.machine, ctx.gpa, path, false);673 output.fmtDone();674}675676pub fn repl(ctx: Ctx) !void {677 output.replStart();678 _ = try exec.nixRepl(ctx.io, &ctx.machine, ctx.gpa);679}680681pub fn develop(ctx: Ctx) !void {682 if (std.mem.eql(u8, ctx.sub, "show")) {683 _ = try exec.nixBuild(ctx.io, &ctx.machine, ctx.gpa, null);684 return;685 }686 const run_cmd: ?[]const u8 = if (ctx.args.len > 0 and std.mem.eql(u8, ctx.sub, "run"))687 ctx.args[0]688 else689 null;690 output.developStart(&.{});691 _ = try exec.nixDevelopPs1(ctx.io, &ctx.machine, ctx.gpa, run_cmd, ctx.environ);692 output.developDone();693}694695pub fn info(ctx: Ctx) !void {696 output.infoHeader(ctx.machine.name);697 const nixos_ver = exec.systemInfo(ctx.gpa, ctx.io, &ctx.machine) catch try ctx.gpa.dupe(u8, "unknown");698 defer ctx.gpa.free(nixos_ver);699 output.infoRow("nixos", std.mem.trim(u8, nixos_ver, " \t\n\r"));700701 const kernel = exec.kernelInfo(ctx.gpa, ctx.io, &ctx.machine) catch try ctx.gpa.dupe(u8, "unknown");702 defer ctx.gpa.free(kernel);703 output.infoRow("kernel", std.mem.trim(u8, kernel, " \t\n\r"));704705 const uptime = exec.uptimeInfo(ctx.gpa, ctx.io, &ctx.machine) catch try ctx.gpa.dupe(u8, "unknown");706 defer ctx.gpa.free(uptime);707 output.infoRow("uptime", std.mem.trim(u8, uptime, " \t\n\r"));708}709710pub fn boot(ctx: Ctx) !void {711 output.bootHeader(ctx.machine.name);712 const entries = exec.bootEntries(ctx.gpa, ctx.io, &ctx.machine) catch &.{};713 defer ctx.gpa.free(entries);714 for (entries, 0..) |entry, i| output.bootEntry(entry, i);715}716717pub fn searchCmd(ctx: Ctx) !void {718 const query = if (ctx.args.len > 0) ctx.args[0] else "";719 api.ensureRegistryPinned(ctx.gpa, ctx.io, ctx.environ);720 const selected = try search_tui.run(ctx.gpa, ctx.io, query, .packages);721 if (selected) |npkg| try selectAndInstall(ctx, npkg);722}723724pub fn optionCmd(ctx: Ctx) !void {725 const query = if (ctx.args.len > 0) ctx.args[0] else "";726 api.ensureRegistryPinned(ctx.gpa, ctx.io, ctx.environ);727 _ = try search_tui.run(ctx.gpa, ctx.io, query, .options);728}729730pub fn install(ctx: Ctx) !void {731 const pkg_name = if (ctx.args.len > 0) ctx.args[0] else return error.PackageNotFound;732733 api.ensureRegistryPinned(ctx.gpa, ctx.io, ctx.environ);734735 736 const pkgs = api.searchPackages(ctx.gpa, ctx.io, pkg_name) catch {737 errors.error_info.setMessage("'{s}' not found in nixpkgs", .{pkg_name});738 errors.error_info.setSuggestion("nina search {s}", .{pkg_name});739 return error.PackageNotFound;740 };741742 if (pkgs.len == 0) {743 744 if (closestPackage(ctx, pkg_name)) |sugg| {745 output.didYouMean(pkg_name, sugg.pname);746 if (output.confirm("")) try selectAndInstall(ctx, sugg);747 return;748 }749 errors.error_info.setMessage("'{s}' not found in nixpkgs", .{pkg_name});750 errors.error_info.setSuggestion("nina search {s}", .{pkg_name});751 return error.PackageNotFound;752 }753754 755 756 var found = pkgs[0];757 for (pkgs) |p| {758 if (std.mem.eql(u8, p.pname, pkg_name)) {759 found = p;760 break;761 }762 }763 try selectAndInstall(ctx, found);764}765766767768769fn closestPackage(ctx: Ctx, query: []const u8) ?types.NixPackage {770 if (query.len < 2) return null;771 var candidates = api.searchPackages(ctx.gpa, ctx.io, query[0..@min(query.len, 4)]) catch &.{};772 if (candidates.len == 0 and query.len > 2) {773 candidates = api.searchPackages(ctx.gpa, ctx.io, query[0..2]) catch &.{};774 }775 if (candidates.len == 0) return null;776777 var best: ?types.NixPackage = null;778 var best_dist: usize = std.math.maxInt(usize);779 for (candidates) |p| {780 const d = editDistance(query, p.pname);781 if (d > 0 and d < best_dist) {782 best_dist = d;783 best = p;784 }785 }786 787 const threshold = @max(@as(usize, 2), query.len / 3);788 return if (best_dist <= threshold) best else null;789}790791792793fn editDistance(a: []const u8, b: []const u8) usize {794 const n = b.len;795 if (a.len == 0) return n;796 if (n == 0) return a.len;797 if (n + 1 > 128) return std.math.maxInt(usize);798 var prev: [128]usize = undefined;799 var curr: [128]usize = undefined;800 var j: usize = 0;801 while (j <= n) : (j += 1) prev[j] = j;802 var i: usize = 1;803 while (i <= a.len) : (i += 1) {804 curr[0] = i;805 j = 1;806 while (j <= n) : (j += 1) {807 const cost: usize = if (std.ascii.toLower(a[i - 1]) == std.ascii.toLower(b[j - 1])) 0 else 1;808 curr[j] = @min(prev[j] + 1, @min(curr[j - 1] + 1, prev[j - 1] + cost));809 }810 @memcpy(prev[0 .. n + 1], curr[0 .. n + 1]);811 }812 return prev[n];813}814815test "editDistance is case-insensitive Levenshtein" {816 try std.testing.expectEqual(@as(usize, 1), editDistance("firefux", "firefox"));817 try std.testing.expectEqual(@as(usize, 0), editDistance("git", "GIT"));818 try std.testing.expectEqual(@as(usize, 2), editDistance("fierfox", "firefox"));819 try std.testing.expectEqual(@as(usize, 7), editDistance("", "firefox"));820}821822823824825826fn selectAndInstall(ctx: Ctx, npkg: types.NixPackage) !void {827 output.installSelectPrompt(npkg.pname, npkg.version);828829 var choice_buf: [8]u8 = undefined;830 var choice_len: usize = 0;831 while (choice_len < choice_buf.len) {832 const n = std.posix.read(std.posix.STDIN_FILENO, choice_buf[choice_len .. choice_len + 1]) catch break;833 if (n == 0) break;834 if (choice_buf[choice_len] == '\n') break;835 choice_len += 1;836 }837 const choice = std.mem.trim(u8, choice_buf[0..choice_len], " \t");838839 if (choice.len == 0 or choice[0] == 'i' or choice[0] == 'I') {840 try profileInstallPkg(ctx, npkg);841 } else if (choice[0] == 's' or choice[0] == 'S') {842 try systemInstallPkg(ctx, npkg);843 } else if (choice[0] == 't' or choice[0] == 'T') {844 try tryPkg(ctx, npkg);845 }846}847848fn profileInstallPkg(ctx: Ctx, npkg: types.NixPackage) !void {849 output.installStart(npkg.pname, .profile);850 output.printSubstep("nix profile add nixpkgs#{s}", .{npkg.attr});851 output.printTeach(try std.fmt.allocPrint(ctx.gpa, "nix profile add nixpkgs#{s}", .{npkg.attr}));852 const code = try exec.profileInstall(ctx.io, &ctx.machine, ctx.gpa, npkg.attr);853 if (code != 0) return error.ProcessFailed;854 output.installDone(npkg.pname);855}856857fn systemInstallPkg(ctx: Ctx, npkg: types.NixPackage) !void {858 const config_path = try std.fmt.allocPrint(ctx.gpa, "{s}/configuration.nix", .{ctx.machine.config_path});859 defer ctx.gpa.free(config_path);860 const line = api.findSystemPackagesLine(ctx.io, config_path) orelse 1;861 output.installEditorPrompt(npkg.pname, config_path, line, ctx.cfg.editor);862 _ = try exec.openEditor(ctx.io, ctx.gpa, ctx.cfg.editor, config_path, line);863 if (ctx.cfg.confirm) {864 if (output.confirm("apply now? ")) {865 try apply(ctx);866 }867 }868}869870fn tryPkg(ctx: Ctx, npkg: types.NixPackage) !void {871 output.tryStart(npkg.pname);872 output.printSubstep("nix shell nixpkgs#{s}", .{npkg.attr});873 output.tryExitHint();874 _ = try exec.nixShellPs1(ctx.io, &ctx.machine, ctx.gpa, npkg.attr, ctx.environ);875 output.tryDone();876}877878pub fn remove(ctx: Ctx) !void {879 const pkg_name = if (ctx.args.len > 0) ctx.args[0] else return error.PackageNotFound;880 output.removeStart(pkg_name);881 const code = try exec.profileRemove(ctx.io, &ctx.machine, ctx.gpa, pkg_name);882 if (code != 0) return error.ProcessFailed;883 output.done();884}885886pub fn list(ctx: Ctx) !void {887 output.listHeader(ctx.machine.name);888889 890 891 892 if (exec.profileListJson(ctx.gpa, ctx.io, &ctx.machine) catch null) |json| {893 defer ctx.gpa.free(json);894 output.listSection("profile");895 renderProfileJson(ctx.gpa, json);896 }897898 899 900 if (exec.systemPackagePaths(ctx.gpa, ctx.io, &ctx.machine) catch null) |paths| {901 defer ctx.gpa.free(paths);902 output.blankLine();903 output.listSection("system");904 var lines = std.mem.splitScalar(u8, paths, '\n');905 while (lines.next()) |line| {906 const t = std.mem.trim(u8, line, " \t\r");907 if (t.len == 0) continue;908 const row = pkgFromStorePath(t);909 910 if (row.version.len == 0) continue;911 output.listPkg(row.name, row.version);912 }913 }914}915916const PkgRow = struct { name: []const u8, version: []const u8 };917918919920921922fn pkgFromStorePath(path: []const u8) PkgRow {923 const base = std.fs.path.basename(path);924 const after_hash = if (base.len > 33 and base[32] == '-') base[33..] else base;925 var i: usize = 0;926 while (i + 1 < after_hash.len) : (i += 1) {927 if (after_hash[i] == '-' and std.ascii.isDigit(after_hash[i + 1])) {928 return .{ .name = after_hash[0..i], .version = stripOutputSuffix(after_hash[i + 1 ..]) };929 }930 }931 return .{ .name = after_hash, .version = "" };932}933934935936937fn stripOutputSuffix(ver: []const u8) []const u8 {938 const suffixes = [_][]const u8{ "-bin", "-man", "-dev", "-doc", "-lib", "-info", "-out", "-static", "-devdoc" };939 for (suffixes) |s| {940 if (std.mem.endsWith(u8, ver, s)) return ver[0 .. ver.len - s.len];941 }942 return ver;943}944945test "pkgFromStorePath splits name and version at first dash-digit" {946 const a = pkgFromStorePath("/nix/store/abcdefghijklmnopqrstuvwxyz012345-ripgrep-15.1.0");947 try std.testing.expectEqualStrings("ripgrep", a.name);948 try std.testing.expectEqualStrings("15.1.0", a.version);949950 const b = pkgFromStorePath("/nix/store/abcdefghijklmnopqrstuvwxyz012345-helix-25.01");951 try std.testing.expectEqualStrings("helix", b.name);952 try std.testing.expectEqualStrings("25.01", b.version);953954 955 const c = pkgFromStorePath("/nix/store/abcdefghijklmnopqrstuvwxyz012345-curl-8.20.0-bin");956 try std.testing.expectEqualStrings("curl", c.name);957 try std.testing.expectEqualStrings("8.20.0", c.version);958}959960fn firstStorePath(el: std.json.Value) ?[]const u8 {961 if (el != .object) return null;962 const sp = el.object.get("storePaths") orelse return null;963 if (sp != .array or sp.array.items.len == 0) return null;964 const first = sp.array.items[0];965 return if (first == .string) first.string else null;966}967968969970fn renderProfileJson(gpa: std.mem.Allocator, json: []const u8) void {971 const parsed = std.json.parseFromSlice(std.json.Value, gpa, json, .{}) catch return;972 defer parsed.deinit();973 if (parsed.value != .object) return;974 const elements = parsed.value.object.get("elements") orelse return;975 switch (elements) {976 .object => |obj| {977 var it = obj.iterator();978 while (it.next()) |entry| {979 const ver = if (firstStorePath(entry.value_ptr.*)) |p| pkgFromStorePath(p).version else "";980 output.listPkg(entry.key_ptr.*, ver);981 }982 },983 .array => |arr| {984 for (arr.items) |el| {985 const path = firstStorePath(el) orelse continue;986 const row = pkgFromStorePath(path);987 output.listPkg(row.name, row.version);988 }989 },990 else => {},991 }992}993994pub fn tryCmd(ctx: Ctx) !void {995 const pkg_name_arg = if (ctx.args.len > 0) ctx.args[0] else return error.PackageNotFound;996 const shell_pkg = types.NixPackage{ .attr = pkg_name_arg, .pname = pkg_name_arg, .version = "", .description = "" };997 try tryPkg(ctx, shell_pkg);998}999100010011002const home_help = [_][2][]const u8{1003 .{ "apply", "apply home-manager configuration" },1004 .{ "apply --dry", "preview changes without activating" },1005 .{ "back", "roll back one generation" },1006 .{ "history", "list all generations" },1007 .{ "edit", "open home.nix" },1008 .{ "check", "validate without applying" },1009 .{ "diff", "compare the latest two generations" },1010 .{ "packages", "list managed packages" },1011};10121013const HomeGen = struct {1014 number: u32,1015 date: []const u8,1016 time: []const u8,1017 path: []const u8,1018};10191020102110221023fn parseHomeGenerations(gpa: std.mem.Allocator, out_str: []const u8) ![]HomeGen {1024 var gens: std.ArrayList(HomeGen) = .empty;1025 var lines = std.mem.splitScalar(u8, out_str, '\n');1026 while (lines.next()) |line| {1027 const t = std.mem.trim(u8, line, " \t\r");1028 if (t.len == 0) continue;1029 var it = std.mem.tokenizeAny(u8, t, " \t");1030 const date = it.next() orelse continue;1031 const time = it.next() orelse continue;1032 var number: ?u32 = null;1033 var path: []const u8 = "";1034 while (it.next()) |tok| {1035 if (std.mem.eql(u8, tok, "id")) {1036 if (it.next()) |n| number = std.fmt.parseInt(u32, n, 10) catch null;1037 } else if (std.mem.startsWith(u8, tok, "/nix/store/")) {1038 path = tok;1039 }1040 }1041 const num = number orelse continue;1042 try gens.append(gpa, .{1043 .number = num,1044 .date = try gpa.dupe(u8, date),1045 .time = try gpa.dupe(u8, time),1046 .path = try gpa.dupe(u8, path),1047 });1048 }1049 return gens.toOwnedSlice(gpa);1050}10511052fn freeHomeGens(gpa: std.mem.Allocator, gens: []HomeGen) void {1053 for (gens) |g| {1054 gpa.free(g.date);1055 gpa.free(g.time);1056 gpa.free(g.path);1057 }1058 gpa.free(gens);1059}10601061test "parseHomeGenerations reads id, date and store path" {1062 const a = std.testing.allocator;1063 const sample =1064 \\2026-06-01 11:56 : id 765 -> /nix/store/aaaa-home-manager-generation1065 \\2026-05-31 10:29 : id 764 -> /nix/store/bbbb-home-manager-generation1066 ;1067 const gens = try parseHomeGenerations(a, sample);1068 defer freeHomeGens(a, gens);1069 try std.testing.expectEqual(@as(usize, 2), gens.len);1070 try std.testing.expectEqual(@as(u32, 765), gens[0].number);1071 try std.testing.expectEqualStrings("2026-06-01", gens[0].date);1072 try std.testing.expectEqualStrings("11:56", gens[0].time);1073 try std.testing.expectEqualStrings("/nix/store/aaaa-home-manager-generation", gens[0].path);1074 try std.testing.expectEqual(@as(u32, 764), gens[1].number);1075}107610771078fn homeCurrentGen(ctx: Ctx) u32 {1079 const out = exec.homeGenerations(ctx.gpa, ctx.io, &ctx.machine) catch return 0;1080 defer ctx.gpa.free(out);1081 const gens = parseHomeGenerations(ctx.gpa, out) catch return 0;1082 defer freeHomeGens(ctx.gpa, gens);1083 return if (gens.len > 0) gens[0].number else 0;1084}10851086pub fn home(ctx: Ctx) !void {1087 if (ctx.sub.len == 0 or std.mem.eql(u8, ctx.sub, "help"))1088 return output.subcommandHelp("home", &home_help, null);10891090 1091 1092 1093 1094 1095 if (!ctx.machine.local) {1096 output.homeLocalOnly(ctx.machine.name);1097 return;1098 }10991100 const mode = exec.detectHomeManager(ctx.gpa, ctx.io, ctx.environ);1101 if (mode == .none) {1102 output.homeNotInstalled();1103 return;1104 }11051106 1107 1108 if (std.mem.eql(u8, ctx.sub, "apply")) return homeApply(ctx, mode);1109 1110 if (std.mem.eql(u8, ctx.sub, "edit")) return homeEdit(ctx);11111112 if (mode == .module) {1113 output.homeModuleManaged();1114 return;1115 }11161117 if (std.mem.eql(u8, ctx.sub, "back")) return homeBack(ctx);1118 if (std.mem.eql(u8, ctx.sub, "history")) return homeHistory(ctx);1119 if (std.mem.eql(u8, ctx.sub, "check")) return homeCheck(ctx);1120 if (std.mem.eql(u8, ctx.sub, "diff")) return homeDiff(ctx);1121 if (std.mem.eql(u8, ctx.sub, "packages")) return homePackages(ctx);11221123 return output.subcommandHelp("home", &home_help, null);1124}11251126fn homePackages(ctx: Ctx) !void {1127 const out = try exec.homeGenerations(ctx.gpa, ctx.io, &ctx.machine);1128 defer ctx.gpa.free(out);1129 const gens = try parseHomeGenerations(ctx.gpa, out);1130 defer freeHomeGens(ctx.gpa, gens);11311132 output.homePackagesHeader(ctx.machine.name);1133 if (gens.len == 0 or gens[0].path.len == 0) return;11341135 1136 1137 1138 1139 1140 const gen_refs = exec.homeGenerationReferences(ctx.gpa, ctx.io, &ctx.machine, gens[0].path) catch return;1141 defer ctx.gpa.free(gen_refs);11421143 var refs = gen_refs;1144 var env_refs: ?[]u8 = null;1145 defer if (env_refs) |e| ctx.gpa.free(e);1146 if (findHomeManagerPath(gen_refs)) |pkg_path| {1147 if (exec.homeGenerationReferences(ctx.gpa, ctx.io, &ctx.machine, pkg_path)) |e| {1148 env_refs = e;1149 refs = e;1150 } else |_| {}1151 }11521153 1154 1155 1156 var seen = std.StringHashMap(void).init(ctx.gpa);1157 defer seen.deinit();1158 var lines = std.mem.splitScalar(u8, refs, '\n');1159 while (lines.next()) |line| {1160 const t = std.mem.trim(u8, line, " \t\r");1161 if (t.len == 0) continue;1162 const row = pkgFromStorePath(t);1163 if (row.version.len == 0) continue;1164 if (std.mem.indexOf(u8, row.name, "home-manager") != null) continue;1165 if (seen.contains(row.name)) continue;1166 seen.put(row.name, {}) catch {};1167 output.listPkg(row.name, row.version);1168 }1169}1170117111721173fn findHomeManagerPath(refs: []const u8) ?[]const u8 {1174 var lines = std.mem.splitScalar(u8, refs, '\n');1175 while (lines.next()) |line| {1176 const t = std.mem.trim(u8, line, " \t\r");1177 if (std.mem.indexOf(u8, t, "home-manager-path") != null) return t;1178 }1179 return null;1180}11811182test "findHomeManagerPath picks the env, not the generation or infra" {1183 const refs =1184 \\/nix/store/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa-bash-5.31185 \\/nix/store/bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb-home-manager-path1186 \\/nix/store/cccccccccccccccccccccccccccccccc-coreutils-9.111187 ;1188 try std.testing.expectEqualStrings(1189 "/nix/store/bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb-home-manager-path",1190 findHomeManagerPath(refs).?,1191 );1192 try std.testing.expectEqual(@as(?[]const u8, null), findHomeManagerPath("/nix/store/x-bash-5.3\n"));1193}11941195fn homeDiff(ctx: Ctx) !void {1196 const out = try exec.homeGenerations(ctx.gpa, ctx.io, &ctx.machine);1197 defer ctx.gpa.free(out);1198 const gens = try parseHomeGenerations(ctx.gpa, out);1199 defer freeHomeGens(ctx.gpa, gens);12001201 1202 if (gens.len < 2 or gens[0].path.len == 0 or gens[1].path.len == 0) {1203 output.homeNothingToDiff();1204 return;1205 }1206 const newer = gens[0];1207 const older = gens[1];1208 output.homeDiffHeader(older.number, newer.number, ctx.machine.name);1209 if (try exec.diffClosuresPaths(ctx.io, &ctx.machine, ctx.gpa, older.path, newer.path) != 0)1210 return error.ProcessFailed;1211}12121213fn homeCheck(ctx: Ctx) !void {1214 output.homeCheckStart(ctx.machine.name);1215 const code = try exec.homeManagerBuild(ctx.io, &ctx.machine, ctx.gpa);1216 if (code != 0) return error.BuildFailed;1217 output.homeCheckOk();1218}12191220fn homeEdit(ctx: Ctx) !void {1221 1222 1223 const path = exec.homeNixPath(ctx.gpa, ctx.environ) orelse {1224 errors.error_info.setMessage("could not resolve ~/.config/home-manager/home.nix", .{});1225 return error.ConfigNotFound;1226 };1227 defer ctx.gpa.free(path);1228 output.homeEditOpen(path, ctx.cfg.editor);1229 _ = try exec.openEditor(ctx.io, ctx.gpa, ctx.cfg.editor, path, null);1230}12311232fn homeHistory(ctx: Ctx) !void {1233 const out = try exec.homeGenerations(ctx.gpa, ctx.io, &ctx.machine);1234 defer ctx.gpa.free(out);1235 const gens = try parseHomeGenerations(ctx.gpa, out);1236 defer freeHomeGens(ctx.gpa, gens);1237 output.homeHistoryHeader(ctx.machine.name);1238 1239 for (gens, 0..) |g, i| output.homeHistoryRow(g.number, g.date, g.time, i == 0);1240}12411242fn homeBack(ctx: Ctx) !void {1243 const out = try exec.homeGenerations(ctx.gpa, ctx.io, &ctx.machine);1244 defer ctx.gpa.free(out);1245 const gens = try parseHomeGenerations(ctx.gpa, out);1246 defer freeHomeGens(ctx.gpa, gens);12471248 1249 if (gens.len < 2) {1250 output.alreadyOldest();1251 return;1252 }1253 const current = gens[0].number;1254 const prev = gens[1].number;12551256 output.homeBackStart(ctx.machine.name, current, prev);1257 if (ctx.cfg.confirm and !output.confirm("")) return;1258 const code = try exec.homeManagerRollback(ctx.io, &ctx.machine, ctx.gpa);1259 if (code != 0) return error.BuildFailed;1260 output.homeBackDone(prev);1261}12621263fn homeApply(ctx: Ctx, mode: exec.HomeManagerMode) !void {1264 1265 if (mode == .module) {1266 output.homeModuleManaged();1267 return;1268 }1269 if (ctx.dry) {1270 output.homeApplyDry(ctx.machine.name);1271 const code = try exec.homeManagerBuild(ctx.io, &ctx.machine, ctx.gpa);1272 if (code != 0) return error.BuildFailed;1273 output.homeApplyDryDone();1274 return;1275 }12761277 if (!try runPreHook(ctx, "pre-home")) {1278 output.aborted();1279 return error.HookAborted;1280 }12811282 output.homeApplyStart(ctx.machine.name);1283 const flake_ref = exec.homeFlakeRef(ctx.gpa, ctx.io, ctx.environ);1284 defer if (flake_ref) |r| ctx.gpa.free(r);1285 if (flake_ref) |ref| {1286 output.printSubstep("home-manager switch --flake {s}", .{ref});1287 } else {1288 output.printSubstep("home-manager switch", .{});1289 }12901291 const start = std.Io.Clock.awake.now(ctx.io);1292 const code = if (flake_ref) |ref|1293 try exec.homeManagerSwitchFlake(ctx.io, &ctx.machine, ctx.gpa, ref)1294 else1295 try exec.homeManagerSwitch(ctx.io, &ctx.machine, ctx.gpa);1296 if (code != 0) return error.BuildFailed;12971298 const elapsed_ns = @max(0, start.untilNow(ctx.io, .awake).toNanoseconds());1299 const elapsed: u64 = @intCast(@divFloor(elapsed_ns, 1_000_000));1300 output.homeApplyDone(homeCurrentGen(ctx), elapsed);1301 runPostHook(ctx, "post-home");1302}1303130413051306const service_help = [_][2][]const u8{1307 .{ "list", "list running services" },1308 .{ "logs <svc>", "show service logs" },1309 .{ "status <svc>", "service status" },1310 .{ "start <svc>", "start a service" },1311 .{ "stop <svc>", "stop a service" },1312 .{ "restart <svc>", "restart a service" },1313 .{ "enable <svc>", "enable at boot" },1314 .{ "disable <svc>", "disable at boot" },1315};13161317pub fn service(ctx: Ctx) !void {1318 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");1319 if (std.mem.eql(u8, ctx.sub, "list")) {1320 output.serviceListHeader(ctx.machine.name);1321 const svcs = exec.serviceList(ctx.gpa, ctx.io, &ctx.machine, ctx.user) catch &.{};1322 defer ctx.gpa.free(svcs);1323 for (svcs) |s| output.serviceRow(s);1324 return;1325 }1326 if (std.mem.eql(u8, ctx.sub, "logs")) {1327 const name = if (ctx.args.len > 0) ctx.args[0] else return error.Internal;1328 const follow = ctx.args.len > 1 and std.mem.eql(u8, ctx.args[1], "-f");1329 output.serviceLogsHeader(name, ctx.last, ctx.machine.name);1330 _ = try exec.serviceLogs(ctx.io, &ctx.machine, ctx.gpa, name, ctx.last, follow, ctx.user);1331 return;1332 }1333 if (std.mem.eql(u8, ctx.sub, "status")) {1334 const name = if (ctx.args.len > 0) ctx.args[0] else return error.Internal;1335 output.serviceVerb("status", name, ctx.machine.name);1336 _ = try exec.serviceCtl(ctx.io, &ctx.machine, ctx.gpa, "status", name, ctx.user);1337 return;1338 }1339 1340 const name = if (ctx.args.len > 0) ctx.args[0] else return error.Internal;1341 if (ctx.cfg.confirm and (std.mem.eql(u8, ctx.sub, "stop") or std.mem.eql(u8, ctx.sub, "disable"))) {1342 if (!output.confirm("")) return;1343 }1344 output.serviceVerb(ctx.sub, name, ctx.machine.name);1345 _ = try exec.serviceCtl(ctx.io, &ctx.machine, ctx.gpa, ctx.sub, name, ctx.user);1346 output.serviceDone();1347}1348134913501351const flake_help = [_][2][]const u8{1352 .{ "init", "create a new flake.nix" },1353 .{ "update", "update flake inputs" },1354 .{ "update <in>", "update a specific input" },1355 .{ "check", "validate the flake" },1356 .{ "show", "inspect flake outputs" },1357 .{ "lock", "regenerate flake.lock" },1358 .{ "clone <url>", "clone a flake repository" },1359};13601361pub fn flake(ctx: Ctx) !void {1362 if (ctx.sub.len == 0 or std.mem.eql(u8, ctx.sub, "help")) return output.subcommandHelp("flake", &flake_help, null);1363 if (std.mem.eql(u8, ctx.sub, "update")) {1364 output.flakeUpdateStart();1365 const input: ?[]const u8 = if (ctx.args.len > 0) ctx.args[0] else null;1366 _ = try exec.flakeUpdate(ctx.io, &ctx.machine, ctx.gpa, input);1367 output.flakeLockWritten();1368 return;1369 }1370 if (std.mem.eql(u8, ctx.sub, "check")) {1371 output.flakeCheckStart();1372 const code = try exec.flakeCheck(ctx.io, &ctx.machine, ctx.gpa);1373 if (code == 0) output.flakeValid() else output.checkFailed("");1374 return;1375 }1376 if (std.mem.eql(u8, ctx.sub, "show")) {1377 const json_out = exec.flakeShow(ctx.gpa, ctx.io, &ctx.machine) catch null;1378 if (json_out) |j| {1379 defer ctx.gpa.free(j);1380 output.flakeShowHeader("flake outputs");1381 1382 const parsed = std.json.parseFromSlice(std.json.Value, ctx.gpa, j, .{}) catch return;1383 defer parsed.deinit();1384 if (parsed.value == .object) {1385 var it = parsed.value.object.iterator();1386 while (it.next()) |entry| {1387 if (entry.value_ptr.* == .object) {1388 var inner = entry.value_ptr.object.iterator();1389 while (inner.next()) |e2| {1390 const desc = if (e2.value_ptr.* == .object)1391 if (e2.value_ptr.object.get("description")) |d|1392 if (d == .string) d.string else ""1393 else1394 ""1395 else1396 "";1397 output.flakeShowEntry(entry.key_ptr.*, e2.key_ptr.*, desc);1398 }1399 }1400 }1401 }1402 }1403 return;1404 }1405 if (std.mem.eql(u8, ctx.sub, "init")) {1406 _ = try exec.flakeInit(ctx.io, &ctx.machine, ctx.gpa);1407 return;1408 }1409 if (std.mem.eql(u8, ctx.sub, "lock")) {1410 _ = try exec.flakeLock(ctx.io, &ctx.machine, ctx.gpa);1411 return;1412 }1413 if (std.mem.eql(u8, ctx.sub, "clone")) {1414 const url = if (ctx.args.len > 0) ctx.args[0] else return error.Internal;1415 _ = try exec.flakeClone(ctx.io, &ctx.machine, ctx.gpa, url);1416 return;1417 }1418}1419142014211422const store_help = [_][2][]const u8{1423 .{ "info", "store size and counts" },1424 .{ "path <attr>", "store path of a package" },1425 .{ "gc", "collect garbage" },1426 .{ "verify", "verify store integrity" },1427 .{ "repair", "repair corrupted paths" },1428};14291430pub fn store(ctx: Ctx) !void {1431 if (ctx.sub.len == 0 or std.mem.eql(u8, ctx.sub, "help")) return output.subcommandHelp("store", &store_help, null);1432 if (std.mem.eql(u8, ctx.sub, "gc")) {1433 _ = try exec.storeGc(ctx.io, &ctx.machine, ctx.gpa);1434 return;1435 }1436 if (std.mem.eql(u8, ctx.sub, "verify")) {1437 output.storeVerifyStart();1438 _ = try exec.storeVerify(ctx.io, &ctx.machine, ctx.gpa);1439 return;1440 }1441 if (std.mem.eql(u8, ctx.sub, "repair")) {1442 if (ctx.cfg.confirm and !output.confirm("")) return;1443 output.storeRepairStart();1444 _ = try exec.storeRepair(ctx.io, &ctx.machine, ctx.gpa);1445 return;1446 }1447 if (std.mem.eql(u8, ctx.sub, "info")) {1448 const si = try exec.storeInfo(ctx.gpa, ctx.io, &ctx.machine);1449 output.storeInfo(si);1450 return;1451 }1452 if (std.mem.eql(u8, ctx.sub, "path")) {1453 const attr = if (ctx.args.len > 0) ctx.args[0] else return error.Internal;1454 const path = try exec.storePath(ctx.gpa, ctx.io, &ctx.machine, attr);1455 defer ctx.gpa.free(path);1456 output.storePath(path);1457 return;1458 }1459 1460 const si = try exec.storeInfo(ctx.gpa, ctx.io, &ctx.machine);1461 output.storeInfo(si);1462}1463146414651466const channel_help = [_][2][]const u8{1467 .{ "list", "list channels" },1468 .{ "add <url> [name]", "add a channel" },1469 .{ "remove <name>", "remove a channel" },1470 .{ "update", "update all channels" },1471};14721473pub fn channel(ctx: Ctx) !void {1474 if (ctx.sub.len == 0 or std.mem.eql(u8, ctx.sub, "help")) return output.subcommandHelp("channel", &channel_help, null);1475 1476 if (flakeSystem(ctx)) output.flakeChannelNote();1477 if (std.mem.eql(u8, ctx.sub, "list")) {1478 output.channelListHeader(ctx.machine.name);1479 const chans = try exec.channelList(ctx.gpa, ctx.io, &ctx.machine);1480 defer ctx.gpa.free(chans);1481 for (chans) |ch| output.channelRow(ch);1482 return;1483 }1484 if (std.mem.eql(u8, ctx.sub, "add")) {1485 const url = if (ctx.args.len > 0) ctx.args[0] else return error.Internal;1486 const name = if (ctx.args.len > 1) ctx.args[1] else "";1487 _ = try exec.channelAdd(ctx.io, &ctx.machine, ctx.gpa, url, name);1488 output.channelDone();1489 return;1490 }1491 if (std.mem.eql(u8, ctx.sub, "remove")) {1492 const name = if (ctx.args.len > 0) ctx.args[0] else return error.Internal;1493 _ = try exec.channelRemove(ctx.io, &ctx.machine, ctx.gpa, name);1494 output.channelDone();1495 return;1496 }1497 if (std.mem.eql(u8, ctx.sub, "update")) {1498 return channelUpdateCmd(ctx);1499 }1500}1501150215031504pub fn profile(ctx: Ctx) !void {1505 if (std.mem.eql(u8, ctx.sub, "list") or ctx.sub.len == 0) {1506 output.profileListHeader();1507 const out = exec.profileList(ctx.gpa, ctx.io, &ctx.machine) catch null;1508 if (out) |o| {1509 defer ctx.gpa.free(o);1510 var lines = std.mem.splitScalar(u8, o, '\n');1511 while (lines.next()) |line| {1512 const t = std.mem.trim(u8, line, " \t\r");1513 if (t.len == 0) continue;1514 output.profileRow(t, "", "");1515 }1516 }1517 return;1518 }1519 if (std.mem.eql(u8, ctx.sub, "install")) {1520 const attr = if (ctx.args.len > 0) ctx.args[0] else return error.PackageNotFound;1521 _ = try exec.profileInstall(ctx.io, &ctx.machine, ctx.gpa, attr);1522 output.done();1523 return;1524 }1525 if (std.mem.eql(u8, ctx.sub, "remove")) {1526 const attr = if (ctx.args.len > 0) ctx.args[0] else return error.PackageNotFound;1527 _ = try exec.profileRemove(ctx.io, &ctx.machine, ctx.gpa, attr);1528 output.done();1529 return;1530 }1531 if (std.mem.eql(u8, ctx.sub, "upgrade")) {1532 _ = try exec.profileUpgrade(ctx.io, &ctx.machine, ctx.gpa);1533 output.done();1534 return;1535 }1536}1537153815391540const pkg_help = [_][2][]const u8{1541 .{ "why <pkg>", "what pulled it in" },1542 .{ "deps <pkg>", "direct dependencies" },1543 .{ "size <pkg>", "closure size" },1544 .{ "path <pkg>", "store path" },1545 .{ "closure <pkg>", "full closure" },1546};15471548pub fn pkg(ctx: Ctx) !void {1549 if (ctx.sub.len == 0 or std.mem.eql(u8, ctx.sub, "help")) return output.subcommandHelp("pkg", &pkg_help, null);1550 const name = if (ctx.args.len > 0) ctx.args[0] else return error.PackageNotFound;1551 if (std.mem.eql(u8, ctx.sub, "why")) {1552 if (try exec.pkgWhy(ctx.io, &ctx.machine, ctx.gpa, name) != 0) return error.PackageNotFound;1553 return;1554 }1555 if (std.mem.eql(u8, ctx.sub, "deps")) {1556 if (try exec.pkgDeps(ctx.io, &ctx.machine, ctx.gpa, name) != 0) return error.PackageNotFound;1557 return;1558 }1559 if (std.mem.eql(u8, ctx.sub, "size")) {1560 const s = try exec.pkgSize(ctx.gpa, ctx.io, &ctx.machine, name);1561 defer ctx.gpa.free(s);1562 output.pkgResult("size", s);1563 return;1564 }1565 if (std.mem.eql(u8, ctx.sub, "path")) {1566 const p = try exec.storePath(ctx.gpa, ctx.io, &ctx.machine, name);1567 defer ctx.gpa.free(p);1568 output.pkgResult("path", p);1569 return;1570 }1571 if (std.mem.eql(u8, ctx.sub, "closure")) {1572 if (try exec.pkgClosure(ctx.io, &ctx.machine, ctx.gpa, name) != 0) return error.PackageNotFound;1573 return;1574 }1575}1576157715781579pub fn why(ctx: Ctx) !void {1580 const name = if (ctx.args.len > 0) ctx.args[0] else return error.PackageNotFound;1581 if (try exec.pkgWhy(ctx.io, &ctx.machine, ctx.gpa, name) != 0) return error.PackageNotFound;1582}1583158415851586pub fn pin(ctx: Ctx) !void {1587 const input = if (ctx.args.len > 0) ctx.args[0] else return error.Internal;1588 const commit = if (ctx.args.len > 1) ctx.args[1] else return error.Internal;1589 exec.editFlakeLock(ctx.gpa, input, commit) catch return error.ProcessFailed;1590 output.pinDone(input, commit);1591}15921593pub fn unpin(ctx: Ctx) !void {1594 const input = if (ctx.args.len > 0) ctx.args[0] else return error.Internal;1595 _ = try exec.flakeUpdate(ctx.io, &ctx.machine, ctx.gpa, input);1596 output.unpinDone(input);1597}1598159916001601pub fn hash(ctx: Ctx) !void {1602 const path = if (ctx.args.len > 0) ctx.args[0] else return error.Internal;1603 const h = try exec.nixHash(ctx.gpa, ctx.io, &ctx.machine, path);1604 defer ctx.gpa.free(h);1605 output.hashResult(std.mem.trim(u8, h, " \t\n\r"));1606}16071608pub fn fetch(ctx: Ctx) !void {1609 const url = if (ctx.args.len > 0) ctx.args[0] else return error.Internal;1610 const h = try exec.nixFetch(ctx.gpa, ctx.io, &ctx.machine, url);1611 defer ctx.gpa.free(h);1612 output.fetchResult(std.mem.trim(u8, h, " \t\n\r"), url);1613}1614161516161617pub fn build(ctx: Ctx) !void {1618 const attr: ?[]const u8 = if (ctx.args.len > 0) ctx.args[0] else null;1619 output.buildStart(attr orelse "default");1620 const code = try exec.nixBuild(ctx.io, &ctx.machine, ctx.gpa, attr);1621 if (code != 0) return error.BuildFailed;1622 output.buildDone();1623}16241625pub fn runCmd(ctx: Ctx) !void {1626 const pkg_arg = if (ctx.args.len > 0) ctx.args[0] else return error.PackageNotFound;1627 output.runStart(pkg_arg);1628 _ = try exec.nixRun(ctx.io, &ctx.machine, ctx.gpa, pkg_arg);1629}1630163116321633pub fn hello(ctx: Ctx) !void {1634 output.hello(ctx.cfg.machines);1635}16361637const ConfigKind = enum { flake, channel, none };1638163916401641fn scanConfig(io: std.Io) ConfigKind {1642 if (exec.pathExists(io, "/etc/nixos/flake.nix")) return .flake;1643 if (exec.pathExists(io, "/etc/nixos/configuration.nix")) return .channel;1644 return .none;1645}16461647164816491650pub fn firstRunWizard(gpa: std.mem.Allocator, io: std.Io, environ: *const std.process.Environ.Map) !void {1651 output.firstRunBanner();1652 output.firstRunNoConfig();16531654 var name_buf: [256]u8 = undefined;1655 var path_buf: [256]u8 = undefined;16561657 const machine_name = output.firstRunPrompt("machine name", "my-machine", &name_buf);16581659 output.firstRunScanning();1660 const kind = scanConfig(io);1661 switch (kind) {1662 .flake => output.firstRunDetected("/etc/nixos/flake.nix", true),1663 .channel => output.firstRunDetected("/etc/nixos/configuration.nix", false),1664 .none => output.firstRunNoneFound(),1665 }16661667 1668 1669 const default_path: []const u8 = if (kind == .none) "" else "/etc/nixos";1670 const config_path = output.firstRunPromptDefault("config path", default_path, kind != .none, &path_buf);16711672 1673 1674 const use_flakes = exec.dirHasFlake(gpa, io, config_path);16751676 const conf_path = try config.configPath(gpa, environ);1677 defer gpa.free(conf_path);1678 if (std.fs.path.dirname(conf_path)) |dir| exec.ensureDir(gpa, io, dir);16791680 1681 const flake_line = if (use_flakes) "flake = true\n" else "";1682 var content_buf: [1024]u8 = undefined;1683 const content = try std.fmt.bufPrint(1684 &content_buf,1685 "{s}[machine]\nname = {s}\nconfig = {s}\nlocal = true\n",1686 .{ flake_line, machine_name, config_path },1687 );16881689 const file = std.Io.Dir.createFile(.cwd(), io, conf_path, .{}) catch {1690 errors.error_info.setMessage("could not write {s}", .{conf_path});1691 return error.Internal;1692 };1693 defer file.close(io);1694 file.writePositionalAll(io, content, 0) catch {1695 errors.error_info.setMessage("could not write {s}", .{conf_path});1696 return error.Internal;1697 };16981699 output.firstRunWritten(conf_path);1700}17011702pub fn setup(gpa: std.mem.Allocator, io: std.Io, environ: *const std.process.Environ.Map) !void {1703 1704 1705 1706 config.migrateLegacyConfig(gpa, io, environ);1707 if (config.configPath(gpa, environ)) |conf_path| {1708 defer gpa.free(conf_path);1709 if (std.Io.Dir.openFile(.cwd(), io, conf_path, .{})) |f| {1710 f.close(io);1711 } else |_| {1712 try firstRunWizard(gpa, io, environ);1713 errors.error_info.reset();1714 }1715 } else |_| {}17161717 output.setupCheck();1718 const status = exec.checkNixFeatures(gpa, io, environ);1719 output.setupFeatures(status.nix_command, status.flakes);17201721 if (status.allEnabled()) {1722 output.setupAllSet();1723 api.ensureRegistryPinned(gpa, io, environ);1724 return;1725 }17261727 1728 1729 1730 1731 if (exec.isNixOS(io)) {1732 const conf_path = "/etc/nixos/configuration.nix";1733 if (exec.nixosConfigEnablesFeatures(gpa, io, conf_path)) {1734 output.setupNixosNeedsRebuild(conf_path);1735 return;1736 }1737 output.setupNixosOffer(exec.NIXOS_FEATURES_LINE, conf_path);1738 if (output.confirmDefaultNo("add it now?")) {1739 if (exec.appendFeaturesToNixosConfig(gpa, io, environ, conf_path)) |_| {1740 output.setupNixosWrote(conf_path);1741 } else |_| {1742 output.setupNixosManual(exec.NIXOS_FEATURES_LINE, conf_path);1743 }1744 } else {1745 output.setupNixosManual(exec.NIXOS_FEATURES_LINE, conf_path);1746 }1747 return;1748 }17491750 1751 output.setupEnabling();1752 const path = try exec.enableNixFeatures(gpa, io, environ);1753 defer gpa.free(path);17541755 output.setupDone(path);1756 api.ensureRegistryPinned(gpa, io, environ);1757}1758175917601761fn logAction(ctx: Ctx, action: []const u8, gen: u32) void {1762 var ts_buf: [32]u8 = undefined;1763 const ts = nowIso(ctx.io, &ts_buf);1764 var buf: [256]u8 = undefined;1765 const entry = std.fmt.bufPrint(&buf, "{{\"ts\":\"{s}\",\"machine\":\"{s}\",\"command\":\"{s}\",\"outcome\":\"success\",\"gen_before\":null,\"gen_after\":{d},\"duration_ms\":0}}\n", .{1766 ts, ctx.machine.name, action, gen,1767 }) catch return;1768 exec.appendNinaLog(ctx.io, ctx.environ, entry);1769}1770177117721773fn nowIso(io: std.Io, buf: []u8) []const u8 {1774 const fallback = "1970-01-01T00:00:00Z";1775 const ns = std.Io.Clock.real.now(io).toNanoseconds();1776 if (ns <= 0) return fallback;1777 const secs: u64 = @intCast(@divFloor(ns, 1_000_000_000));1778 const es = std.time.epoch.EpochSeconds{ .secs = secs };1779 const ed = es.getEpochDay();1780 const yd = ed.calculateYearDay();1781 const md = yd.calculateMonthDay();1782 const ds = es.getDaySeconds();1783 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", .{1784 yd.year, md.month.numeric(), @as(u32, md.day_index) + 1,1785 ds.getHoursIntoDay(), ds.getMinutesIntoHour(), ds.getSecondsIntoMinute(),1786 }) catch return fallback;1787}