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
4 files src/commands.zig+11 −10src/exec.zig+16 −4src/main.zig+5 −0src/output.zig+2 −1
src/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);

view file

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     }

view file

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")) {

view file

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 

view file