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 const start = std.Io.Clock.awake.now(ctx.io);577 const code = try exec.nixosRebuildSwitch(ctx.io, &ctx.machine, ctx.gpa);578 if (code != 0) {579 output.applyFailed("");580 return error.BuildFailed;581 }582 const elapsed_ns = @max(0, start.untilNow(ctx.io, .awake).toNanoseconds());583 const elapsed: u64 = @intCast(@divFloor(elapsed_ns, 1_000_000));584 const gen = exec.getCurrentGeneration(ctx.gpa, ctx.io, &ctx.machine) catch 0;585 output.applyDone(gen, elapsed, false);586 runPostHook(ctx, "post-upgrade");587}588589pub fn log(ctx: Ctx) !void {590 output.logHeader(ctx.machine.name);591 const content = exec.readNinaLog(ctx.gpa, ctx.io, ctx.environ, ctx.last) catch {592 593 return;594 };595 defer ctx.gpa.free(content);596 var lines = std.mem.splitScalar(u8, content, '\n');597 while (lines.next()) |line| {598 const t = std.mem.trim(u8, line, " \t\r");599 if (t.len == 0) continue;600 if (t[0] == '{') {601 renderJsonLogLine(ctx.gpa, t);602 } else {603 604 var parts = std.mem.tokenizeScalar(u8, t, '\t');605 const date = parts.next() orelse continue;606 const time = parts.next() orelse "";607 const action = parts.next() orelse "";608 const detail = parts.next() orelse "";609 output.logRow(date, time, action, detail);610 }611 }612}613614615616fn renderJsonLogLine(gpa: std.mem.Allocator, line: []const u8) void {617 const parsed = std.json.parseFromSlice(std.json.Value, gpa, line, .{}) catch {618 output.logRow(line, "", "", ""); 619 return;620 };621 defer parsed.deinit();622 if (parsed.value != .object) return;623 const o = parsed.value.object;624625 const ts = jsonStr(o, "ts");626 const date = if (ts.len >= 10) ts[0..10] else ts;627 const time = if (ts.len >= 16) ts[11..16] else "";628 const action = jsonStr(o, "command");629 const outcome = jsonStr(o, "outcome");630631 var buf: [160]u8 = undefined;632 var detail: []const u8 = outcome;633 if (o.get("gen_after")) |g| {634 if (g == .integer) {635 detail = std.fmt.bufPrint(&buf, "{s} -> gen {d}", .{ outcome, g.integer }) catch outcome;636 }637 }638 output.logRow(date, time, action, detail);639}640641fn jsonStr(o: std.json.ObjectMap, key: []const u8) []const u8 {642 if (o.get(key)) |v| {643 if (v == .string) return v.string;644 }645 return "";646}647648pub fn edit(ctx: Ctx) !void {649 const hardware = ctx.args.len > 0 and std.mem.eql(u8, ctx.args[0], "hardware");650 const filename = if (hardware) "hardware-configuration.nix" else "configuration.nix";651 const path = try std.fmt.allocPrint(ctx.gpa, "{s}/{s}", .{ ctx.machine.config_path, filename });652 defer ctx.gpa.free(path);653 output.editOpen(path, ctx.cfg.editor);654 _ = try exec.openEditor(ctx.io, ctx.gpa, ctx.cfg.editor, path, null);655}656657pub fn fmt(ctx: Ctx) !void {658 const path = try std.fmt.allocPrint(ctx.gpa, "{s}/configuration.nix", .{ctx.machine.config_path});659 defer ctx.gpa.free(path);660 if (ctx.check) {661 const code = try exec.nixpkgsFmt(ctx.io, &ctx.machine, ctx.gpa, path, true);662 if (code != 0) output.fmtNeedsFormat() else output.fmtDone();663 return;664 }665 output.fmtStart(path);666 _ = try exec.nixpkgsFmt(ctx.io, &ctx.machine, ctx.gpa, path, false);667 output.fmtDone();668}669670pub fn repl(ctx: Ctx) !void {671 output.replStart();672 _ = try exec.nixRepl(ctx.io, &ctx.machine, ctx.gpa);673}674675pub fn develop(ctx: Ctx) !void {676 if (std.mem.eql(u8, ctx.sub, "show")) {677 _ = try exec.nixBuild(ctx.io, &ctx.machine, ctx.gpa, null);678 return;679 }680 const run_cmd: ?[]const u8 = if (ctx.args.len > 0 and std.mem.eql(u8, ctx.sub, "run"))681 ctx.args[0]682 else683 null;684 output.developStart(&.{});685 _ = try exec.nixDevelopPs1(ctx.io, &ctx.machine, ctx.gpa, run_cmd, ctx.environ);686 output.developDone();687}688689pub fn info(ctx: Ctx) !void {690 output.infoHeader(ctx.machine.name);691 const nixos_ver = exec.systemInfo(ctx.gpa, ctx.io, &ctx.machine) catch try ctx.gpa.dupe(u8, "unknown");692 defer ctx.gpa.free(nixos_ver);693 output.infoRow("nixos", std.mem.trim(u8, nixos_ver, " \t\n\r"));694695 const kernel = exec.kernelInfo(ctx.gpa, ctx.io, &ctx.machine) catch try ctx.gpa.dupe(u8, "unknown");696 defer ctx.gpa.free(kernel);697 output.infoRow("kernel", std.mem.trim(u8, kernel, " \t\n\r"));698699 const uptime = exec.uptimeInfo(ctx.gpa, ctx.io, &ctx.machine) catch try ctx.gpa.dupe(u8, "unknown");700 defer ctx.gpa.free(uptime);701 output.infoRow("uptime", std.mem.trim(u8, uptime, " \t\n\r"));702}703704pub fn boot(ctx: Ctx) !void {705 output.bootHeader(ctx.machine.name);706 const entries = exec.bootEntries(ctx.gpa, ctx.io, &ctx.machine) catch &.{};707 defer ctx.gpa.free(entries);708 for (entries, 0..) |entry, i| output.bootEntry(entry, i);709}710711pub fn searchCmd(ctx: Ctx) !void {712 const query = if (ctx.args.len > 0) ctx.args[0] else "";713 api.ensureRegistryPinned(ctx.gpa, ctx.io, ctx.environ);714 const selected = try search_tui.run(ctx.gpa, ctx.io, query, .packages);715 if (selected) |npkg| try selectAndInstall(ctx, npkg);716}717718pub fn optionCmd(ctx: Ctx) !void {719 const query = if (ctx.args.len > 0) ctx.args[0] else "";720 api.ensureRegistryPinned(ctx.gpa, ctx.io, ctx.environ);721 _ = try search_tui.run(ctx.gpa, ctx.io, query, .options);722}723724pub fn install(ctx: Ctx) !void {725 const pkg_name = if (ctx.args.len > 0) ctx.args[0] else return error.PackageNotFound;726727 api.ensureRegistryPinned(ctx.gpa, ctx.io, ctx.environ);728729 730 const pkgs = api.searchPackages(ctx.gpa, ctx.io, pkg_name) catch {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 if (pkgs.len == 0) {737 738 if (closestPackage(ctx, pkg_name)) |sugg| {739 output.didYouMean(pkg_name, sugg.pname);740 if (output.confirm("")) try selectAndInstall(ctx, sugg);741 return;742 }743 errors.error_info.setMessage("'{s}' not found in nixpkgs", .{pkg_name});744 errors.error_info.setSuggestion("nina search {s}", .{pkg_name});745 return error.PackageNotFound;746 }747748 749 750 var found = pkgs[0];751 for (pkgs) |p| {752 if (std.mem.eql(u8, p.pname, pkg_name)) {753 found = p;754 break;755 }756 }757 try selectAndInstall(ctx, found);758}759760761762763fn closestPackage(ctx: Ctx, query: []const u8) ?types.NixPackage {764 if (query.len < 2) return null;765 var candidates = api.searchPackages(ctx.gpa, ctx.io, query[0..@min(query.len, 4)]) catch &.{};766 if (candidates.len == 0 and query.len > 2) {767 candidates = api.searchPackages(ctx.gpa, ctx.io, query[0..2]) catch &.{};768 }769 if (candidates.len == 0) return null;770771 var best: ?types.NixPackage = null;772 var best_dist: usize = std.math.maxInt(usize);773 for (candidates) |p| {774 const d = editDistance(query, p.pname);775 if (d > 0 and d < best_dist) {776 best_dist = d;777 best = p;778 }779 }780 781 const threshold = @max(@as(usize, 2), query.len / 3);782 return if (best_dist <= threshold) best else null;783}784785786787fn editDistance(a: []const u8, b: []const u8) usize {788 const n = b.len;789 if (a.len == 0) return n;790 if (n == 0) return a.len;791 if (n + 1 > 128) return std.math.maxInt(usize);792 var prev: [128]usize = undefined;793 var curr: [128]usize = undefined;794 var j: usize = 0;795 while (j <= n) : (j += 1) prev[j] = j;796 var i: usize = 1;797 while (i <= a.len) : (i += 1) {798 curr[0] = i;799 j = 1;800 while (j <= n) : (j += 1) {801 const cost: usize = if (std.ascii.toLower(a[i - 1]) == std.ascii.toLower(b[j - 1])) 0 else 1;802 curr[j] = @min(prev[j] + 1, @min(curr[j - 1] + 1, prev[j - 1] + cost));803 }804 @memcpy(prev[0 .. n + 1], curr[0 .. n + 1]);805 }806 return prev[n];807}808809test "editDistance is case-insensitive Levenshtein" {810 try std.testing.expectEqual(@as(usize, 1), editDistance("firefux", "firefox"));811 try std.testing.expectEqual(@as(usize, 0), editDistance("git", "GIT"));812 try std.testing.expectEqual(@as(usize, 2), editDistance("fierfox", "firefox"));813 try std.testing.expectEqual(@as(usize, 7), editDistance("", "firefox"));814}815816817818819820fn selectAndInstall(ctx: Ctx, npkg: types.NixPackage) !void {821 output.installSelectPrompt(npkg.pname, npkg.version);822823 var choice_buf: [8]u8 = undefined;824 var choice_len: usize = 0;825 while (choice_len < choice_buf.len) {826 const n = std.posix.read(std.posix.STDIN_FILENO, choice_buf[choice_len .. choice_len + 1]) catch break;827 if (n == 0) break;828 if (choice_buf[choice_len] == '\n') break;829 choice_len += 1;830 }831 const choice = std.mem.trim(u8, choice_buf[0..choice_len], " \t");832833 if (choice.len == 0 or choice[0] == 'i' or choice[0] == 'I') {834 try profileInstallPkg(ctx, npkg);835 } else if (choice[0] == 's' or choice[0] == 'S') {836 try systemInstallPkg(ctx, npkg);837 } else if (choice[0] == 't' or choice[0] == 'T') {838 try tryPkg(ctx, npkg);839 }840}841842fn profileInstallPkg(ctx: Ctx, npkg: types.NixPackage) !void {843 output.installStart(npkg.pname, .profile);844 output.printSubstep("nix profile add nixpkgs#{s}", .{npkg.attr});845 output.printTeach(try std.fmt.allocPrint(ctx.gpa, "nix profile add nixpkgs#{s}", .{npkg.attr}));846 const code = try exec.profileInstall(ctx.io, &ctx.machine, ctx.gpa, npkg.attr);847 if (code != 0) return error.ProcessFailed;848 output.installDone(npkg.pname);849}850851fn systemInstallPkg(ctx: Ctx, npkg: types.NixPackage) !void {852 const config_path = try std.fmt.allocPrint(ctx.gpa, "{s}/configuration.nix", .{ctx.machine.config_path});853 defer ctx.gpa.free(config_path);854 const line = api.findSystemPackagesLine(ctx.io, config_path) orelse 1;855 output.installEditorPrompt(npkg.pname, config_path, line, ctx.cfg.editor);856 _ = try exec.openEditor(ctx.io, ctx.gpa, ctx.cfg.editor, config_path, line);857 if (ctx.cfg.confirm) {858 if (output.confirm("apply now? ")) {859 try apply(ctx);860 }861 }862}863864fn tryPkg(ctx: Ctx, npkg: types.NixPackage) !void {865 output.tryStart(npkg.pname);866 output.printSubstep("nix shell nixpkgs#{s}", .{npkg.attr});867 output.tryExitHint();868 _ = try exec.nixShellPs1(ctx.io, &ctx.machine, ctx.gpa, npkg.attr, ctx.environ);869 output.tryDone();870}871872pub fn remove(ctx: Ctx) !void {873 const pkg_name = if (ctx.args.len > 0) ctx.args[0] else return error.PackageNotFound;874 output.removeStart(pkg_name);875 const code = try exec.profileRemove(ctx.io, &ctx.machine, ctx.gpa, pkg_name);876 if (code != 0) return error.ProcessFailed;877 output.done();878}879880pub fn list(ctx: Ctx) !void {881 output.listHeader(ctx.machine.name);882883 884 885 886 if (exec.profileListJson(ctx.gpa, ctx.io, &ctx.machine) catch null) |json| {887 defer ctx.gpa.free(json);888 output.listSection("profile");889 renderProfileJson(ctx.gpa, json);890 }891892 893 894 if (exec.systemPackagePaths(ctx.gpa, ctx.io, &ctx.machine) catch null) |paths| {895 defer ctx.gpa.free(paths);896 output.blankLine();897 output.listSection("system");898 var lines = std.mem.splitScalar(u8, paths, '\n');899 while (lines.next()) |line| {900 const t = std.mem.trim(u8, line, " \t\r");901 if (t.len == 0) continue;902 const row = pkgFromStorePath(t);903 904 if (row.version.len == 0) continue;905 output.listPkg(row.name, row.version);906 }907 }908}909910const PkgRow = struct { name: []const u8, version: []const u8 };911912913914915916fn pkgFromStorePath(path: []const u8) PkgRow {917 const base = std.fs.path.basename(path);918 const after_hash = if (base.len > 33 and base[32] == '-') base[33..] else base;919 var i: usize = 0;920 while (i + 1 < after_hash.len) : (i += 1) {921 if (after_hash[i] == '-' and std.ascii.isDigit(after_hash[i + 1])) {922 return .{ .name = after_hash[0..i], .version = stripOutputSuffix(after_hash[i + 1 ..]) };923 }924 }925 return .{ .name = after_hash, .version = "" };926}927928929930931fn stripOutputSuffix(ver: []const u8) []const u8 {932 const suffixes = [_][]const u8{ "-bin", "-man", "-dev", "-doc", "-lib", "-info", "-out", "-static", "-devdoc" };933 for (suffixes) |s| {934 if (std.mem.endsWith(u8, ver, s)) return ver[0 .. ver.len - s.len];935 }936 return ver;937}938939test "pkgFromStorePath splits name and version at first dash-digit" {940 const a = pkgFromStorePath("/nix/store/abcdefghijklmnopqrstuvwxyz012345-ripgrep-15.1.0");941 try std.testing.expectEqualStrings("ripgrep", a.name);942 try std.testing.expectEqualStrings("15.1.0", a.version);943944 const b = pkgFromStorePath("/nix/store/abcdefghijklmnopqrstuvwxyz012345-helix-25.01");945 try std.testing.expectEqualStrings("helix", b.name);946 try std.testing.expectEqualStrings("25.01", b.version);947948 949 const c = pkgFromStorePath("/nix/store/abcdefghijklmnopqrstuvwxyz012345-curl-8.20.0-bin");950 try std.testing.expectEqualStrings("curl", c.name);951 try std.testing.expectEqualStrings("8.20.0", c.version);952}953954fn firstStorePath(el: std.json.Value) ?[]const u8 {955 if (el != .object) return null;956 const sp = el.object.get("storePaths") orelse return null;957 if (sp != .array or sp.array.items.len == 0) return null;958 const first = sp.array.items[0];959 return if (first == .string) first.string else null;960}961962963964fn renderProfileJson(gpa: std.mem.Allocator, json: []const u8) void {965 const parsed = std.json.parseFromSlice(std.json.Value, gpa, json, .{}) catch return;966 defer parsed.deinit();967 if (parsed.value != .object) return;968 const elements = parsed.value.object.get("elements") orelse return;969 switch (elements) {970 .object => |obj| {971 var it = obj.iterator();972 while (it.next()) |entry| {973 const ver = if (firstStorePath(entry.value_ptr.*)) |p| pkgFromStorePath(p).version else "";974 output.listPkg(entry.key_ptr.*, ver);975 }976 },977 .array => |arr| {978 for (arr.items) |el| {979 const path = firstStorePath(el) orelse continue;980 const row = pkgFromStorePath(path);981 output.listPkg(row.name, row.version);982 }983 },984 else => {},985 }986}987988pub fn tryCmd(ctx: Ctx) !void {989 const pkg_name_arg = if (ctx.args.len > 0) ctx.args[0] else return error.PackageNotFound;990 const shell_pkg = types.NixPackage{ .attr = pkg_name_arg, .pname = pkg_name_arg, .version = "", .description = "" };991 try tryPkg(ctx, shell_pkg);992}993994995996const home_help = [_][2][]const u8{997 .{ "apply", "apply home-manager configuration" },998 .{ "apply --dry", "preview changes without activating" },999 .{ "back", "roll back one generation" },1000 .{ "history", "list all generations" },1001 .{ "edit", "open home.nix" },1002 .{ "check", "validate without applying" },1003 .{ "diff", "compare the latest two generations" },1004 .{ "packages", "list managed packages" },1005};10061007const HomeGen = struct {1008 number: u32,1009 date: []const u8,1010 time: []const u8,1011 path: []const u8,1012};10131014101510161017fn parseHomeGenerations(gpa: std.mem.Allocator, out_str: []const u8) ![]HomeGen {1018 var gens: std.ArrayList(HomeGen) = .empty;1019 var lines = std.mem.splitScalar(u8, out_str, '\n');1020 while (lines.next()) |line| {1021 const t = std.mem.trim(u8, line, " \t\r");1022 if (t.len == 0) continue;1023 var it = std.mem.tokenizeAny(u8, t, " \t");1024 const date = it.next() orelse continue;1025 const time = it.next() orelse continue;1026 var number: ?u32 = null;1027 var path: []const u8 = "";1028 while (it.next()) |tok| {1029 if (std.mem.eql(u8, tok, "id")) {1030 if (it.next()) |n| number = std.fmt.parseInt(u32, n, 10) catch null;1031 } else if (std.mem.startsWith(u8, tok, "/nix/store/")) {1032 path = tok;1033 }1034 }1035 const num = number orelse continue;1036 try gens.append(gpa, .{1037 .number = num,1038 .date = try gpa.dupe(u8, date),1039 .time = try gpa.dupe(u8, time),1040 .path = try gpa.dupe(u8, path),1041 });1042 }1043 return gens.toOwnedSlice(gpa);1044}10451046fn freeHomeGens(gpa: std.mem.Allocator, gens: []HomeGen) void {1047 for (gens) |g| {1048 gpa.free(g.date);1049 gpa.free(g.time);1050 gpa.free(g.path);1051 }1052 gpa.free(gens);1053}10541055test "parseHomeGenerations reads id, date and store path" {1056 const a = std.testing.allocator;1057 const sample =1058 \\2026-06-01 11:56 : id 765 -> /nix/store/aaaa-home-manager-generation1059 \\2026-05-31 10:29 : id 764 -> /nix/store/bbbb-home-manager-generation1060 ;1061 const gens = try parseHomeGenerations(a, sample);1062 defer freeHomeGens(a, gens);1063 try std.testing.expectEqual(@as(usize, 2), gens.len);1064 try std.testing.expectEqual(@as(u32, 765), gens[0].number);1065 try std.testing.expectEqualStrings("2026-06-01", gens[0].date);1066 try std.testing.expectEqualStrings("11:56", gens[0].time);1067 try std.testing.expectEqualStrings("/nix/store/aaaa-home-manager-generation", gens[0].path);1068 try std.testing.expectEqual(@as(u32, 764), gens[1].number);1069}107010711072fn homeCurrentGen(ctx: Ctx) u32 {1073 const out = exec.homeGenerations(ctx.gpa, ctx.io, &ctx.machine) catch return 0;1074 defer ctx.gpa.free(out);1075 const gens = parseHomeGenerations(ctx.gpa, out) catch return 0;1076 defer freeHomeGens(ctx.gpa, gens);1077 return if (gens.len > 0) gens[0].number else 0;1078}10791080pub fn home(ctx: Ctx) !void {1081 if (ctx.sub.len == 0 or std.mem.eql(u8, ctx.sub, "help"))1082 return output.subcommandHelp("home", &home_help, null);10831084 1085 1086 1087 1088 1089 if (!ctx.machine.local) {1090 output.homeLocalOnly(ctx.machine.name);1091 return;1092 }10931094 const mode = exec.detectHomeManager(ctx.gpa, ctx.io, ctx.environ);1095 if (mode == .none) {1096 output.homeNotInstalled();1097 return;1098 }10991100 1101 1102 if (std.mem.eql(u8, ctx.sub, "apply")) return homeApply(ctx, mode);1103 1104 if (std.mem.eql(u8, ctx.sub, "edit")) return homeEdit(ctx);11051106 if (mode == .module) {1107 output.homeModuleManaged();1108 return;1109 }11101111 if (std.mem.eql(u8, ctx.sub, "back")) return homeBack(ctx);1112 if (std.mem.eql(u8, ctx.sub, "history")) return homeHistory(ctx);1113 if (std.mem.eql(u8, ctx.sub, "check")) return homeCheck(ctx);1114 if (std.mem.eql(u8, ctx.sub, "diff")) return homeDiff(ctx);1115 if (std.mem.eql(u8, ctx.sub, "packages")) return homePackages(ctx);11161117 return output.subcommandHelp("home", &home_help, null);1118}11191120fn homePackages(ctx: Ctx) !void {1121 const out = try exec.homeGenerations(ctx.gpa, ctx.io, &ctx.machine);1122 defer ctx.gpa.free(out);1123 const gens = try parseHomeGenerations(ctx.gpa, out);1124 defer freeHomeGens(ctx.gpa, gens);11251126 output.homePackagesHeader(ctx.machine.name);1127 if (gens.len == 0 or gens[0].path.len == 0) return;11281129 1130 1131 1132 1133 1134 const gen_refs = exec.homeGenerationReferences(ctx.gpa, ctx.io, &ctx.machine, gens[0].path) catch return;1135 defer ctx.gpa.free(gen_refs);11361137 var refs = gen_refs;1138 var env_refs: ?[]u8 = null;1139 defer if (env_refs) |e| ctx.gpa.free(e);1140 if (findHomeManagerPath(gen_refs)) |pkg_path| {1141 if (exec.homeGenerationReferences(ctx.gpa, ctx.io, &ctx.machine, pkg_path)) |e| {1142 env_refs = e;1143 refs = e;1144 } else |_| {}1145 }11461147 1148 1149 1150 var seen = std.StringHashMap(void).init(ctx.gpa);1151 defer seen.deinit();1152 var lines = std.mem.splitScalar(u8, refs, '\n');1153 while (lines.next()) |line| {1154 const t = std.mem.trim(u8, line, " \t\r");1155 if (t.len == 0) continue;1156 const row = pkgFromStorePath(t);1157 if (row.version.len == 0) continue;1158 if (std.mem.indexOf(u8, row.name, "home-manager") != null) continue;1159 if (seen.contains(row.name)) continue;1160 seen.put(row.name, {}) catch {};1161 output.listPkg(row.name, row.version);1162 }1163}1164116511661167fn findHomeManagerPath(refs: []const u8) ?[]const u8 {1168 var lines = std.mem.splitScalar(u8, refs, '\n');1169 while (lines.next()) |line| {1170 const t = std.mem.trim(u8, line, " \t\r");1171 if (std.mem.indexOf(u8, t, "home-manager-path") != null) return t;1172 }1173 return null;1174}11751176test "findHomeManagerPath picks the env, not the generation or infra" {1177 const refs =1178 \\/nix/store/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa-bash-5.31179 \\/nix/store/bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb-home-manager-path1180 \\/nix/store/cccccccccccccccccccccccccccccccc-coreutils-9.111181 ;1182 try std.testing.expectEqualStrings(1183 "/nix/store/bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb-home-manager-path",1184 findHomeManagerPath(refs).?,1185 );1186 try std.testing.expectEqual(@as(?[]const u8, null), findHomeManagerPath("/nix/store/x-bash-5.3\n"));1187}11881189fn homeDiff(ctx: Ctx) !void {1190 const out = try exec.homeGenerations(ctx.gpa, ctx.io, &ctx.machine);1191 defer ctx.gpa.free(out);1192 const gens = try parseHomeGenerations(ctx.gpa, out);1193 defer freeHomeGens(ctx.gpa, gens);11941195 1196 if (gens.len < 2 or gens[0].path.len == 0 or gens[1].path.len == 0) {1197 output.homeNothingToDiff();1198 return;1199 }1200 const newer = gens[0];1201 const older = gens[1];1202 output.homeDiffHeader(older.number, newer.number, ctx.machine.name);1203 if (try exec.diffClosuresPaths(ctx.io, &ctx.machine, ctx.gpa, older.path, newer.path) != 0)1204 return error.ProcessFailed;1205}12061207fn homeCheck(ctx: Ctx) !void {1208 output.homeCheckStart(ctx.machine.name);1209 const code = try exec.homeManagerBuild(ctx.io, &ctx.machine, ctx.gpa);1210 if (code != 0) return error.BuildFailed;1211 output.homeCheckOk();1212}12131214fn homeEdit(ctx: Ctx) !void {1215 1216 1217 const path = exec.homeNixPath(ctx.gpa, ctx.environ) orelse {1218 errors.error_info.setMessage("could not resolve ~/.config/home-manager/home.nix", .{});1219 return error.ConfigNotFound;1220 };1221 defer ctx.gpa.free(path);1222 output.homeEditOpen(path, ctx.cfg.editor);1223 _ = try exec.openEditor(ctx.io, ctx.gpa, ctx.cfg.editor, path, null);1224}12251226fn homeHistory(ctx: Ctx) !void {1227 const out = try exec.homeGenerations(ctx.gpa, ctx.io, &ctx.machine);1228 defer ctx.gpa.free(out);1229 const gens = try parseHomeGenerations(ctx.gpa, out);1230 defer freeHomeGens(ctx.gpa, gens);1231 output.homeHistoryHeader(ctx.machine.name);1232 1233 for (gens, 0..) |g, i| output.homeHistoryRow(g.number, g.date, g.time, i == 0);1234}12351236fn homeBack(ctx: Ctx) !void {1237 const out = try exec.homeGenerations(ctx.gpa, ctx.io, &ctx.machine);1238 defer ctx.gpa.free(out);1239 const gens = try parseHomeGenerations(ctx.gpa, out);1240 defer freeHomeGens(ctx.gpa, gens);12411242 1243 if (gens.len < 2) {1244 output.alreadyOldest();1245 return;1246 }1247 const current = gens[0].number;1248 const prev = gens[1].number;12491250 output.homeBackStart(ctx.machine.name, current, prev);1251 if (ctx.cfg.confirm and !output.confirm("")) return;1252 const code = try exec.homeManagerRollback(ctx.io, &ctx.machine, ctx.gpa);1253 if (code != 0) return error.BuildFailed;1254 output.homeBackDone(prev);1255}12561257fn homeApply(ctx: Ctx, mode: exec.HomeManagerMode) !void {1258 1259 if (mode == .module) {1260 output.homeModuleManaged();1261 return;1262 }1263 if (ctx.dry) {1264 output.homeApplyDry(ctx.machine.name);1265 const code = try exec.homeManagerBuild(ctx.io, &ctx.machine, ctx.gpa);1266 if (code != 0) return error.BuildFailed;1267 output.homeApplyDryDone();1268 return;1269 }12701271 if (!try runPreHook(ctx, "pre-home")) {1272 output.aborted();1273 return error.HookAborted;1274 }12751276 output.homeApplyStart(ctx.machine.name);1277 const flake_ref = exec.homeFlakeRef(ctx.gpa, ctx.io, ctx.environ);1278 defer if (flake_ref) |r| ctx.gpa.free(r);1279 if (flake_ref) |ref| {1280 output.printSubstep("home-manager switch --flake {s}", .{ref});1281 } else {1282 output.printSubstep("home-manager switch", .{});1283 }12841285 const start = std.Io.Clock.awake.now(ctx.io);1286 const code = if (flake_ref) |ref|1287 try exec.homeManagerSwitchFlake(ctx.io, &ctx.machine, ctx.gpa, ref)1288 else1289 try exec.homeManagerSwitch(ctx.io, &ctx.machine, ctx.gpa);1290 if (code != 0) return error.BuildFailed;12911292 const elapsed_ns = @max(0, start.untilNow(ctx.io, .awake).toNanoseconds());1293 const elapsed: u64 = @intCast(@divFloor(elapsed_ns, 1_000_000));1294 output.homeApplyDone(homeCurrentGen(ctx), elapsed);1295 runPostHook(ctx, "post-home");1296}1297129812991300const service_help = [_][2][]const u8{1301 .{ "list", "list running services" },1302 .{ "logs <svc>", "show service logs" },1303 .{ "status <svc>", "service status" },1304 .{ "start <svc>", "start a service" },1305 .{ "stop <svc>", "stop a service" },1306 .{ "restart <svc>", "restart a service" },1307 .{ "enable <svc>", "enable at boot" },1308 .{ "disable <svc>", "disable at boot" },1309};13101311pub fn service(ctx: Ctx) !void {1312 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");1313 if (std.mem.eql(u8, ctx.sub, "list")) {1314 output.serviceListHeader(ctx.machine.name);1315 const svcs = exec.serviceList(ctx.gpa, ctx.io, &ctx.machine, ctx.user) catch &.{};1316 defer ctx.gpa.free(svcs);1317 for (svcs) |s| output.serviceRow(s);1318 return;1319 }1320 if (std.mem.eql(u8, ctx.sub, "logs")) {1321 const name = if (ctx.args.len > 0) ctx.args[0] else return error.Internal;1322 const follow = ctx.args.len > 1 and std.mem.eql(u8, ctx.args[1], "-f");1323 output.serviceLogsHeader(name, ctx.last, ctx.machine.name);1324 _ = try exec.serviceLogs(ctx.io, &ctx.machine, ctx.gpa, name, ctx.last, follow, ctx.user);1325 return;1326 }1327 if (std.mem.eql(u8, ctx.sub, "status")) {1328 const name = if (ctx.args.len > 0) ctx.args[0] else return error.Internal;1329 output.serviceVerb("status", name, ctx.machine.name);1330 _ = try exec.serviceCtl(ctx.io, &ctx.machine, ctx.gpa, "status", name, ctx.user);1331 return;1332 }1333 1334 const name = if (ctx.args.len > 0) ctx.args[0] else return error.Internal;1335 if (ctx.cfg.confirm and (std.mem.eql(u8, ctx.sub, "stop") or std.mem.eql(u8, ctx.sub, "disable"))) {1336 if (!output.confirm("")) return;1337 }1338 output.serviceVerb(ctx.sub, name, ctx.machine.name);1339 _ = try exec.serviceCtl(ctx.io, &ctx.machine, ctx.gpa, ctx.sub, name, ctx.user);1340 output.serviceDone();1341}1342134313441345const flake_help = [_][2][]const u8{1346 .{ "init", "create a new flake.nix" },1347 .{ "update", "update flake inputs" },1348 .{ "update <in>", "update a specific input" },1349 .{ "check", "validate the flake" },1350 .{ "show", "inspect flake outputs" },1351 .{ "lock", "regenerate flake.lock" },1352 .{ "clone <url>", "clone a flake repository" },1353};13541355pub fn flake(ctx: Ctx) !void {1356 if (ctx.sub.len == 0 or std.mem.eql(u8, ctx.sub, "help")) return output.subcommandHelp("flake", &flake_help, null);1357 if (std.mem.eql(u8, ctx.sub, "update")) {1358 output.flakeUpdateStart();1359 const input: ?[]const u8 = if (ctx.args.len > 0) ctx.args[0] else null;1360 _ = try exec.flakeUpdate(ctx.io, &ctx.machine, ctx.gpa, input);1361 output.flakeLockWritten();1362 return;1363 }1364 if (std.mem.eql(u8, ctx.sub, "check")) {1365 output.flakeCheckStart();1366 const code = try exec.flakeCheck(ctx.io, &ctx.machine, ctx.gpa);1367 if (code == 0) output.flakeValid() else output.checkFailed("");1368 return;1369 }1370 if (std.mem.eql(u8, ctx.sub, "show")) {1371 const json_out = exec.flakeShow(ctx.gpa, ctx.io, &ctx.machine) catch null;1372 if (json_out) |j| {1373 defer ctx.gpa.free(j);1374 output.flakeShowHeader("flake outputs");1375 1376 const parsed = std.json.parseFromSlice(std.json.Value, ctx.gpa, j, .{}) catch return;1377 defer parsed.deinit();1378 if (parsed.value == .object) {1379 var it = parsed.value.object.iterator();1380 while (it.next()) |entry| {1381 if (entry.value_ptr.* == .object) {1382 var inner = entry.value_ptr.object.iterator();1383 while (inner.next()) |e2| {1384 const desc = if (e2.value_ptr.* == .object)1385 if (e2.value_ptr.object.get("description")) |d|1386 if (d == .string) d.string else ""1387 else1388 ""1389 else1390 "";1391 output.flakeShowEntry(entry.key_ptr.*, e2.key_ptr.*, desc);1392 }1393 }1394 }1395 }1396 }1397 return;1398 }1399 if (std.mem.eql(u8, ctx.sub, "init")) {1400 _ = try exec.flakeInit(ctx.io, &ctx.machine, ctx.gpa);1401 return;1402 }1403 if (std.mem.eql(u8, ctx.sub, "lock")) {1404 _ = try exec.flakeLock(ctx.io, &ctx.machine, ctx.gpa);1405 return;1406 }1407 if (std.mem.eql(u8, ctx.sub, "clone")) {1408 const url = if (ctx.args.len > 0) ctx.args[0] else return error.Internal;1409 _ = try exec.flakeClone(ctx.io, &ctx.machine, ctx.gpa, url);1410 return;1411 }1412}1413141414151416const store_help = [_][2][]const u8{1417 .{ "info", "store size and counts" },1418 .{ "path <attr>", "store path of a package" },1419 .{ "gc", "collect garbage" },1420 .{ "verify", "verify store integrity" },1421 .{ "repair", "repair corrupted paths" },1422};14231424pub fn store(ctx: Ctx) !void {1425 if (ctx.sub.len == 0 or std.mem.eql(u8, ctx.sub, "help")) return output.subcommandHelp("store", &store_help, null);1426 if (std.mem.eql(u8, ctx.sub, "gc")) {1427 _ = try exec.storeGc(ctx.io, &ctx.machine, ctx.gpa);1428 return;1429 }1430 if (std.mem.eql(u8, ctx.sub, "verify")) {1431 output.storeVerifyStart();1432 _ = try exec.storeVerify(ctx.io, &ctx.machine, ctx.gpa);1433 return;1434 }1435 if (std.mem.eql(u8, ctx.sub, "repair")) {1436 if (ctx.cfg.confirm and !output.confirm("")) return;1437 output.storeRepairStart();1438 _ = try exec.storeRepair(ctx.io, &ctx.machine, ctx.gpa);1439 return;1440 }1441 if (std.mem.eql(u8, ctx.sub, "info")) {1442 const si = try exec.storeInfo(ctx.gpa, ctx.io, &ctx.machine);1443 output.storeInfo(si);1444 return;1445 }1446 if (std.mem.eql(u8, ctx.sub, "path")) {1447 const attr = if (ctx.args.len > 0) ctx.args[0] else return error.Internal;1448 const path = try exec.storePath(ctx.gpa, ctx.io, &ctx.machine, attr);1449 defer ctx.gpa.free(path);1450 output.storePath(path);1451 return;1452 }1453 1454 const si = try exec.storeInfo(ctx.gpa, ctx.io, &ctx.machine);1455 output.storeInfo(si);1456}1457145814591460const channel_help = [_][2][]const u8{1461 .{ "list", "list channels" },1462 .{ "add <url> [name]", "add a channel" },1463 .{ "remove <name>", "remove a channel" },1464 .{ "update", "update all channels" },1465};14661467pub fn channel(ctx: Ctx) !void {1468 if (ctx.sub.len == 0 or std.mem.eql(u8, ctx.sub, "help")) return output.subcommandHelp("channel", &channel_help, null);1469 1470 if (flakeSystem(ctx)) output.flakeChannelNote();1471 if (std.mem.eql(u8, ctx.sub, "list")) {1472 output.channelListHeader(ctx.machine.name);1473 const chans = try exec.channelList(ctx.gpa, ctx.io, &ctx.machine);1474 defer ctx.gpa.free(chans);1475 for (chans) |ch| output.channelRow(ch);1476 return;1477 }1478 if (std.mem.eql(u8, ctx.sub, "add")) {1479 const url = if (ctx.args.len > 0) ctx.args[0] else return error.Internal;1480 const name = if (ctx.args.len > 1) ctx.args[1] else "";1481 _ = try exec.channelAdd(ctx.io, &ctx.machine, ctx.gpa, url, name);1482 output.channelDone();1483 return;1484 }1485 if (std.mem.eql(u8, ctx.sub, "remove")) {1486 const name = if (ctx.args.len > 0) ctx.args[0] else return error.Internal;1487 _ = try exec.channelRemove(ctx.io, &ctx.machine, ctx.gpa, name);1488 output.channelDone();1489 return;1490 }1491 if (std.mem.eql(u8, ctx.sub, "update")) {1492 return channelUpdateCmd(ctx);1493 }1494}1495149614971498pub fn profile(ctx: Ctx) !void {1499 if (std.mem.eql(u8, ctx.sub, "list") or ctx.sub.len == 0) {1500 output.profileListHeader();1501 const out = exec.profileList(ctx.gpa, ctx.io, &ctx.machine) catch null;1502 if (out) |o| {1503 defer ctx.gpa.free(o);1504 var lines = std.mem.splitScalar(u8, o, '\n');1505 while (lines.next()) |line| {1506 const t = std.mem.trim(u8, line, " \t\r");1507 if (t.len == 0) continue;1508 output.profileRow(t, "", "");1509 }1510 }1511 return;1512 }1513 if (std.mem.eql(u8, ctx.sub, "install")) {1514 const attr = if (ctx.args.len > 0) ctx.args[0] else return error.PackageNotFound;1515 _ = try exec.profileInstall(ctx.io, &ctx.machine, ctx.gpa, attr);1516 output.done();1517 return;1518 }1519 if (std.mem.eql(u8, ctx.sub, "remove")) {1520 const attr = if (ctx.args.len > 0) ctx.args[0] else return error.PackageNotFound;1521 _ = try exec.profileRemove(ctx.io, &ctx.machine, ctx.gpa, attr);1522 output.done();1523 return;1524 }1525 if (std.mem.eql(u8, ctx.sub, "upgrade")) {1526 _ = try exec.profileUpgrade(ctx.io, &ctx.machine, ctx.gpa);1527 output.done();1528 return;1529 }1530}1531153215331534const pkg_help = [_][2][]const u8{1535 .{ "why <pkg>", "what pulled it in" },1536 .{ "deps <pkg>", "direct dependencies" },1537 .{ "size <pkg>", "closure size" },1538 .{ "path <pkg>", "store path" },1539 .{ "closure <pkg>", "full closure" },1540};15411542pub fn pkg(ctx: Ctx) !void {1543 if (ctx.sub.len == 0 or std.mem.eql(u8, ctx.sub, "help")) return output.subcommandHelp("pkg", &pkg_help, null);1544 const name = if (ctx.args.len > 0) ctx.args[0] else return error.PackageNotFound;1545 if (std.mem.eql(u8, ctx.sub, "why")) {1546 if (try exec.pkgWhy(ctx.io, &ctx.machine, ctx.gpa, name) != 0) return error.PackageNotFound;1547 return;1548 }1549 if (std.mem.eql(u8, ctx.sub, "deps")) {1550 if (try exec.pkgDeps(ctx.io, &ctx.machine, ctx.gpa, name) != 0) return error.PackageNotFound;1551 return;1552 }1553 if (std.mem.eql(u8, ctx.sub, "size")) {1554 const s = try exec.pkgSize(ctx.gpa, ctx.io, &ctx.machine, name);1555 defer ctx.gpa.free(s);1556 output.pkgResult("size", s);1557 return;1558 }1559 if (std.mem.eql(u8, ctx.sub, "path")) {1560 const p = try exec.storePath(ctx.gpa, ctx.io, &ctx.machine, name);1561 defer ctx.gpa.free(p);1562 output.pkgResult("path", p);1563 return;1564 }1565 if (std.mem.eql(u8, ctx.sub, "closure")) {1566 if (try exec.pkgClosure(ctx.io, &ctx.machine, ctx.gpa, name) != 0) return error.PackageNotFound;1567 return;1568 }1569}1570157115721573pub fn why(ctx: Ctx) !void {1574 const name = if (ctx.args.len > 0) ctx.args[0] else return error.PackageNotFound;1575 if (try exec.pkgWhy(ctx.io, &ctx.machine, ctx.gpa, name) != 0) return error.PackageNotFound;1576}1577157815791580pub fn pin(ctx: Ctx) !void {1581 const input = if (ctx.args.len > 0) ctx.args[0] else return error.Internal;1582 const commit = if (ctx.args.len > 1) ctx.args[1] else return error.Internal;1583 exec.editFlakeLock(ctx.gpa, input, commit) catch return error.ProcessFailed;1584 output.pinDone(input, commit);1585}15861587pub fn unpin(ctx: Ctx) !void {1588 const input = if (ctx.args.len > 0) ctx.args[0] else return error.Internal;1589 _ = try exec.flakeUpdate(ctx.io, &ctx.machine, ctx.gpa, input);1590 output.unpinDone(input);1591}1592159315941595pub fn hash(ctx: Ctx) !void {1596 const path = if (ctx.args.len > 0) ctx.args[0] else return error.Internal;1597 const h = try exec.nixHash(ctx.gpa, ctx.io, &ctx.machine, path);1598 defer ctx.gpa.free(h);1599 output.hashResult(std.mem.trim(u8, h, " \t\n\r"));1600}16011602pub fn fetch(ctx: Ctx) !void {1603 const url = if (ctx.args.len > 0) ctx.args[0] else return error.Internal;1604 const h = try exec.nixFetch(ctx.gpa, ctx.io, &ctx.machine, url);1605 defer ctx.gpa.free(h);1606 output.fetchResult(std.mem.trim(u8, h, " \t\n\r"), url);1607}1608160916101611pub fn build(ctx: Ctx) !void {1612 const attr: ?[]const u8 = if (ctx.args.len > 0) ctx.args[0] else null;1613 output.buildStart(attr orelse "default");1614 const code = try exec.nixBuild(ctx.io, &ctx.machine, ctx.gpa, attr);1615 if (code != 0) return error.BuildFailed;1616 output.buildDone();1617}16181619pub fn runCmd(ctx: Ctx) !void {1620 const pkg_arg = if (ctx.args.len > 0) ctx.args[0] else return error.PackageNotFound;1621 output.runStart(pkg_arg);1622 _ = try exec.nixRun(ctx.io, &ctx.machine, ctx.gpa, pkg_arg);1623}1624162516261627pub fn hello(ctx: Ctx) !void {1628 output.hello(ctx.cfg.machines);1629}16301631const ConfigKind = enum { flake, channel, none };1632163316341635fn scanConfig(io: std.Io) ConfigKind {1636 if (exec.pathExists(io, "/etc/nixos/flake.nix")) return .flake;1637 if (exec.pathExists(io, "/etc/nixos/configuration.nix")) return .channel;1638 return .none;1639}16401641164216431644pub fn firstRunWizard(gpa: std.mem.Allocator, io: std.Io, environ: *const std.process.Environ.Map) !void {1645 output.firstRunBanner();1646 output.firstRunNoConfig();16471648 var name_buf: [256]u8 = undefined;1649 var path_buf: [256]u8 = undefined;16501651 const machine_name = output.firstRunPrompt("machine name", "my-machine", &name_buf);16521653 output.firstRunScanning();1654 const kind = scanConfig(io);1655 switch (kind) {1656 .flake => output.firstRunDetected("/etc/nixos/flake.nix", true),1657 .channel => output.firstRunDetected("/etc/nixos/configuration.nix", false),1658 .none => output.firstRunNoneFound(),1659 }16601661 1662 1663 const default_path: []const u8 = if (kind == .none) "" else "/etc/nixos";1664 const config_path = output.firstRunPromptDefault("config path", default_path, kind != .none, &path_buf);16651666 1667 1668 const use_flakes = exec.dirHasFlake(gpa, io, config_path);16691670 const conf_path = try config.configPath(gpa, environ);1671 defer gpa.free(conf_path);1672 if (std.fs.path.dirname(conf_path)) |dir| exec.ensureDir(gpa, io, dir);16731674 1675 const flake_line = if (use_flakes) "flake = true\n" else "";1676 var content_buf: [1024]u8 = undefined;1677 const content = try std.fmt.bufPrint(1678 &content_buf,1679 "{s}[machine]\nname = {s}\nconfig = {s}\nlocal = true\n",1680 .{ flake_line, machine_name, config_path },1681 );16821683 const file = std.Io.Dir.createFile(.cwd(), io, conf_path, .{}) catch {1684 errors.error_info.setMessage("could not write {s}", .{conf_path});1685 return error.Internal;1686 };1687 defer file.close(io);1688 file.writePositionalAll(io, content, 0) catch {1689 errors.error_info.setMessage("could not write {s}", .{conf_path});1690 return error.Internal;1691 };16921693 output.firstRunWritten(conf_path);1694}16951696pub fn setup(gpa: std.mem.Allocator, io: std.Io, environ: *const std.process.Environ.Map) !void {1697 1698 1699 1700 config.migrateLegacyConfig(gpa, io, environ);1701 if (config.configPath(gpa, environ)) |conf_path| {1702 defer gpa.free(conf_path);1703 if (std.Io.Dir.openFile(.cwd(), io, conf_path, .{})) |f| {1704 f.close(io);1705 } else |_| {1706 try firstRunWizard(gpa, io, environ);1707 errors.error_info.reset();1708 }1709 } else |_| {}17101711 output.setupCheck();1712 const status = exec.checkNixFeatures(gpa, io, environ);1713 output.setupFeatures(status.nix_command, status.flakes);17141715 if (status.allEnabled()) {1716 output.setupAllSet();1717 api.ensureRegistryPinned(gpa, io, environ);1718 return;1719 }17201721 1722 1723 1724 1725 if (exec.isNixOS(io)) {1726 const conf_path = "/etc/nixos/configuration.nix";1727 if (exec.nixosConfigEnablesFeatures(gpa, io, conf_path)) {1728 output.setupNixosNeedsRebuild(conf_path);1729 return;1730 }1731 output.setupNixosOffer(exec.NIXOS_FEATURES_LINE, conf_path);1732 if (output.confirmDefaultNo("add it now?")) {1733 if (exec.appendFeaturesToNixosConfig(gpa, io, environ, conf_path)) |_| {1734 output.setupNixosWrote(conf_path);1735 } else |_| {1736 output.setupNixosManual(exec.NIXOS_FEATURES_LINE, conf_path);1737 }1738 } else {1739 output.setupNixosManual(exec.NIXOS_FEATURES_LINE, conf_path);1740 }1741 return;1742 }17431744 1745 output.setupEnabling();1746 const path = try exec.enableNixFeatures(gpa, io, environ);1747 defer gpa.free(path);17481749 output.setupDone(path);1750 api.ensureRegistryPinned(gpa, io, environ);1751}1752175317541755fn logAction(ctx: Ctx, action: []const u8, gen: u32) void {1756 var ts_buf: [32]u8 = undefined;1757 const ts = nowIso(ctx.io, &ts_buf);1758 var buf: [256]u8 = undefined;1759 const entry = std.fmt.bufPrint(&buf, "{{\"ts\":\"{s}\",\"machine\":\"{s}\",\"command\":\"{s}\",\"outcome\":\"success\",\"gen_before\":null,\"gen_after\":{d},\"duration_ms\":0}}\n", .{1760 ts, ctx.machine.name, action, gen,1761 }) catch return;1762 exec.appendNinaLog(ctx.io, ctx.environ, entry);1763}1764176517661767fn nowIso(io: std.Io, buf: []u8) []const u8 {1768 const fallback = "1970-01-01T00:00:00Z";1769 const ns = std.Io.Clock.real.now(io).toNanoseconds();1770 if (ns <= 0) return fallback;1771 const secs: u64 = @intCast(@divFloor(ns, 1_000_000_000));1772 const es = std.time.epoch.EpochSeconds{ .secs = secs };1773 const ed = es.getEpochDay();1774 const yd = ed.calculateYearDay();1775 const md = yd.calculateMonthDay();1776 const ds = es.getDaySeconds();1777 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", .{1778 yd.year, md.month.numeric(), @as(u32, md.day_index) + 1,1779 ds.getHoursIntoDay(), ds.getMinutesIntoHour(), ds.getSecondsIntoMinute(),1780 }) catch return fallback;1781}