34a678d5d87d
service: add --user flag for user service support across all service commands — parse --user (main.zig Args+Ctx); exec.serviceList/serviceCtl/serviceLogs take a user bool: --user injects 'systemctl --user'/'journalctl --user' and drops sudo for user-owned units; covers list/logs/status/start/stop/restart/enable/disable; service help footer documents --user — build+tests green, argv traced via fake systemctl/journalctl (logs --user -> 'journalctl --user -u <svc> -n N --no-pager', status --user -> 'systemctl --user --no-pager status <svc>' no sudo, system paths unchanged)
- time
- 2026-06-10 01:14 UTC · 4 months ago
- machine
- koh2
- agent
- claude-opus-4-8
- session
- nina community improvements
- parent
- 6a4241b6957a
↓ nina-34a678d5.tar.gzatlas at this save
files changed4 files
src/commands.zig+11 −10src/exec.zig+16 −4src/main.zig+5 −0src/output.zig+2 −1src/commands.zig+11 −10
@@ -27,6 +27,7 @@2727 all: bool,2828 last: u32,2929 json: bool,30+ user: bool, // --user: operate on user (vs system) services3031 };3132 3233 pub fn apply(ctx: Ctx) !void {@@ -132,7 +133,7 @@132133 };133134 134135 pub fn genCmd(ctx: Ctx) !void {135- if (ctx.sub.len == 0 or std.mem.eql(u8, ctx.sub, "help")) return output.subcommandHelp("gen", &gen_help);136+ if (ctx.sub.len == 0 or std.mem.eql(u8, ctx.sub, "help")) return output.subcommandHelp("gen", &gen_help, null);136137 if (std.mem.eql(u8, ctx.sub, "list")) return history(ctx);137138 if (std.mem.eql(u8, ctx.sub, "current")) {138139 const n = try exec.getCurrentGeneration(ctx.gpa, ctx.io, &ctx.machine);@@ -845,10 +846,10 @@845846 };846847 847848 pub fn service(ctx: Ctx) !void {848- if (ctx.sub.len == 0 or std.mem.eql(u8, ctx.sub, "help")) return output.subcommandHelp("service", &service_help);849+ 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");849850 if (std.mem.eql(u8, ctx.sub, "list")) {850851 output.serviceListHeader(ctx.machine.name);851- const svcs = exec.serviceList(ctx.gpa, ctx.io, &ctx.machine) catch &.{};852+ const svcs = exec.serviceList(ctx.gpa, ctx.io, &ctx.machine, ctx.user) catch &.{};852853 defer ctx.gpa.free(svcs);853854 for (svcs) |s| output.serviceRow(s);854855 return;@@ -857,13 +858,13 @@857858 const name = if (ctx.args.len > 0) ctx.args[0] else return error.Internal;858859 const follow = ctx.args.len > 1 and std.mem.eql(u8, ctx.args[1], "-f");859860 output.serviceLogsHeader(name, ctx.last, ctx.machine.name);860- _ = try exec.serviceLogs(ctx.io, &ctx.machine, ctx.gpa, name, ctx.last, follow);861+ _ = try exec.serviceLogs(ctx.io, &ctx.machine, ctx.gpa, name, ctx.last, follow, ctx.user);861862 return;862863 }863864 if (std.mem.eql(u8, ctx.sub, "status")) {864865 const name = if (ctx.args.len > 0) ctx.args[0] else return error.Internal;865866 output.serviceVerb("status", name, ctx.machine.name);866- _ = try exec.serviceCtl(ctx.io, &ctx.machine, ctx.gpa, "status", name);867+ _ = try exec.serviceCtl(ctx.io, &ctx.machine, ctx.gpa, "status", name, ctx.user);867868 return;868869 }869870 // start/stop/restart/enable/disable@@ -872,7 +873,7 @@872873 if (!output.confirm("")) return;873874 }874875 output.serviceVerb(ctx.sub, name, ctx.machine.name);875- _ = try exec.serviceCtl(ctx.io, &ctx.machine, ctx.gpa, ctx.sub, name);876+ _ = try exec.serviceCtl(ctx.io, &ctx.machine, ctx.gpa, ctx.sub, name, ctx.user);876877 output.serviceDone();877878 }878879 @@ -889,7 +890,7 @@889890 };890891 891892 pub fn flake(ctx: Ctx) !void {892- if (ctx.sub.len == 0 or std.mem.eql(u8, ctx.sub, "help")) return output.subcommandHelp("flake", &flake_help);893+ if (ctx.sub.len == 0 or std.mem.eql(u8, ctx.sub, "help")) return output.subcommandHelp("flake", &flake_help, null);893894 if (std.mem.eql(u8, ctx.sub, "update")) {894895 output.flakeUpdateStart();895896 const input: ?[]const u8 = if (ctx.args.len > 0) ctx.args[0] else null;@@ -956,7 +957,7 @@956957 };957958 958959 pub fn store(ctx: Ctx) !void {959- if (ctx.sub.len == 0 or std.mem.eql(u8, ctx.sub, "help")) return output.subcommandHelp("store", &store_help);960+ if (ctx.sub.len == 0 or std.mem.eql(u8, ctx.sub, "help")) return output.subcommandHelp("store", &store_help, null);960961 if (std.mem.eql(u8, ctx.sub, "gc")) {961962 _ = try exec.storeGc(ctx.io, &ctx.machine, ctx.gpa);962963 return;@@ -999,7 +1000,7 @@9991000 };10001001 10011002 pub fn channel(ctx: Ctx) !void {1002- if (ctx.sub.len == 0 or std.mem.eql(u8, ctx.sub, "help")) return output.subcommandHelp("channel", &channel_help);1003+ if (ctx.sub.len == 0 or std.mem.eql(u8, ctx.sub, "help")) return output.subcommandHelp("channel", &channel_help, null);10031004 // On a flake-managed system, nix-channel operations generally don't apply.10041005 if (ctx.cfg.flake) output.flakeChannelNote();10051006 if (std.mem.eql(u8, ctx.sub, "list")) {@@ -1074,7 +1075,7 @@10741075 };10751076 10761077 pub fn pkg(ctx: Ctx) !void {1077- if (ctx.sub.len == 0 or std.mem.eql(u8, ctx.sub, "help")) return output.subcommandHelp("pkg", &pkg_help);1078+ if (ctx.sub.len == 0 or std.mem.eql(u8, ctx.sub, "help")) return output.subcommandHelp("pkg", &pkg_help, null);10781079 const name = if (ctx.args.len > 0) ctx.args[0] else return error.PackageNotFound;10791080 if (std.mem.eql(u8, ctx.sub, "why")) {10801081 _ = try exec.pkgWhy(ctx.io, &ctx.machine, ctx.gpa, name);src/exec.zig+16 −4
@@ -313,8 +313,12 @@313313 314314 // --- Services ---315315 316-pub fn serviceList(gpa: std.mem.Allocator, io: std.Io, machine: *const types.Machine) ![]types.ServiceInfo {317- const r = try capture(gpa, io, machine, &.{ "systemctl", "list-units", "--type=service", "--no-legend", "--no-pager" });316+pub fn serviceList(gpa: std.mem.Allocator, io: std.Io, machine: *const types.Machine, user: bool) ![]types.ServiceInfo {317+ const cmd: []const []const u8 = if (user)318+ &.{ "systemctl", "--user", "list-units", "--type=service", "--no-legend", "--no-pager" }319+ else320+ &.{ "systemctl", "list-units", "--type=service", "--no-legend", "--no-pager" };321+ const r = try capture(gpa, io, machine, cmd);318322 defer gpa.free(r.stderr);319323 defer gpa.free(r.stdout);320324 if (r.exit_code != 0) return error.ProcessFailed;@@ -349,14 +353,22 @@349353 return svcs.toOwnedSlice(gpa);350354 }351355 352-pub fn serviceCtl(io: std.Io, machine: *const types.Machine, gpa: std.mem.Allocator, verb: []const u8, name: []const u8) !u8 {356+pub fn serviceCtl(io: std.Io, machine: *const types.Machine, gpa: std.mem.Allocator, verb: []const u8, name: []const u8, user: bool) !u8 {353357 // --no-pager: `systemctl status` pipes to a pager on a tty and would block.358+ // User services run under the caller's own systemd manager, so no sudo.359+ if (user) {360+ return stream(io, machine, gpa, &.{ "systemctl", "--user", "--no-pager", verb, name });361+ }354362 return stream(io, machine, gpa, &.{ "sudo", "systemctl", "--no-pager", verb, name });355363 }356364 357-pub fn serviceLogs(io: std.Io, machine: *const types.Machine, gpa: std.mem.Allocator, name: []const u8, last: u32, follow: bool) !u8 {365+pub fn serviceLogs(io: std.Io, machine: *const types.Machine, gpa: std.mem.Allocator, name: []const u8, last: u32, follow: bool, user: bool) !u8 {358366 const n_str = try std.fmt.allocPrint(gpa, "{d}", .{last});359367 defer gpa.free(n_str);368+ if (user) {369+ if (follow) return stream(io, machine, gpa, &.{ "journalctl", "--user", "-u", name, "-f" });370+ return stream(io, machine, gpa, &.{ "journalctl", "--user", "-u", name, "-n", n_str, "--no-pager" });371+ }360372 if (follow) {361373 return stream(io, machine, gpa, &.{ "journalctl", "-u", name, "-f" });362374 }src/main.zig+5 −0
@@ -21,6 +21,7 @@2121 all: bool = false,2222 json: bool = false,2323 last: u32 = 50,24+ user: bool = false,2425 };2526 2627 pub fn main(startup: std.process.Init) !void {@@ -78,6 +79,7 @@7879 .all = args.all,7980 .last = args.last,8081 .json = args.json,82+ .user = args.user,8183 };8284 if (std.mem.eql(u8, args.command, "setup")) {8385 commands.setup(ca, io, startup.environ_map) catch |err| {@@ -157,6 +159,7 @@157159 .all = args.all,158160 .last = args.last,159161 .json = args.json,162+ .user = args.user,160163 };161164 162165 dispatch(ctx, args.command) catch |err| {@@ -262,6 +265,8 @@262265 result.no_apply = true;263266 } else if (std.mem.eql(u8, a, "--all")) {264267 result.all = true;268+ } else if (std.mem.eql(u8, a, "--user")) {269+ result.user = true;265270 } else if (std.mem.eql(u8, a, "--json")) {266271 result.json = true;267272 } else if (std.mem.eql(u8, a, "--last")) {src/output.zig+2 −1
@@ -694,11 +694,12 @@694694 695695 // Render a grouped command's subcommand list, shown when the group is invoked696696 // with no subcommand or an explicit `help`. Rows are {invocation, description}.697-pub fn subcommandHelp(group: []const u8, rows: []const [2][]const u8) void {697+pub fn subcommandHelp(group: []const u8, rows: []const [2][]const u8, footer: ?[]const u8) void {698698 p("{s}{s} subcommands\n\n", .{ hdr(), group });699699 for (rows) |row| {700700 p(" {s:<16} {s}{s}{s}\n", .{ row[0], c(DIM), row[1], c(RESET) });701701 }702+ if (footer) |f| p("\n {s}{s}{s}\n", .{ c(DIM), f, c(RESET) });702703 p("\n nina {s} help {s}for this message{s}\n", .{ group, c(DIM), c(RESET) });703704 }704705